Linux内存管理
Linux内存管理(一)Linux进程空间管理
Linux内存管理(二)物理内存管理(上)
Linux内存管理(三)物理内存管理(下)
Linux内存管理(四)用户态内存映射
Linux内存管理(五)内核态内存映射
由于物理内存是连续的,页也是连续的,每个页的大小一样,从0开始给每个页编号,每个页用struct page表示,存放在一个大数组里。因此对于任何一个地址,只要除以页的大小,就可以得到对应页的编号,根据下标就可以找到对应的struct page结构,这种模型是最经典的平坦内存模型
所有的CPU总是通过总线去访问内存,这是最经典的内存使用方法,它可以使用平坦内存模型来管理内存
在这种模式下,所有的CPU都在总线的一侧,所有的内存组成一大块内存在总线的另外一侧,CPU访问内存都需要通过总线访问,而且距离都是一样的,这种模式称为SMP(Symmetric multiprocessing),即为对称多处理器。这种模式有一个显著的缺点,就是每个CPU访问内存都需要通过总线,那么总线就会成为瓶颈
为了提高性能,有了一种更加高级的模式,NUMA(Non-uniform memory access),非一致内存访问。这种模式下,内存不是组成连续的一大块,而是每个CPU都有自己的一块内存,CPU访问内存不需要经过总线,所以速度上会更快,每个CPU和内存组成一个NUMA节点。但是在本地内存不足的情况下,每个CPU会去其他NUMA节点申请内存,此时内存的访问时间就比较长
这样内存被分为多个节点,每个节点都分成一个一个的页。由于页是全局唯一定位的,所以每个页都需要有一个全局唯一的页号。但是由于物理内存不再是连续的,所以页号也不是连续的,于是内存模型就变成了非连续内存模型,管理起来就会比较复杂
下面解析当前主流场景,NUMA方式
为了表示一个NUMA节点,内核定义了struct pglist_struct
这样一个结构体,如下
typedef struct pglist_data {
struct zone node_zones[MAX_NR_ZONES];
struct zonelist node_zonelists[MAX_ZONELISTS];
int nr_zones;
struct page *node_mem_map;
unsigned long node_start_pfn;
unsigned long node_present_pages; /* total number of physical pages */
unsigned long node_spanned_pages; /* total size of physical page range, including holes */
int node_id;
......
} pg_data_t;
例如:64M物理内存隔着4M的空洞,然后再是另外的64M,换算成页数目,分别是16K、1K、16K。那么node_spanned_pages就是33K,node_spanned_pages就是32K
每个节点被分为一个一个的区域zone,存放在node_zones数组中,数组的大小为MAX_NR_ZONES,定义如下
enum zone_type {
#ifdef CONFIG_ZONE_DMA
ZONE_DMA,
#endif
#ifdef CONFIG_ZONE_DMA32
ZONE_DMA32,
#endif
ZONE_NORMAL,
#ifdef CONFIG_HIGHMEM
ZONE_HIGHMEM,
#endif
ZONE_MOVABLE,
__MAX_NR_ZONES
};
这里说明以下,这些分区都是对物理内存的说明
内核中有一个数组用来存放节点
struct pglist_data *node_data[MAX_NUMNODES] __read_mostly;
到这里,将内存分为节点,将节点分为区域,下面来看一看区域的定义
区域是zone结构体表示
struct zone {
......
struct pglist_data *zone_pgdat;
struct per_cpu_pageset __percpu *pageset;
unsigned long zone_start_pfn;
/*
* spanned_pages is the total pages spanned by the zone, including
* holes, which is calculated as:
* spanned_pages = zone_end_pfn - zone_start_pfn;
*
* present_pages is physical pages existing within the zone, which
* is calculated as:
* present_pages = spanned_pages - absent_pages(pages in holes);
*
* managed_pages is present pages managed by the buddy system, which
* is calculated as (reserved_pages includes pages allocated by the
* bootmem allocator):
* managed_pages = present_pages - reserved_pages;
*
*/
unsigned long managed_pages;
unsigned long spanned_pages;
unsigned long present_pages;
const char *name;
......
/* free areas of different sizes */
struct free_area free_area[MAX_ORDER];
/* zone flags, see below */
unsigned long flags;
/* Primarily protects free_area */
spinlock_t lock;
......
} ____cacheline_internodealigned_in_
spanned_pages = zone_end_pfn - zone_start_pfn
,表示spanned_pages就是结束页面减去起始页面的页面数,不管中间是否存在空洞spanned_pages - absent_pages(pages in holes)
,表示减去空洞后的页面数managed_pages = present_pages - reserved_pages
,表示这个zone中被伙伴系统管理的所有的page数目在了解区域后,再来看组成物理内存最基本的单位页,页的数组结构使用struct page表示。这个结构体定义非常的复杂,因为支持多种使用模式,所以定义了许多union
struct page {
unsigned long flags;
union {
struct address_space *mapping;
void *s_mem; /* slab first object */
atomic_t compound_mapcount; /* first tail page */
};
union {
pgoff_t index; /* Our offset within mapping. */
void *freelist; /* sl[aou]b first free object */
};
union {
unsigned counters;
struct {
union {
atomic_t _mapcount;
unsigned int active; /* SLAB */
struct { /* SLUB */
unsigned inuse:16;
unsigned objects:15;
unsigned frozen:1;
};
int units; /* SLOB */
};
atomic_t _refcount;
};
};
union {
struct list_head lru; /* Pageout list */
struct dev_pagemap *pgmap;
struct { /* slub per cpu partial pages */
struct page *next; /* Next partial slab */
int pages; /* Nr of partial slabs left */
int pobjects; /* Approximate # of objects */
};
struct rcu_head rcu_head;
struct {
unsigned long compound_head; /* If bit zero is set */
unsigned int compound_dtor;
unsigned int compound_order;
};
};
union {
unsigned long private;
struct kmem_cache *slab_cache; /* SL[AU]B: Pointer to slab */
};
......
}
第一模式
要用就使用一整页。这一整页的内存要么直接跟虚拟地址建立映射关系,这中称为匿名页(Anonymous Page)。或者关联一个文件,然后再跟虚拟地址建立映射
如果某一页使用此模式,那么union就使用以下的结构
第二种模式
仅需要分配一小块内存,并不需要一整页。为了满足这种需求,Linux系统采用了一种被称为slab allocator的技术,用于分配slab中的一小块内存。它的基本工作原理是申请一整块页,然后划分成许多小块的存储池,用复杂的队列来维护这些小块的状态(被分配了 / 被放回池子了 / 应该被回收)
slab对于队列的维护过于复杂,后来出现了一种不使用队列的分配器slub allocator,它保留的slab的用户接口,可以把它看作是slab的另一种实现
还有一种小块内存的分配器slob
如果某一页被切分为一小块一小块,那么union中就会使用以下结构
前面讲了物理内存的组织,从NUMA节点到区域到页再到小块。接下俩看物理内存的分配
对于要分配比较大的内存,例如分配页级别的,可以使用伙伴系统(Buddy System)
Linux内存管理的页大小为4K。把所有空闲的页分组为11个页块链表,每个链表管理相应大小的页块,有1、2、4、8、16、32、64、128、256、512、1024个连续页的页块。最大可以申请1024个连续的页,对应4M大小的连续内存。每个页块第一页的起始地址是该页块大小的整数倍
在 struct zone 里面有以下的定义
struct free_area free_area[MAX_ORDER];
MAX_ORDER表示2的指数
#define MAX_ORDER 11
当申请的页块大小介于free_area中两个页块大小之间时,会选取更大的一个页块大小,或者如果对应的大小没有空闲的页块,那么也会分配一个更大的页块。在得到一个更大的页块后,会将其进行拆分,然后将空闲的页块继续插入到对应页块大小的链表中
例如申请一个128个页的页块,如果没有,那么就找256,然后一直如此,直到能够找到。如果找到的是256个页的页块。那么就会将其拆分为128和128个页大小的页块,然后将一个空闲的页块添加到128对应的页块链表中
对于这些内容,可以在 alloc_pages 函数中找到定义
static inline struct page *
alloc_pages(gfp_t gfp_mask, unsigned int order)
{
return alloc_pages_current(gfp_mask, order);
}
/**
* alloc_pages_current - Allocate pages.
*
* @gfp:
* %GFP_USER user allocation,
* %GFP_KERNEL kernel allocation,
* %GFP_HIGHMEM highmem allocation,
* %GFP_FS don't call back into a file system.
* %GFP_ATOMIC don't sleep.
* @order: Power of two of allocation size in pages. 0 is a single page.
*
* Allocate a page from the kernel page pool. When not in
* interrupt context and apply the current process NUMA policy.
* Returns NULL when no page can be allocated.
*/
struct page *alloc_pages_current(gfp_t gfp, unsigned order)
{
struct mempolicy *pol = &default_policy;
struct page *page;
......
page = __alloc_pages_nodemask(gfp, order,
policy_node(gfp, pol, numa_node_id()),
policy_nodemask(gfp, pol));
......
return page;
}
接下来调用 get_page_from_freelist,这是伙伴系统的核心。它会先循环查找对应节点的zone,如果找不到,那么就看备用节点的zone
static struct page *
get_page_from_freelist(gfp_t gfp_mask, unsigned int order, int alloc_flags,
const struct alloc_context *ac)
{
......
for_next_zone_zonelist_nodemask(zone, z, ac->zonelist, ac->high_zoneidx, ac->nodemask) {
struct page *page;
......
page = rmqueue(ac->preferred_zoneref->zone, zone, order,
gfp_mask, alloc_flags, ac->migratetype);
......
}
每一个zone,都有伙伴系统维护的各种大小的队列
rmqueue 就是找到合适大小的队列,然后将页块取下来
最终会调用到 __rmqueue_smallest
static inline
struct page *__rmqueue_smallest(struct zone *zone, unsigned int order,
int migratetype)
{
unsigned int current_order;
struct free_area *area;
struct page *page;
/* Find a page of the appropriate size in the preferred list */
for (current_order = order; current_order < MAX_ORDER; ++current_order) {
area = &(zone->free_area[current_order]);
page = list_first_entry_or_null(&area->free_list[migratetype],
struct page, lru);
if (!page)
continue;
list_del(&page->lru);
rmv_page_order(page);
area->nr_free--;
expand(zone, page, order, current_order, area, migratetype);
set_pcppage_migratetype(page, migratetype);
return page;
}
return NULL;
从指定的区域中,按照当前指定的指数开始查找,如果找不到,那么就到更大的指数查找。除了将页块从链表取下,还要将多余的页面插入到合适的链表中,expand 就是完成这个工作
static inline void expand(struct zone *zone, struct page *page,
int low, int high, struct free_area *area,
int migratetype)
{
unsigned long size = 1 << high;
while (high > low) {
area--;
high--;
size >>= 1;
......
list_add(&page[size].lru, &area->free_list[migratetype]);
area->nr_free++;
set_page_order(&page[size], high);
}
}
如果有多个CPU,就会有多个NUMA节点。每个节点使用 struct pglist_data 表示,存放在一个数组中
每个节点分为多个区域,每个区域使用 struct zone 表示,也存放在一个数组中
每个区域分为多个页,空闲页存放在 struct free_area 中,使用伙伴系统进行管理和分配
每一页都是使用 struct page 表示