首页 > 代码库 > Linux内核剖析 之 内存管理

Linux内核剖析 之 内存管理

1. 内存管理区

    为什么分成不同的内存管理区?

    ISA总线的DMA处理器有严格的限制:只能对物理内存前16M寻址。

    内核线性地址空间只有1G,CPU不能直接访问所有的物理内存。


    ZONE_DMA                  小于16M内存页框

    ZONE_NORMAL          16M~896M内存页框

    ZONE_HIGHMEM        大于896M内存页框


    ZONE_DMA和ZONE_NORMAL区域包含的页框,通过线性的映射到内核线性地址空间,内核可以直接访问(相应的内核页表早已经建立)。

    ZONE_HIGHMEN 动态映射到内核线性地址空间。

    为什么既有线性映射又有动态映射?

    线性映射,保持了内核页表的稳定性,减少了TLB miss;动态映射使得内核可以访问到所有的物理内存。

    内核将页框的信息保存在类型为struct page的页描述符中,所有的页描述符存放在数组mem_map(下标即为页框号)中。

struct page {
unsigned long flags; //标志,同时高位用来指向页框所在的管理区,包含多达32个页框的状态标志
//例如:PG_highmem 页框至于ZONE_HIGHMEM管理区
// PG_slab 页框包含在slab中
atomic_t _count; //页框引用计数
atomic_t _mapcount; //页框中的页表项数目
unsigned long private; //在不同情况下,意义不同.当页框为空闲时,由伙伴系统使用
struct address_space *mapping;
unsigned long index; //在不同情况下,意义不同
struct list_head lru; //双向链表指针
};

    每个管理区都有自己的描述符:

struct zone {
unsigned long free_pages; //管理区中空闲页数目
unsigned long pages_min //保留页框的数目 (保留页框池)
unsigned long pages_low;
unsigned long pages_high; //两个watermark,与回收页框,管理区分配器有关
struct per_cpu_pageset pageset[]; //(每CPU页框高速缓存)
spinklock_t lock; //保护该描述符
unsigned long lowmem_reserve[] //在内存不足的临界情况下,管理区必须保留的页框数
struct free_area free_area[]; //管理区空闲页框块(伙伴系统)
struct page *zone_mem_map; //指向管理区第一个页框
unsigned long zone_start_pfn; //第一个页框的下标
char *name; //管理区名字(DMA, NORMAL, HighMem)
..........
}
    管理区描述符还包含有很多其他字段,与回收页框等等有关。


保留的页框池

    有内存分配请求时,如果有足够多的空闲内存可用,请求就被立刻满足,否则,必须回收一些内存,阻塞当前运行的内核代码(内核控制路径),直到有内存被释放。

    但是,中断或临界区内代码不能被阻塞,原子内存分配请求。

    为了减少原子内存分配请求失败,内核保留了一个页框池。

    保留池大小 = [sqrt(16*直接映射内存)] (KB)

    ZONE_DMA、ZONE_NORMAL按各自大小比例贡献。

    struct zone中的unsigned long pages_min  //保留页框的数目  (保留页框池)。

 

2.分区页框分配器

   

    被称为分区页框分配器的内核子系统,处理对连续页框组的内存分配请求。

    管理区分配器:接收动态内存分配和释放的请求。(搜索能满足请求的内存管理区,触发页框回收。。。)

    每CPU页框高速缓存:快速满足本地CPU发出的单一页框请求。

    伙伴系统:解决外碎片问题。

 

2.1 请求和释放页框

struct page * alloc_pages(unsigned int gfp_mask, unsigned int order)
gfp_mask:一组标志,指明如何寻找空闲的页框
order:请求分配2order个页框
若成功,返回第一个分配页框的页描述符地址
gfp_mask分为两类:

区修饰符:

    __GFP_DMA

    __GFP_HIGHMEM

行为修饰符:

    __GFP_WAIT      允许内核阻塞等待空闲页框的当前内核控制路径

    __GFP_HIGH       允许内核访问保留页框池

    __GFP_COLD       所请求页框可能为”冷的“(每CPU高速缓存)

    __GFP_REPEAT   内核重试内存分配直到成功为止

    __GFP_NOFALL  与 __GFP_REPEAT 相同

    __GFP_NORETRY  一次内存分配后失败后不再重试



void __free_pages(struct page *page, unsigned int order)
释放页框函数


2.2 伙伴系统

    频繁的请求和释放不同大小的连续页框,必然导致在已分配页框的块内分散了许多小块的空闲页框。由此产生的问题是:即使有足够的空闲页框可以满足请求,但当要分配一个大块的连续页框时,无法满足请求。这就是著名的内存管理问题:外碎片问题。

    linux采用伙伴系统算法来解决外碎片的问题。

    把所有的空闲页框分组为11个块链表。每个块链表包含大小为1,2,4,8,16,32,64,128,256,512,1024个连续的页框。每个块的第一个页框的物理地址必须是该块大小的整数倍。例如,大小为16个页框的块,其起始地址为16x212的倍数。

    算法原理:

    通过一个简单的例子说明该算法的工作原理——

    假设请求一个256个页框的块,先在256个页框的链表内检查是否有一个空闲的块。如果没有这样的块,算法会查找下一个更大的块,在512个页框的链表中找一个空闲块。如果存在这样的块,内核就把512的页框分成两半,一半用作满足请求,另一半插入256个页框的链表中。如果512个页框的块链表也没有空闲块,就继续找更大的块,1024个页框的块。如果这样的块存在,内核把1024个页框的256个页框用作请求,然后从剩余的768个中拿出512个插入512个页框的链表中,把最后256个插入256个页框的链表中。

    页框块的释放过程:

    内核试图把大小为b的一对空闲伙伴块合并为一个大小为2b的单独块。

    满足以下条件的两个块成为伙伴:

    两个块具有相同的大小:b;它们的物理地址连续。

    第一个页框的物理地址为2xbx212的倍数。

    该算法是迭代的,如果成功合并所释放的块,它会试图合并2b的块。

 

    数据结构:

   

分配块:

static struct page *__rmqueue(struct zone *zone, unsigned int order)
{
struct free_area * area;
unsigned int current_order;
struct page *page;
unsigned int index;
for (current_order = order; current_order < MAX_ORDER; ++current_order) {
area = zone->free_area + current_order;
if (list_empty(&area->free_list))
continue; //从order开始,搜索第一个满足要求的页框块
page = list_entry(area->free_list.next, struct page, lru);
list_del(&page->lru); //将找到的页框块的第一个页框描述符从链表删除
index = page - zone->zone_mem_map;
if (current_order != MAX_ORDER-1)
MARK_USED(index, current_order, area);
zone->free_pages -= 1UL << order; //管理区的内存页框块减少
return expand(zone, page, index, order, current_order, area);
//如果current_order>order,把剩余未分配的页框块,插入合适的链表
?}
return NULL;
}
static inline struct page * expand(struct zone *zone, struct page *page, unsigned long index, int low, int high, struct free_area *area)
{//以前面提到的例子为例
unsigned long size = 1 << high;
while (high > low) {
area--;
high--;
size >>= 1;
BUG_ON(bad_range(zone, &page[size]));
list_add(&page[size].lru, &area->free_list);
MARK_USED(index + size, high, area);
}
return page;
}

释放块: 

    page—需要释放页框块的首个页框的页描述符

    base—管理区首个页框的页描述符

    order—页框块页框个数的对数

    area—页数为2order的块的链表

static inline void __free_pages_bulk (struct page *page, struct page *base,
struct zone *zone, struct free_area *area, unsigned int order)
{
unsigned long page_idx, index, mask;
if (order)
destroy_compound_page(page, order);
mask = (~0UL) << order;
page_idx = page - base; //块中第一个页框的索引号,相对于管理区的首个页框
if (page_idx & ~mask) //页框块的起始物理地址必须是2orderx212 所以page_idx需要是2order的倍数
BUG();
index = page_idx >> (1 + order);
zone->free_pages += 1 << order;
while (order < MAX_ORDER-1) {
struct page *buddy1, *buddy2;
BUG_ON(area >= zone->free_area + MAX_ORDER);
if (!__test_and_change_bit(index, area->map)) //检查伙伴块是否已经被分配
/* //???????
* the buddy page is still allocated.
*/
break;
/* Move the buddy up one level. */
buddy1 = base + (page_idx ^ (1 << order)); //伙伴块的索引号
buddy2 = base + page_idx;
BUG_ON(bad_range(zone, buddy1));
BUG_ON(bad_range(zone, buddy2));
list_del(&buddy1->lru);
mask <<= 1;
order++;
area++;
index >>= 1;
page_idx &= mask; //合并后块中第一个页框的索引
}
list_add(&(base + page_idx)->lru, &area->free_list);
}

2.3 每CPU页框高速缓存

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

    每个内存管理区为每个CPU提供了两个高速缓存:热高速缓存和冷高速缓存

    热高速缓存:它存在的页框中所包含的内容可能就在CPU硬件高速缓存中。

    如果进程在分配页框后,就向页框中写,那么从热高速缓存中获得页框对性能有利。如果页框将被DMA操作,则从冷高速缓存中获得页框,对性能有利(节约了热高速缓存)

 

    数据结构:

struct zone {
struct per_cpu_pageset pageset[];
}
    每个CPU对应两个per_cpu_pageset结构:一个描述热高速缓存,另一个描述冷高速缓存。
struct per_cpu_pageset {
int count; //高速缓存中页框的个数
int low; //下界,高速缓存需要补充
int high; //上界,高速缓存需要释放到伙伴系统中
int batch; //每次添加或释放的个数
struct list_head list //高速缓存中包含的页框描述符链表
}
    通过每CPU页框高速缓存分配页框:

    struct page *buffered_rmqueue(struct zone *zone, int order, int gfp_flags);

 

     释放页框到每CPU页框高速缓存:

     void fastcall free_hot_cold_page(struct page *page, int cold);

 

2.4 管理区分配器

    管理区分配器是内存页框分配器的前端,必须找到一个包含足够多空闲页框的内存区,满足内存请求。

    注意:

        管理区分配器保护保留的页框池;

        当内存不足,且允许阻塞当前内核控制路径时,触发页框回收算法;

        保存小而珍贵的ZONE_DMA区域。

    管理区分配器的核心:__alloc_pages()


3.高端内存页框的内核映射

    到目前为止所介绍的函数,都是获得一块连续的内存页框,要想访问物理内存,必须要将页框映射到线性地址空间中。

    过程:(1)分配物理页框;(2)得到未映射到任何物理页框的线性地址;(3)更新页表,使得线性地址映射到物理页框。

    

    对于ZONE_DMA和ZONE_NORMAL区域,线性映射到内核地址空间前896M。

    根据物理地址可以直接得到相应的线性地址,并且此部分的内核页表在系统初始化时已经建立。 

__va((unsigned long)(page-mem_pag) << 12)

    对于ZONE_HIGHMEM区域,必须建立到内核线性地址空间后128M的动态映射。

    并且这种映射是暂时的,通过重复使用线性地址,使得整个高端内存能够在不同的时间被访问。

 

    出于不同的目的,内核使用三种不同的机制将页框映射到高端内存。

    *永久内核映射

    *临时内核映射(固定映射)

    *非连续内存分配


    后128M的线性地址,被分为如下三个部分:


 

3.1 永久内核映射

     永久内核映射允许内核建立高端页框到内核线性地址空间的长期映射。可能阻塞当前进程,不能在中断处理程序等不允许阻塞的代码中调用。

    永久内核映射使用内核页表中的一个专门的页表,页表的地址存放在pkmap_page_table变量中。页表的表项数目:(LAST_PKMAP   1024)

    该页表映射的线性地址空间PKMAP_BASE  0xff800000UL (3G+1016M) ——>0x3fe页全局目录的第873项。

    也就是,内核通过永久内核映射,最多同时访问4M高端内存。

 

    页表中每一项都有一个计数器:

static int pkmap_count[LAST_PKMAP];

    计数器为0 

        对应的页表项没有映射到任何高端内存页框,并且是可用的。

    计数器为1 

      对应的页表项没有映射到任何高端内存页框,但不能使用,因为自从它最后一次使用以来,其相应的TLB表项还未被刷新。

    计数器为n 

        对应的页表项映射到一个高端内存页框。 


void *kmap(struct page *page)
{
might_sleep();
if (page < highmem_start_page)
return page_address(page); //不在高端内存中,直接返回映射的线性地址
return kmap_high(page);
}

void fastcall *kmap_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[PKMAP_NR(vaddr)]++;
if (pkmap_count[PKMAP_NR(vaddr)] < 2)
BUG();
spin_unlock(&kmap_lock);
return (void*) vaddr;
}
static inline unsigned long map_new_virtual(struct page *page)
{
unsigned long vaddr;
int count;
start:
count = LAST_PKMAP;
/* Find an empty entry */
for (;;) {
last_pkmap_nr = (last_pkmap_nr + 1) & LAST_PKMAP_MASK;
if (!last_pkmap_nr) {
flush_all_zero_pkmaps();
count = LAST_PKMAP;
}
if (!pkmap_count[last_pkmap_nr])
break; /* Found a usable entry */
if (--count)
continue;
/*
* Sleep for somebody else to unmap their entries
*/
{
DECLARE_WAITQUEUE(wait, current);
__set_current_state(TASK_UNINTERRUPTIBLE);
add_wait_queue(&pkmap_map_wait, &wait);
spin_unlock(&kmap_lock);
schedule();
remove_wait_queue(&pkmap_map_wait, &wait);
spin_lock(&kmap_lock);
/* Somebody else might have mapped it while we slept */
if (page_address(page))
return (unsigned long)page_address(page);
/* Re-start */
goto start;
}
}
vaddr = PKMAP_ADDR(last_pkmap_nr);
set_pte(&(pkmap_page_table[last_pkmap_nr]), mk_pte(page, kmap_prot));
//建立内核页表
pkmap_count[last_pkmap_nr] = 1;
set_page_address(page, (void *)vaddr);
return vaddr;
}
    当没有空闲的页表项时,永久内核映射可能阻塞当前进程。

 

3.2 临时内核映射

FIXADDR_START  (3G + 1020M)

FIXADDR_TOP     (0xfffff000UL)     (除去最后一页)


enum km_type {
KM_BOUNCE_READ,
KM_SKB_SUNRPC_DATA,
KM_SKB_DATA_SOFTIRQ,
KM_USER0,
KM_USER1,
KM_BIO_SRC_IRQ,
KM_BIO_DST_IRQ,
KM_PTE0,
KM_PTE1,
KM_IRQ0,
KM_IRQ1,
KM_SOFTIRQ0,
KM_SOFTIRQ1,
KM_TYPE_NR
};
    临时内核映射不会阻塞当前进程。

    同一个窗口永不会被两个不同的控制路径同时使用

void *kmap_atomic(struct page *page, enum km_type type)
{
enum fixed_addresses idx;
unsigned long vaddr;
/* even !CONFIG_PREEMPT needs this, for in_atomic in do_page_fault */
inc_preempt_count(); //禁止内核抢占
if (page < highmem_start_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, mk_pte(page, kmap_prot)); //建立页表
__flush_tlb_one(vaddr);
return (void*) vaddr;
}
 

3.3  非连续内存区管理

    主要特点:通过连续的线性地址访问非连续的页框。

    优点:非连续的页框,避免了外碎片;

    缺点:打乱了内核页表。

    Linux在为活动的?交换区分配数据结构, 为模块分配空间某些I/O驱动程序等等方面都用到了非连续内存区


    从high_memory(896M)到PKMAP_BASE(1016M)之间的线性地址空间用于非连续内存区。

    high_memory与VMALLOC_START之间有8M(VMALLOC_OFFSET)的安全区,为了捕获对内存的越界访问。

    vmalloc区之间的4KB的安全区也是出于同样的考虑。

 

数据结构:

    每个非连续内存区都对应一个类型为 vm_struct 的描述符

struct vm_struct {
void *addr; //内存区起始线性地址
unsigned long size; //内存区的大小加4096(内存区之间安全区的大小)
unsigned long flags;
struct page **pages; //页描述符指针数组
unsigend int nr_pages; //指针数组长度(内存区页的个数)
unsigned long phys_addr; //该字段为0
struct vm_struct *next; //指向下一个vm_struct
}

分配非连续内存区

void *vmalloc(unsigend long size):
void *__vmalloc(unsigned long size, int gfp_mask, pgprot_t prot)
{
struct vm_struct *area;
struct page **pages;
unsigned int nr_pages, array_size, i;
size = PAGE_ALIGN(size); //(((addr)+PAGE_SIZE-1)&PAGE_MASK)
if (!size || (size >> PAGE_SHIFT) > num_physpages)
return NULL;
area = get_vm_area(size, VM_ALLOC);
//在VMALLOC_START和 VMALLOC_END之间寻找大小为(szie+4096)的空闲连续地址空间
if (!area)
return NULL;
nr_pages = size >> PAGE_SHIFT;
array_size = (nr_pages * sizeof(struct page *));
area->nr_pages = nr_pages;
area->pages = pages = kmalloc(array_size, (gfp_mask & ~__GFP_HIGHMEM));
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_mask);
//vmalloc基于伙伴系统, 每次从伙伴系统分配一页
if (unlikely(!area->pages[i])) {
/* Successfully allocated i pages, free them in __vunmap() */
area->nr_pages = i;
goto fail;
}
}
if (map_vm_area(area, prot, &pages)) //建立页表
goto fail;
return area->addr;
fail:
vfree(area->addr);
return NULL;
}
     内核页表与进程页表(进程如何共享内核页表)

   

    前面讲的几种动态映射都会修改内核页表。

    永久内核映射与非连续内存区修改内核页表后,进程页表中内核线性地址空间部分需要与其保持一致。

    那么内核如何确保对内核页表的修改,能传递到各个进程页表中?

===>>>

    当内核建立新页表。内核态的进程访问内核线性地址。对应的页目录项为空,缺页发生

    缺页处理程序检查这个缺页的线性地址是否在主内核页表中。一旦缺页处理程序发现主内核页表相应的页目录项不为空,

    就把页目录项的值拷贝到进程的相应位置,并恢复进程的执行。

    当内核撤销线性地址到物理页框的映射,把页表项设为0;

    内核态进程访问线性地址,相应页表项为0,缺页发生,缺页处理程序认为这样的访问是一个错误。


Linux内核剖析 之 内存管理