目录
页框管理
页描述符
UMA和NUMA
内存管理区
保留的页框池
分区页框分配器
请求和释放页框
释放页框
高端内存页框的内核映射
伙伴系统算法
连续页框块释放:
1.数据结构
2.分配块
3.释放块
RAM中,剩下的自由部分被称为动态内存!因为这不仅是内进程所需要的宝贵资源,也是内核执行本身所需要的宝贵资源!实际上整个系统的性能都高度取决于如何有效的管理动态内存!因此内存管理是操作系统中非常重要的话题之一!
页框管理
Intel
的Pentinum
处理器可采用两种不同的页框大小:4KB,4MB
(如PAE
被激活,则为2MB
)。Linux
采用4KB
页框大小作为标准的内存分配单元。
-
由分页单元引发的缺页异常很容易得到解释,或由于请求的页存在但不允许进程对其访问,或由于请求的页不存在。第二种情况下,内存分配器必须找到一个4KB的空闲页框,并将其分配给进程。
-
虽然
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
数组中。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 | 系统挂起、恢复时使用 |
UMA和NUMA
习惯上,认为计算机内存是一种均匀,共享的资源。在忽略硬件高速缓存作用的情况下,期望不管内存单元处于何处,CPU
处于何处,CPU
对内存单元的访问都需相同的时间(UMA)。可惜,这些假设在某些体系结构上并不总是成立。如,对某些多处理器Alpha
或MIPS
计算机,就不成立。Linux2.6
支持非一致内存访问模型,在这种模型中,给定CPU
对不同内存单元的访问时间可能不一样。系统的物理内存被划分为几个节点(node
)。节点既封装了内存资源,也封装了CPU
资源。在一个单独的节点内,任一给定CPU
访问页面所需的时间都是相同的。然而, 对不同CPU
,这个时间可能就不同。这就是NUMA的特性!
对每个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
体系结构的两种硬件约束:
-
ISA
总线的直接内存存取(DMA
)处理器有一个严格的限制:它们只能对RAM
的前16MB
寻址、 -
在具有大容量
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
数据结构,这就是管理区描述符指针数组。
保留的页框池
可用两种不同的方法来满足内存分配请求。如有足够的空闲内存可用,请求就会被立刻满足。否则,必须回收一些内存,且将发出请求的内核控制路径阻塞,直到内存被释放。(NUMA
下默认策略是本地节点内内存不足,从本地节点回收内存后再次尝试分配。若选择本地节点内存不足时,优先查看其他节点是否存在足量内存时,若存在从其他节点完成剩余部分分配的方案,在应用需要大内存场景下可能更高效)
当请求内存时,一些内核控制路径不能被阻塞。比如,这种情况发生在处理中断或执行临界区内的代码时。此时,一条内核控制路径应产生原子内存分配请求。原子请求从不被阻塞:如没有足够的空闲页,则仅仅是分配失败而已。尽管无法保证一个原子内存分配请求决不失败,但内核会设法尽量减少这种不幸事件发生的可能性。为做到这一点,内核为原子内存分配请求保留了一个页框池,只有在内存不足时才使用。
保留内存的数量(以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
。
分区页框分配器
分区页框分配器被称作分区页框分配器的内核子系统,处理对连续页框组的内存分配请求。它的主要组成如下:
管理区分配器下分给ZONE_DMA内存管理区,ZONE_NORMAL内存管理区,ZONE_HIGHMEM内存管理区。为了更加很合适的分配内存,每一个个管理区又有伙伴系统和Per-CPU页框高速缓存
其中,名为"管理区分配器"部分接受动态内存分配和释放的请求。在请求分配的情况下,该部分搜索一个能满足所请求的一组连续页框内存的管理区。在每个管理区内,页框被名为"伙伴系统"的部分来处理。为达到更好的系统性能,一小部分页框保留在高速缓存中用于快速地满足对单个页框的分配请求。
请求和释放页框
API | 说明 |
---|---|
alloc_pages(gfp_mask, order) | 用这个函数请求2^order 个连续的页框。它返回第一个所分配页框描述符的地址,或,如分配失败,则返回NULL 。 |
alloc_page(gfp_mask) | 用于获得一个单独页框的宏;扩展为:alloc_pages(gfp_mask, 0) 。 |
__get_free_pages(gfp_mask, order) | 类似alloc_pages ,返回第一个所分配页的线性地址。 |
__get_free_page(gfp_mask) | 用于获得一个单独页框的宏;扩展为:__get_free_pages(gfp_mask, 0) |
get_zeroed_page(gfp_mask) | 获取填满0的页框;它调用:alloc_pages(gfp_mask |__GFP_ZERO, 0); |
__get_dma_pages(gfp_mask, order) | 获得适用于DMA的页框,它扩展为:__get_free_pages(gfp_mask |__GFP_DMA, order); |
参数gfp_mask
是一组标志,指明了如何寻找空闲的页框。能在gfp_mask
中使用的标志如下:
标志 | 说明 |
---|---|
__GFP_DMA | 所请求的页框必须处于ZONE_DMA管理区。等价于GFP_DMA |
__GFP_HIGHMEM | 所请求的页框处于ZONE_HIGHMEM管理区。 |
__GFP_WAIT | 允许内核对等待空闲页框的当前进程进行阻塞 |
__GFP_HIGH | 允许内核访问保留的页框池 |
__GFP_IO | 允许内核在低端内存页上执行I/O传输以释放页框 |
__GFP_FS | 如清0,则不允许内核执行依赖文件系统的操作 |
__GFP_COLD | 所请求的页框可能为"冷的" |
__GFP_NOWARN | 一次内存分配失败将不会产生警告信息 |
__GFP_REPEAT | 内核重试内存分配直到成功 |
__GFP_NOFAIL | 与__GFP_REPEAT相同 |
__GFP_NORETRY | 一次内存分配失败后不再重试 |
__GFP_NO_GROW | slab分配器不允许增大slab高速缓存 |
__GFP_COMP | 属于扩展页的页框 |
__GFP_ZERO | 任何返回的页框必须被填满0 |
实际上,Linux
使用预定义标志值的组合。
组名 | 相应标志 |
---|---|
GFP_ATOMIC | __GFP_HIGH |
GFP_NOIO | __GFP_WAIT |
GFP_NOFS | __GFP_WAIT |
GFP_KERNEL | __GFP_WAIT |
GFP_USER | __GFP_WAIT |
GFP_HIGHUSER | __GFP_WAIT |
__GFP_DMA
和__GFP_HIGHMEM
被称作管理区修饰符;它们标示寻找空闲页框时内核所搜索的管理区。contig_page_data
节点描述符的node_zonelists
是一个管理区描述符链表的数组,它代表后备管理区:对管理区修饰符的每一个设置,相应的链表包含的内存管理区能在原来的管理区缺少页框的情况下被用于满足内存分配请求。在80x86 UMA
体系结构中,后备管理区如下:
-
如
__GFP_DMA
被置位,则只能从ZONE_DMA
内存管理区获取页框 -
如
__GFP_HIGHMEM
没被置位,按优先次序从ZONE_NORMAL
,ZONE_DMA
内存管理区获取页框 -
__GFP_HIGHMEM
被置位,按优先次序从ZONE_HIGHMEM
,ZONE_NORMAL
,ZONE_DMA
内存管理区获取页框
释放页框
下面4个函数和宏中的任一个都可释放页框
API | 说明 |
---|---|
__free_pages(page, order) | 先检查page指向的页描述符;如该页框未被保留,就把描述符的count字段减1。如count 变为0,就假定从与page 对应的页框开始的2^order 个连续页框不再被使用。此时,函数释放页框。 |
free_pages(addr, order) | 类似于__free_pages(page, order) ,它接收的参数为要释放的第一个页框的线性地址addr |
__free_page(page) | 释放page 所指描述符对应的页框;扩展为:__free_pages(page, 0) |
free_page(addr) | 释放线性地址为addr 的页框;扩展为:free_pages(addr, 0) |
高端内存页框的内核映射
与直接映射的物理内存末端,高端内存的始端所对应的线性地址存放在high_memory
变量。被设置为896MB
。896MB
边界以上的页框并不会采用直接映射方式对应到内核线性地址空间的第4个GB中相应位置,因此,内核不能直接访问它们。意味着,返回所分配页框线性地址的页分配器函数不适用于高端内存。即不适用于ZONE_HIGHMEM
内存管理区内的页框。如,假定内核调__get_free_pages(GFP_HIGHMEM, 0)
在高端内存分配一个页框,如分配器在高端内存确实分配了一个页框,则__get_free_pages
不能返回它的线性地址。依次类推,内核不能使用这个页框;甚至更坏情况下,也不能释放该页框。
在64位硬件平台上不存在这个问题,因为可使用的线性地址空间远大于能按照的RAM大小。简言之,这些体系结构的ZONE_HIGHMEM
管理区总是空的。但在32位平台上,如80x86体系结构,Linux设计者不得不找到某种方法来允许内核使用所有可使用的RAM,达到PAE所支持的64GB。
采用的方法如下:
-
高端内存页框的分配只能通过
alloc_pages
和它的快捷函数alloc_page
。这些函数不返回第一个被分配页框的线性地址,因为如该页框属于高端内存,则这样的线性地址根本不存在。这些函数返回第一个被分配页框的页描述符的线性地址。这些线性地址总是存在的,因为所有页描述符被分配在低端内存,它们在内核初始化阶段完成后就不会改变。 -
没有线性地址的高端内存中的页框不能被内核访问。故,内核线性地址空间的最后
128MB
中的一部分专门用于映射高端内存页框。这种映射是暂时的。通过重复使用线性地址,使得整个高端内存能在不同的时间被访问。
内核可采用三种不同的机制将页框映射到高端内存(线性地址):分别叫永久内核映射,临时内核映射,非连续内存分配。 建立永久内核映射可能阻塞当前进程;这发生在空闲页表项不存在时,即在高端内存上没有页表项可用作页框的"窗口"时。永久内核映射不能用于中断处理程序和可延迟函数。 建立临时内核映射不会要求阻塞当前进程;它的缺点是只有很少的临时内核映射可同时建立起来。使用临时内核映射的内核控制路径必须保证当前没其他的内核控制路径在使用同样的映射。意味着内核控制路径永不能被阻塞,否则其他内核控制路径很可能使用同一个窗口来映射其他的高端内存页。永久内核映射在64位体系下高端内存区不存在,自然也无永久内核映射
永久内核映射允许内核建立高端页框到内核地址空间(线性地址)的长期映射。它们使用主内核页表中一个专门的页表。地址存放在pkmap_page_table
。页表中的表项数由LAST_PKMAP
宏产生。页表照样含512或1024项,这取决于PAE是否被激活;因此,内核一次最多访问2MB或4MB的高端内存。该页表映射的线性地址从PKMAP_BASE
开始。pkmap_count
数组包含LAST_PKMAP
个计数器,pkmap_page_table
页表中的每一项都有一个。
计数器为0:对应的页表项没映射任何高端内存页框,且是可用的
计数器为1:对应的页表项没映射任何高端内存页框,但它不能使用,因为自从它最后一次使用以来,其相应的TLB表项还未被刷新。
计数器为n:相应的页表项映射一个高端内存页框,意味着正好有n-1个内核成分在使用这个页框。
为记录高端内存页框与永久内核映射包含的线性地址之间的关系,内核使用了page_address_htable
散列表。该表包含一个page_address_map
数据结构,用于为高端内存中的每一页框进行当前映射,该数据结构还包含一个指向页描述符的指针和分配给该页框的线性地址。
-
page_address–传入页框描述符线性地址,返回对应页框的线性地址
page_address
返回页框(物理地址)对应的线性地址,如页框在高端内存(线性地址)中且没被映射,则返回NULL
。这个函数接受一个页描述符指针page
(描述一个页框)作为参数,区分以下两种情况: (1).如页框不在高端内存(PG_highmem为0),则采用直接映射。则线性地址总是存在且是通过计算页框下标,然后将其转换成物理地址,最后根据相应的物理地址得到线性地址。 (2).如页框在高端内存(PG_highmem为1),该函数就到page_address_htable
散列表中查找。如在散列表中找到页框,page_address
就返回它的线性地址,否则返回NULL
。
-
kmap–建立永久内核映射。
void* kmap(struct page* page) {if(!PageHighMem(page))return page_address(page);return kmap_high(page); // 如页框确实属于高端内存,则调kmap_high }
void *kamp_high(struct page* page) {unsigned long vaddr;spin_lock(&kmap_lock);// 永久内核映射对所有处理器可见。防止多核并发,需加锁保护。vaddr = (unsigned long)page_address(page);// 查找哈希表if(!vaddr)vaddr = map_new_virtual(page);// 向哈希表插入,并返回线性地址pkmap_count[(vaddr-PKMAP_BASE) >> PAGE_SHIFT]++;// 通过线性地址找到索引spin_unlock(&kmap_lock);return (void*)vaddr; }
中断处理程序和可延迟函数不能调kmap
。kmap_high
通过调page_address
检查页框是否已经被映射。如不是,调map_new_virtual
把页框的物理地址插入到pkmap_page_table
的一个项,并在page_address_htable
中加入一个元素。然后,kmap_high
使页框的线性地址所对应的计数器加1来将调用该函数的新内核成分考虑在内。最后,kmap_high
释放kmap_lock
并返回对该页框进行映射的线性地址。
-
map_new_virtual–完成页表注册,完成哈希表注册 本质上执行两个嵌套循环:
for(;;) {int count;DECLARE_WAITQUEUE(wait, current);for(count = LAST_PKMAP; count > 0; --count)// 遍历所有表项{last_pkmap_nr = (last_pkmap_nr+1)&(LAST_PKMAP-1);// 后半部分搜索没找到可用表项时。先刷新,再从开始位置再搜索一遍if(!last_pkmap_nr){flush_all_zero_pkmaps();// 将使用者不存在的槽位清理腾出多余位置count = LAST_PKMAP;}// 找到可用槽位if(!pkmap_count[last_pkmap_nr]){unsigned long vaddr = PKMAP_BASE+(last_pkmap_nr<<PAGE_SHIFT);// 计算此位置对应线性地址set_pte(&(pkmap_page_table[last_pkmap_nr]), mk_pte(page/*页框物理地址*/, __pgprot(0x63)));// 设置页表。完成页表注册。pkmap_count[last_pkmap_nr] = 1;// 表示页表映射建立了。但此页表项映射的页框并没有使用者。set_page_address(page, (void*)vaddr);// 哈希表注册return vaddr;// 返回线性地址}}// 执行到这里,表示后半部分没搜索到可用表项,且刷新从头搜依然没搜到current->state = TASK_UNINTERRUPTIBLE;add_wait_queue(&pkmap_map_wait, &wait);// 向完成队列加入新的等待项spin_unlock(&kmap_lock);// 放弃cpu之前先释放锁。schedule();// 主动放弃cpu,让内核选择另一进程运行。// 走到这里,一定是其他进程腾出表项后,发现有人在等待空闲表项。所以,让等待者变为就绪,将进程重新加入cpu的可调度队列。// 某次调度,等待者被调度恢复后继续执行这里remove_wait_queue(&pkmap_map_wait, &wait);// 将自己从等待队列移除spin_lock(&kmap_lock);// 重新加锁if(page_address(page)) // 再次尝试页表注册,哈希表注册前,先检查,是否其他内核线程已经完成了注册工作。return (unsigned long)page_address(page);// 其他内核线程已经完成注册后,可以直接返回。 }
内循环中,函数扫描pkmap_count
中所有计数器直到找到一个空值。当在pkmap_count
中找到一个未使用项时,大的if代码块运行。这段代码确定该项对应的线性地址,为它在pkmap_page_table
页表中创建一个项,将count
置1,调set_page_address
插入一个新元素到page_address_htable
散列表,返回线性地址。
搜索从上次因调map_new_virtual
而跳出的地方开始。在pkmap_count
中搜索完最后一个计数器尚未找到空闲槽位时,又从下标为0计数器开始搜索。继续之前,map_new_virtual
调flush_all_zero_pkmaps
开始寻址计数器为1的另一趟扫描。每个值为1的计数器都表示在pkmap_page_table
中表项是空闲的,但不能使用,因为相应的TLB
表项还没被刷新。flush_all_zero_pkmaps
把它们的计数器重置为0,删除page_address_htable
散列表中对应的元素,并对pkmap_page_table
里的所有项上进行TLB
刷新。
如内循环在pkmap_count
中没找到空的计数器,map_new_virtual
就阻塞当前进程,直到某个进程释放了pkmap_page_table
页表中的一个表项,通过把current
插入到pkmap_map_wait
等待队列,把current
设置为TASK_UNINTERRUPTIBLE
,并调schedule
放弃CPU来达到此目的。一旦进程被唤醒,函数就调page_address
检查是否存在另一个进程已映射了该页。如还没其他进程映射该页,则内循环重新开始。
-
kunmap撤销先前由kmap建立的永久内核映射。如页确实在高端内存中,则调kunmap_high。
void kunmap_high(struct page* page) {spin_lock(&kmap_lock);// 这是检测此高端内存内页框释放后,此页框占据的页表表项是否没了使用者,进而可被清理后复用(用来服务于另一个页框)if((--pkmap_count[((unsigned long)page_address(page)-PKMAP_BASE)>>PAGE_SHIFT]) == 1){if(waitqueue_active(&pkmap_map_wait))// 检测等待队列上是否有等待对象wake_up(&pkmap_map_wait);//唤醒首个等待对象spin_unlock(&kmap_lock);} }
上述括号内的表达式从页的线性地址计算出pkmap_count
数组的索引。计数器被减1并与1相比。匹配成功表明没进程在使用页了。函数最终能唤醒由map_new_virtual
添加在等待队列中的进程。
在高端内存的任一页框都可通过一个"窗口"映射到内核地址空间。留给临时内核映射的窗口数是非常少的。
每个CPU有它自己的包含13个窗口的集合,它们用enum km_type
数据结构表示。该数据结构中定义的每个符号,如KM_BOUNCE_READ
,KM_USER0
或KM_PTE0
,标识了窗口的线性地址。内核必须确保同一窗口永不会被两个不同的控制路径同时使用。故,km_type
中的每个符号只能由一种内核成分使用,并以该成分命名。最后一个符号KM_TYPE_NR
本身并不表示一个线性地址,但由每个CPU
用来产生不同的可用窗口数。
在km_type
中的每个符号(除了最后一个)都是固定映射的线性地址的一个下标。enum fixed_address
数据结构包含符号FIX_KMAP_BEGIN
和FIX_KMAP_END
;把后者的值赋成下标FIX_KMAP_BEGIN+(KM_TYPE_NR*NR_CPUS)-1
。这种方式下,系统中的每个CPU
都有KM_TYPE_NR
个固定映射的线性地址。此外,内核用fix_to_virt(FIX_KMAP_BEGIN)
线性地址对应的页表项的地址初始化kmap_pte
变量。
1.kmap_atomic–建立临时内核映射。
void* kmap_atomic(struct page* page, enum km_type type) {enum fixed_address idx;unsigned long vaddr;current_thread_info()->preempt_count++;// 这样就禁止了内核抢占if(!PageHighMem(page))return page_address(page);idx = type + KM_TYPE_NR * smp_processor_id();// 取得正确索引vaddr = fix_to_virt(FIX_KMAP_BEGIN+idx);// 取得对应线性地址set_pte(kmap_pte-idx/* pte表项地址 */, mk_pte(page/* 页框描述符线性地址 */, 0x63));// 页表注册__flush_tlb_single(vaddr);// TLB刷新return (void*)vaddr; }
type
参数和CPU
标识符指定必须用哪个固定映射的线性地址映射请求页。如页框不属于高端内存,则该函数返回页框的线性地址;否则,用页的物理地址及Present,Accessed,Read/Write
和Dirty
位建立该固定映射的线性地址对应的页表项。最后,该函数刷新适当的TLB
项并返回线性地址。
2.kunmap_atomic–撤销临时内核映射。 在80x86结构中,这个函数减少当前进程的preempt_count
。因此,如在请求临时内核映射之前能抢占内核控制路径, 则在同一个映射被撤销后可再次抢占。此外,kunmap_atomic
检查当前进程的TIF_NEED_RESCHED
标志是否被置位。如是,就调schedule
。
伙伴系统算法
内核应为分配一组连续的页框建立一种健壮,高效的分配策略。频繁地请求和释放不同大小的一组连续页框,必然导致在已分配页框的块内分散了许多小块的空闲页框。
本质上,避免外碎片的方法有两种:
-
利用分页单元把一组非连续的空闲页框映射到连续的线性地址区间。
-
开发一种适当的技术来记录现存的空闲连续页框块的情况,以尽量避免为满足对小块的请求而分割大的空闲块。
基于以下三种原因,内核首选第二种方法:
-
某些情况下,连续的页框确实是必要的。因为仅连续的线性地址不足以满足请求。典型例子就是给DMA处理器分配缓存区的内存请求。因为当在一次单独的I/O操作中传送几个磁盘扇区的数据时,DMA忽略分页机制而直接访问地址总线(直接采用物理地址),故,所请求的缓冲区必须位于连续的页框中。
-
即使连续页框的分配并不是很必要,但它在保持内核页表不变方面所起的作用也不容忽视。在内核页表中,只需要为这些连续的页框创建一个条目,而不是为每个页框创建一个单独的条目。这可以减少内核页表的大小,并降低内存管理的开销。在查找页表时,操作系统只需要查找一个条目,而不是多个条目。操作系统只需要查找一个页表条目,就可以确定该虚拟地址对应的物理地址。连续页框的分配可以使得内存块更加连续和紧凑。这有助于提高内存利用率,因为操作系统可以更有效地管理和调度内存。修改页表会怎样?频繁修改页表势必导致平均访问内存次数增加,因为这会使CPU频繁刷新TLB(TLB不命中率提高)的内容。
-
内核通过4MB的页可访问大块连续的物理内存。这样减少了TLB失效率(TLB命中率提高),提高了访问内存的平均速度。
Linux采用著名的伙伴系统算法来解决外碎片。它把所有的空闲页框分组为11个块链表,每个块链表分别包含大小为1,2,4,8,16,32,64,128,256,512,1024
个连续的页框的块的集合。对1024个页框的最大请求对应着4MB大小的连续RAM块。伙伴系统保证每个块的第一个页框的物理地址是该块大小的整数倍。例如,大小为16个页框的块,其起始地址是16 \times 2^{12}的倍数
通过举例来说明算法的工作原理:连续页框块申请–假设申请256个连续页框
-
先在256个页框的链表中检查是否有一个空闲块。如存在,分配此块。
-
如没有,算法会查找下一个更大的页块。即在512个页框的链表中找一个空闲块。 如存在,内核把空闲块分为两部分。一半用作满足请求。另一半作为新块插入到256个页框的链表。
-
如在512个页框的块链表没找到空闲块,就继续在1024找。 如找到,内核把1024个页框块的划分为一个256个页框的块用于满足需求。剩余部分划分为一个256页框的新快,一个512页框的新快分别插入对应的链表。
-
如1024页框链表还没找到,算法就放弃并发出错误信号。(意味着连续页框分配最大只能一次分配4MB内存)、
连续页框块释放:
连续页框块释放时,内核会检查释放块是否可以现有空闲块合并成更大的空闲块。允许合并时,将参与合并的空闲块从链表移除,组成一个新块。对新的块持续如此迭代,直到迭代到无法合并时,块加入链表。
合并成立条件:
-
两个块有相同的大小,记作
b
。 -
它们的物理地址是连续的。
-
第一个块的第一个页框的物理地址是2 \times b \times 2^{12}的倍数。
1.数据结构
Linux 2.6为每个管理区使用不同的伙伴系统。因此,在80x86结构中,有三种伙伴系统:
-
第一种处理适合
ISA DMA
的页框。 -
第二种处理"常规"页框。
-
第三种处理高端内存页框。
每个伙伴系统使用的主要数据结构如下:
-
前面介绍过的
mem_map
数组。实际上,每个管理区都关系到mem_map
元素的子集。子集中的第一个元素和元素的个数分别由管理区描述符的zone_mem_map
和size
字段指定。 -
包含有
11
个元素,元素类型为free_area
的一个数组,每个元素对应一种特定块大小的链表。该数组存放在管理区描述符的free_area
字段中。
考虑管理区描述符中free_area
数组的第k
个元素,它标识所有大小为2^k个页框的空闲块。这个元素的free_list
字段是双向循环链表的头,这个双向循环链表集中了大小为2^k个页框的空闲块对应的页描述符。更精确地说,是空闲块中起始页框的页描述符;指向链表中相邻元素的指针存放在页描述符的lru
字段中。除了链表头外,free_area
数组的第k
个元素同样包含字段nr_free
,它指定了大小为2^k个页框的空闲块的个数。如没大小为2^k个页框的空闲块,则nr_free
等于0
且free_list
为空。
一个大小为2^k个页框的空闲块的第一个页框的描述符的private字段存放了块的order
,即k
。正是由于此字段,页块被释放时,内核可确定这个块的伙伴是否也空闲。如是,它可以把两个块结合成大小为2^{k + 1}页框的新块。
2.分配块
__rmqueue
–用来在管理区找到一个空闲块
-
参数:管理区描述符地址,
order
。order
表示请求的空闲页块大小的对数值。 -
返回值:如页框被成功分配,
__rmqueue
就返回第一个被分配页框的页描述符。否则, 返回NULL。
__rmqueue
假设调用者已经禁止了本地中断,并获得了保护伙伴系统数据结构的zone->lock
自旋锁。 从所请求order
的链表开始,它扫描每个可用块链表进行循环搜索,如需要搜索更大的order
,就继续搜索。
struct free_area* area; unsigned int current_order; for(current_order = order; current_order < 11; ++current_order) {area = zone->free_area + current_order;if(!list_empty(&area->free_list))goto block_found; } return NULL;
如直到循环结束还没找到合适的空闲块,则__rmqueue
就返回NULL
。否则,找到了一个合适的空闲块,这种情况下,从链表中删除它的第一个页框描述符,并减少管理区描述符中的free_pages
的值。
block_found:// 1.定位到链表首个有效元素// 2.链表首个有效元素是一个struct page对象的lru字段。// 3.从lru字段地址导出隶属的struct page对象起始地址page = list_entry(area->free_list.next, struct page, lru);// 从隶属的双向链表中移除该节点list_del(&page->lru);// 清理page的private字段ClearPagePrivate(page);// 暂时被设置为0page->private = 0;// 更新有效块数area->nr_free--;// 更新隶属管理区内空闲页框数zone->free_pages -= 1UL << order;
当为了满足2^h个页框的请求而有必要使用2^k个页框的块时(h<k
), 程序就分配前面的2^h个页框,把后面2^k - 2^h个页框循环再分配给free_area
链表中下标在h
到k
之间的元素:
// 这是获得得到块尺寸 size = 1 << curr_order; while(curr_order > order) {// 规模小一级空闲块area--;// 规模curr_order--;// 页数size >>= 1;// page是分配出去的块的首个页框。page+size得到剩余可放入当前规模块链表的起始页框buddy = page + size;// 将该页框放入当前规模块链表list_add(&buddy->lru, &area->free_list);// 规模块中可用块数量更新area->nr_free++;// 设置该page的private以记录其隶属的块的规模buddy->private = curr_order;// 设置page的标志。来表示其private字段有效。SetPagePrivate(buddy); } return page;// 被分配出去的块的首个page的private字段无效
因为__rmqueue
已经找到了合适的空闲块,所以它返回所分配的第一个页框对应的页描述符的地址page
。 上述分配过程看,每次分配页框会被规整到2的幂次后再执行页框分配(造成分配时内部碎片,牺牲容量,换取性能优化)。
3.释放块
__free_pages_bulk–按伙伴系统的策略释放页框
参数:
-
page:被释放块中所包含的第一个页框描述符的地址
-
zone:管理区描述符的地址
-
order:块大小的对数
函数假设调用者已禁止本地中断(防止外部中断打断执行流程)并获得了保护伙伴系统数据结构的zone->lock
(防止其他处理器打断执行流程)自旋锁。__free_pages_bulk
先声明和初始化一些局部变量:
struct page* base = zone->zone_mem_map; unsigned long buddy_idx, page_idx = page - base; struct page* buddy, *coalesced; int order_size = 1 << order;// 页数
page_idx
包含块中第一个页框的下标,这是相对于管理区中第一个页框而言的。order_size
用于增加管理区中空闲页框的计数器:
zone->free_pages += order_size;
现在函数开始执行循环,最多循环(10-order
)次,每次都尽量把一个块和它的伙伴进行合并。函数以最小块开始,然后向上移动到顶部:
while(order < 10) {// order是当前规模// 这里的意思是将page_idx的二进制下第order位取反。// 若此位之前是1,buddy_idx此位是0。这样取得前一个buddy。因为只有前一个buddy才能作为合并后buddy的起始部分。对齐要求。// 若此位之前是0,buddy_idx此位是1。这样取得后一个buddy。此时只有自己才能作为合并后buddy的起始部分。对齐要求。buddy_idx = page_idx ^ (1 << order);buddy = base + buddy_idx;// 验证此page是否符合作为规模为order的buddy块首个page的条件if(!page_is_buddy(buddy, order))break;list_del(&buddy->lru);// 将此buddy块从隶属的双向链表移除zone->free_area[order].nr_free--;// 更新本来隶属的规模中可有块数量// 清理此块首个page的privateClearPagePrivate(buddy);buddy->private = 0;// 确定合并块的首个page的索引。// page_idx的二进制下第order位// 若此位之前是1,buddy_idx此位是0。// 这样合并后块内首个page索引,取buddy_idx// 若此位之前是0,buddy_idx此位是1。// 这样合并后块内首个page索引,取page_idx// page_idx &= buddy_idx得到的结果其余位和page_idx一致。但第order位固定为0。符合上述要求。 page_idx &= buddy_idx;// 这样我们得到了规模为order+1的块及块内首个page。继续迭代。order++; }
在循环体内,函数寻找块的下标buddy_idx
,它是拥有page_idx
页描述符下标的块的伙伴。结果这个下标可被简单地如下计算:
buddy_idx = page_idx ^ (1 << order);
实际上,使用(1<<order
)掩码的异或转换page_idx
第order
位的值。 因此,如这个位原先是0
,buddy_idx
就等于page_idx+order_size
;如这个位原先是1
,buddy_idx
就等于page_idx - order_size
。 一旦知道了伙伴块下标,就可通过下式很容易获得伙伴块的页描述符:
buddy = base + buddy_idx;
现在调page_is_buddy
来检查buddy是否描述了大小为order_size
的空闲页框块的第一个页。
int page_is_buddy(struct page* page, int order) {if(PagePrivate(buddy) && page->private == order && !PageReserved(buddy) && page_count(page) == 0)return 1;return 0; }
buddy
的第一个页必须为空闲(_count
等于-1
),它必须属于动态内存,它的private
字段必须有意义,最后private
字段必须存放将要被释放的块的order
。如所有这些条件都符合,伙伴块就被释放,且函数将它从以order
排序的空闲块链表上删除,并再执行一次循环以寻找两倍大小的伙伴块。如page_is_buddy
中至少有一个条件没被满足,则该函数跳出循环,因为获得的空闲块不能再和其他空闲块合并。函数将它插入适当的链表并以块大小的order
更新第一个页框的private
。
// 得到最终合并块的首个page coalesced = base + page_idx; // 设置其private coalesced->private = order; SetPagePrivate(coalesced); // 将page加入对应规模块的双向链表 list_add(&coalesced->lru, &zone->free_area[order].free_list); zone->free_area[order].nr_free++;// 更新对应规模内有效块数