目录
2.释放页框到每CPU页框高速缓存–位于伙伴系统和页框使用者的中间层
11.空闲Slab对象的本地高速缓存–slab分配器和内存申请使用者的中间层
15.内存池–使用者可以直接与kmem_cache交互,也可与mempool_t交互
每CPU页框高速缓存
内核经常请求和释放单个页框。为提升系统性能,每个内存管理区定义了一个"每CPU"页框高速缓存。所有"每CPU"高速缓存包含一些预先分配的页框,它们被用于满足本地CPU发出的单一内存请求。
实际上,这里为每个内存管理区和每个CPU提供了两个高速缓存: 一个热高速缓存,它存放的页框中所包含的内容很可能就在CPU硬件高速缓存中;还有一个冷高速缓存。
如内核或用户态进程在刚分配到页框后就立即向页框写,那么从热高速缓存中获得页框就对系统性能有利。实际上,每次对页框存储单元的访问都会导致从页框中给硬件高速缓存"窃取"一行。当然,除非硬件高速缓存包含有一行:它映射刚被访问的"热"页框单元。反过来,如页框将要被DMA操作填充,则从冷高速缓存中获得页框是方便的。在这种情况下,不会涉及到CPU,且硬件高速缓存的行不会被修改。
实现每CPU页框高速缓存的主要数据结构是存放在内存管理区描述符的pageset
字段中的一个per_cpu_pageset
数组数据结构。该数组包含为每个CPU提供的一个元素;这个元素依次由两个per_cpu_pages
描述符组成,一个留给热高速缓存,另一个留给冷高速缓存。
per_cpu_pages
描述符的字段
类型 | 名称 | 描述 |
---|---|---|
int | count | 高速缓存中的页框个数 |
int | low | 下界,表示高速缓存需要补充 |
int | high | 上界 |
int | batch | 在高速缓存中将要添加或被删去的页框个数 |
struct list_head | list | 高速缓存中包含的页框描述符链表 |
内核使用两个位来监视热高速缓存和冷高速缓存的大小:如页框个数低于下界low,内核通过从伙伴系统中分配batch个单一页框来补充对应的高速缓存。否则,如页框个数高于上界high,内核从高速缓存中释放batch个页框到伙伴系统。值batch,low,high本质上取决于内存管理区中包含的页框个数。
1.通过每CPU页框高速缓存分配页框
buffered_rmqueue–在指定的内存管理区中分配页框,它使用每CPU
页框高速缓存来处理单一页框请求。 参数:
-
内存管理区描述符的地址,
-
请求分配的内存大小的对数
order
, -
分配标志
gfp_flags
。
如pfp_flags
中的__GFP_COLD
标志被置位,则页框应当从冷高速缓存中获取,否则,它应从热高速缓存中获取。(此标志只对单一页框分配有意义)
函数操作:
-
如
order
不等于0
,每CPU
页框高速缓存就不能被使用;跳到4 -
检查由
__GFP_COLD
标志所标识的内存管理区本地每CPU
高速缓存是否需补充。这种情况下,它执行:-
通过反复调
__rmqueue
从伙伴系统中分配batch
个单一页框 -
将已分配页框的描述符插入高速缓存链表
-
通过给
count
增加实际被分配页框来更新它
-
-
如
coun
t为正,则函数从高速缓存链表获得一个页框,count
减1跳到5。 -
这里,内存请求还没被满足,或是因为请求跨越了几个连续页框,或是因为被选中的页框高速缓存为空,调
__rmqueue
从伙伴系统分配所请求的页框。 -
如内存请求得到满足,初始化第一个页框的页描述符:清除一些标志,将
private
置为0,将页框引用计数器置1。如gfp_flags
中__GPF_ZERO
被置位,则函数将被分配的内存区域填充0。返回(第一个)页框的页描述符地址。 -
如内存分配请求失败,则返回NULL。
2.释放页框到每CPU页框高速缓存–位于伙伴系统和页框使用者的中间层
free_hot_page–释放单个页框到每CPU页框热高速缓存 free_cold_page–释放单个页框到每CPU页框冷高速缓存
它们都是free_hot_cold_page
的封装。 参数:
-
将要释放的页框的描述符地址
page
, -
cold
标志;
free_hot_cold_page
操作:
-
从
page->flags
获取包含该页框的内存管理区描述符地址 -
获取由
cold
标志选择的管理区高速缓存的per_cpu_pages
描述符地址 -
检查高速缓存是否应被清空:如
count
高于或等于high
,则调free_pages_bulk
,将管理区描述符,将被释放的页框个数(batch
),高速缓存链表的地址及数字 0 传递给该函数。free_pages_bulkl
依次反复调__free_pages_bulk
来释放指定数量的页框到内存管理区的伙伴系统中。 -
把释放的页框添加到高速缓存链表上,增加
count
。
热高速缓存中页框,适合供cpu使用。冷高速缓存中页框,适合供DMA
使用。
管理区分配器
管理区分配器是内核页框分配器的前端。该构建必须分配一个包含足够多空闲页框的内存区,使它能满足内存请求。
-
管理区分配器必须满足几个目标:
-
应当保护保留的页框池
-
当内存不足且允许阻塞当前进程时它应触发页框回收算法。一旦某些页框被释放,管理区分配器将再次尝试分配。
-
如可能,它应保存小而珍贵的
ZONE_DMA
内存管理区。例如,如是对ZONE_NORMAL
或ZONE_HIGHMEM
页框的请求,则管理区分配器会不太愿意分配ZONE_DMA
内存管理区中的页框。对一组连续页框的每次请求实质上是通过执行alloc_pages
宏来处理的。接着,这个宏又依次调__alloc_pages
,该函数是管理区分配器的核心。
1.__alloc_pages–通过内存管理区执行页框分配 __alloc_pages
参数:
-
gfp_mask–在内存分配请求中指定的标志。
-
order–将要分配的一组连续页框数量的对数。
-
zonelist–指向zonelist数据结构的指针,该数据结构按优先次序描述了适用于内存分配的内存管理区。
__alloc_pages
扫描包含在zonelist
数据结构中的每个内存管理区。
for(i = 0; (z = zonelist->zones[i]) != NULL; i++) { if(zone_watermark_ok(z, order, ...)) { page = buffered_rmqueue(z, order, gfp_mask); if(page) return page; } }
对每个内存管理区,该函数将空闲页框的个数与一个阈值作比较,该阈值取决于内存分配标志、当前进程的类型、管理区被函数检查过的次数。实际上, 如空闲内存不足,则每个内存管理区一般会被检查几遍,每一遍在所请求的空闲内存最低量的基础上使用更低的阈值扫描。
zone_watermark_ok
接收几个参数,它们决定内存管理区中空闲页框个数的阈值min。特别是,如满足下列两个条件则函数返回1。
-
除被分配的页框外,在内存管理区中至少还有
min
个空闲页框。不包括为内存不足保留的页框。 -
除了被分配的页框外,这里在
order
至少为k
的块中起码还有min/2^{k}个空闲页框,对于k
,取值在1和分配的order
之间。
阈值min
的值由zone_watermark_ok
确定:
-
作为参数传递的基本值可是内存管理区界值
pages_min
,pages_low
和pages_high
中的任意一个。 -
作为参数传递的
gfp_high
标志被置位,则base
值被2
除。通常,如gfp_mask
中的__GFP_WAIT
标志被置位(即能从高端内存中分配页框),则这个标志等于1。 -
如作为参数传递的
can_try_harder
被置位,则阈值将会再减少四分之一。如gfp_mask
中的__GFP_WAIT
被置位,或如当前进程是一个实时进程且在进程上下文中(在中断处理程序和可延迟函数之外)已经完成了内存分配,则can_try_harder
标志等于1。
__alloc_pages
执行:
-
执行对内存管理区的第一次扫描 第一次扫描中,阈值
min
被设为z->pages_low
其中z指向正被分析的管理区描述符。 -
如函数在上一步没终止,则没剩下多少空闲内存:函数唤醒
kswapd
内核线程来异步地开始回收页框。 -
执行对内存管理区的第二次扫描,将值
z->pages_min
作为阈值base
传递。实际阈值由can_try_harder
和gfp_high
决定。 -
如函数在上一步没终止,则系统内存肯定不足。如产生内存分配请求的内核控制路径不是一个中断处理程序或一个可延迟函数,且它试图回收页框(或是
current
的PF_MEMALLOC
被置位,或它的PF_MEMDIE
被置位),则函数随即执行对内存管理区的第三次扫描,试图分配页框并忽略内存的阈值。即不调zone_watermark_ok
。唯有这种情况下才允许内核控制路径耗用为内存不足预留的页。这种情况下,产生内存请求的内核控制路径最终将试图释放页框,因此只要有可能它就应得到它所请求的。如没有任何内存管理区包含足够的页框,函数返回NULL
来提示调用者发生了错误。 -
这里,正调用的内核控制路径并没试图回收内存。如
gfp_mask
的__GFP_WAIT
没被置位,函数就返回NULL
来提示该内核控制路径内存分配失败:此时,如不阻塞当前进程就没办法满足请求。 -
在这里当前进程能被阻塞:调
cond_resched
检查是否有其他的进程需CPU
。 -
设置
current
的PF_MEMALLOC
来表示进程已经准备好执行内存回收 -
将一个执行
reclaim_state
数据结构的指针存入current->reclaim_state
。这个数据结构只包含一个字段reclaimed_slab
。 -
调
try_to_free_pages
寻找一些页框来回收。函数可能阻塞当前进程。一旦函数返回,__alloc_pages
就重设current
的PF_MEMALLOC
并再次调cond_resched
。 -
如上一步已释放了一些页框,则函数还要执行一次与3步相同的内存管理区扫描。如内存分配请求不能被满足,则函数决定是否应继续扫描内存管理区;如
__GFP_NORETRY
被清除,且内存分配请求跨越了多达8
个页框或__GFP_REPEAT
和__GFP_NOFAIL
其中之一被置位,则函数就调blk_congestion_wait
使进程休眠一会儿,跳回6
。否则,返回NULL
。 -
如
9
没释放任何页框,就意味着内核遇到很大的麻烦。如允许内核控制路径依赖于文件系统的操作来杀死一个进程且__GFP_NORETRY
为0
,则执行:-
使用等于
z->pages_high
的阈值再一次扫描内存管理区 -
调
out_of_memory
通过杀死一个进程开始释放一些内存 -
跳回
1
。
-
2.__free_pages–通过内存管理区执行释放页框 参数:
-
将要释放的第一个页框的页描述符的地址,
-
将要释放的一组连续页框的数量的对数。
步骤:
-
检查第一个页框是否真正属于动态内存(
PG_reserved
清0);如不是,终止。 -
减少
page->count
;如仍大于或等于0
,终止。 -
如
order
等于0
,则调free_hot_base
来释放页框给适当内存管理区的每CPU
热高速缓存。 -
如
order
大于0
,则它将页框加入到本地链表中,调free_pages_bulk
把它们释放到适当的内存管理区的伙伴系统中。
内存高速缓存
伙伴系统算法采用页框作为基本内存区,这适合于对大块内存的请求,如何处理对小内存区的请求? 引入一种新的数据结构来描述在同一个页框中如何分配小内存区。
内核建立了13个按几何分布的空闲内存区链表,它们的大小从32字节到131072字节。
1.内存高速缓存层次
内存高速高速缓存包含多个slab,每个slab由一个或多个连续的页框组成。这些页框中既包含已分配的对象,也包含空闲的对象。内核周期性地扫描高速缓存并释放空slab对应的页框。
2.内存高速缓存描述符
每个内存高速缓存由kmem_cache_t
类型的数据结构来描述。
类型 | 名称 | 说明 |
---|---|---|
struct array_cache*[] | array | 每CPU指针数组指向包含空闲对象的本地高速缓存 |
unsigned int | batchcount | 要转移进本地高速缓存或从本地高速缓存中转出的大批对象的数量 |
unsigned int | limit | 本地高速缓存中空闲对象的最大数目。 |
struct kmem_list3 | lists | 参见下一个表 |
unsigned int | objsize | 高速缓存中包含的对象的大小 |
unsigned int | flags | 描述高速缓存永久属性的一组标志 |
unsigned int | num | 封装在一个单独slab中的对象个数 |
unsigned int | free_limit | 整个slab高速缓存中空闲对象的上限 |
spinlock_t | spinlock | 高速缓存自旋锁 |
unsigned int | gfporder | 一个单独slab中包含的连续页框数目的对数 |
unsigned int | gfpflags | 分配页框时传递给伙伴系统函数的一组标志 |
size_t | colour | slab使用的颜色个数 |
unsigned int | colour_off | slab中的基本对齐偏移 |
unsigned int | colour_next | 下一个被分配的slab使用的颜色 |
kmem_cache_t* | slabp_cache | 指针指向包含slab描述符的普通slab高速缓存 |
unsigned int | slab_size | 单个slab的大小 |
unsigned int | dflags | 描述高速缓存动态属性的一组标志 |
void* | ctor | 指向与高速缓存相关的构造方法的指针 |
void* | dtor | 指向与高速缓存相关的析构方法的指针 |
const char* | name | 存放高速缓存名字的字符数组 |
struct list_head | next | 高速缓存描述符双向链表使用的指针 |
kmem_cache_t
描述符的lists
是一个结构体
类型 | 名称 | 说明 |
---|---|---|
struct list_head | slabs_partial | 包含空闲和非空闲对象的slab描述符双向循环链表 |
struct list_head | slabs_full | 不包含空闲对象的slab描述符双向循环链表 |
struct list_head | slabs_free | 只包含空闲对象的slab描述符双向循环链表 |
unsigned long | free_objects | 高速缓存中空闲对象的个数 |
int | free_touched | 由slab分配器的页回收算法使用 |
unsigned long | next_reap | 由slab分配器的页回收算法使用 |
struct array_cache* | shared | 指向所有CPU共享的一个本地高速缓存的指针 |
3.slab描述符
类型 | 名称 | 说明 |
---|---|---|
struct list_head | list | slab描述符的三个双向循环链表中的一个 |
unsigned long | colouroff | slab中第一个对象的偏移 |
void* | s_mem | slab中第一个对象的地址 |
unsigned int | inuse | 当前正使用的slab中的对象个数 |
unsigned int | free | slab中下一个空闲对象的下标,如没剩下空闲对象则为BUFCTL_END |
slab
描述符可存放在两个可能的地方: 外部slab
描述符–存放在slab
外部,位于cache_sizes
指向的一个不适合ISA DMA
的普通高速缓存中。 内部slab
描述符–存放在slab
内部,位于分配给slab
的第一个页框的起始位置
当对象小于512MB
,或当内部碎片为slab
描述符和对象描述符在slab
中留下足够的空间时,slab
分配器选第二种方案。 如slab
描述符存放在slab
外部,则高速缓存描述符的flags
中CFLAGS_OFF_SLAB
置1
。
4.普通和专用高速缓存
高速缓存被分为两种类型:普通和专用。 普通高速缓存只由slab分配器用于自己的目的,专用高速缓存由内核的其余部分使用。
普通高速缓存是: 1.第一个高速缓存叫kmem_cache
,包括由内核使用的其余高速缓存的高速缓存描述符。cache_cache
变量包含第一个高速缓存的描述符。 2.另外一些高速缓存包含用作普通用途的内存区。
内存区大小的范围一般包括13
个几何分布的内存区。一个叫malloc_sizes
的表分分别指向26
个高速缓存描述符,与其相关的内存区大小为32,64,128,256,512,1024,2048,4096,8192,16384,32768,65536
和131072
字节。对每种大小,都有两个高速缓存:一个适用于ISA DMA
分配,另一个适用于常规分配。
在系统初始化期间调kmem_cache_init
来建立普通高速缓存。 专用高速缓存由kmem_cache_create
创建。函数从cache_cache
普通高速缓存中为新的高速缓存分配一个高速缓存描述符,插入到高速缓存描述符的cache_chain
。 kmem_cache_destroy
撤销一个高速缓存,并将它从cache_chain链表上删除。
kmem_cache_shrink
通过反复调slab_destroy
来撤销高速缓存中所有的slab
。 所有普通和专用高速缓存的名字都可在运行期间通过读/proc/slabinfo
得到。这个文件也指明每个高速缓存中空闲对象的个数和已分配对象的个数。
5.为slab分配页框,释放页框
kmem_getpages–slab的页框获取 参数: cachep
–指向需额外页框的高速缓存的高速缓存描述符,请求页框的个数由存放在cache->gfporder
中的order
决定。 flags
–说明如何请求页框。这组标志与存放在高速缓存描述符的gfpflags
中的专用高速缓存分配标志相结合。
在UMA系统上该函数本质上等价于
void* kmem_getpages(kmem_cache_t* cachep, int flags) { struct page* page; int i; flags |= cachep->gfpflags; page = alloc_pages(flags, cachep->gfporder); if(!page) return NULL; i = (1 << cache->gfporder); if(cachep->flags & SLAB_RECLAIM_ACCOUNT) atomic_add(i, &slab_reclaim_pages); while(i--) SetPageSlab(page++); return page_address(page); }
如已创建了slab
高速缓存且SLAB_RECLARM_ACCOUNT
标志已经置位,则内核检查是否有足够的内存来满足一些用户态请求时,分配给slab
的页框将被记录为可回收的页。函数还将所分配页框的页描述符中的PG_slab
标志置位。
kmem_freepages–释放分配给slab的页框
void kmem_freepages(kmem_cache_t* cachep, void* addr) { unsigned long i = (1 << cachep->gfporder); struct page* page = virt_to_page(addr); if(current->reclaim_state) current->reclaim_state->reclaimed_slab += i; while(i--) ClearPageSlab(page++); free_pages((unsigned long)addr, cachep->gfporder); if(cachep->flags & SLAB_RECLAIM_ACCOUNT) atomic_sub(1 << cachep->gfporder, &slab_reclaim_pages); }
函数从线性地址addr
开始释放页框,这些页框曾分配给由cachep
标识的高速缓存中的slab
。如当前进程正在执行内存回收,reclaim_state
结构的reclaimed_slab
就被适当地增加,于是刚被释放的页就能通过页框回收算法被记录下来。此外,如SLAB_RECLAIM_ACCOUNT
标志置位,slab_reclaim_pages
则被适当地减少。
6.内存给高速缓存分配slab
一个新创建的高速缓存没有包含任何slab
,因此也没空闲的对象。只有当以下两个条件都为真时,才给高速缓存分配slab
。
-
已发出一个分配新对象的请求
-
高速缓存不包含任何空闲对象
这些情况发生时,通过调cache_grow
给高速缓存分配一个新的slab
。这个函数调kmem_getpages
从分区页框分配器获得一组页框来存放一个单独的slab
,然后又调alloc_slabmgmt
获得一个新的slab
描述符。如高速缓存描述符的CFLGS_OFF_SLAB
置位,则从高速缓存描述符的slabp_cache
字段指向的普通高速缓存中分配这个slab
描述符,否则,从slab
的第一个页框中分配这个slab
描述符。
给定一个页框,内核需确定它是否被slab
分配器使用。如是, 迅速得到相应高速缓存和slab
描述符地址。故,cache_grow
扫描分配给新slab
的页框的所有页描述符,将高速缓存描述符和slab
描述符的地址分别赋给页描述符中lru
的next
和prev
。只有当页框空闲时伙伴系统的函数才会使用lru
。所以,lru
不会误用。分配给slab
的页框设置PG_slab
标志。
在高速缓存中给定一个slab
,可通过使用slab
描述符的s_mem
和高速缓存描述符的gfporder
来找到依赖的页框描述符。接着,cache_grow
调cache_init_objs
,将构造方法应用到新slab
包含的所有对象上。最后,cache_grow
调list_add_tail
将新的slab
描述符添加到高速缓存描述符*cachep
的slab
链表末端,并更新高速缓存中的空闲对象计数器
list_add_tail(&slab->list, &cachep->lists->slabs_free); cachep->lists->free_objects += cachep->num;
7.从高速缓存中释放slab
在两种条件下才能撤销slab:
-
内存高速缓存中有太多的空闲对象
-
被周期性调用的定时器函数确定是否有完全未使用的slab能被释放。
在两种情况下,调slab_destroy
撤销一个slab
,并释放相应的页框到分区页框分配器。
void slab_destroy(kmem_cache_t *cachep, slab_t *slabp) { if(cachep->dtor) { int i; for(i = 0; i < cachep->num; i++) { void* objp = slabp->s_mem + cachep->objsize * i; (cachep->dtor)(objp, cachep, 0); } } kmem_freepages(cachep, slabp->s_mem - slabp->colouroff); if(cachep->flags & CFLAGS_OFF_SLAB) kmem_cache_free(cachep->slabp_cache, slabp); }
检查高速缓存是否为它的对象提供了析构,如是,使用析构方法释放slab
中所有对象。objp
记录当前已检查的对象。kmem_freepages
把slab
使用的所有连续页框返回给伙伴系统。如slab
描述符存放在slab
外面,就从slab
描述符的高速缓存释放这个slab
描述符。实际上,该函数稍微复杂些。如,可使用SLAB_DESTROY_BY_RCU
来创建slab
高速缓存,这意味着应使用call_rcu
注册一个回调来延期释放slab
。回调函数接着调kmem_freepages
,也可能调kmem_cache_free
。
8.对象描述符
每个对象有类型为kmem_bufctl_t
的一个描述符,对象描述符存放在一个数组中,位于相应的slab
描述符后。类似slab
描述符,slab
的对象描述符也可用两种可能的方式来存放:
-
外部对象描述符–存放在
slab
的外面,位于高速缓存描述符的slabp_cache
字段指向的一个普通高速缓存中。内存区的大小取决于在slab
中所存放的对象个数。 -
内部对象描述符–存放在
slab
内部,正好位于描述符所描述的对象前
对象描述符只不过是一个无符号整数,只有在对象空闲时才有意义。它包含的是下一个空闲对象在slab中的下标。因此实现了slab内部空闲对象的一个简单链表。空闲对象链表中的最后一个元素的对象描述符用常规值BUFCTL_END
标记。
9.对齐内存中的对象
slab
分配器所管理的对象可在内存中进行对齐。即存放它们的内存单元的起始物理地址是一个给定常量的倍数。通常是2
的倍数,这个常量就叫对齐因子。slab
分配器所允许的最大对齐因子是4096
,即页框大小。
通常,如内存单元的物理地址是字大小(即计算机内部内存总线宽度)对齐的,则微机对内存单元的存取会非常快。因此,缺省下,kmem_cache_create
根据BYTES_PER_WORD
宏所指定的字大小来对齐对象。对于,80x86
处理器,这个宏产生值4
。当创建一个新的slab
高速缓存时,就可让它所包含的对象在第一级高速缓存中对齐。为做到这点,设置SLAB_HWCACHE_ALIGN
标志。
kmem_cache_create
按如下方式处理请求:
-
如对象的大小大于高速缓存行的一半,就在
RAM
中根据L1_CACHE_BYTES
的倍数对齐对象 -
否则,对象的大小就是
L1_CACHE_BYTES
的因子取整。这可保证一个小对象不会横跨两个高速缓存行。
10.slab着色
同一硬件高速缓存行可映射RAM
中很多不同的块。相同大小的对象倾向于存放在高速缓存内相同的偏移量处。在不同slab
内具有相同偏移量的对象最终很可能映射在同一高速缓存行中。高速缓存的硬件可能因此而花费内存周期在同一高速缓存行与RAM
内存单元之间来来往往传送两个对象,而其他的高速缓存行并未充分使用。
slab
分配器通过一种叫slab
着色的策略,尽量降低高速缓存的这种不愉快行为:把叫做颜色的不同随机数分配给slab
。我们考虑某个高速缓存,它的对象在RAM
中被对齐。意味着对象的地址肯定是某个给定正数值的倍数。连对齐的约束也考虑在内,在slab
内放置对象就有很多种可能的方式。方式的选择取决于对下列变量所作的决定:
-
num
–可在slab中存放的对象个数 -
osize
–对象的大小。包括对齐的字节。 -
dsize
–slap描述符的大小加上所有对象描述符的大小,就等于硬件高速缓存行大小的最小倍数。如slab描述符和对象描述符都放在slap外部,这个值等于0。 -
free
–在slab内未用字节个数
一个slab
中的总字节长度=(num*osize)+dsize+free
。free
总是小于osize
,不过可大于aln
。slab
分配器利用空闲未用的字节free
来对slab
着色。术语着色只是用来再细分slab
,并允许内存分配器把对象展开在不同的线性地址中。这样的话,内核从微处理器的硬件高速缓存中可获得最好性能。具有不同颜色的slab
把slab
的第一个对象存放在不同的内存单元,同时满足对齐约束。
可用颜色的个数是free/aln
(这个值存放在高速缓存描述符的colour
字段)。故,第一个颜色表示0
,最后一个颜色表示为(free/aln)-1
。一种特殊的情况是,如free
比aln
小,则colour
被设为0
,不过所有slab
都使用颜色0
,故颜色的真正个数是1
。
如用颜色col
对一个slab
着色,则,第一个对象的偏移量(相对于slab
的起始地址)就等于col*aln+dsize
。着色本质上导致把slab
中的一些空闲区域从末尾移到开始。只有当free
足够大时,着色才起作用。显然,如对象没请求对齐,或如果slab
内的未使用字节数小于所请求的对齐(free<=aln
),则唯一可能着色的slab
就是具有颜色0
的slab
,即,把这个slab
的第一个对象的偏移量赋为0
。
通过把当前颜色存放在高速缓存描述符的colour_next
字段,就可在一个给定对象类型的slab
之间平等地发布各种颜色。 cache_grow
把colour_next
所表示的颜色赋给一个新的slab
,并递增这个字段的值。当colour_next
的值变为colour
后,又从0
开始。这样,每个新创建的slab
都与前一个slab
具有不同的颜色,直到最大可用颜色。此外,cache_grow
从高速缓存描述符的colour_off
字段获得值aln
,根据slab
内对象的个数计算dsize
,最后把col*aln+dsize
的值存放到slab
描述符的colouroff
字段中。
11.空闲Slab对象的本地高速缓存–slab分配器和内存申请使用者的中间层
Linux 2.6
对多处理器系统上slab
分配器的实现不同于Solaris 2.4
最初实现。为减少处理器之间对自旋锁的竞争并更好利用硬件高速缓存,slab
分配器的每个高速缓存包含一个被称作slab
本地高速缓存的每CPU
数据结构,该结构由一个指向被释放对象的小指针数组组成。slab
对象的大多数分配和释放只影响本地数组,只有在本地数组下溢或上溢时才涉及slab
数据结构。类似前面的每CPU
页框高速缓存。高速缓存描述符的array
字段是一组指向array_cache
数据结构的指针,系统中的每个CPU
对应于一个元素。每个array_cache
数据结构是空闲对象的本地高速缓存的一个描述符。
类型 | 名称 | 说明 |
---|---|---|
unsigned int | avail | 指向本地高速缓存中可使用对象的指针的个数。同时作为高速缓存中第一个空槽的下标 |
unsigned int | limit | 本地高速缓存的大小。即本地高速缓存中指针的最大个数 |
unsigned int | batchcount | 本地高速缓存重新填充或腾空时使用的块大小 |
unsigned int | touched | 如本地高速缓存最近已被使用过,则该标志设为1 |
本地高速缓存描述符并不包含本地高速缓存本身的地址;事实上,它正好位于描述符之后。当然,本地高速缓存存放的是指向已释放对象的指针。对象本身总是位于高速缓存的slab
中。
当创建一个新的slab
高速缓存时,kmem_cache_create
决定本地高速缓存的大小(将这个值存放在高速缓存描述符的Limit
字段),分配本地高速缓存,将它们的指针存放在高速缓存描述符的array
字段。batchcount
字段的初始值,即从一个本地高速缓存的块里添加或删除的对象的个数,被初始化为本地高速缓存大小的一半。
在多处理器系统中,slab
高速缓存含一个附加的本地高速缓存,它的地址被存放在高速缓存描述符的lists.shared
中。共享的本地高速缓存正如它的名字暗示那样,被所有CPU
共享,它使得将空闲对象从一个本地高速缓存移动到另一个高速缓存的任务更容易。它的初始大小等于batchcount
字段值的8
倍。
12.分配slab对象
通过调kmem_cache_alloc
可获得新对象。参数cachep
指向高速缓存描述符,新空闲对象必须从该高速缓存描述符获得,参数flag
表示传递给分区页框分配器函数的标志。该高速缓存的所有slab
应是满的
void* kmem_cache_alloc(kmem_cache_t* cachep, int flags) { unsigned long save_flags; void* objp; struct array_cache* ac; local_irq_save(save_flags);// 禁止本cpu上外部中断,保存标志信息 ac = cache_p->array[smp_processor_id()];// 从内存高速缓存中取得当前cpu的本地高速缓存 if(ac->avail) { ac->touched = 1; objp = ((void**)(ac+1))[--ac->avail]; } else objp = cache_alloc_refill(cache_p, flags); local_irq_restore(save_flags);// 恢复中断设置,恢复标志信息 return objp; }
先试图从本地高速缓存获得一个空闲对象。如有,avail
就包含指向最后被释放的对象的项在本地高速缓存中的下标。 因为本地高速缓存数组正好存放在ac
描述符后面。故, ((void**)(ac+1))[--ac->avail];
获得空闲对象地址,递减ac->avail
。 当本地高速缓存没空闲对象时,cache_alloc_refill
重新填充本地高速缓存并获得一个空闲对象。
cache_alloc_refill:
-
将本地高速缓存描述符地址放在
ac
局部变量ac = cachep->array[smp_processor_id()]
-
获得
cachep->spinlock
-
如
slab
高速缓存包含共享本地高速缓存,且该共享本地高速缓存包含一些空闲对象,就通过从共享本地高速缓存中上移ac->batchcount
个指针来重新填充CPU
的本地高速缓存。跳6
。 -
试图填充本地高速缓存,填充值为高速缓存的
slab
中包含的多达ac->batchcount
个空闲对象的指针-
查看高速缓存描述符的
slabs_partial
和slabs_free
,获得slab
描述符的地址slabp
,该slab
描述符的相应slab
或部分被填充,或为空。如不存在这样的描述符,跳5
。
-
对
slab
中的每个空闲对象,增加slab
描述符的inuse
,将对象的地址插入本地高速缓存,更新free
使得它存放了slab
中下一空闲对象下标
slabp->inuse++; ((void**)(ac+1))[ac->avail++] = slabp->s_mem + slabp->free * cachep->obj_size; slabp->free = ((kmem_bufctl_t*)(slabp+1))[slabp->free];
-
如必要,将清空的
slab
插入到适当的链表上,可以是slab_full
,也可是slab_partial
。
-
-
这里,被加到本地高速缓存上的指针个数被存放在
ac->avail
,函数递减同样数量的kmem_list3
结构的free_objects
来说明这些对象不再空闲 -
释放
cachep->spinlock
-
如现在
ac->avail
字段大于0
(一些高速缓存再填充的情况发生了),函数将ac->touched
设为1
,返回最后插入到本地高速缓存的空闲对象指针:return ((void**)(ac+1))[--ac->avail];
-
否则,没发生高速缓存缓存再填充情况,调
cache_grow
获得一个新slab
。从而获得新的空闲对象。 -
如
cache_grow
失败了,函数返回NULL
。否则,返回1
。
13. 释放slab对象
void kmem_cache_free(kmem_cache_t* cachep, void *objp) { unsigned long flags; struct array_cache* ac; local_irq_save(flags);// 禁止本地中断,保存标志信息 ac = cachep->array[smp_procesor_id()];// 获取本地CPU高速缓存 if(ac->avail == ac->limit)// 本地cpu高速缓存满了 cache_flusharray(cachep, ac); ((void**)(ac+1))[ac->avail++] = objp;// 没满,直接放入 local_irq_restore(flags);// 恢复本地中断,恢复标志信息 }
先检查本地高速缓存是否有空间给指向一个空闲对象的额外指针,如有,该指针就被加到本地高速缓存然后返回。否则,它首选调cache_flusharray
清空本地高速缓存,然后将指针加到本地高速缓存。
cache_flusharray:
-
获得
cachep->spinlock
-
如
slab
高速缓存包含一个共享本地高速缓存,且如该共享本地缓存还没满,函数就通过从CPU
的本地高速缓存中上移ac->batchcount
个指针来重新填充共享本地高速缓存 -
调
free_block
将当前包含在本地高速缓存中的ac->batchcount
个对象归还给slab
分配器。 对在地址objp
处的每个对象,执行如下:-
增加高速缓存描述符的
lists.free_objects
-
确定包含对象的
slab
描述符的地址slabp = (struct slab*)(virt_to_page(objp)->lru.prev);
记住,
slab
页的描述符的lru.prev
指向相应的slab
描述符 -
从它的
slab
高速缓存链表(cachep->lists.slabs_partial
或cachep->lists.slabs_full
)上删除slab
描述符。 -
计算
slab
内对象的下标objnr = (objp - slabp->s_mem) / cachep->objsize;
-
将
slabp->free
的当前值存放在对象描述符中,并将对象的下标放入slabp->free
(最后被释放的对象将再次成为首先被分配的对象,提升硬件高速缓存命中率)((kmem_bufctl_t*)(slabp+1))[objnr] = slabp->free;// 利用对象内存(空闲对象)作为单向链表的索引值 slabp->free = objnr;// 下次分配将从上次释放对象开始分配(提升硬件高速缓存命中率)
-
递减
slabp->inuse
-
如
slabp->inuse
等于0
(即slab
中所有对象空闲)且整个slab
高速缓存中空闲对象的个数(cachep->lists.free_objects
)大于cachep->free_limit
字段中存放的限制,则函数将slab
的页框释放到分区页框分配器cachep->lists.free_objects -= cachep->num; slab_destroy(cachep, slabp);
存放在
cachep->free_limit
字段中的值通常等于cachep->num+(1+N)*cachep->batchcount
,其中N
代表系统中CPU
的个数 -
否则,如
slab->inuse
等于0
,但整个slab
高速缓存中空闲对象的个数小于cachep->free_limit
,函数就将slab
描述符插入到cachep->lists.slab_free
链表中 -
最后,如
slab->inuse
大于0
,slab
被部分填充,则函数将slab
描述符插入到cachep->lists.slabs_partial
链表
-
-
释放
cachep->spinlock
-
通过减去被移到共享本地高速缓存或被释放到
slab
分配器的对象的个数来更新本地高速缓存描述符的avail
-
移动本地高速缓存数组起始处的那个本地高速缓存中的所有指针。因为,已经把第一个对象指针从本地高速缓存上删除,故剩下的指针必须上移。
14.通用对象
如对存储器的请求不频繁,就用一组普通高速缓存来处理。普通高速缓存中的对象具有几何分布的大小,范围为32~131072
字节。
void* kmalloc(size_t size, int flags) { struct cache_sizes *csizep = malloc_sizes; kmem_cache_t* cachep; for(; csizep->cs_size; csizep++) { if(size > csizep->cs_size) continue; if(flag & __GFP_DMA) cachep = csizep->cs_dmacachep; else cachep = csizep->cs_cachep; return kmem_cache_alloc(cachep, flags); } return NULL; }
函数用malloc_sizes
表为所请求的大小分配最近的2
的幂次方大小内存。然后,调kmem_cache_alloc
分配对象。 依据flag,决定是采用适用于ISA DMA
页框的高速缓存描述符,还是适用于"常规"页框的高速缓存描述符。
void kfree(const void* objp) { kmem_cache_t* c; unsigned long flags; if(!objp) return; local_irq_save(flags); c = (kmem_cache_t*)(virt_to_page(objp)->lru.next); kmem_cache_free(c, (void*)objp); local_irq_restore(flags); }
通过读取内存区所在的第一个页框描述符的lru.next
子字段,就可确定出合适的高速缓存描述符。 通过调kmem_cache_free
来释放相应的内存区。
15.内存池–使用者可以直接与kmem_cache交互,也可与mempool_t交互
是Linux2.6
的一个新特性。基本上讲,一个内存池允许一个内核成分,如块设备子系统,仅在内存不足的紧急情况下分配一些动态内存来使用。不应该将内存池与前面"保留的页框池"一节描述的保留页框混淆。实际上,这些页框只能用于满足中断处理程序或内部临界区发出的原子内存分配请求。而内存池是动态内存的储备,只能被特定的内核成分(即池的"拥有者")使用。拥有者通常不使用储备;但,如动态内存变得极其稀有以至于所有普通内存分配请求都将失败的话,那么作为最后的解决手段, 内核成分就能调特定的内存池函数提取储备得到所需的内存。因此,创建一个内存池就像手头存放一些罐装食物作为储备,当没有新鲜食物时就使用开罐器。
一个内存池常常叠加在slab分配器之上–即,它用来保存slab对象的储备。但,一般而言,内存池能被用来分配任何一种类型的动态内存,从整个页框到使用kmalloc分配的小内存区。故,我们一般将内存池处理的内存单元看作"内存元素"。
内存池由mempool_t
描述
类型 | 名称 | 说明 |
---|---|---|
spinlock_t | lock | 用来保护对象字段的自旋锁 |
int | min_nr | 内存池中元素的最大个数 |
int | curr_nr | 当前内存池中元素的个数 |
void** | elements | 指向一个数组的指针,该数组由指向保留元素的指针组成 |
void* | pool_data | 池的拥有者可获得的私有数据 |
mempool_alloc_t* | alloc | 分配一个元素的方法 |
mempool_free_t* | free | 释放一个元素的方法 |
wait_queue_head_t | wait | 当内存池为空时使用的等待队列 |
min_nr
字段存放了内存池中元素的初始个数。即,存放在该字段的值代表了内存元素的个数。内存池拥有者确信能从内存分配器得到这个数目。curr_nr
字段总是低于或等于min_nr
,它存放了内存池中当前包含的内存元素个数。内存元素自身被一个指针数组引用,指针数组地址存放在elements
。
alloc,free
与基本的内存分配器交互,分别用于获得和释放一个内存元素,两个方法可是拥有内存池的内核成分提供的定制函数。当内存元素是slab
对象时,alloc,free
一般由mempool_alloc_slab
和mempool_free_slab
实现,它们只是分别调kmem_cache_alloc
和kmem_cache_free
。这种情况下,mempool_t
对象的pool_data
字段存放了slab
高速缓存描述符的地址。
mempool_create创建一个新的内存池; 它接收的参数为:内存元素的个数min_nr
,实现alloc,free
方法的函数的地址,赋给pool_data
字段的值。 函数分别为mempool_t
对象和指向内存元素的指针数组分配内存,然后反复调alloc
方法来得到min_nr
个内存元素。 相反地,mempool_destroy
释放池中所有内存元素,然后释放元素数组和mempool_t
对象自己。
mempool_alloc–从内存池分配一个元素: 内核调mempool_alloc
,将mempool_t
对象的地址和内存分配标志传递给它。 函数本质上依据参数所指定的内存分配标志,试图通过调alloc
从基本内存分配器分配一个内存元素。 如成功,函数返回获得的内存元素而不触及内存池。否则,如分配失败,就从内存池获得内存元素。 当然,内存不足情况下过多的分配会用尽内存池:这种情况下,如__GFP_WAIT
标志置位,则mempool_alloc
阻塞当前进程直到有一个内存元素被释放到内存池中。
mempool_free–释放一个元素到内存池 内核调mempool_free
。如内存池未满,则函数将元素加到内存池。否则,mempool_free
调free
方法来释放元素到基本内存分配器。
非连续内存区管理
把内存区映射到一组连续的页框是最好的选择,会充分利用高速缓存并获得较低的平均访问时间。 如对内存区的请求不频繁,则通过连续的线性地址来访问非连续的页框这样一种分配模式会很有意义。这种模式优点是避免了外碎片,缺点是打乱内核页表。显然,非连续内存区大小必须是4096倍数。
Linux在几个方面使用非连续内存区,如:为活动的交换区分配数据结构,为模块分配空间,或者给某些I/O驱动程序分配缓冲区。此外,非连续内存区还提供了另一种使用高端内存页框的方法。
1.非连续内存区的线性地址
要查找线性地址的一个空闲区,可从PAGE_OFFSET
开始查找。
-
线性内存区的开始部分包含的是对前
896MB RAM
进行映射的线性地址。直接映射的物理内存末尾所对应的线性地址保存在high_memory
-
线性内存区的结尾部分包含的是固定映射的线性地址。
-
从
PKMAP_BASE
开始,查找用于高端内存页框的永久内核映射的线性地址 -
其余的线性地址可用于非连续内存区。
在直接内存映射的末尾与第一个内存区之间插入一个大小为8MB
的安全区,目的是为了"捕获"对内存的越界访问。 出于同样的理由,插入其他4KB
大小的安全区来隔离非连续的内存区。
以下针对32
位处理器:
-
直接映射线性地址区域:
[PAGE_OFFSET, high_memory]
-
vmalloc
线性地址空间:[VMALLOC_START,VMALLOC_END]
-
永久内核映射的线性地址空间:
[PKMAP_BASE,FIXADDRSTART]
-
固定映射的线性地址空间:
[FIXADDR_START,4GB]
为非连续内存区保留的线性地址空间的起始地址由VMALLOC_START
定义,末尾地址由VMALLOC_END
定义。
2.非连续内存区的描述符
每个非连续内存区都对应着一个类型为vm_struct
的描述符
类型 | 名称 | 说明 |
---|---|---|
void* | addr | 内存区内第一个内存单元的线性地址 |
unsigned long | size | 内存区的大小加4096 |
unsigned long | flags | 非连续内存区映射的内存的类型 |
struct page** | pages | 指向nr_pages数组的指针,该数组由指向页描述符的指针组成 |
unsigned int | nr_pages | 内存区填充的页的个数 |
unsigned long | phys_addr | 该字段设为0,除非内存已被创建来映射一个硬件设备的I/O共享内存 |
struct vm_struct* | next | 指向下一个vm_struct结构的指针 |
通过next
,这些描述符被插入到一个简单的链表中,链表的第一个元素的地址存放在vmlist
变量中。对这个链表的访问依靠vmlist_lock
读写自旋锁来保护。
-
flags
字段标识了非连续区映射的内存的类型: -
VM_ALLOC
表示使用vmalloc
得到的页, -
VM_MAP
表示使用vmap
映射的已经被分配的页, -
VM_IOREMAP
表示使用ioremap
映射的硬件设备的板上内存。
get_vm_area–在线性地址VMALLOC_START和VMALLOC_END之间查找一个空闲区域 参数:
-
将被创建的内存区的字节大小,
-
指定空闲区类型的标志
步骤:
-
调
kmalloc
为vm_struct
类型的新描述符获得一个内存区 -
为写得到
vmlist_lock
锁,并扫描类型为vm_struct
的描述符链表来查找线性地址一个空闲区域,至少覆盖size+4096
个地址(4096
是内存区之间的安全区间大小) -
如存在这样一个区间,函数就初始化描述符的字段,释放
vmlist_lock
,并以返回非连续内存区描述符的起始地址而结束 -
否则,
get_vm_area
释放先前得到的描述符,释放vmlist_lock
,返回NULL
3.分配非连续内存区
vmalloc–给内核分配一个非连续内存区
参数:
-
size–表示所请求内存区的大小
void* vmalloc(unsigned long size) { struct vm_struct *area; struct page **pages; unsigned int array_size, i; size = (size + PAGE_SIZE - 1) & PAGE_MASK; area = get_vm_area(size, VM_ALLOC); if(!area) return NULL; area->nr_pages = size >> PAGE_SHIFT; array_size = (area->nr_pages * sizeof(struct page*)); area->pages = pages = kmalloc(array_size, GFP_KERNEL); if(!area->pages) { remove_vm_area(area->addr); kfree(area); return NULL; } memset(area->pages, 0, array_size); for(i = 0; i < area->nr_pages; i++) { area->pages[i] = alloc_page(GFP_KERNEL | __GFP_HIGHMEM); if(!area->pages[i]) { area->nr_pages = i; fail: vfree(area->addr); return NULL; } } // 通过页表表项逐个处理构建连续线性地址和离散物理地址之间的映射 if(map_vm_area(area, __pgprot(0x63), &pages)) goto fail; return area->addr; } }
-
函数首先将
size
设为4096
的整数倍,然后,vmalloc
调get_vm_area
来创建一个新的描述符,并返回分配给这个内存区的线性地址。描述符的flags
字段被初始化为VM_ALLOC
标志,该标志意味着通过使用vmalloc
函数,非连续的物理页框将被映射到一个线性地址空间。 -
然后,
vmalloc
调kmalloc
来请求一组连续页框,这组页框足够包含一个页描述符指针数组。调memset
将所有这些指针设为NULL
。接着重复调alloc_page
,每一次为区间中nr_pages
个页的每一个分配一个页框,并把对应页描述符的地址存放在area->pages
中。 -
到这里,已经得到一个新的连续线性地址空间,且已分配了一组非连续页框来映射这些线性地址。 最后重要的步骤是修改内核使用的页表项,以此表明分配给非连续内存区的每个页框现在对应着一个线性地址,这个线性地址被包含在
vmalloc
产生的连续线性地址空间中。
map_vm_area 参数:
-
area
–指向内存区的vm_struct
描述符的指针 -
prot
–已分配页框的保护位。它总是被置为0x63
,对应着Present,Accessed,Read/Write,Dirty
-
pages
–指向一个指针数组的变量的地址,该指针数组的指针指向页描述符
过程:
-
函数首先把内存区的开始和末尾的线性地址分别分配给局部变量
address
和end
:
address = area->addr; end = address + (area->size - PAGE_SIZE);
-
记住,
area->size
存放的是内存区的实际地址加上4KB
的安全区间。 函数使用pgd_offset_k
宏来得到在主内核页全局目录中的目录项,该项对应于内存区起始线性地址,然后获得内核页表自旋锁:pgd = pgd_offset_k(address); spin_lock(&init_mm.page_table_lock);
-
然后,函数执行下列循环
int ret = 0; for(i = pgd_index(address); i < pgd_index(end-1); i++) { pud_t* pud = pud_alloc(&init_mm, pgd, address); ret = -ENOMEM; if(!pud) break; next = (address + PGDIR_SIZE) & PGDIR_MASK; if(next < address || next > end) next = end; if(map_area_pud(pud, address, next, prot, pages)) break; address = next; pgd++; ret = 0; } spin_unlock(&init_mm.page_table_lock); flush_cache_vmap((unsigned long)area->addr, end); return ret
-
每次循环都首先调
pub_alloc
来为新内存区创建一个页上级目录,并把它的物理地址写入内核页全局目录的合适表项。 调alloc_area_pud
为新的页上级目录分配所有相关的页表。接下来,把常量2
的30
幂次(在PAE被激活的情况下,否则为2
的22
幂次)与address
的当前值相加(2
的30
幂次就是一个页上级目录所跨越的线性地址范围的大小)。 最后增加指向页全局目录的指针pgd
。 循环结束的条件是:指向非连续内存区的所有页表项全被建立。 -
map_area_pud
为页上级目录所指向的所有页表执行一个类似的循环:do{ pmd_t* pmd = pmd_alloc(&init_mm, pud, address); if(!pmd) return -ENOMEM; if(map_area_pmd(pmd, address, end-address, prot, pages)) return -ENOMEM; address = (address + PUD_SIZE) & PUD_MASK; pud++; } while(address < end);
-
map_area_pmd
为页中间目录所指向的所有页表执行一个类似的循环do{ pre_t* pte = pte_alloc_kernel(&init_mm, pmd, address); if(!pte) return -ENOMEM; if(map_area_pte(pte, address, end-address, prot, pages)) return -ENOMEM; address = (address + PMD_SIZE) & PMD_MASK; pmd++; } while(address < end);
-
pte_alloc_kernel
分配一个新的页表,并更新页中间目录中相应的目录项。 接下来,map_area_pte
为页表中相应的表项分配所有的页框。address
值增加2^{22}(2^{22}就是一个页表所跨越的线性地址区间的大小),且循环反复执行map_area_pte
主循环为:do{ struct page* page = **pages; set_pte(pte, mk_pte(page, prot)); address += PAGE_SIZE; pte++; (*pages)++; } while(address < end);
-
将被映射的页框的页描述符地址
page
从地址pages
处的变量指向的数组项读得的。 通过set_pte
和mk_pte
宏,把新页框的物理地址写进页表。把常量4096
(即一个页框的长度)加到address
上之后,循环又重复执行。
注意,map_vm_area
并不触及当前进程的页表。故,当内核态的进程访问非连续内存区时,缺页发生。 因为该线性内存区所对应的进程页表的表项为空。然而,缺页处理程序要检查这个缺页线性地址是否在主内核页表中(即init_mm.pgd
页全局目录和它的子页表)一旦处理程序发现一个主内核页表含有这个线性地址的非空项,就把它的值拷贝到相应的进程页表项中,并恢复进程的正常执行。
除了vmalloc
外,非连续内存区还能由vmalloc_32
分配,该函数与vmalloc
相似,但它只从ZONE_NORMAL
和ZONE_DMA
管理区分配页框。
4.释放非连续内存区
vfree–释放vmalloc和vmalloc_32创建的非连续内存区
vunmap–释放vmap创建的内存区
两个函数都使用同一个参数,它们都依赖于__vunmap
来作实质性的工作。:
-
将要释放的内存区的起始线性地址address,
__vunmap 参数:
将要释放的内存区的起始地址的地址addr,
标志deallocate_pages,如被映射到内存区的页框应当被释放到分区页框分配器,则这个标志被置位,否则被清除。
过程:
-
调
remove_vm_area
得到vm_struct
描述符的地址area
,清除非连续内存区中的线性地址对应的内核的页表项 -
如
deallocate_pages
被置位,函数扫描指向页描述符的area->pages
指针数组; 对数组的每一个元素,调__free_page
释放页框到分区页框分配器。执行kfree(area->pages)
来释放数组自身。 -
调
kfree(area)
来释放vm_struct
write_lock(&vmlist_lock); for(p = &vmlist; (tmp = *p); p = &tmp->next) { if(tmp->addr == addr) { unmap_vm_area(tmp); *p = tmp->next; break; } } write_unlock(&vmlist_lock); return tmp;
内存区本身通过调unmap_vm_area
来释放。
unmap_vm_area
参数:
-
指向内存区的vm_struct描述符的指针area。
过程:
address = area->addr; end = address + area->size; pgd = pgd_offset_k(address); for(i = pgd_index(address); i <= pgd_index(end-1); i++) { next = (address + PGDIR_SIZE) & PGDIR_MASK; if(next <= address || next > end) next = end; unmap_area_pud(pgd, address, next - address); address = next; pgd++; }
unmap_area_pud
依次在循环中执行map_area_pud
的反操作:
do { unmap_area_pmd(pud, address, end - address); address = (address + PUD_SIZE) & PUD_MASK; pud++; } while(address && (address < end));
unmap_area_pmd
函数在循环体中执行map_area_pmd
的反操作
do { unmap_area_pte(pmd, address, end - address); address = (address + PMD_SIZE) & PMD_MASK; pmd++; } while(address < end);
最后,unmap_area_pte
在循环中执行map_area_ate
的反操作
do { pte_t page = ptep_get_and_clear(pte); address += PAGE_SIZE; pte++; if(!pte_none(page) && !pte_present(page)) printk("Whee ... Swapped out page in kernel page table\n"); } while(address < end);
在每次循环过程中,ptep_get_and_clear
将pte
指向的页表项设为0
。与vmalloc
一样,内核修改主内核页全局目录和它的子页表中的相应项,但映射第4个GB的进程页表的项保持不变。因为内核永远不会回收扎根于主内核页全局目录中的页上级目录,页中间目录,页表。如,假定内核态的进程访问一个随后要释放的非连续内存区。进程的页全局目录项等于主内核页全局目录中的相应项。这些目录项指向相同的页上级目录,页中间目录,页表。
unmap_area_pte
只清除页表中的项(不回收页表本身)。进程对已释放非连续内存区的进一步访问必将由于空的页表项而触发缺页异常。缺页异常处理程序会认为这样的访问是一个错误,因为主内核页表不包含有效的表项。