在前面的博文里,我们讲解了基于80x86体系的Linux内核分段和分页机制,并详细地讨论了Linux的内存布局。有了这些基本概念以后,我们就来详细讨论内核如何动态地管理那些可用的内存空间。
对于80386这种32位的处理器结构,Linux采用4KB页框大小作为标准的内存分配单元。内核必须记录每个页框的当前状态,例如,区分哪些页 框包含的是属于进程的页,而哪些页框包含的是内核代码或内核数据。内核还必须能够确定动态内存中的页框是否空闲,如果动态内存中的页框不包含有用的数据, 那么这个页框就是空闲的。在以下情况下页框是不空闲的:包含用户态进程的数据、某个软件高速缓存的数据、动态分配的内核数据结构、设备驱动程序缓冲的数 据、内核模块的代码等等。
内核用数据结构page描述一个页框的状态信息,所有的页描述符存放在全局mem_map数组中,其数组的下标为页框号(pfn)。 因为每个描述符长度为32字节,所以mem_map所需要的空间略小于整个RAM的1%。
那么一个页描述符怎样与一个占据4k的页框相联系(映射)呢?有了mem_map数组,这个问题就很简单了。因为如果知道了page数据的地址pd,用pd去减去mem_map就得到了pd的页框号pfn。那么这个物理页的物理地址是physAddr = pfn << PAGE_SHIFT 。
在得知该物理页的物理地址是physAddr后,就可以视physAddr的大小得到它的虚拟地址:
1.physAddr < 896M 对应虚拟地址是 physAddr + PAGE_OFFSET (PAGE_OFFSET=3G)
2.physAddr >= 896M 对应虚拟地址不是静态映射的,通过内核的高端虚拟地址映射得到一个虚拟地址。
在得到该页的虚拟地址之后,内核就可以正常访问这个物理页了。
内核提供一个virt_to_page(addr)宏来产生线性地址addr对应的页描述符地址。pfn_to_page(pfn)宏产生与页框号 pfn对应的页描述符地址。相反,也提供page_to_pfn(pg)宏来产生页描述符对应的页的页框号pfn。注意,针对80x86结构,上述宏并不 是直接通过men_map数组来确定页框号的,而是通过内存管理区的zone_mem_map来确定的,不过原理是一样的:
#define page_to_pfn(pg) /
({ /
struct page *__page = pg; /
struct zone *__zone = page_zone(__page); /
(unsigned long)(__page - __zone->zone_mem_map) /
+ __zone->zone_start_pfn; /
})
这里千万要注意!不要混淆一个概念。这里的physAddr虽然表示物理地址,但是并不能说明该地址的数据就一定存在于物理内存中。那么如何判断这个页到底在不在内存中呢?你看,前面的知识就用到了——分页机制。 也就是说,如果这个页因为各种各样五花八门的原因被交换出去了,那么它对应的页的Present标志就为0。这里就牵涉到缺页异常了,要深入了解,请关注笔者后面的博文。
在这里我们只需要对数据结构page详细讨论以下两个字段:
1、_count:页的引用计数器。如果该字段为-1,则相应页框空闲,并可被 分配给任一进程或内核本身;如果该字段的值大于或等于0,则说明页框被分配给一个或多个进程,或用于存放一些内核数据结构。page_count()函数 返回_count加1后的值,也就是该页的使用者的数目。
2、flags:包含多达32个用来描述页框状态的标志。对于每个PG_xyz标志,内核都定义了操纵其值的一些宏。通常,PageXyz宏返回标志的值,而SetPageXyz和ClearPageXyz宏分别设置和清除相应的位。
标志名 |
含义 |
PG_locked |
页被锁定,例如,在磁盘I/O 操作中涉及的页。 |
PG_error |
在传输页时发生错误 |
PG_referenced |
刚刚访问过的页 |
PG_uptodate |
在完成读操作后置位,除非发生磁盘I/O 错误 |
PG_dirty |
页已经被修改 |
PG_lru |
页在活动或非活动页链表中 |
PG_active |
页在活动页链表中 |
PG_slab |
包含在slab 中的页框 |
PG_highmem |
页框属于ZONE_HIGHMEM 管理区 |
PG_checked |
由一些文件系统(如Ext2 和Ext3 )使用的标志 |
PG_arch_1 |
在80x86 体系结构上没有使用 |
PG_reserved |
页框留给内核代码或没有使用 |
PG_private |
页描述符的private 字段存放了有意义的数据 |
PG_writeback |
正在使用writepage 方法将页写到磁盘上 |
PG_nosave |
系统挂起 / 唤醒时使用 |
PG_compound |
通过扩展分页机制处理页框 |
PG_swapcache |
页属于对换高速缓存 |
PG_mappedtodisk |
页框中的所有数据对应于磁盘上分配的块 |
PG_reclaim |
为回收内存对页已经做了写入磁盘的标记 |
PG_nosave_free |
系统挂起 / 恢复时使用 |
系统是怎么为进程或内核分配一个内存空间,或者说怎么给他们分配一个线性页描述符所对应线性地址的页面呢?这个需要借助内核的分区页框分配器和伙伴系统算法。在讨论这些细节之前,先介绍一些必要的概念。
Linux2.6支持非统一内存访问(NUMA)模型,在这种模型中,给定CPU对不同内存单元的访问时间可能不一样。系统的物理内存被划分为几个 节点(node)。在一个单独的节点内,任一给定CPU访问页面所需要的时间都是相同的,而对于不同的CPU,这个时间就不同。对每个CPU而言,内核都 试图把耗时节点的访问次数减到最少,这就必须要将那些CPU最常引用的内核数据结构的存放位置选好。
每个节点由一个类型为pg_data_t的描述符表示,所有节点的描述符存放在一个单向链表中,它的第一个元素由内核全局变量pgdat_list 指向。在x86体系中,即使是多核,内存访问时间也是相同的,所以不需要NUMA,但是内核还是使用节点,不过,这只是一个单独的节点,它包含了系统中所 有的物理内存。因此,pgdat_list变量指向一个链表,此链表只有一个元素组成的,这个元素就是节点0描述符,它被存放在 contig_page_data变量中。
pg_data_t描述符中要注意到的三个字段分别是node_zones、node_zonelists、node_mem_map,分别是 zone_t[]、zonelist_t[]和page类型。前两个是用来描述内存管理区的,下面马上要谈到;node_mem_map是本节点所有页的 页描述符数组。内核将这三个字段放在里边,就是为内存区、页框建立一些列的联系。
由于Linux内核必须处理80x86体系结构中的两种硬件约束:
(1)ISA总线的直接内存存取(DMA)处理器有一个严格的限制:他们只能对RAM的前16MB寻址。
(2)在具有较大容量RAM的现代32位计算机中,CPU不能直接访问所有的物理内存,因为线性地址空间太小。
为了应对这两种限制,Linux2.6把每个内存节点的物理内存划分成3个管理区(zone)。在80x86的UMA体系结构中的管理区分为:
ZONE_DMA:包含低于16MB的内存页框
ZONE_NORMAL:包含高于16MB而低于896MB的内存页框
ZONE_HIGHMEM:包含从896MB开始高于896MB的内存页框
ZONE_DMA和ZONE_NORMAL区包含内存“常规”页框,通过把他们线性地址映射到线性地址空间的第4个GB,内核就可以直接进行访问。 ZONE_HIGHMEM区包含的内存页不能由内核直接访问,尽管它们也可以通过高端内存内核映射,线性映射到线性地址空间的第4个GB。
每个内存管理区都有自己的描述符zone_t,其字段中很多用于回收页框时使用。其实每个页描述符page都有到内存节点和到内存节点管理区的链 接。那我们为啥看不到呢,原因是为了节省空间,这些链接的存放方式和典型的指针不同,是被编码成索引存放在flags字段的高位。
zone_t字段如下:
类型 |
名称 |
说明 |
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 |
等待队列散列表数组大小,值为2order |
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字段的最高位来编码内存节点和管理区是绰绰有余的。Linux提供 page_zone()函数用来接收一个页描述符的地址作为它的参数;它读取该描述符中的flags字段的最高位,然后通过查看zone_table数组 来确定相应管理区描述符的地址。顺便提一下,在系统启动时用,内核将所有内存节点的所有管理区描述符的地址放到这个zone_table数组里边。
当内核调用一个内存分配函数时,必须指明请求页框所在的管理区。内核通常指明它愿意使用哪个管理区。为了在内存分配请求中指定首选管理区,内核使用 zonelist数据结构,这就是管理区描述符指针数组,在80x86中只有三个zone,所以zonelist数据结构中指向这三个zone的指针按照 一定规则排列。如图,则zonelist数组就是这三个zone的排列组合。
例如,要分配一个用来做DMA的页框,则在指定zonelist数组中的某个zonelist元素中获得首选的zone,应该是ZONE_DMA,如果该区空间已使用完,就选ZONE_NORMA区,随后再是ZONE_HIGHMEM。