给出了下述内存划分的图示(该情形多少简化了一些,在我们详细讲解数据结构时,读者可以看到这一点)。
首先,内存划分为结点。每个结点关联到系统中的一个处理器,在内核中表示为pg_data_t的实例(稍后定义该数据结构)。各个结点又划分为内存域,是内存的进一步细分。还有一个高端内存区域无法直接映射。一个结点最多由3个内存域组成。内核引入了下列常量来区分它们。
NUMA系统中的内存划分
内核引入了下列常量来枚举系统中的所有内存域:.
<mmzone.h>
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
};
ZONE_NORMAL标记了可直接映射到内核段的普通内存域。这是在所有体系结构上保证都会存在的唯一内存域,但无法保证该地址范围对应了实际的物理内存。例如,如果AMD64系统有2 GiB内存,那么所有内存都属于ZONE_DMA32范围,而ZONE_NORMAL则为空。
ZONE_HIGHMEM标记了超出内核段的物理内存。(注意,ZONE_HIGHMEM的范围是896M~结束,该区域即为高端内存,内核不能直接使用。)
出于性能考虑,在为进程分配内存时,内核总是试图在当前运行的CPU相关联的NUMA结点上进行。但这并不总是可行的,例如,该结点的内存可能已经用尽。对此类情况,每个结点都提供了一个备用列表(借助于struct zonelist)。该列表包含了其他结点(和相关的内存域),可用于代替当前结点分配内存。列表项的位置越靠后,就越不适合分配。
数据结构
pg_data_t是用于表示结点的基本元素,定义如下:
<mmzone.h>
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;
struct bootmem_data * bdata;
unsigned long node_start_pfn;
unsigned long node_present_pages; /* 物理内存页的总数 */
unsigned long node_spanned_pages; /* 物理内存页的总长度,包含洞在内 */
int node_id;
struct pglist_data * pgdat_next;
wait_queue_head_t kswapd_wait;
struct task_struct * kswapd;
int kswapd_max_order;
} pg_data_t;
node_zones是一个数组,包含了结点中各内存域的数据结构。
node_zonelists指定了备用结点及其内存域的列表,以便在当前结点没有可用空间时,在备用结点分配内存。
结点中不同内存域的数目保存在nr_zones。
node_mem_map是指向page实例数组的指针,用于描述结点的所有物理内存页。它包含了结点中所有内存域的页。
在系统启动期间,内存管理子系统初始化之前,内核也需要使用内存(另外,还必须保留部分内存用于初始化内存管理子系统)。为解决这个问题,内核使用了3.4.3节讲解的自举内存分
配器(boot memory allocator)。bdata指向自举内存分配器数据结构的实例。
node_start_pfn是该NUMA结点第一个页帧的逻辑编号。系统中所有结点的页帧是依次编号的,每个页帧的号码都是全局唯一的(不只是结点内唯一)。node_start_pfn在UMA系统中总是0,因为其中只有一个结点,因此其第一个页帧编号总是0。node_present_pages指定了结点中页帧的数目,而node_spanned_pages则给出了该结点以页帧为单位计算的长度。二者的值不一定相同,因为结点中可能有一些空洞,并不对应真正的页帧。
node_id是全局结点ID。系统中的NUMA结点都从0开始编号。
结点状态管理
如果系统中结点多于一个,内核会维护一个位图,用以提供各个结点的状态信息。状态是用位掩码指定的,可使用下列值:
内核使用zone结构来描述内存域。其定义如下:
<nodemask.h>
enum node_states {
N_POSSIBLE, /* 结点在某个时候可能变为联机 */
N_ONLINE, /* 结点是联机的 */
N_NORMAL_MEMORY, /* 结点有普通内存域 */
#ifdef CONFIG_HIGHMEM
N_HIGH_MEMORY, /* 结点有普通或高端内存域 */
#else
N_HIGH_MEMORY = N_NORMAL_MEMORY,
#endif
N_CPU, /* 结点有一个或多个CPU */
NR_NODE_STATES
};
状态N_POSSIBLE、N_ONLINE和N_CPU用于CPU和内存的热插拔。对内存管理有必要的标志是N_HIGH_MEMORY和N_NORMAL_MEMORY。如果结点有普通或高端内存则使用N_HIGH_MEMORY,仅当结点没有高端内存才设置N_NORMAL_MEMORY。
<mmzone.h>
struct zone {
/*通常由页分配器访问的字段 */
unsigned long pages_min, pages_low, pages_high;
unsigned long lowmem_reserve[MAX_NR_ZONES];
struct per_cpu_pageset pageset[NR_CPUS];
/*
* 不同长度的空闲区域
*/
spinlock_t lock;
struct free_area free_area[MAX_ORDER];
ZONE_PADDING(_pad1_)
/* 通常由页面收回扫描程序访问的字段 */
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; /* 上一次回收以来扫描过的页 */
unsigned long flags; /* 内存域标志,见下文 */
/* 内存域统计量 */
atomic_long_t vm_stat[NR_VM_ZONE_STAT_ITEMS];
int prev_priority;
ZONE_PADDING(_pad2_)
/* 很少使用或大多数情况下只读的字段 */
wait_queue_head_t* wait_table;
unsigned long wait_table_hash_nr_entries;
unsigned long wait_table_bits;
/* 支持不连续内存模型的字段。 */
struct pglist_data *zone_pgdat;
unsigned long zone_start_pfn;
unsigned long spanned_pages; /* 总长度,包含空洞 */
unsigned long present_pages; /* 内存数量(除去空洞) */
/*
* 很少使用的字段:
*/
char *name;
} ____cacheline_maxaligned_in_smp;
该结构比较特殊的方面是它由ZONE_PADDING分隔为几个部分。这是因为对zone结构的访问非常频繁。内核使用ZONE_PADDING宏生成“填充”字段添加到结构中,以确保每个自旋锁都处于自身的
缓存行中。还使用了编译器关键字__cacheline_maxaligned_in_smp,用以实现最优的高速缓存对齐方式。
pages_min、pages_high、pages_low是页换出时使用的“水印”。如果内存不足,内核可以将页写到硬盘。这3个成员会影响交换守护进程的行为。
如果空闲页多于pages_high,则内存域的状态是理想的。
如果空闲页的数目低于pages_low,则内核开始将页换出到硬盘。
如果空闲页的数目低于pages_min,那么页回收工作的压力就比较大,因为内存域中急需空闲页。
lowmem_reserve数组分别为各种内存域指定了若干页,用于一些无论如何都不能失败的关键
性内存分配。各个内存域的份额根据重要性确定。
pageset是一个数组,用于实现每个CPU的热/冷页帧列表。内核使用这些列表来保存可用于
满足实现的“新鲜”页。但冷热页帧对应的高速缓存状态不同:有些页帧也很可能仍然在高
速缓存中,因此可以快速访问,故称之为热的;未缓存的页帧与此相对,故称之为冷的。
free_area是同名数据结构的数组,用于实现伙伴系统。每个数组元素都表示某种固定长度的一些连续内存区。对于包含在每个区域中的空闲内存页的管理,free_area是一个起点。
第二部分涉及的结构成员,用来根据活动情况对内存域中使用的页进行编目。如果页访问频
繁,则内核认为它是活动的;而不活动页则显然相反。在需要换出页时,这种区别是很重要
的。如果可能的话,频繁使用的页应该保持不动,而多余的不活动页则可以换出而没有什么
损害。
具体涉及的结构成员如下:
active_list是活动页的集合,而inactive_list则不活动页的集合(page实例)。
nr_scan_active和nr_scan_inactive指定在回收内存时需要扫描的活动和不活动页的数目。
pages_scanned指定了上次换出一页以来,有多少页未能成功扫描。
flags描述内存域的当前状态。允许使用下列标志:
<mmzone.h>
typedef enum {
ZONE_ALL_UNRECLAIMABLE, /* 所有的页都已经“钉”住 */
ZONE_RECLAIM_LOCKED, /* 防止并发回收 */
ZONE_OOM_LOCKED,
/* 内存域即可被回收 */
} zone_flags_t;
zone_start_pfn是内存域第一个页帧的索引。
冷热页
struct zone的pageset成员用于实现冷热分配器(hot-n-cold allocator)。内核说页是热的,意味着页已经加载到CPU高速缓存,与在内存中的页相比,其数据能够更快地访问。相反,冷页则不在高
速缓存中。在多处理器系统上每个CPU都有一个或多个高速缓存,各个CPU的管理必须是独立的。
pageset是一个数组,其容量与系统能够容纳的CPU数目的最大值相同。
<mmzone.h>
struct zone {
...
struct per_cpu_pageset pageset[NR_CPUS];
...
};
NR_CPUS是一个可以在编译时间配置的宏常数。在单处理器系统上其值总是1,针对SMP系统编
译的内核中,其值可能在2和32(在64位系统上是64)之间。该值并不是系统中实际存在的CPU数目,而是内核支持的CPU的最大数目。
数组元素的类型为per_cpu_pageset,定义如下:
该结构由一个带有两个数组项的数组构成,第一项管理热页,第二项管理冷页。
有用的数据保存在per_cpu_pages中。①
①(内核版本2.6.25在本书撰写时仍然在开发中,该版本会将分别管理冷页和热页的两个列表合并为一个。热页放置在列表头部,而冷页置于列表尾部。通过测量发现,与一个列表相比,两个独立的列表不会带来实质性的好处,因此引入了该修改)
页帧
C语言的联合很适合于该问题,尽管它未能增加struct page的清晰程度。考虑一个例子:一个物理内存页能够通过多个地方的不同页表映射到虚拟地址空间,内核想要跟踪有多少地方映射了该
页。为此,struct page中有一个计数器用于计算映射的数目。如果一页用于slub分配器(将整页细分为更小部分的一种方法,请参见3.6.1节),那么可以确保只有内核会使用该页,而不会有其他地方
使用,因此映射计数信息就是多余的。因此内核可以重新解释该字段,用来表示该页被细分为多少个小的内存对象使用。在数据结构定义中,这种双重解释如下所示:
<mm_types.h>
struct page {
...
union {
atomic_t _mapcount; /* 内存管理子系统中映射的页表项计数,
* 用于表示页是否已经映射,还用于限制逆向映射搜索。
*/
unsigned int inuse; /* 用于SLUB分配器:对象的数目 */
};
...
}
<mm.h>
struct page {
unsigned long flags; /* 原子标志,有些情况下会异步更新 */
atomic_t _count; /* 使用计数,见下文。 */
union {
atomic_t _mapcount; /* 内存管理子系统中映射的页表项计数,
* 用于表示页是否已经映射,还用于限制逆向映射搜索。
*/
unsigned int inuse; /* 用于SLUB分配器:对象的数目 */
};
union {
struct {
unsigned long private; /* 由映射私有,不透明数据:
* 如果设置了PagePrivate,通常用于buffer_heads;
* 如果设置了PageSwapCache,则用于swp_entry_t;
* 如果设置了PG_buddy,则用于表示伙伴系统中的阶。
*/
struct address_space *mapping; /* 如果最低位为0,则指向inode
* address_space,或为NULL。
* 如果页映射为匿名内存,最低位置位,
* 而且该指针指向anon_vma对象:
* 参见下文的PAGE_MAPPING_ANON。
*/
};
...
struct kmem_cache *slab; /* 用于SLUB分配器:指向slab的指针 */
struct page *first_page; /* 用于复合页的尾页,指向首页 */
};
union {
pgoff_t index; /* 在映射内的偏移量 */
void *freelist; /* SLUB: freelist req. slab lock */
};
struct list_head lru; /* 换出页列表,例如由zone->lru_lock保护的active_list!
*/
#if defined(WAT_PAGE_VIRTUAL)
void *virtual; /* 内核虚拟地址(如果没有映射则为NULL,即高端内存) */
#endif /* WANT_PAGE_VIRTUAL */
};
_count是一个使用计数,表示内核中引用该页的次数。在其值到达0时,内核就知道page实例当前不使用,因此可以删除。如果其值大于0,该实例决不会从内存删除。如果读者不熟悉引用计数器,可以在附录C查阅更详细的资料。
_mapcount表示在页表中有多少项指向该页。
lru是一个表头,用于在各种链表上维护该页,以便将页按不同类别分组,最重要的类别是活动和不活动页。
内核可以将多个毗连的页合并为较大的复合页(compound page)。分组中的第一个页称作首页(head page),而所有其余各页叫做尾页(tail page)。所有尾页对应的page实例中,都将first_page设置为指向首页。
mapping指定了页帧所在的地址空间。index是页帧在映射内部的偏移量。地址空间是一个非常一般的概念,例如,可以用在向内存读取文件时。地址空间用于将文件的内容(数据)与装载数据的内存区关联起来。通过一个小技巧,①mapping不仅能够保存一个指针,而且还能包含一些额外的信息,用于判断页是否属于未关联到地址空间的某个匿名内存区。如果将mapping置为1,则该指针并不指向address_space的实例,而是指向另一个数据结构(anon_vma),该结构对实现匿名页的逆向映射很重要,该结构在5.内存映射讨论。对该指针的双重使用是可能的,因为address_space实例总是对齐到sizeof(long)。因此在Linux支持的所有计算机上,指向该实例的指针最低位总是0。该指针如果指向address_space实例,则可以直接使用。如果使用了技巧将最低位设置为1,内核可使用下列操作恢复来恢复指针:
anon_vma = (struct anon_vma *) (mapping -PAGE_MAPPING_ANON)
private是一个指向“私有”数据的指针,虚拟内存管理会忽略该数据。根据页的用途,可以用不同的方式使用该指针。大多数情况下它用于将页与数据缓冲区关联起来,在后续章节中描述。
virtual用于高端内存区域中的页,换言之,即无法直接映射到内核内存中的页。virtual用于存储该页的虚拟地址。