13. 内存管理
13.1. 引言
Linux对物理内存的描述机制有两种:UMA和NUMA。Linux把物理内存划分为三个层次来管理:存储节点(Node)、管理区(Zone)和页面 (Page)。UMA对应一致存储结构,它只需要一个Node就可以描述当前系统中的物理内存,但是NUMA的出现打破了这种平静,此时需要多个 Node,它们被统一定义为一个名为discontig_node_data的数组。为了和UMA兼容,就将描述UMA存储结构的描述符 contig_page_data放到该数组的第一个元素中。内核配置选项
CONFIG_NUMA决定了当前系统是否支持NUMA机制。此时无论UMA还 是NUMA,它们都是对应到一个类型为pg_data_t的数组中,便于统一管理。 图 71. Node Zone和Page的关系
上图描述Linux管理物理内存的三个层次之间的拓扑关系。从图中可以看出一个存储节点由pg_data_t描述,一个UMA系统中只有一个Node,而 在NUMA中则可以存在多个Node。它由CONFIG_NODES_SHIFT配置选项决定,它是CONFIG_NUMA的子选项,所以只有配置了 CONFIG_NUMA,该选项才起作用。UMA情况下,NODES_SHIFT被定义为0,MAX_NUMNODES也即为1。 include/linux/numa.h
#ifdef CONFIG_NODES_SHIFT
#define NODES_SHIFT CONFIG_NODES_SHIFT #else
#define NODES_SHIFT 0 #endif
#define MAX_NUMNODES (1 << NODES_SHIFT) 这里主要介绍UMA机制。contig_page_data被定义如下: mm/page_alloc.c
struct pglist_data __refdata contig_page_data = { .bdata = &bootmem_node_data[0] };
EXPORT_SYMBOL(contig_page_data);
struct pglist_data即是pg_data_t的原型。了解pg_data_t中的结构成员对于了解内存管理是必经之路:
enum zone_type { ZONE_DMA,
ZONE_NORMAL, ZONE_MOVABLE, ......
__MAX_NR_ZONES };
typedef struct pglist_data {
struct zone node_zones[MAX_NR_ZONES];
struct zonelist node_zonelists[MAX_ZONELISTS]; int nr_zones;
#ifdef CONFIG_FLAT_NODE_MEM_MAP /* means !SPARSEMEM */ struct page *node_mem_map;
#ifdef CONFIG_CGROUP_MEM_RES_CTLR struct page_cgroup *node_page_cgroup; #endif #endif
struct bootmem_data *bdata;
...... /* for CONFIG_MEMORY_HOTPLUG */
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; struct task_struct *kswapd; int kswapd_max_order; } pg_data_t;
?
?
?
? ? ? ? ? ? ? ? ? ?
node_zones:当前节点中包含的最大管理区数。MAX_NR_ZONES在include/linux/bounds.h定义,该文件是在编译过程中根据管理区类型定义中的__MAX_NR_ZONES变量自动生成的。
node_zonelists: 内存分配器所使用的管理区链表数组,
MAX_ZONELISTS的值在配置CONFIG_NUMA时为2,否则为1。索引为0的链表表示后援 (Fallback)链表,也即当该链表中的第一个不满足分配内存时,依次尝试链表的其他管理区。索引为1,的链表则用来针对
GFP_THISNODE的 内存申请,此时只能申请指定的该链表中的管理区。 nr_zones:指定当前节点中的管理区数,也即node_zones中实际用到的管理区数。它的取值范围为[1, MAX_NR_ZONES]。对于UMA来说,它的值为1。
node_mem_map:节点中页描述符数组首地址。 node_page_cgroup:
bdata:系统引导时用的Bootmem分配器。 node_start_pfn:节点中第一个页框的下标。
node_present_pages:节点中的页面数,不包含孔洞。 node_spanned_pages:节点中的页面总数,包含孔洞。 node_id:节点标识符,在节点数组中唯一存在。
kswapd_wait:kswapd页换出守护进程使用的等待队列。 kswapd: 指针指向kswaps内核线程的进程描述符。
kswapd_max_order:kswapd将要创建的空闲块大小取对数的值。
注意到zonelist中的_zonerefs元素,它用来实现分配器分配内存时候的管理区后援功能。MAX_ZONES_PER_ZONELIST被定 义为所有节点中包含的最多管理区的和并加上1,加1的目的是在后援链表中,可以检测是否遍历到最后一个节点了,如果是说明申请失败。
/* Maximum number of zones on a zonelist */
#define MAX_ZONES_PER_ZONELIST (MAX_NUMNODES * MAX_NR_ZONES)
struct zonelist {
struct zonelist_cache *zlcache_ptr; // NULL or &zlcache
struct zoneref _zonerefs[MAX_ZONES_PER_ZONELIST + 1]; #ifdef CONFIG_NUMA
struct zonelist_cache zlcache; // optional ... #endif };
节点中的管理区都在free_area_init_core函数中初始化。调用关系如下所示: start_kernel->setup_arch->paging_init->bootmem_init->bootmem_free_node->free_area_init_node->free_area_init_core
在理想的计算机体系结构中,一个物理页框就是一个内存存储单元,可用于任何事情:存放内核数据和用户数据,磁盘缓冲数据等。热河中磊的数据页都可以存放在 任何页框中,没有什么限制。但是,实际的计算机体系结构有硬件的制约,这制约页框可以使用的方式。尤其是Linux内核必须处理80x86体系结构的两种 硬件约束:
ISA总线的直接内存存取DMA访问控制器只能对RAM的低16MB寻址。 ? 在具有大容量RAM的现代32位计算机中,由于线性地址空间的限制,CPU不能直接访问所有的物理内存。
?
最后一种限制不仅存在于80x86,而存在于所有的体系结构中。为了应对这两种限制,Linux把每个内存节点的物理内存划分为多个(通常为3个)管理区(zone)。在80x86 UMA体系结构中的管理区为:
ZONE_DMA,包含低于16MB的内存页框。
? ZONE_NORMAL,包含高于16MB且低于896MB的内存页框。 ? ZONE_HIGHMEM,包含从896MB开始的内存页框。
?
对于ARM来说,ZONE_HIGHMEM被名为ZONE_MOVABLE的宏取代,而ZONE_DMA也不会仅限于最低的16MB,而可能对应所有的内存区域,此时只有内存节点ZONE_DMA有效,所以ZONE_DMA并不一定名副其实的用来作为DMA访问之用。
ZONE_DMA和ZONE_NORMAL区包含内存的\常规\页框,通过把它们线性的映射到线性地址的第4个 GB(0xc0000000-0xcfffffff),内核就可以直接访问。相反ZONE_HIGHMEM或者ZONE_MOVABLE区包含的内存页不 能由内核直接访问,尽管它们也线性地映射到了线性地址空间的第4个GB。每个内存管理区都有自己的描述符struct zone。它用来保存管理区的跟踪信息:内存使用统计,空闲区,锁定区等。
include/linux/mmzone.h struct zone {
/* Fields commonly accessed by the page allocator */
unsigned long pages_min, pages_low, pages_high;
unsigned long lowmem_reserve[MAX_NR_ZONES]; struct per_cpu_pageset pageset[NR_CPUS];
struct free_area free_area[MAX_ORDER];
ZONE_PADDING(_pad1_)
/* Fields commonly accessed by the page reclaim scanner */ spinlock_t lru_lock; struct {
struct list_head list; unsigned long nr_scan; } lru[NR_LRU_LISTS];
unsigned long recent_rotated[2]; unsigned long recent_scanned[2];
unsigned long pages_scanned; /* since last reclaim */ unsigned long flags; /* zone flags, see below */
/* Zone statistics */
atomic_long_t vm_stat[NR_VM_ZONE_STAT_ITEMS]; };
? ? ? ? ? ?
pages_min,记录管理区中空闲页的数目。 pages_low,回收页框使用的下届,同时也被管理区分配器作为阈值使用。 pages_high,回收页框使用的上届,同时也被管理区分配器作为阈值使用。 lowmem_reserve,指明在处理内存不足的临界情况下每个管理区必须保留的页框数目。
pageset,单一页框的特殊告诉缓存。
在申请内存时,会遇到两种情况:如果有足够的空闲页可用,请求就会被立刻满足;否则,必须回收一些内存,并且将发出请求的内核控制路径阻塞,直到有内存被 释放。不过有些内存请求不能被阻塞。这种情况发生在处理中断或在执行临界区内的代码时。在这些情况下,一条内核控制路径应使用原子内存分配请求 (GFP_ATOMIC)。原子请求从不被阻塞;如果没有足够的空闲页,则仅仅是分配失败而已。
内核为了尽可能保证一个原子内存分配请求成功,它为原子内存分配请求保留了一个页框池,只有在内存不足时才使用。保留内存的数量存放在min_free_kbytes变量中,单位为KB。
mm/page_alloc.c
int min_free_kbytes = 1024; .....
/* min_free_kbytes = sqrt(lowmem_kbytes * 16); */
lowmem_kbytes = nr_free_buffer_pages() * (PAGE_SIZE >> 10);