Linux 内存管理 III

DRAM作为Memory Hierarchy 中除CPU内Cache外最快的一级,对于内核性能来说是稀缺资源,所以Linux内核对于物理内存相关的数据结构设计地十分精巧,比如常见的用于分配内存的buddy、Slab分配器、用于规整内存的KSM、用于快速回收内存的RMAP等等,无论哪种精巧的技术,都离不开本文需要讨论的几个与物理内存管理的基础类:node、zone和page。

node

多CPU的硬件架构可以分为两种:UMA和NUMA,前者由多个CPU共享一个内存控制器,这样主板上的所有内存对于所有的CPU都是一样的,缺点是内存控制器容易称为系统瓶颈。NUMA架构中每个CPU使用自己的DRAM Controller,即每个CPU都有接在自己本地的内存控制下的LOCAL内存区间和接在其他CPU内存控制器下的REMOTE内存,显然,对于任何一个CPU而言,访问LOCAL内核要比访问REMOTE的内存要快很多,这样的设计就造成了地址空间中一个物理地址使用不同的CPU的访问不同,即物理地址空间的不均匀,NUMA架构通常可以更好的发挥硬件性能,是当下服务器的主流架构,缺点是系统对内核的内存管理提出了新的挑战。

在NUMA架构中,Linux内核使用”node”这一概念来描述一个CPU节点,这里CPU和NUMA中的CPU一样,都指的是一个CPU芯片,而不是多核CPU中的一个物理核或逻辑核。

通过上图可以看出,”node”是Linux物理内存管理的最高级别。系统中使用全局变量struct pglist_data *node_data[MAX_NUMNODES] __read_mostly来管理系统中所有的node,并进而管理系统中所有的内存。

//include/linux/mmzone.h
typedef struct pglist_data {
        struct zone node_zones[MAX_NR_ZONES];
        struct zonelist node_zonelists[MAX_ZONELISTS];
        int nr_zones;
        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;
        wait_queue_head_t kswapd_wait;
        wait_queue_head_t pfmemalloc_wait;
        struct task_struct *kswapd;     /* Protected by
                                           mem_hotplug_begin/end() */
        int kswapd_max_order;
        enum zone_type classzone_idx;                                     
} pg_data_t;

从中可以看出,一个pdlist_data对象描述了该node的起始pfn以及pfn范围、其管理的zone以及相应的kswapd线程(即kswapd是一个node一个)。

node中除了即将讨论的struct zone[],还有一个[struct zone_list],该域用于分配页框时,查找从哪个zone分配,保存了zone的优先级顺序,即使用zonelist[]来封装跨zone的分配policy,比ZONE_HIGHMEM内存不足,接下来是从ZONE_NORMAL中分配还是拒不分配。其组织结构如下:

zone

是对node中的物理内存区间按照功能需求进行进一步的划分。典型的32bit 系统中的Zone划分及其使用方式示意图如下:

Linux启动时会关闭MMU,得以直接访问物理地址空间,以便进行物理地址空间管理相关结构的初始化,图示为对物理地址空间进行分Zone,从低地址到高地址分别为,ZONE_DMA,ZONE_NORMAL,ZONE_HIGHMEM。

ZONE_DMA的出现主要是因为部分DMA引擎的寻址范围有限,且必须地址连续,所以低地址区间可以为与DMA引擎有类似约束的硬件使用。当然,现在很多DMA引擎已经支持32bit甚至64bit寻址,也有一些配备了IOMMU的DMA引擎,这些技术令其传输buffer不必在ZONE_DMA中分配连续内存。所以,ZONE_DMA本质上是根据主板需求确认的。显然,ZONE_DMA需要归内核管理,被线性映射到的内核虚拟地址空间。
ZONE_NORMAL和ZONE_DMA一样,也是内核虚拟地址空间中线性映射的部分,在开机的时候就已经映射好了(注意,映射是指页表项的建立,分配是指页所属的确定,二者不是一回事),和ZONE_DMA统称为LOW MEM线性映射的好处很明显——快,因为虚拟地址到物理地址的转换不需要查询页表,所以线性映射区的访问速度比需要查询页表的ZONE_HIGHMEM更快,因此,内核将频繁使用的数据如kernel代码,GDT,IDT,PGD,mem_map数组等存放在ZONE_NORMAL中。在32bit系统中,低896MB的物理地址空间都是线性映射区,对于内核来说,这些内存足够使用。
ZONE_HIGHMEM即高端内存,简单的模型中,高于896MB的物理地址空间都是高端内存,对应到虚拟地址空间就是3G~3G+896MB,位于内核空间。这个区域的特点是在使用的时候才建立映射,比如在32bit的系统中,寻址能力最大只有4GB,如果主存大到使物理地址空间超过了4G,那么就会出现”虚拟地址空间<物理地址空间”的情况,无法做到虚拟地址和物理地址的”一一映射”(注意,不是”线性映射”),为了充分利用主存,内核使用高端内存机制,即在高端内存中,同一个虚拟地址空间可以”分时地”映射到不同的物理地址空间,在使用完毕后再去映射,以此充分利用内存。典型的应用有虚拟地址空间的用户态部分、内核使用vmalloc分配的内存、使用ioremap将物理地址空间中的寄存器区间映射到vmalloc映射区等场景。

对于64bit的系统来说,虚拟地址空间通常足够容纳所有的主存(即便是48bit寻址,也已经有256T的内存地址空间),所以也就没有了ZONE_HIGHMEM,此外,处于兼容性的考虑,新增了ZONE_DMA32,这个DMA32对于32bit系统中的ZONE_DMA。我们可以通过读取**“/proc/zoneinfo”**来获取内存zone相关的信息

$cat /proc/zoneinfo |more
Node 0, zone      DMA
...
Node 0, zone    DMA32
...
Node 0, zone   Normal
...

讨论zone往往离不开两个关键概念: buddy与watermark。

buddy in zone

常说的buddy算法的作用对象就是一个zone,,zone中的free_area即是为buddy配备的,对于buddy管理的每一个order,都对应一个free_area[order],个中关系可以使用下图表示:

由于buddy是最底层的管理物理内存的算法,所以基于其上的kmalloc、vmalloc、malloc都随之按照zone来分配。需要注意的是,zone和node、page一样,都是针对物理地址空间中物理内存区间的描述,不包括物理地址空间的IO空间(如果有的话)

watermark in zone

除了buddy,zone中另一个重要的成员就是watermark了,这个水位是Linux内存回收的执行依据。内存回收是指通过将主存中的不活跃的Anonymous Page内容移到Backing Device,将其分配给更活跃的任务使用,在 windows中,这个Backing Device就是虚拟内存,在Linux中,就是Swap 分区/文件。Swap根据系统配置、基于LRU原则回收Mapped Page以及Anonymous Page,对于Mapped Page,首先检查其是否Dirty,如果是,则将其writeback到backed file中,再将页面回收; 对于Anonymous Page,就将其Swap Out到Swap分区/文件,再将页面回收。

zoneinfo提供了大量的关于Zone的信息,这里主要关心的是watermark。如果从时间维度考察Swap,那么这些watermark就是Swap回收的时机。Swap使用二级机制完成内存回收:kswapd+direct reclaim。那何时启动回收呢?内核为每个Zone都设置了三个watermark:high、low、min。当某个Zone的内存可用量达到low时,就会启动kswapd线程,该线程在后台回收内存,直到该Zone的可用内存达到high,在此期间,应用程序的内存申请请求不会被拒绝。kswapd在大部分情况下都可以及时回收内存,但当应用申请内存的速度大于kswapd回收内存的速度,就会导致这个Zone的watermark达到min,此时,内核会启动Direct Reclaim机制,阻塞应用的申请内存请求,直到回收到了足够的可用内存。

$cat /proc/zoneinfo |more
Node 0, zone      DMA
  pages free     3965
        min      13
        low      16
...
Node 0, zone    DMA32
  pages free     20452
        min      1535
        low      1918
        high     2302
...
Node 0, zone   Normal
  pages free     53103
        min      15347
        low      19183
...

min_free_kbytes
表示预留内存,即便在一个Zone中的内存已经在min以下,也能从中分配内存,这部分内存留给系统中至关重要的任务,比如回收内存的代码也需要分配内存。一个Zone中的3个watermark也是基于这个文件的值计算出的。缺省情况下,内核会根据系统配置的内存计算该值,如果不满意,也可以自己配置

min_free_kbytes = sqrt(lowmem_kbytes 16) = 4 sqrt(lowmem_kbytes)

内核通过这个值计算watermark[min]、watermark[low]、watermark[high]的值

watermark[min] =per_zone_min_free_pages
watermark[high] – watermark[low] = watermark[low] – watermark[min] = per_zone_min_free_pages * 1/4

lowmem_reserve_ratio
min_free_kbytes负责在Zone内保留足够的内存,lowmem_reserve_ratio负责确保一个Zone不会因另一个Zone内存不足而被分配殆尽。

$cat /proc/sys/vm/lowmem_reserve_ratio 
256	256	32	1

这四个数值用于计算每个Zone的protection数组,protection[]通过参与watermark的计算来影响能否从当前Zone分配内存页,反映了在其他Zone”有难”时,当前Zone的”好心”程度(aggressive approach)从/proc/zoneinfo里可以查看:

$cat /proc/zoneinfo |more
Node 0, zone      DMA
        protection: (0, 1850, 19946, 19946, 19946)
Node 0, zone    DMA32
        protection: (0, 0, 18096, 18096, 18096)
Node 0, zone   Normal
        protection: (0, 0, 0, 0, 0)

以ZONE_DMA32为例,protection: (0, 0, 18096, 18096, 18096),protection[0]=0表示该Zone为index = 0的Zone(即ZONE_DMA)的watermark加成0个Page,而为index = 2的Zone(即ZONE_NORMAL)的watermark加成19946个Page。显然,watermark被加的越高,基于Swap机制,从当前Zone划分内存的难度越大。当用户程序想要从ZONE_NORMAL分配1036个Page,设ZONE_NORMAL当前的page_free是1,watermark[min]+protection[2] = 4 + 0= 4 > page_free,所以ZONE_NORMAL无法满足需求,内核就会转而向ZONE_DMA32求救,经过计算,ZONE_DMA32当前的page_free = 1000, watermark[min] + protection[2] = 4 + 18096 = 18100 > page_free,所以也不满足水位条件,拒绝分配。可以看出,正是由于ZONE_DMA32通过protection[2]对来自ZONE_NORMAL的分配请求的watermark加成,才阻止了内核从ZONE_DMA32分配原本应该从ZONE_NORMAL分配的内存。同样的情况如果本身就发生在ZONE_DMA中,就会发现,watermark[high] + protection[0] = 6 +0 < page_free,允许分配。

min_unmapped_ratio
仅用于NUMA架构,当一个Zone中的符合当前zone_reclaim_mode下reclaim的条件的page数量达到该值时,才会触发回收。

page-cluster
用于控制从swap分区/文件执行Swap In的时候,一次读取的页面数(预读),用对数表示,即一次读取2^page-cluster的大小,这里的连续指的是Swap空间的连续,而不是内存地址空间的连续。

swappiness
反映使用swap空间积极性。考虑到Swap空间仅作为Anonymous Page的Backing Device,所以可以认为是对Anonymous Page的回收”倾向性”。swappiness = 0表示建议内核在回收页面时尽可能少的回收Anonymous Page,这意味着更多回收Mapped Page,由于Mapped Page回收时可能伴随着Dirty Flush,会增大系统IO压力;swappiness = 100表示建议在回收页面时内核尽可能多的回收Anonymous Page。缺省情况下,swappiness = 60。
  需要注意的是,swappiness 仅仅是”建议”,内核在回收页面的时候,会考虑包括swappiness在内的很多因素。

zone_reclaim_mode

当前Zone内存不足时,是否尝试从下一个Zone中获取空闲内存,该值可以配置
0表示不足时不回收本Zone的内存,从其他Zone中申请
1表示开启reclaim,内存不足时会从本Zone回收。
2表示开启reclaim,内存不足时会从本Zone回收,回收Mapped Pages
4表示开启reclaim,内存不足时会从本Zone回收,以Swap形式回收

page

我们知道,Linux对于整个物理地址空间都使用”页框号”来进行分页,也就是pfn,但是对于使用IO映射的物理地址空间来说,物理地址空间不止有物理内存区间,还有IO区间,所以,并不是说所有的pfn都对应着物理内存区间。

但是对于物理内存空间来说,无论是在bootm阶段还是buddy阶段,内核需要使用的每一个物理内存页框都会有有相应的struct page对象。在buddy阶段,系统在进行内存初始化的时候就会根据系统中物理内存的大小、起止地址、系统配置来分配相应数量的page对象。系统中所有的page结构都通过一个全局数组struct page mem_map[]来管理,使用数组,就可以快速的完成pfn->page对象的转换。

init

Linux在内核启动的时候会首先建立一套简单的内存管理机制——bootm,这套机制主要使用位图来管理内存页,在内核准备好后,就基于bootm构造真正工作时使用的buddy和slab内存分配器。

start_kernel()							//init/main.c
	setup_arch()						//arch/arm/kernel/setup.c
		setup_processor()	
		setup_machine_fdt()				//arch/arm/kernel/devtree.c
			early_init_dt_verify()
			of_flat_dt_match_machine()
			early_init_dt_scan_nodes()	//drivers/of/fdt.c
				early_init_dt_scan_memory()
				of_get_flat_dt_prop()
				of_get_flat_dt_prop()
				dt_mem_next_cell()
				early_init_dt_add_memory_arch()
		init struct mm_struct init_mm
		parse_early_param()
		setup_dma_zone()
		paging_init()					//arch/arm/mm/mmu.c
			prepare_page_table()
			map_lowmem()
				init struct map_desc map
				create_mapping()
			dma_contiguous_remap()
			devicemaps_init()
			bootmem_init()				//arch/arm/mm/init.c
				find_limits()
				arm_memory_present()
				sparse_init()
				zone_sizes_init()	
					free_area_init_node()		//mm/page_alloc.c
						alloc_node_mem_map()
						free_area_init_core()
							zone_spanned_pages_in_node()
							calc_memmap_size()
							init struct zone zone
							mod_zone_page_state()
							set_pageblock_order()
							init_currently_empty_zone()
							memmap_init()
			__flush_dcache_page()		
	build_all_zonelists(NULL, NULL);
	page_alloc_init();
	mm_init();


初始化zone实例的核心函数是free_area_init_node(),也是在这个函数中初始化了物理内存中重要的全局变量:mem_map,这个全局变量的类型是struct page*,事实上,系统中所有的struct page对象(它们一起描述了系统所有可用的物理内存)都放在一个struct page[]中,而mem_map就是这个数组的首地址。此处有两点需要解释,其一,在分配mem_map数组的时候,buddy、slab等机制还未建立,所以alloc_node_mem_map()是基于bootm机制分配这块内存的;其二,由于buddy算法管理的物理页框号必须是2的整数次幂,因此,如果硬件接线不能满足这个要求,就会导致部分物理内存不可用,这点从alloc_node_mem_map()的实现即可看出:

4954 static void __init_refok alloc_node_mem_map(struct pglist_data *pgdat){
4962         if (!pgdat->node_mem_map) {
4963                 unsigned long size, start, end;
4964                 struct page *map;  
4971                 start = pgdat->node_start_pfn & ~(MAX_ORDER_NR_PAGES - 1);
4972                 end = pgdat_end_pfn(pgdat);
4973                 end = ALIGN(end, MAX_ORDER_NR_PAGES);
4974                 size =  (end - start) * sizeof(struct page);
4975                 map = alloc_remap(pgdat->node_id, size);
4976                 if (!map)
4977                         map = memblock_virt_alloc_node_nopanic(size,
4978                                                                pgdat->node_id);
4979                 pgdat->node_mem_map = map + (pgdat->node_start_pfn - start);
4980         }
4985         if (pgdat == NODE_DATA(0)) {
4986                 mem_map = NODE_DATA(0)->node_mem_map;
4991         }
4994 }

–4967–>对node_start_pfn向下对齐获取start
–4973–>对pgdat_end_pfn()向上对齐获取end
–4974–>根据start、end计算描述这些页框所需的struct page[]的大小
–4975–>基于bootm分配struct page[]所需内存
–4979–>由于之前的对齐,会导致start<=实际物理内存起始地址 end >=实际物理内存结束地址,此处将start处的偏差消除,以便是mem_map的第一个page对象描述的就是可用的物理页框。

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.

Discover more from sketch2sky

Subscribe now to keep reading and get access to the full archive.

Continue reading