首发于术道经纬
Linux中的物理内存管理 [二]

Linux中的物理内存管理 [二]

Node

介绍完了page和zone,沿着自底向上的顺序,最后就是表示node的结构体pglist_data了。

typedef struct pglist_data {
     int nr_zones;
     struct zone node_zones[MAX_NR_ZONES];
     struct zonelist node_zonelists[MAX_ZONELIST];

     unsigned long node_size;
     struct page *node_mem_map;

     int node_id;
     unsigned long node_start_paddr;
     struct pglist_data *node_next;

     spinlock_t lru_lock;
     ...
} pg_data_t; 
  • nr_zones表示这个node含有多少个zones,node_zones[]则是一个包含各个zone结构体的数组。
  • node_zonelists[]包含了2个zonelist,一个是由本node的zones组成,另一个是由从本node分配不到内存时可选的备用zones组成,相当于是选择了一个退路,所以叫fallback。
enum {
	ZONELIST_FALLBACK,	/* zonelist with fallback */
#ifdef CONFIG_NUMA
	ZONELIST_NOFALLBACK,	/* zonelist without fallback (__GFP_THISNODE) */
#endif
	MAX_ZONELISTS
};

如果能从指定的目标node获得内存,则为NUMA hit;只能从备选node中获取,则为NUMA miss,可通过"/sys/devices/system/node/node*/numastat"查看。

  • node_size是指这个node含有多少个page frames,node_mem_map指向node中所有struct page构成的mem_map数组。
  • node_id是这个node的逻辑ID,也就是在NUMA系统中的编号。现在Linux中的内存分配函数区分不同的node,靠的就是这个node_id,类似于文件描述符fd。
  • node_start_paddr(在2.6内核中被换成了node_start_pfn)是该node的起始物理地址。node_next指向由多个node构成的NUMA单向链表pgdat_list中的下一个节点。如果是UMA系统,只有一个node,则node_start_pfn为0,node_next为NULL。

你看,表示node的结构体pglist_data通过"node_zones"包含了它的下一级,也就是表示zone的结构体zone_struct,zone_struct又通过"zone_pgdat"指向包含它的node。zone_struct中"zone_mem_map"指向它的下一级,也就是表示page frame的struct page,按理struct page中也应该有一个元素是指向包含它的zone,可是好像没看到对不对?

并不是在那个省略号里,而是……就在struct page的"flags"里,它用flags的高8位存储了它所属的zone和node。这是通过page flags找到page所属的node的方法:

static inline int page_to_nid(const struct page *page)
{
	struct page *p = (struct page *)page;
	return (PF_POISONED_CHECK(p)->flags >> NODES_PGSHIFT) & NODES_MASK;
}

这是通过page flags找到page所属的zone的:

static inline enum zone_type page_zonenum(const struct page *page)
{
	return (page->flags >> ZONES_PGSHIFT) & ZONES_MASK;
}

static inline struct zone *page_zone(const struct page *page)
{
	return &NODE_DATA(page_to_nid(page))->node_zones[page_zonenum(page)];
}

事实上,page flags完整的组织是这样的(在NODE和ZONE前面,还有一个SECTION,将在这篇文章中介绍):

只有低位的bits才是上文提到的那些表示page frame属性和状态的标志位。在32位系统中,只有32个bits的flags的资源已经非常紧张了。然而,为了避免在struct page中单独开辟内存空间引发反对的声浪,ZONE, NODE和SECTION这三个家伙还硬挤进了flags里,占去flags那么多宝贵的bits,以至于现在要再添加一个标志位都变得极其困难。

对,就是加小小的一个bit,也会在Linux社区遭到一众的抵制,可以说是“锱铢必较”。这里俨然已经成为了kernel王国中地价最贵的地方,就像纽约的曼哈顿一样,真正的寸土寸金。

其实挤进来的这三个家伙也不容易,寄人篱下,一共只分到了8个bits,能表达的zone, node和section的数目也是着实有限的。好在如果是64位系统的话,flags可以是64个bits,所以如果有的特性一定要新加标志位的话,那就请移步64位的世界吧,32位的世界实在是没法满足您了。

关于这个问题的讨论可参考2009年的这篇文章和2019年的这篇文章,十年过去了,面对这片兵家必争之地,大家还在削尖了脑袋往里面钻。

Node和Zone的初始化

介绍完了这些核心的数据结构,来看看它们是怎么被使用的。

free_area_init_nodes()遍历系统中所有的nodes,调用free_area_init_node()依次初始化各个node。

for_each_online_node(nid) {                                    
	free_area_init_node(nid, pgdat, NULL, 
	                    find_min_pfn_for_node(nid), NULL);
}

free_area_init_core()则遍历node内的所有zones并依次初始化。

for (j = 0; j < MAX_NR_ZONES; j++) {                  
	struct zone *zone = pgdat->node_zones + j;                  
	size = zone_spanned_pages_in_node(nid, j, zones_size);                  
	realsize = size - zone_absent_pages_in_node(nid, j, zholes_size);
}

size就是上文介绍的strut zone里的spanned_pages,realsize就是present_pages。

获取物理内存

Linux为获取page frame提供了两个基本函数:

struct page * alloc_pages(gfp_t gfp_mask, unsigned int order)
unsigned long __get_free_pages(gfp_t gfp_mask, unsigned int order) 

两者的参数是一模一样的,区别体现在返回值上,alloc_pages()返回的是指向第一个page的struct page的指针,__get_free_pages()返回的是第一个page映射后的虚拟地址。其实__get_free_pages()就是比alloc_pages()多了一个地址转换的工作,因为CPU直接使用的是虚拟地址,这样做也是为了给调用者提供更大的方便。

unsigned long __get_free_pages(gfp_t gfp_mask, unsigned int order)
{
page = alloc_pages(gfp_mask, order);          
if (page = NULL)
	return (unsigned long) page_address(page);
}

关于__get_free_pages()的返回值用unsigned long是否合适,可参考这篇文章的讨论。调用这2个函数都会获得 2^{order} 个page frames,这些pages在物理地址上是连续的。之所以要求是2的n次方,这是由底层的buddy分配机制决定的。order需大于或等于0,如果只需要一个page,可设置order为0,也可以直接调用现成的alloc_page(gfp_mask)。

那谁会放着可以一口气分配多个page frames的alloc_pages()不用,而是去调用alloc_page()一个个的分呢?那就是前面介绍到的vmalloc()啦,因为vmalloc()分配的物理内存可能是不连续的,所以不能直接使用alloc_pages()。

for (i = 0; i < area->nr_pages; i++) {
     struct page *page;
     if (node == NUMA_NO_NODE)
	 page = alloc_page(alloc_mask);
     else 
         page = alloc_pages_node(node, alloc_mask, order);
}

kmalloc(size, flags)则是按字节分配,根据内核4.19的代码(/include/linux/slab.h), 如果申请的空间小于32MB,那么kmalloc将使用slab分配器,否则将通过alloc_pages()直接使用buddy分配器。对于slub分配器,划分的界限是2个pages大小。

void *kmalloc(size_t size, gfp_t flags)
{
    if (size > KMALLOC_MAX_CACHE_SIZE)
         return kmalloc_large(size, flags);
    ...
}

不管是直接调用alloc_pages()还是使用kmalloc(),都有用到一组限定了从何处获取以及如何获取空闲物理内存的GFP(Get Free Page)标志位。关于GFP的介绍,请看下文


原创文章,转载请注明出处。

编辑于 08-01

文章被以下专栏收录