Linux存在于各种体系结构上,所以描述内存需要架构独立的方法。本章会描述用来管理memory bank,页框的数据结构以及那些影响VM行为的flags
VM第一个重要的流行概念是Non Uniform Memory Access(NUMA)。对于大型机来说,不同banks内的内存由于和处理器的距离不同,访问代价也不同。比如,一个内存bank可以指定给每一个CPU,或者一个非常适合DMA操作的内存bank可以指定给它附近的设备或者卡。
我们把bank称为一个node,在linux系统中不管是Non Unifomr Memory Access(NUMA)还是Uniform Memory Access(UMA),都使用struct pglist_data (typedef pg_data_t)来表示一个节点。系统中的每一个节点都存放在链表pgdat_list中,node通过pg_data_t->node_next链接到下一个node。对于PC desktops这样的UMA结构,仅仅有一个称为contig_page_data的静态pg_data_t结构。我们将在2.1节继续讨论node
每个node又被划分为多个内存区,这些内存区称之为zone。struct zone_struct用来描述zone,zone有如下类型:ZONE_DMA,ZONE_NORMAL,ZONE_HIGHMEM。不同类型的zone用做不同的场合。ZONE_DMA zone包含低端物理内存适合于那些无法访问超过16MB物理内存的设备。ZONE_NORMAL则被直接映射到kernel线性地址空间的低地址区。我们将在4.1节进一步讨论。ZONE_HIGHMEM则是那些无法直接映射到kernel空间的剩余内存。
对于X86机器,内存zones如下:
ZONE_DMA First 16MiB of memory
ZONE_NORMAL 16MiB - 896MiB
ZONE_HIGHMEM 896MiB - End
内核大部分操作都是使用ZONE_NORMAL,所以ZONE_NORMAL是性能最关键的zone,在2.2节我们将进一步讨论这些zones。 系统内存是由固定尺寸的page frames组成,物理page frame由一个struct page表示,这些page structs都保存在全局mem_map数组中,mem_map通常存储在ZONE_NORMAL的起始位置或者内核镜像保留区域的后面。
我们会在2.4节详细讨论struct page,在3.7节详细讨论全局mem_map数组。图2.1演示了这些数据结构之间的关系。
因为可以被内核直接访问的内存数目(ZONE_NORMAL区大小)是有限的,Linux通过high memory支持更多的物理内存,high memory我们将在2.7 节讨论。在介绍high memory管理之前,先讨论nodes,zones和pages是如何表示的。
2.1 Nodes
之前已经提到过,每一个内存node由pg_data_t描述,也就是类型strcut pglist_data。当分配一个page时,Linux使用本地节点分配策略从距离当前CPU最近的node分配内存,因为进程很有可能也在这个CPU上运行。这个数据结构在
struct bootmem_data;
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
#ifndef CONFIG_NO_BOOTMEM
struct bootmem_data *bdata;
#endif
#ifdef CONFIG_MEMORY_HOTPLUG
/*
* Must be held any time you expect node_start_pfn, node_present_pages
* or node_spanned_pages stay constant. Holding this will also
* guarantee that any pfn_valid() stays that way.
*
* Nests above zone->lock and zone->size_seqlock.
*/
spinlock_t node_size_lock;
#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: 是一个数组,包含这个节点内的zones
node_zonelists: 这是node的分配顺序,在mm/page_alloc.c中的build_zonelists()函数创建这个node_zonelists,指定了备用节点列表,当前节点没有可用空间时,在备用节点分配内存。比如在ZONE_HIGHMEM分配失败,会尝试从ZONE_NORMAL分配,如果ZONE_NORMAL失败则尝试从ZONE_DMA分配。
nr_zones: 这个node包含的zone数目,通常是1 ~ 3。并不是所有的node都有三个zones,比如一个bank可能没有ZONE_DMA。
node_mem_map:node由多个physical frame组成,每一个物理页框都有一个page结构表示,node_mem_map是第一个物理页框的page结构,这个page结构在全局mem_map数组的某个位置。
bdata:在系统启动期间,内存管理子系统那个初始化之前,内核也需要使用内存。boot memory allocator使用这个成员,我们将在第五章讨论boot memory allocator
node_start_pfn:是该节点内第一个页帧的逻辑编号。系统中所有节点的页帧是依次编号的,每个页帧号也是全局唯一的。
node_present_pages:指定了节点中页帧的数目,
node_spanned_pages:该节点包含的页帧范围,包括holes。
系统中所有的nodes都通过list pgdat_list维护。在初始化函数init_bootmem_core()中创建这个list,我们将在5.3节讨论pgdat_list的初始化。在kernel 2.4.18之前,遍历pgdat_list链表的代码如下:
pg_data_t *pgdat;
pgdat = pgdat_list;
do {
/* do something with pgdata_t */
} while ((pgdat = pgdat->node_next));
2.2 Zones
每个zone通过struct zone_struct描述,struct zone_struct保存着页面使用的统计信息,空闲信息和locks,这个结构在
typedef struct zone_struct {
spinlock_t lock;
unsigned long free_pages;
unsigned long pages_min, pages_low, pages_high;
int need_balance;
free_area_t free_area[MAX_ORDER];
wait_queue_head_t *wait_table;
unsigned long wait_table_size;
unsigned long wait_table_shift;
struct pglist_data *zone_pgdat;
struct page *zone_mem_map;
unsigned long zone_start_paddr;
unsigned long zone_start_mapnr;
char *name;
unsigned long size;
} zone_t;
数据结构成员简介:
lock:一个spinlock,保护对zone的并发访问
free_pages:zone内空闲pages的总数
pages_min, page_low, pages_high:这个zone的watermark,这三个值会影响交换守护进程的行为,2.3节详细描述这几个watermark。我们从这里可以看出,交换守护进程是针对每个zone的使用情况进行处理的。
need_balance:这个flag用来通知kswapd balance这个zone。当一个zone的可用页面数目达到zone watermark时,标记need_balance标志。
free_area:zone 空闲区位图,标识每一个page的使用情况,buddy分配器使用这个位图来进行分配和释放。
wait_table:page页面上进程等待队列的hash table,wait_on_page()和unlock_page函数将使用这个hsah table。我们将在2.2.3节讨论wait_table
wait_table_size:这个hash table的等待队列数目,是2的幂次方
wait_table_shift:
zone_pgdat:指向zone所在node 对应的结构pg_data_t
zone_mem_map:这个zone中第一个物理页框对应page
zone_start_paddr:这个zone的起始物理地址
zone_start_mapnr:第一个物理页框对应的页帧号,也就是在全局mem_map中的偏移
name:描述这个zone的字符串:“DMA”, “Normal”和“HighMem”
size:zone所包含的页框数目
2.2.1 Zone Watermarks
当系统的可用内存变低时,页面pageout守护进程kswapd被唤醒,开始释放空闲pages。在压力较高的情况下,守护进程立刻同步的释放内存,我们称之为direct-reclaim。这些控制pageout行为的参数在FreeBSD和Solaris都有类似实现。
每一个zone都有三个watermarks:分别称为pages_low,pages_min和pages_high,可以用来判断zone当前的内存压力。pages_min在free_area_init_core()中计算,计算公式为ZoneSizeInPages/128,但是最低不低于20 pages,最高不超过255 pages。
系统根据zone内page使用情况,在处于不同的watermark时,会采取不同的动作
pages_low: 当空闲页面数目低于pages_low时,kswapd被buddy allocator唤醒释放pages。值缺省是pages_min的两倍。
pages_min: 当空闲页面数目低于pages_min时,allocator将代替kswapd同步释放pages,也就是直接回收
pages_high: 当kswapd被唤醒来释放pages时,回收的pages已经达到pages_high标记的页面数,kswapd将停止回收,进入休眠。缺省值一般为pages_min的三倍。
不管这些参数在类似系统中叫什么名字,作用都是类似的,辅助pageout守护进程或者线程进行页面回收。
2.2.2 Calculating the Size of Zones
zone的尺寸是在函数setup_memory中计算,如图2.3
PFN物理页框号,是物理页框在内存中的page 偏移。系统内的第一个PFN,保存在min_low_pfn,是loaded kernel镜像后面第一个page。这个值保存在mm/bootmem.c中
最后一个物理页框max_pfn是怎么计算的呢? 不同的系统计算方法不同
max_low_pfn是指低端最大物理页框号,用来标记ZONE_NORMAL的结尾。这是内核可以直接访问的最大物理内存,这个地址值和kernel/userspace地址空间划分相关。这个值保存在mm/bootmem.c中,对于低内存系统,max_pfn是等于max_low_pfn的。
使用min_low_pfn, max_low_pfn和max_pfn,可以很容易的计算出high memory的起始和结束地址 highstart_pfn, highend_pfn。
2.2.3 Zone Wait Queue Table
2.3 Zone Initialization
2.4 Initializing mem_map
mem_map 在系统启动时创建,在UMA系统中free_area_init()使用contig_page_data作为node,global mem_map作为这个ode的本地mem_map。
free_area_init_core()为当前节点分配一个本地mem_map。mem_map数组是通过boot memory 分配器alloc_bootmem_node来分配的。
2.5 Pages
系统内的每一个物理page frame都有一个相应的page来跟踪这个page的状态,在2.2kernel,这个结构和System V中的类似,但是对于其他的UNIX变种,这个结构做了写改变。struct page在
typedef struct page {
struct list_head list;
struct address_space *mapping;
unsigned long index;
struct page *next_hash;
atomic_t count;
unsigned long flags;
struct list_head lru;
struct page **prev_hash;
struct buffer_head *buffers;
#if defined(CONFIG_HIGHMEM) || defined(WAIT_PAGE_VIRTUAL)
void *virtual;
#endif
} mem_map_t;
下面是page结构中成员变量的简介:
list:pages可以属于多种链表,这个list成员就是用来挂接到这些链表的。例如,一个映射的pages可以属于address_space中三个循环链表中的一个。他们是clean_pages, dirty_pages和locked_pages。在slab分配器中,当一个page已经被slab分配器分配,它被用来存储page所管理的slab和cache结构指针。它也可以用来把page内空闲block连接到一起
mapping:当文件和设备做内存mapped时,他们的inode有一个address_space。page的这个成员就指向这个address space如果pages属于这个文件。如果page 是匿名映射,那么address_space指向swapper_space。
index:这个成员有两个用途,具体是哪一个取决于page的状态。如果page是文件映射的一部分,那么index是文件内的页偏移。如果page是swap cache的一部分,那么index则是交换地址空间swapper_space内的偏移量。如果pages内的块正在被特定进程释放中,正在被释放block的order存放在index中,这个值被函数_free_pages_ok()设置。
next_hash:如果Pages作为文件映射的一部分,那么pages使用inode和offset做hash。这个next_hash把具有相同hash的pages链接到一起。
count:page的索引计数。如果变为0,那么这个page可以被释放。如果大于0,那么说明他被一个或者多个进程使用,或者被kernel使用中
flags:存储了体系结构无关的标志,用来描述page的状态。这些状态标记在
lru:对于page替换策略,active_list或者inactive_list链表中的pages可能会被交换出。lru是这些这些可交换pages的Least Recently Used 链表的链表头,我们将在chapter 10讨论这两个链表。
pprev_hash:和next_hash互为补充,形成一个双向链表。
buffers:如果一个page和一个块设备的buffer相关联,那么这个成员用来记录buffer_head。如果一个匿名page被交换文件后备时,那么这个page也有相应的buffer_head。因为这个page不得不同步数据到后备storage的block-size 块中。
virtual:正常情况下,仅仅ZONE_DMA和ZONE_NORMAL的pages被kernel直接映射。对于ZONE_HIGHMEM的pages,无法直接映射到内核内存中的页,当一个page被映射后,这个成员记录了page的虚拟地址。
2.6 Mapping Pages to Zones
最近的kernel版本2.4.18,struct page增加了成员指针zone,指向page所在的zone。后来这个成员被认为没什么作用,因为即便是一个指针,对于大量的struct page,仍然消耗许多的内存。在最近的kernel,这个zone成员被去掉了。取而代之的是通过page->flags中的最高ZONE_SHIFT位来决定page属于哪个zone。这些位记录了page对应的zone在zone_table中的索引。
在系统启动时会建立zones的zone_table,zone_table在mm/page_alloc.c中声明如下:
zone_t *zone_table[MAX_NR_ZONES*MAX_NR_NODES];
EXPORT_SYMBOL(zone_table);
MAX_NR_ZONES是一个node中zones的最大数目,比如3。MAX_NR_NODES是系统nodes的最大数目。函数EXPORT_SYMBOL使得zone_table可以被loadable modules访问。这个表就像一个多维数组。在free_area_init_core时,node内的所有pages都被初始化。首先把这个zone记录到zone_table中
zone_table[nid * MAX_NR_ZONES + j] = zone
nid是node的ID,j是zone在这个node中的索引,zone是zone_t结构,对于每一个page,都会执行函数set_page_zone
set_page_zone(page, nid * MAX_NR_ZONES + j)
参数@page的zone index被设置为nid * MAX_NR_ZONES+j
2.7 High Memory
因为内核可用的地址空间(ZONE_NORMAL)大小是有限的,所以kernel通过high memory支持更多的物理内存。在32-bit x86系统上有两个阀值,一个是4GiB,一个是64GiB。
4GiB限制是32-bit物理地址最大可寻址空间,为了访问1GiB到4GiB范围的内存(1GiB这个下限并不是确定的,不仅和预留的vmalloc空间有关,而且还和kernel/user space的地址空间划分有关),kernel需要使用kmap临时映射pages到ZONE_NORMAL。我们将在第九章进一步讨论。
第二个限制64GiB是和PAE相关的,Intel的发明允许在32-bit系统上访问更多的RAM。它使用额外的位进行内存寻址,寻址范围可以达到2^36(64GiB)
虽然理论上PAE允许寻址范围是64GiB的,但是实际中linux进程无法访问那么多的RAM,因为虚拟地址空间仍然是4GiB。如果用户想在他们的进程内使用malloc分配所有的物理内存,那么是不可能的。
此外PAE也不允许kernel本身有那么多的RAM,结构page被用来描述每一个页框,结构page的大小为44字节,存放在ZONE_NORMAL这个虚拟地址空间中。这意味着1GiB的物理内存将占用11MiB的内存,而对于16GiB则是176MiB,这就导致ZONE_NORMAL变得更小。虽然目前看起来还不算太坏,但是我们再考虑其他小结构体,比如Page Table Entry(PTEs),那么在最坏的情况下需要16MiB(16GiB内存需要4M个PTEs)空间。这使得16MiB成为x86 linux可用物理内存的实际限制。如果需要访问更多的物理内存,给个简单明了的建议,使用64-bit系统。