RAM的某些部分永久分配给内核,
来存放内核代码及静态内核数据结构。
RAM的其余部分称为动态内存,
这不仅是进程所需的宝贵资源,
也是内核本身所需的宝贵资源。
页框管理
Intel的Pentinum处理器可采用两种不同的页框大小:
4KB,4MB(如PAE被激活,则为2MB)。
Linux采用4KB页框大小作为标准的内存分配单元。
1.由分页单元引发的缺页异常很容易得到解释,
或由于请求的页存在但不允许进程对其访问,
或由于请求的页不存在。
第二种情况下,
内存分配器必须找到一个4KB的空闲页框,
并将其分配给进程。
2.虽然4KB,4MB都是磁盘块大小的倍数,
但绝大多数情况下,
当主存和磁盘之间传输小块数据时更高效。
页描述符
内核必须记录每个页框当前的状态。
如,内核必须能区分哪些页框包含的是属于进程的页,
哪些页框包含的是内核代码或内核数据。
类似地,内核还必须能确定动态内存中的页框是否空闲。
页框的状态信息保存在一个类型为page的页描述符中,
其中的字段如表所示:
类型 | 名字 | 说明 |
---|
unsigned long | flags | 一组标志。对页框所在的管理区进行编号。 |
atomic_t | _count | 页框的引用计数器 |
atomic_t | _mapcount | 页框中的页表项数量(没有则为-1) |
unsigned long | private | 可用于正使用页的内核成分 |
struct address_space* | mapping | 当页被插入页高速缓存时使用。或当页属于匿名区时使用。 |
unsigned long | index | 作为不同的含义被几种内核成分使用。 |
struct list_head | lru | 包含页的最近最少使用双向链表的指针 |
所有的页描述符存放在mem_map数组中。
每个描述符长度为32字节,
所以mem_map所需要的空间略小于整个RAM的1%。
virt_to_page(addr)宏产生线性地址addr对应的页描述符地址。
pfn_to_page(pfn)宏产生与页框号pfn对应的页描述符地址。
让我们较详细地描述以下两个字段:
_count页的引用计数器。如字段为-1,则相应页框空闲,并可被分配给任一进程或内核本身。如该字段值大于或等于0,则说明页框被分配给了一个或多个进程,或用于存放一些内核数据结构。page_count返回__count加1后的值,即该页的使用者的数目。
flags包含多达32个用来描述页框状态的标志。对每个PG_xyz标志,内核都定义了操纵其值的一些宏。通常,PageXyz宏返回标志的值,SetPageXyz和ClearPageXyz宏分别设置和清除相应的位。
标志名 | 含义 |
---|
PG_locked | 页被锁住。如,在磁盘I/O操作中涉及的页 |
PG_error | 在传输页时发生I/O错误 |
PG_referenced | 刚刚访问过的页 |
PG_uptodate | 在完成读操作后置位,除非发生磁盘I/O错误 |
PG_dirty | 页已经被修改 |
PG_lru | 页在活动或非活动页链表中 |
PG_active | 页在活动页链表中 |
PG_highmem | 页框属于ZONE_HIGHMEM管理区 |
PG_checked | 由一些文件系统使用的标志 |
PG_arch_1 | 在80x86体系结构上没有使用 |
PG_reserved | 页框留给内核代码或没有使用 |
PG_private | 页描述符的private字段存放了有意义的数据 |
PG_writeback | 正使用writeback方法将页写到磁盘上 |
PG_nosave | 系统挂起、唤醒时使用 |
PG_compound | 通过扩展分页机制处理页框 |
PG_swapcache | 页属于对换高速缓存 |
PG_mappedtodisk | 页框中的所有数据对应于磁盘上分配的块 |
PG_reclaim | 为回收内存对页已经做了写入磁盘的标记 |
PG_nosave_free | 系统挂起、恢复时使用 |
非一致内存访问(NUMA)
习惯上,认为计算机内存是一种均匀,共享的资源。
在忽略硬件高速缓存作用的情况下,
期望不管内存单元处于何处,CPU处于何处,
CPU对内存单元的访问都需相同的时间。
可惜,
这些假设在某些体系结构上并不总是成立。
如,对某些多处理器Alpha或MIPS计算机,就不成立。Linux2.6支持非一致内存访问模型,
在这种模型中,
给定CPU对不同内存单元的访问时间可能不一样。
系统的物理内存被划分为几个节点(node)。
在一个单独的节点内,
任一给定CPU访问页面所需的时间都是相同的。
然而,
对不同CPU,这个时间可能就不同。对每个CPU而言,
内核都试图把耗时节点的访问次数减到最少,
这就要小心地选择CPU最常引用的内核数据结构的存放位置。每个节点中的物理内存又可分为几个管理区。
每个节点都有一个类型为pg_data_t的描述符。
类型 | 名字 | 说明 |
---|
struct zone[] | node_zones | 节点中管理区描述符的数组 |
struct zonelist[] | node_zonelists | 页分配器使用的zonelist数据结构的数组 |
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 | 节点标识符 |
pg_data_t* | pgdat_next | 节点内存链表中的下一项 |
wait_queue_head_t | kswapd_wait | kswapd页换出守护进程使用的等待队列 |
struct task_struct* | kswapd | 指针指向kswapd内核线程的进程描述符 |
int | kswapd_max_order | kswapd将要创建的空闲块大小取对数的值 |
同样,我们只关注80x86。
IBM兼容PC使用一致内存访问模型,
因此,
并不真正需要NUMA的支持。
然而,
即使NUMA的支持没有编译进内核,
Linux还是使用节点。
不过,
这是一个单独的节点,
它包含了系统中所有的物理内存。
故,pgdat_list指向一个链表,
此链表是由一个元素组成的。
这个元素就是节点0描述符,它被存放在config_page_data。在80x86结构中,
把物理内存分组在一个单独的节点中可能显得没用处;
但,这种方式有助于内核代码的处理具有可移植性。
内存管理区
在一个理想的计算机体系结构中,
一个页框就是一个内存存储单元,
可用于任何事情:
存放内核数据和用户数据,缓冲磁盘数据等等。
任何种类的数据页都可存放在任何页框中。但实际的计算机体系结构有硬件的制约,
这限制了页框可使用的方式。
尤其是,Linux内核必须处理80x86体系结构的两种硬件约束:
1.ISA总线的直接内存存取(DMA)处理器有一个严格的限制:
它们只能对RAM的前16MB寻址、
2.在具有大容量RAM的现代32位计算机中,
CPU不能直接访问所有的物理内存,
因此线性地址空间太小。为应对这两种限制,
Linux2.6把每个内存节点的物理内存划分为三个管理区。
在80x86UMA体系结构中的管理区为:
ZONE_DMA包含低于16MB的内存页框
ZONE_NORMAL包含高于16MB且低于896MB的内存页框
ZONE_HIGHMEM包含从896MB开始高于896MB的内存页框
ZONE_DMA区包含的页框可由老式基于ISA的设备通过DMA使用。ZONE_DMA和ZONE_NORMAL区包含内存的"常规"页框,
通过把它们线性地址映射到线性地址空间的第4个GB,
内核就可直接进行访问。
相反,
ZONE_HIGHMEM区包含的内存页不能由内核直接访问,
尽管它们页线性地映射到了线性地址空间的第4个GB。
在64位体系结构上,
ZONE_HIGHMEM区总是空的。每个内存管理区都有自己的描述符。
类型 | 名称 | 说明 |
---|
unsigned long | free_pages | 管理区中空闲页的数目 |
unsigned long | pages_min | 管理区中保留页的数目 |
unsigned long | pages_low | 回收页框使用的下界;同时也被管理区分配器作为阀值使用 |
unsigned long | pages_high | 回收页框使用的上届;同时也被管理区分配器作为阀值使用 |
unsigned long[] | lowmem_reserve | 指明在处理内存不足的临界情况下每个管理区必须保留的页框数目 |
struct per_cpu_pageset[] | pageset | 用于实现单一页框的特殊高速缓存 |
spinlock_t | lock | 保护该描述符的自旋锁 |
struct free_area[] | free_area | 标识出管理区的空闲页框块 |
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 | nr_active | 管理区的活动链表上的页数目 |
unsigned long | nr_inactive | 管理区的非活动链表上的页数目 |
unsigned long | pages_scanned | 管理区内回收页框时使用的计数器 |
int | all_unreclaimable | 在管理区中填满不可回收页时此标志被置位 |
int | temp_priority | 临时管理区的优先级 |
int | prev_priority | 管理区优先级,范围在12和0之间 |
wait_queue_head_t* | wait_table | 进等待队列的散列表,这些进程正在等待管理区中的某页 |
unsigned long | wait_table_size | 等待队列散列表的大小 |
unsigned long | wait_table_bits | 等待队列散列表数组大小,值为2^order |
struct pglist_data* | zone_pgdat | 内存节点 |
struct page* | zone_mem_map | 指向管理区的第一个页描述符的指针 |
unsigned long | zone_start_pfn | 管理区第一个页框的下标 |
unsigned long | spanned_pages | 以页为单位的管理区的总大小,包括洞 |
unsigned long | present_pages | 以页为单位的管理区的总大小,不包括洞 |
char* | name | 指针指向管理区的传统名称:“DMA”,“NORMAL”,“HighMem” |
每个页描述符都有到内存节点和节点内管理区的链接。
为节省空间,这些链接被编码成索引存放在flags字段的高位。
实际上,
刻画页框的标志的数目是有限的。
保留flags字段的最高位来编码特定内存节点和管理区号总是可能的。
page_zone接收一个页描述符的地址作为它的参数;
它读取页描述符中flags字段的最高位,
然后通过查看zone_table数组来确定相应管理区描述符的地址。
在启动时用所有内存节点的所有管理区描述符的地址初始化这个数组。当内核调一个内存分配函数时,
必须指明请求页框所在的管理区。
内核通常指明它愿意使用哪个管理区。为了在内存分配请求中指定首选管理区,
内核使用zonelist数据结构,
这就是管理区描述符指针数组。
保留的页框池
可用两种不同的方法来满足内存分配请求。
如有足够的空闲内存可用,
请求就会被立刻满足。
否则,必须回收一些内存,且将发出请求的内核控制路径阻塞,直到内存被释放。当请求内存时,
一些内核控制路径不能被阻塞--如,这种情况发生在处理中断或执行临界区内的代码时。
此时,
一条内核控制路径应产生原子内存分配请求。
原子请求从不被阻塞:
如没有足够的空闲页,则仅仅是分配失败而已。尽管无法保证一个原子内存分配请求决不失败,
但内核会设法尽量减少这种不幸事件发生的可能性。
为做到这一点,
内核为原子内存分配请求保留了一个页框池,
只有在内存不足时才使用。保留内存的数量(以KB为单位)存放在min_free_kbytes中。
它的初始值在内核初始化时设置,
并取决于直接映射到内核线性地址空间的第4个GB的物理内存的数量
--即,取决于包含在ZONE_DMA和ZONE_NORMAL内存管理区内的页框数目。
保留池的大小=sqrt(16*直接映射内存)(KB)
但,min_free_kbytes的初始值不能小于128也不能大于65536。
ZONE_DMA和ZONE_NORMAL内存管理区将一定数量的页框贡献给保留内存,
这个数目与两个管理区的相对大小成比例。
例,如ZONE_NORMAL管理区比ZONE_DMA大8倍,
则页框的7/8从ZONE_NORMAL获得。
1/8从ZONE_DMA获得。管理区描述符的pages_min存储了管理区内保留页框的数目。
这个字段和pages_low,pages_high一起还在页框回收算法中起作用。
pages_low总是设为pages_min的值的5/4,
pages_high总是被设为pages_min的3/2。
分区页框分配器