1. Linux中内存模型:平坦、非连续和稀疏模型
Linux中的内存模型说的是站在cpu的角度,物理内存的分布情况。
- 平坦模型:从任意一个进程的角度看,在其访问物理内存的时候,物理地址空间是连续的,没有空洞的。这种内存模型下,物理内存可以看成是由一个连续的page frame构成的,每一个page frame对应有一个struct page实例,这样物理内存可以看成是由page数组组成的,可以用一个mem_map数组表示。这样page frame number和mem_map数组下标是线性的。
- 非连续内存模型:cpu在访问物理内存的时候不是连续,里面是存在空洞的。他可以将每个由空洞隔开的物理内存分为多个节点,每个节点内的物理内存是连续的。这里需要区分非连续内存模型和NUMA的关系,NUMA是Linux内存架构中的一种,他是memory和进程之间的关系。但是NUMA和非连续内存模型在形式上很相像。
- 稀疏模型:这个模型自己没有太理解好,给自己后续的成长空间
2、Linux内存架构中的node、zone和page
Linux内存架构分为UMA(统一内存访问)和NUMA(非统一内存访问),UMA可以看成是一个节点的NUMA。NUMA是因为物理内存组织是分布式的,每个cpu有自己的本地内存,可以快速访问本地内存,可以通过总线进行访问其它cpu的本地内存。
node: 在NUMA架构中,是先划分为不同的节点node,然后每个节点又划分为不同的内存域,每个内存域由很多page构成。节点node用struct pglist_data结构表示。
typedef struct pglist_data {
int nr_zones;//内存域的个数,一般就是DMA、Normal和Highmem
struct zone node_zones[MAX_NR_ZONES];//当前节点的内存域
struct zonelist node_zonelists[MAX_ZONELIST];//备用内存域链表
unsigned long node_size;//当前node中有多少page frame
struct page *node_mem_map;//当前node中的所有page构成的数组
int node_id;//node id
unsigned long node_start_paddr;//node的起始物理地址
struct pglist_data *node_next;//指向下一个node的
spinlock_t lru_lock;
...
} pg_data_t;
zone:硬件的限制,内核对不同的page frame采用不同的处理方法,将相同属性的page frame归到一个zone中,主要分为DMA、Normal和Highmem,切记zone是对物理地址的划分,不是对虚拟地址的划分。
- DMA可以直接在内存和外设之间进行数据的读写,不需要cpu的直接参与。由于硬件的限制,DMA并不是可以访问所有的内存,实际只可以访问16M一下的内存。
- Highmem:适用于要访问的物理内存地址空间大于虚拟地址空间的,不能直接建立直接映射场景的高端内存。
- normal:除开dma和highmem的物理内存。
struct zone {
spinlock_t lock;
unsigned long spanned_pages;//zone含有的page frame数目
unsigned long present_pages; //zone中抛开内存空洞page frame的数目
unsigned long nr_reserved_highatomic;//保留内存
atomic_long_t managed_pages;//buddy管理的page frame数目
struct free_area free_area[MAX_ORDER];//空闲链表组成的,按不同连续page frame规格组成
unsigned long _watermark[NR_WMARK];
long lowmem_reserve[MAX_NR_ZONES];
atomic_long_t vm_stat[NR_VM_ZONE_STAT_ITEMS];
unsigned long zone_start_pfn;//起始物理页面号
struct pglist_data *zone_pgdat;
struct page *zone_mem_map;
...
}
page:在Linux中物理内存按每页4KB的大小进行划分(也有大页4MB的划分),每个page frame都有一个struct page的结构来表示。每个struct page结构体占用32字节,也是依据体系的。
struct page {
unsigned long flags;//表示page frame的状态和属性
atomic_t count; //引用计数
atomic_t _mapcount; //被映射的个数,就是在page table中有多少个entry含有这个page frame
struct list_head lru;//page frame根据页的活跃程度分别挂在active_list或inactive_list双向链表中,lru就是指向所在链表中前后节点的指针
struct address_space *mapping;//如果当前page frame属于某个文件的话,则mapping指向文件inode 对应的address space
unsigned long index;
...
}
申请物理内存:实际物理内存大小 + vm_struct(虚拟地址空间管理结构) + struct page实例。
3.Linux中的内存分配:buddy和slab分配器
在Linux中内存分配的基本单位是page frame页,对应的分配器主要是buddy伙伴分配器和slab。在说这两个分配器之前,可以先说下最原始的分配模型:空闲链表和内存池。
- 空闲链表:就是将内存中的所有空闲内存块用链表的形式链起来free list。在进行内存分配时,扫描free list分配一个空闲的块给当前进程。但是这样是很容易产生外部碎片的。
- 内存池:将一块大内存分成很多小内存,不同的内存又按照不同的尺寸分成大小相同的块。,同一内存池中的空闲内存块按照free list链接起来。
buddy伙伴分配器就是基于内存池的思想建立起来,他是在内存释放的时候,会先去检查物理上相邻的左右page frame,如果相邻的page frame也是空闲的就会合成一个更大的空闲内存。
每个内存域都关联了一个staruct zone实例,其中保存了用于管理伙伴数据的主要数组。
struct zone {
/* free areas of different sizes */
struct free_area free_area[MAX_ORDER];
...
}
struct free_area {
struct list_head free_list[MIGRATE_TYPES];//空闲链表,并且按不同的类型进行的区分
unsigned long nr_free;//当前内存区中空闲内存块的数目
};
- free_area:这个是当前内存域的空闲页链表,由buddy进行管理。他是按照不同的连续页框的内存规格进行链表相连。MAX_ORDER=11,分别表示一次分配了多少个连续的page frame。
- nr_free:当前内存区中的空闲块的数目。
-
free_list:为了方便内存的回收,buddy在分配的时候,依据页的移动类型进行细分,方便内存的回收。可移动、可回收和不可移动等类型。
5.slab内存分配
buddy内存分配的单位是page frame的,他的大小是4KB。对于小于4KB的小内存分配来说也用buddy的话会产生很多内部碎片,内存分配使用的效率不高。这就诞生了slab分配器的原因。slab分配器是基于对象进行管理的,相同类型的对象归位一类,每当要分配该类型对象的时候直接分配,这样避免了内部碎片。
struct kmem_cache {
struct array_cache *array[NR_CPUS];//per_cpu数据,记录了本地高速缓存的信息,也是用于跟踪最近释放的对象,每次分配和释放都要直接访问它。
unsigned int limit;//本地高速缓存中空闲对象的最大数目
unsigned int buffer_size;/*buffer的大小,就是对象的大小*/
unsigned int flags; /* constant flags */
unsigned int num; /* # of objs per slab *//*slab中有多少个对象*/
gfp_t gfpflags; /*与伙伴系统交互时所提供的分配标识*/
struct list_head next;//用于将高速缓存连入cache chain
//用于组织该高速缓存中的slab
struct kmem_list3 *nodelists[MAX_NUMNODES];/*最大的内存节点*/
};
struct kmem_list3 {
/*三个链表中存的是一个高速缓存slab*/
/*在这三个链表中存放的是cache*/
struct list_head slabs_partial; //包含空闲对象和已经分配对象的slab描述符
struct list_head slabs_full;//只包含非空闲的slab描述符
struct list_head slabs_free;//只包含空闲的slab描述符
unsigned long free_objects; /*高速缓存中空闲对象的个数*/
unsigned int free_limit; //空闲对象的上限
unsigned int colour_next; /* Per-node cache coloring *//*即将要着色的下一个*/
spinlock_t list_lock;
struct array_cache *shared; /* shared per node */
struct array_cache **alien; /* on other nodes */
unsigned long next_reap; /* updated without locking *//**/
int free_touched; /* updated without locking */
};
struct slab {
struct list_head list; //用于将slab连入keme_list3的链表
unsigned long colouroff; //该slab的着色偏移
void *s_mem; /* 指向slab中的第一个对象*/
unsigned int inuse; /* num of objs active in slab */已经分配出去的对象
kmem_bufctl_t free; //下一个空闲对象的下标
unsigned short nodeid; //节点标识符
};
struct array_cache {
unsigned int avail;/*当前cpu上有多少个可用的对象*/
unsigned int limit;/*per_cpu里面最大的对象的个数,当超过这个值时,将对象返回给伙伴系统*/
unsigned int batchcount;/*一次转入和转出的对象数量*/
unsigned int touched;/*标示本地cpu最近是否被使用*/
spinlock_t lock;/*自旋锁*/
void *entry[]; /*
* Must have this definition in here for the proper
* alignment of array_cache. Also simplifies accessing
* the entries.
*/
};
slab分配过程,每次分配的时候直接从本地cpu告诉缓存中进行交互,这里是软件的高速缓存(kmem_cache中的array_cache),在cpu高速缓存中没有空闲对象的时候,就会从kmem_list中的slabs_partial链表中找一个空闲slab进行分配对象,partial中没有的时候再去slabs_free链表中找,slabs_free中没有的时候slab分配器通过buddy分配器申请page frame去形成一个新的slab进而来分配内存。这里可以看到slab分配器是基于buddy分配器之上的一个抽象。每个类型的对象都有自己的kmem_cache,这样会有很多的cache管理结构。