有人的地方就有江湖。要介绍内存管理这个江湖,首先还得从这里面的主要人物讲起。
在NUMA结构中,物理内存首先被分成若干nodes。每一个node进一步被分成若干zones。每一个zone又关联了一个描述page frames的数组,该数组包含了属于该zone的所有page frame的描述符。
不难看出,在这个江湖里主要有三位重要人物:nodes, zones 和 page frames。这三者的关系和地位大体可以用下图来描述(该图取自“Professional Linux Kernel Architecture”):
下面我们对这三位人物逐一介绍。请注意这里主要描述了我们关心的内容。
1. Node
在Kernel中,node是由结构体 pg_data_t 来描述的。
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 struct page *node_mem_map; #endif 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: 每个node被分为若干zones,这些zones的信息就保存在这个数组中。
node_zonelists: 当我们要在某一个指定的zone中分配内存,而该zone中又没有足够多的空闲内存时,怎么办?正所谓狡兔三窟,我们得给自己留点后路,node_zonelists正是这种情况下的后路。它指定了所有的备选的zones。当然这些备选zones也是有优先顺序的,毕竟只有小三也不能满足需求时,才会再去找小四。
nr_zones: 顾名思义,就是该node中zone的个数。
node_mem_map: 属于该node的所有page frame的数组。page frame就是最基层的士兵,无论你是一个团(zone),还是一个师(node),说到底都是有一个个士兵组成的。
node_start_pfn: 注意所有node中的page frame是统一顺序编号的,并不是说这个师里有个士兵9527,那个师里还有个士兵9527。9527在整个部队中只有一个。而node_start_pfn就是该node中第一个page frame的编号。
node_present_pages: 该node中page frames的个数。
node_spanned_pages:这个也是该node中page frames的个数,不过把memory holes也包含进来了,所以node_spanned_pages一般要比node_present_pages大。所谓的memory hole,就是内存空间中有些区域可能会被预留用来做I/O映射,或是被BIOS预留,这样地址空间就有了一个个窟窿。啊,忽然想起了一个朋友的真实故事。。。当年他加入某NB团队做当时无比高大的分布式内存管理,地址空间address space中间有memory hole。于是他们team的一位大美女便给链表命名为as_hole。当时没人有异议,只是每次code review,大家看到这个变量的时候都会呐呐地绕开,直到后来。。。(我这么直接引用别人的故事,还是真实的故事,是不是侵权了啊?请版权所有者联系我,我把这段给删掉。)
node_id: 该node的编号,0 1 2 ...
kswapd_wait, kswapd, kswapd_max_order: 这三个成员变量用于swapping 机制,现在先略去不讲。
2. Memory Zones
这个人物比较复杂,在这里我们先有个第一印象,混个脸熟。以后会经常打交道,再慢慢了解。
在Kernel中,memory zone是由结构体 struct zone 来描述的。
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]; /* * free areas of different sizes */ spinlock_t lock; struct free_area free_area[MAX_ORDER]; unsigned long *pageblock_flags; ZONE_PADDING(_pad1_) /* Fields commonly accessed by the page reclaim scanner */ spinlock_t lru_lock; struct list_head active_list; struct list_head inactive_list; unsigned long nr_scan_active; unsigned long nr_scan_inactive; 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]; int prev_priority; ZONE_PADDING(_pad2_) /* Rarely used or read-mostly fields */ wait_queue_head_t * wait_table; unsigned long wait_table_hash_nr_entries; unsigned long wait_table_bits; /* * Discontig memory support fields. */ struct pglist_data *zone_pgdat; unsigned long zone_start_pfn; unsigned long spanned_pages; /* total size, including holes */ unsigned long present_pages; /* amount of memory (excluding holes) */ /* * rarely used fields: */ const char *name; } ____cacheline_internodealigned_in_smp;
这个结构体的内容比较多。其中有两个ZONE_PADDING,把该结构体分成了三部分。
为什么会有ZONE_PADDING这个东东在这里呢?事情是这样的。在SMP系统中,多个CPU经常会同时访问同一个zone结构体。这样lock就必不可少了。该结构体主要有两个lock,zone->lock和zone->lru_lock。为了能让这两个lock在不同的CPU cache line中,只好忍忍心在他俩之间隔上一条银河了。
第一部分的内容主要由page allocator使用,用来申请分配内存页。
pages_min, pages_low, pages_high: 这哥仨被称为"watermarks",在申请物理内存以及内存回收过程中都会起到作用。
如果空闲内存页数大于pages_high,那么这个zone就被认为是很空闲的。
如果空闲内存页数小于pages_low, 需要内存回收了。
如果空闲内存页数小于pages_min, 内存回收亚历山大,危险危险。
lowmem_reserve:指定了每一个zone必须预留多少内存页。这些预留的内存页主要用来处理low-on-memory的紧急情况。
pageset: 这是一个per-CPU的内存页缓存。内核先提前申请好一些内存页,放在这个缓存中。当需要申请单页内存时,就可以直接在这个缓存中拿了。这是Kernel的一个惯用伎俩,以后还会见到更多类似的例子。
free_area: 这个就是大名鼎鼎的buddy system所在的地方了。后面会有专门的博文来讲buddy system。在这里只要知道,这个成员变量用来存放该zone中的空闲内存页。
pageblock_flags: 这个也是用于buddy system,用在防止碎片的机制中(anti-fragmentation)。
ZONE_PADDING(_pad1_) 华丽的分割线
第二部分的内容主要由内存回收机制使用。所有的内存页按其活跃情况被分类:active OR inactive。不同类型的内存页会被放在不同的LRU链表上。当要进行内存回收时,这个分类就很重要了。那些inactive的内存页会优先被回收掉。正应了鲁迅的那句话:不在沉默中爆发,就在沉默中灭亡。
active_list, inactive_list: 活跃/不活跃的内存页的链表。
nr_scan_active, nr_scan_inactive: 当进行内存回收时,需要扫描多少活跃/不活跃的内存页。
flags:描述了该zone的状态。
typedef enum { ZONE_ALL_UNRECLAIMABLE, /* all pages pinned */ ZONE_RECLAIM_LOCKED, /* prevents concurrent reclaim */ ZONE_OOM_LOCKED, /* zone is in OOM killer zonelist */ } zone_flags_t;
vm_stat: 保存了关于该zone的各种统计信息,例如NR_FREE_PAGES, NR_FILE_PAGES等。这些信息会随时随地更新。
prev_priority:内存回收时用来保存上次扫描该zone时的权值。(是不是还是搞不清楚这是干啥的?没关系,当我们讲到内存回收的细节时,还会再回头看这些变量的。)
ZONE_PADDING(_pad2_) 华丽的分割线
第三部分主要是很少用到的,或是主要用来读的内容。
wait_table, wait_table_hash_nr_entries, wait_table_bits: 这三位师兄弟实现了一个等待队列。当某一个内存页暂时不可用时,想要使用该内存页的进程便会等待在该队列中。
zone_pgdat: 该zone所属的node。这个是我用来寻找组织的,很重要的。
zone_start_pfn:该zone中第一个内存页的编号。
spanned_pages:该zone中内存页的个数,包含memory holes。 《==== 是不是似曾相识啊
present_pages: 该zone中真正能用的内存页的个数。
name: 出来混江湖,总得有个名号啊。
(TO BE CONTINUED...)
本文出自 “内核部落格” 博客,转载请与作者联系!
原文:http://richardguo.blog.51cto.com/9343720/1663328