Linux内核深入理解 - 内存管理(下)

目录

每CPU页框高速缓存

1.通过每CPU页框高速缓存分配页框

2.释放页框到每CPU页框高速缓存–位于伙伴系统和页框使用者的中间层

管理区分配器

内存高速缓存

1.内存高速缓存层次

2.内存高速缓存描述符

3.slab描述符

4.普通和专用高速缓存

5.为slab分配页框,释放页框

6.内存给高速缓存分配slab

7.从高速缓存中释放slab

8.对象描述符

9.对齐内存中的对象

10.slab着色

11.空闲Slab对象的本地高速缓存–slab分配器和内存申请使用者的中间层

12.分配slab对象

13. 释放slab对象

14.通用对象

15.内存池–使用者可以直接与kmem_cache交互,也可与mempool_t交互

非连续内存区管理

1.非连续内存区的线性地址

2.非连续内存区的描述符

3.分配非连续内存区

4.释放非连续内存区


每CPU页框高速缓存

内核经常请求和释放单个页框。为提升系统性能,每个内存管理区定义了一个"每CPU"页框高速缓存。所有"每CPU"高速缓存包含一些预先分配的页框,它们被用于满足本地CPU发出的单一内存请求。

实际上,这里为每个内存管理区和每个CPU提供了两个高速缓存: 一个热高速缓存,它存放的页框中所包含的内容很可能就在CPU硬件高速缓存中;还有一个冷高速缓存。

如内核或用户态进程在刚分配到页框后就立即向页框写,那么从热高速缓存中获得页框就对系统性能有利。实际上,每次对页框存储单元的访问都会导致从页框中给硬件高速缓存"窃取"一行。当然,除非硬件高速缓存包含有一行:它映射刚被访问的"热"页框单元。反过来,如页框将要被DMA操作填充,则从冷高速缓存中获得页框是方便的。在这种情况下,不会涉及到CPU,且硬件高速缓存的行不会被修改。

实现每CPU页框高速缓存的主要数据结构是存放在内存管理区描述符的pageset字段中的一个per_cpu_pageset数组数据结构。该数组包含为每个CPU提供的一个元素;这个元素依次由两个per_cpu_pages描述符组成,一个留给热高速缓存,另一个留给冷高速缓存。

per_cpu_pages描述符的字段

类型名称描述
intcount高速缓存中的页框个数
intlow下界,表示高速缓存需要补充
inthigh上界
intbatch在高速缓存中将要添加或被删去的页框个数
struct list_headlist高速缓存中包含的页框描述符链表

内核使用两个位来监视热高速缓存和冷高速缓存的大小:如页框个数低于下界low,内核通过从伙伴系统中分配batch个单一页框来补充对应的高速缓存。否则,如页框个数高于上界high,内核从高速缓存中释放batch个页框到伙伴系统。值batch,low,high本质上取决于内存管理区中包含的页框个数。

1.通过每CPU页框高速缓存分配页框

buffered_rmqueue–在指定的内存管理区中分配页框,它使用每CPU页框高速缓存来处理单一页框请求。 参数:

  • 内存管理区描述符的地址,

  • 请求分配的内存大小的对数order

  • 分配标志gfp_flags

pfp_flags中的__GFP_COLD标志被置位,则页框应当从冷高速缓存中获取,否则,它应从热高速缓存中获取。(此标志只对单一页框分配有意义)

函数操作:

  1. order不等于0,每CPU页框高速缓存就不能被使用;跳到4

  2. 检查由__GFP_COLD标志所标识的内存管理区本地每CPU高速缓存是否需补充。这种情况下,它执行:

    1. 通过反复调__rmqueue从伙伴系统中分配batch个单一页框

    2. 将已分配页框的描述符插入高速缓存链表

    3. 通过给count增加实际被分配页框来更新它

  3. count为正,则函数从高速缓存链表获得一个页框,count减1跳到5。

  4. 这里,内存请求还没被满足,或是因为请求跨越了几个连续页框,或是因为被选中的页框高速缓存为空,调__rmqueue从伙伴系统分配所请求的页框。

  5. 如内存请求得到满足,初始化第一个页框的页描述符:清除一些标志,将private置为0,将页框引用计数器置1。如gfp_flags__GPF_ZERO被置位,则函数将被分配的内存区域填充0。返回(第一个)页框的页描述符地址。

  6. 如内存分配请求失败,则返回NULL。

2.释放页框到每CPU页框高速缓存–位于伙伴系统和页框使用者的中间层

free_hot_page–释放单个页框到每CPU页框热高速缓存 free_cold_page–释放单个页框到每CPU页框冷高速缓存

它们都是free_hot_cold_page的封装。 参数:

  • 将要释放的页框的描述符地址page

  • cold标志;

free_hot_cold_page操作:

  1. page->flags获取包含该页框的内存管理区描述符地址

  2. 获取由cold标志选择的管理区高速缓存的per_cpu_pages描述符地址

  3. 检查高速缓存是否应被清空:如count高于或等于high,则调free_pages_bulk,将管理区描述符,将被释放的页框个数(batch),高速缓存链表的地址及数字 0 传递给该函数。free_pages_bulkl依次反复调__free_pages_bulk来释放指定数量的页框到内存管理区的伙伴系统中。

  4. 把释放的页框添加到高速缓存链表上,增加count

热高速缓存中页框,适合供cpu使用。冷高速缓存中页框,适合供DMA使用。

管理区分配器

管理区分配器是内核页框分配器的前端。该构建必须分配一个包含足够多空闲页框的内存区,使它能满足内存请求。

  • 管理区分配器必须满足几个目标:

  • 应当保护保留的页框池

  • 当内存不足且允许阻塞当前进程时它应触发页框回收算法。一旦某些页框被释放,管理区分配器将再次尝试分配。

  • 如可能,它应保存小而珍贵的ZONE_DMA内存管理区。例如,如是对ZONE_NORMALZONE_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确定:

  1. 作为参数传递的基本值可是内存管理区界值pages_minpages_lowpages_high中的任意一个。

  2. 作为参数传递的gfp_high标志被置位,则base值被2除。通常,如gfp_mask中的__GFP_WAIT标志被置位(即能从高端内存中分配页框),则这个标志等于1。

  3. 如作为参数传递的can_try_harder被置位,则阈值将会再减少四分之一。如gfp_mask中的__GFP_WAIT被置位,或如当前进程是一个实时进程且在进程上下文中(在中断处理程序和可延迟函数之外)已经完成了内存分配,则can_try_harder标志等于1。

__alloc_pages执行:

  1. 执行对内存管理区的第一次扫描 第一次扫描中,阈值min被设为z->pages_low其中z指向正被分析的管理区描述符。

  2. 如函数在上一步没终止,则没剩下多少空闲内存:函数唤醒kswapd内核线程来异步地开始回收页框。

  3. 执行对内存管理区的第二次扫描,将值z->pages_min作为阈值base传递。实际阈值由can_try_hardergfp_high决定。

  4. 如函数在上一步没终止,则系统内存肯定不足。如产生内存分配请求的内核控制路径不是一个中断处理程序或一个可延迟函数,且它试图回收页框(或是currentPF_MEMALLOC被置位,或它的PF_MEMDIE被置位),则函数随即执行对内存管理区的第三次扫描,试图分配页框并忽略内存的阈值。即不调zone_watermark_ok。唯有这种情况下才允许内核控制路径耗用为内存不足预留的页。这种情况下,产生内存请求的内核控制路径最终将试图释放页框,因此只要有可能它就应得到它所请求的。如没有任何内存管理区包含足够的页框,函数返回NULL来提示调用者发生了错误。

  5. 这里,正调用的内核控制路径并没试图回收内存。如gfp_mask__GFP_WAIT没被置位,函数就返回NULL来提示该内核控制路径内存分配失败:此时,如不阻塞当前进程就没办法满足请求。

  6. 在这里当前进程能被阻塞:调cond_resched检查是否有其他的进程需CPU

  7. 设置currentPF_MEMALLOC来表示进程已经准备好执行内存回收

  8. 将一个执行reclaim_state数据结构的指针存入current->reclaim_state。这个数据结构只包含一个字段reclaimed_slab

  9. try_to_free_pages寻找一些页框来回收。函数可能阻塞当前进程。一旦函数返回,__alloc_pages就重设currentPF_MEMALLOC并再次调cond_resched

  10. 如上一步已释放了一些页框,则函数还要执行一次与3步相同的内存管理区扫描。如内存分配请求不能被满足,则函数决定是否应继续扫描内存管理区;如__GFP_NORETRY被清除,且内存分配请求跨越了多达8个页框或__GFP_REPEAT__GFP_NOFAIL其中之一被置位,则函数就调blk_congestion_wait使进程休眠一会儿,跳回6。否则,返回NULL

  11. 9没释放任何页框,就意味着内核遇到很大的麻烦。如允许内核控制路径依赖于文件系统的操作来杀死一个进程且__GFP_NORETRY0,则执行:

    1. 使用等于z->pages_high的阈值再一次扫描内存管理区

    2. out_of_memory通过杀死一个进程开始释放一些内存

    3. 跳回1

2.__free_pages–通过内存管理区执行释放页框 参数:

  • 将要释放的第一个页框的页描述符的地址,

  • 将要释放的一组连续页框的数量的对数。

步骤:

  1. 检查第一个页框是否真正属于动态内存(PG_reserved清0);如不是,终止。

  2. 减少page->count;如仍大于或等于0,终止。

  3. order等于0,则调free_hot_base来释放页框给适当内存管理区的每CPU热高速缓存。

  4. order大于0,则它将页框加入到本地链表中,调free_pages_bulk把它们释放到适当的内存管理区的伙伴系统中。

内存高速缓存

伙伴系统算法采用页框作为基本内存区,这适合于对大块内存的请求,如何处理对小内存区的请求? 引入一种新的数据结构来描述在同一个页框中如何分配小内存区。

内核建立了13个按几何分布的空闲内存区链表,它们的大小从32字节到131072字节。

1.内存高速缓存层次

内存高速高速缓存包含多个slab,每个slab由一个或多个连续的页框组成。这些页框中既包含已分配的对象,也包含空闲的对象。内核周期性地扫描高速缓存并释放空slab对应的页框。

2.内存高速缓存描述符

每个内存高速缓存由kmem_cache_t类型的数据结构来描述。

类型名称说明
struct array_cache*[]array每CPU指针数组指向包含空闲对象的本地高速缓存
unsigned intbatchcount要转移进本地高速缓存或从本地高速缓存中转出的大批对象的数量
unsigned intlimit本地高速缓存中空闲对象的最大数目。
struct kmem_list3lists参见下一个表
unsigned intobjsize高速缓存中包含的对象的大小
unsigned intflags描述高速缓存永久属性的一组标志
unsigned intnum封装在一个单独slab中的对象个数
unsigned intfree_limit整个slab高速缓存中空闲对象的上限
spinlock_tspinlock高速缓存自旋锁
unsigned intgfporder一个单独slab中包含的连续页框数目的对数
unsigned intgfpflags分配页框时传递给伙伴系统函数的一组标志
size_tcolourslab使用的颜色个数
unsigned intcolour_offslab中的基本对齐偏移
unsigned intcolour_next下一个被分配的slab使用的颜色
kmem_cache_t*slabp_cache指针指向包含slab描述符的普通slab高速缓存
unsigned intslab_size单个slab的大小
unsigned intdflags描述高速缓存动态属性的一组标志
void*ctor指向与高速缓存相关的构造方法的指针
void*dtor指向与高速缓存相关的析构方法的指针
const char*name存放高速缓存名字的字符数组
struct list_headnext高速缓存描述符双向链表使用的指针

kmem_cache_t描述符的lists是一个结构体

类型名称说明
struct list_headslabs_partial包含空闲和非空闲对象的slab描述符双向循环链表
struct list_headslabs_full不包含空闲对象的slab描述符双向循环链表
struct list_headslabs_free只包含空闲对象的slab描述符双向循环链表
unsigned longfree_objects高速缓存中空闲对象的个数
intfree_touched由slab分配器的页回收算法使用
unsigned longnext_reap由slab分配器的页回收算法使用
struct array_cache*shared指向所有CPU共享的一个本地高速缓存的指针
3.slab描述符
类型名称说明
struct list_headlistslab描述符的三个双向循环链表中的一个
unsigned longcolouroffslab中第一个对象的偏移
void*s_memslab中第一个对象的地址
unsigned intinuse当前正使用的slab中的对象个数
unsigned intfreeslab中下一个空闲对象的下标,如没剩下空闲对象则为BUFCTL_END

slab描述符可存放在两个可能的地方: 外部slab描述符–存放在slab外部,位于cache_sizes指向的一个不适合ISA DMA的普通高速缓存中。 内部slab描述符–存放在slab内部,位于分配给slab的第一个页框的起始位置

当对象小于512MB,或当内部碎片为slab描述符和对象描述符在slab中留下足够的空间时,slab分配器选第二种方案。 如slab描述符存放在slab外部,则高速缓存描述符的flagsCFLAGS_OFF_SLAB1

4.普通和专用高速缓存

高速缓存被分为两种类型:普通和专用。 普通高速缓存只由slab分配器用于自己的目的,专用高速缓存由内核的其余部分使用。

普通高速缓存是: 1.第一个高速缓存叫kmem_cache,包括由内核使用的其余高速缓存的高速缓存描述符。cache_cache变量包含第一个高速缓存的描述符。 2.另外一些高速缓存包含用作普通用途的内存区。

内存区大小的范围一般包括13个几何分布的内存区。一个叫malloc_sizes的表分分别指向26个高速缓存描述符,与其相关的内存区大小为32,64,128,256,512,1024,2048,4096,8192,16384,32768,65536131072字节。对每种大小,都有两个高速缓存:一个适用于ISA DMA分配,另一个适用于常规分配。

在系统初始化期间调kmem_cache_init来建立普通高速缓存。 专用高速缓存由kmem_cache_create创建。函数从cache_cache普通高速缓存中为新的高速缓存分配一个高速缓存描述符,插入到高速缓存描述符的cache_chainkmem_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描述符的地址分别赋给页描述符中lrunextprev。只有当页框空闲时伙伴系统的函数才会使用lru。所以,lru不会误用。分配给slab的页框设置PG_slab标志。

在高速缓存中给定一个slab,可通过使用slab描述符的s_mem和高速缓存描述符的gfporder来找到依赖的页框描述符。接着,cache_growcache_init_objs,将构造方法应用到新slab包含的所有对象上。最后,cache_growlist_add_tail将新的slab描述符添加到高速缓存描述符*cachepslab链表末端,并更新高速缓存中的空闲对象计数器

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_freepagesslab使用的所有连续页框返回给伙伴系统。如slab描述符存放在slab外面,就从slab描述符的高速缓存释放这个slab描述符。实际上,该函数稍微复杂些。如,可使用SLAB_DESTROY_BY_RCU来创建slab高速缓存,这意味着应使用call_rcu注册一个回调来延期释放slab。回调函数接着调kmem_freepages,也可能调kmem_cache_free

8.对象描述符

每个对象有类型为kmem_bufctl_t的一个描述符,对象描述符存放在一个数组中,位于相应的slab描述符后。类似slab描述符,slab的对象描述符也可用两种可能的方式来存放:

  1. 外部对象描述符–存放在slab的外面,位于高速缓存描述符的slabp_cache字段指向的一个普通高速缓存中。内存区的大小取决于在slab中所存放的对象个数。

  2. 内部对象描述符–存放在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按如下方式处理请求:

  1. 如对象的大小大于高速缓存行的一半,就在RAM中根据L1_CACHE_BYTES的倍数对齐对象

  2. 否则,对象的大小就是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+freefree总是小于osize,不过可大于alnslab分配器利用空闲未用的字节free来对slab着色。术语着色只是用来再细分slab,并允许内存分配器把对象展开在不同的线性地址中。这样的话,内核从微处理器的硬件高速缓存中可获得最好性能。具有不同颜色的slabslab的第一个对象存放在不同的内存单元,同时满足对齐约束。

可用颜色的个数是free/aln(这个值存放在高速缓存描述符的colour字段)。故,第一个颜色表示0,最后一个颜色表示为(free/aln)-1。一种特殊的情况是,如freealn小,则colour被设为0,不过所有slab都使用颜色0,故颜色的真正个数是1

如用颜色col对一个slab着色,则,第一个对象的偏移量(相对于slab的起始地址)就等于col*aln+dsize。着色本质上导致把slab中的一些空闲区域从末尾移到开始。只有当free足够大时,着色才起作用。显然,如对象没请求对齐,或如果slab内的未使用字节数小于所请求的对齐(free<=aln),则唯一可能着色的slab就是具有颜色0slab,即,把这个slab的第一个对象的偏移量赋为0

通过把当前颜色存放在高速缓存描述符的colour_next字段,就可在一个给定对象类型的slab之间平等地发布各种颜色。 cache_growcolour_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 intavail指向本地高速缓存中可使用对象的指针的个数。同时作为高速缓存中第一个空槽的下标
unsigned intlimit本地高速缓存的大小。即本地高速缓存中指针的最大个数
unsigned intbatchcount本地高速缓存重新填充或腾空时使用的块大小
unsigned inttouched如本地高速缓存最近已被使用过,则该标志设为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:

  1. 将本地高速缓存描述符地址放在ac局部变量ac = cachep->array[smp_processor_id()]

  2. 获得cachep->spinlock

  3. slab高速缓存包含共享本地高速缓存,且该共享本地高速缓存包含一些空闲对象,就通过从共享本地高速缓存中上移ac->batchcount个指针来重新填充CPU的本地高速缓存。跳6

  4. 试图填充本地高速缓存,填充值为高速缓存的slab中包含的多达ac->batchcount个空闲对象的指针

    1. 查看高速缓存描述符的slabs_partialslabs_free,获得slab描述符的地址slabp,该slab描述符的相应slab或部分被填充,或为空。如不存在这样的描述符,跳5

    1. 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];
    1. 如必要,将清空的slab插入到适当的链表上,可以是slab_full,也可是slab_partial

  5. 这里,被加到本地高速缓存上的指针个数被存放在ac->avail,函数递减同样数量的kmem_list3结构的free_objects来说明这些对象不再空闲

  6. 释放cachep->spinlock

  7. 如现在ac->avail字段大于0(一些高速缓存再填充的情况发生了),函数将ac->touched设为1,返回最后插入到本地高速缓存的空闲对象指针:return ((void**)(ac+1))[--ac->avail];

  8. 否则,没发生高速缓存缓存再填充情况,调cache_grow获得一个新slab。从而获得新的空闲对象。

  9. 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:

  1. 获得cachep->spinlock

  2. slab高速缓存包含一个共享本地高速缓存,且如该共享本地缓存还没满,函数就通过从CPU的本地高速缓存中上移ac->batchcount个指针来重新填充共享本地高速缓存

  3. free_block将当前包含在本地高速缓存中的ac->batchcount个对象归还给slab分配器。 对在地址objp处的每个对象,执行如下:

    1. 增加高速缓存描述符的lists.free_objects

    2. 确定包含对象的slab描述符的地址

      slabp = (struct slab*)(virt_to_page(objp)->lru.prev);

      记住,slab页的描述符的lru.prev指向相应的slab描述符

    3. 从它的slab高速缓存链表(cachep->lists.slabs_partialcachep->lists.slabs_full)上删除slab描述符。

    4. 计算slab内对象的下标

      objnr = (objp - slabp->s_mem) / cachep->objsize;
    5. slabp->free的当前值存放在对象描述符中,并将对象的下标放入slabp->free(最后被释放的对象将再次成为首先被分配的对象,提升硬件高速缓存命中率)

      ((kmem_bufctl_t*)(slabp+1))[objnr] = slabp->free;// 利用对象内存(空闲对象)作为单向链表的索引值
      slabp->free = objnr;// 下次分配将从上次释放对象开始分配(提升硬件高速缓存命中率)
    6. 递减slabp->inuse

    7. 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的个数

    8. 否则,如slab->inuse等于0,但整个slab高速缓存中空闲对象的个数小于cachep->free_limit,函数就将slab描述符插入到cachep->lists.slab_free链表中

    9. 最后,如slab->inuse大于0slab被部分填充,则函数将slab描述符插入到cachep->lists.slabs_partial链表

  4. 释放cachep->spinlock

  5. 通过减去被移到共享本地高速缓存或被释放到slab分配器的对象的个数来更新本地高速缓存描述符的avail

  6. 移动本地高速缓存数组起始处的那个本地高速缓存中的所有指针。因为,已经把第一个对象指针从本地高速缓存上删除,故剩下的指针必须上移。

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_tlock用来保护对象字段的自旋锁
intmin_nr内存池中元素的最大个数
intcurr_nr当前内存池中元素的个数
void**elements指向一个数组的指针,该数组由指向保留元素的指针组成
void*pool_data池的拥有者可获得的私有数据
mempool_alloc_t*alloc分配一个元素的方法
mempool_free_t*free释放一个元素的方法
wait_queue_head_twait当内存池为空时使用的等待队列

min_nr字段存放了内存池中元素的初始个数。即,存放在该字段的值代表了内存元素的个数。内存池拥有者确信能从内存分配器得到这个数目。curr_nr字段总是低于或等于min_nr,它存放了内存池中当前包含的内存元素个数。内存元素自身被一个指针数组引用,指针数组地址存放在elements

alloc,free与基本的内存分配器交互,分别用于获得和释放一个内存元素,两个方法可是拥有内存池的内核成分提供的定制函数。当内存元素是slab对象时,alloc,free一般由mempool_alloc_slabmempool_free_slab实现,它们只是分别调kmem_cache_allockmem_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_freefree方法来释放元素到基本内存分配器。

非连续内存区管理

把内存区映射到一组连续的页框是最好的选择,会充分利用高速缓存并获得较低的平均访问时间。 如对内存区的请求不频繁,则通过连续的线性地址来访问非连续的页框这样一种分配模式会很有意义。这种模式优点是避免了外碎片,缺点是打乱内核页表。显然,非连续内存区大小必须是4096倍数。

Linux在几个方面使用非连续内存区,如:为活动的交换区分配数据结构,为模块分配空间,或者给某些I/O驱动程序分配缓冲区。此外,非连续内存区还提供了另一种使用高端内存页框的方法。

1.非连续内存区的线性地址

要查找线性地址的一个空闲区,可从PAGE_OFFSET开始查找。

  1. 线性内存区的开始部分包含的是对前896MB RAM进行映射的线性地址。直接映射的物理内存末尾所对应的线性地址保存在high_memory

  2. 线性内存区的结尾部分包含的是固定映射的线性地址。

  3. PKMAP_BASE开始,查找用于高端内存页框的永久内核映射的线性地址

  4. 其余的线性地址可用于非连续内存区。

在直接内存映射的末尾与第一个内存区之间插入一个大小为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 longsize内存区的大小加4096
unsigned longflags非连续内存区映射的内存的类型
struct page**pages指向nr_pages数组的指针,该数组由指向页描述符的指针组成
unsigned intnr_pages内存区填充的页的个数
unsigned longphys_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之间查找一个空闲区域 参数:

  • 将被创建的内存区的字节大小,

  • 指定空闲区类型的标志

步骤:

  1. kmallocvm_struct类型的新描述符获得一个内存区

  2. 为写得到vmlist_lock锁,并扫描类型为vm_struct的描述符链表来查找线性地址一个空闲区域,至少覆盖size+4096个地址(4096是内存区之间的安全区间大小)

  3. 如存在这样一个区间,函数就初始化描述符的字段,释放vmlist_lock,并以返回非连续内存区描述符的起始地址而结束

  4. 否则,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的整数倍,然后,vmallocget_vm_area来创建一个新的描述符,并返回分配给这个内存区的线性地址。描述符的flags字段被初始化为VM_ALLOC标志,该标志意味着通过使用vmalloc函数,非连续的物理页框将被映射到一个线性地址空间。

  • 然后,vmallockmalloc来请求一组连续页框,这组页框足够包含一个页描述符指针数组。调memset将所有这些指针设为NULL。接着重复调alloc_page,每一次为区间中nr_pages个页的每一个分配一个页框,并把对应页描述符的地址存放在area->pages中。

  • 到这里,已经得到一个新的连续线性地址空间,且已分配了一组非连续页框来映射这些线性地址。 最后重要的步骤是修改内核使用的页表项,以此表明分配给非连续内存区的每个页框现在对应着一个线性地址,这个线性地址被包含在vmalloc产生的连续线性地址空间中。

map_vm_area 参数:

  • area–指向内存区的vm_struct描述符的指针

  • prot–已分配页框的保护位。它总是被置为0x63,对应着Present,Accessed,Read/Write,Dirty

  • pages–指向一个指针数组的变量的地址,该指针数组的指针指向页描述符

过程:

  • 函数首先把内存区的开始和末尾的线性地址分别分配给局部变量addressend

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为新的页上级目录分配所有相关的页表。接下来,把常量230幂次(在PAE被激活的情况下,否则为222幂次)与address的当前值相加(230幂次就是一个页上级目录所跨越的线性地址范围的大小)。 最后增加指向页全局目录的指针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_ptemk_pte宏,把新页框的物理地址写进页表。把常量4096(即一个页框的长度)加到address上之后,循环又重复执行。

注意,map_vm_area并不触及当前进程的页表。故,当内核态的进程访问非连续内存区时,缺页发生。 因为该线性内存区所对应的进程页表的表项为空。然而,缺页处理程序要检查这个缺页线性地址是否在主内核页表中(即init_mm.pgd页全局目录和它的子页表)一旦处理程序发现一个主内核页表含有这个线性地址的非空项,就把它的值拷贝到相应的进程页表项中,并恢复进程的正常执行。

除了vmalloc外,非连续内存区还能由vmalloc_32分配,该函数与vmalloc相似,但它只从ZONE_NORMALZONE_DMA管理区分配页框。

4.释放非连续内存区

vfree–释放vmalloc和vmalloc_32创建的非连续内存区

vunmap–释放vmap创建的内存区

两个函数都使用同一个参数,它们都依赖于__vunmap来作实质性的工作。:

  • 将要释放的内存区的起始线性地址address,

__vunmap 参数:

将要释放的内存区的起始地址的地址addr,

标志deallocate_pages,如被映射到内存区的页框应当被释放到分区页框分配器,则这个标志被置位,否则被清除。

过程:

  1. remove_vm_area得到vm_struct描述符的地址area,清除非连续内存区中的线性地址对应的内核的页表项

  2. deallocate_pages被置位,函数扫描指向页描述符的area->pages指针数组; 对数组的每一个元素,调__free_page释放页框到分区页框分配器。执行kfree(area->pages)来释放数组自身。

  3. 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_clearpte指向的页表项设为0。与vmalloc一样,内核修改主内核页全局目录和它的子页表中的相应项,但映射第4个GB的进程页表的项保持不变。因为内核永远不会回收扎根于主内核页全局目录中的页上级目录,页中间目录,页表。如,假定内核态的进程访问一个随后要释放的非连续内存区。进程的页全局目录项等于主内核页全局目录中的相应项。这些目录项指向相同的页上级目录,页中间目录,页表。

unmap_area_pte只清除页表中的项(不回收页表本身)。进程对已释放非连续内存区的进一步访问必将由于空的页表项而触发缺页异常。缺页异常处理程序会认为这样的访问是一个错误,因为主内核页表不包含有效的表项。

  • 23
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值