上次讲的buddy system算法虽然效率很高,但是要从buddy system中分配出一个内存页块来,还是要做不少工作的,有时想想都会觉得很累。

在系统运行过程中,Kernel经常会有单个页面的申请和释放操作。为了进一步提高性能,也为了让生活变得轻松一点,Kernel采用了这样一种cache机制:

Memory zone为每个CPU定义了page frame cache。Kernel会在适当的时机提前从buddy system中分配好若干单页,放在这些cache中。以后Kernel若要申请单个页面,直接从cache中拿一个就可以了,不用再去和buddy system打交道。

实际上,memory zone为每个CPU定义了两个page frame cache。一个hot cache,一个cold cache。hot还是cold,主要是相对于CPU的缓存来说的。

一般来说,从hot cache中分配页面可以提高系统性能,因为该页面的内容很可能还保存在CPU缓存中。

那cold cache有什么用呢?这个cache中的page frame一般用在DMA操作中。我们知道,DMA操作不涉及CPU,所以也就不涉及CPU缓存,因此用于DMA操作的page frame就没必要从hot cache中分配。从cold cache中为DMA分配page frame有助于保持hot cache中的页面还是hot的。

好了,让我们来看一看这个cache机制是如何实现的。

1. 数据结构

memory zone的描述符中,有这样一个成员变量

struct zone {    ...        struct per_cpu_pageset  pageset[NR_CPUS];        ...}

这个就是为每个CPU准备的page frame cache。

struct per_cpu_pageset {    struct per_cpu_pages pcp[2];    /* 0: hot.  1: cold */        ...} ____cacheline_aligned_in_smp;

可见每个CPU有两个cache: hot and cold。

struct per_cpu_pages {    int count;      /* number of pages in the list */    int high;       /* high watermark, emptying needed */    int batch;      /* chunk size for buddy add/remove */    struct list_head list;  /* the list of pages */};

每个cache的结构非常简单。Kernel提前从buddy system中分配好的单个页面放在list中,list里包含的页面个数保存在count中。

每次申请和释放单个页面时,Kernel都会check一下count值:在申请单个页面时,如果发现count的值为0,则会填充cache;在释放单个页面后,如果发现count的值大于等于high watermark,则会缩减cache。每次填充或缩减一个batch的量。

之前讲过了buddy system算法是如何分配和释放一个页块的。那么增加了per-cpu page frame cache之后,分配和释放页块时会有哪些不同呢?

2. 分配一个页块

分配一个页块是由函数buffered_rmqueue来完成的。它主要利用我们讲过的__rmqueue来从buddy system中申请内存页块,不过当申请单个页面时,它会利用per-cpu page frame cache。

static struct page *buffered_rmqueue(struct zonelist *zonelist,            struct zone *zone, int order, gfp_t gfp_flags){    unsigned long flags;    struct page *page;    int cold = !!(gfp_flags & __GFP_COLD);    int cpu;    int migratetype = allocflags_to_migratetype(gfp_flags);

是使用hot cache还是cold cache是由__GFP_COLD位来决定的。 migratetype是buddy system用来减少外碎片的机制,暂且忽略。

如果申请的是单个页面,那么Kernel就会使用per-cpu page frame cache。当然在从cache中拿page frame之前,会check一下,如果cache已经空了,就需要先填充cache。

again:    cpu  = get_cpu();    if (likely(order == 0)) {        struct per_cpu_pages *pcp;        pcp = &zone_pcp(zone, cpu)->pcp[cold];        local_irq_save(flags);        if (!pcp->count) {            pcp->count = rmqueue_bulk(zone, 0,                    pcp->batch, &pcp->list, migratetype);            if (unlikely(!pcp->count))                goto failed;        }

填充的工作由函数rmqueue_bulk来完成。这个函数非常简单,就是利用__rmqueue从buddy system中申请batch个单个页面放进cache中。

如果填充过后cache依旧为空,说明内存已经非常短缺,返回NULL。

        page = list_entry(pcp->list.next, struct page, lru);        list_del(&page->lru);        pcp->count--;

如果cache不为空,则从cache中拿出一个page frame。

上面是针对申请单个页面的情况。如果申请多个页面,则利用__rmqueue从buddy system中申请。

    } else {        spin_lock_irqsave(&zone->lock, flags);        page = __rmqueue(zone, order, migratetype);        spin_unlock(&zone->lock);        if (!page)            goto failed;    }        ...    failed:    local_irq_restore(flags);    put_cpu();    return NULL;}

3. 释放一个页块

释放一个页块是由函数__free_pages来完成的。它主要利用我们讲过的__free_one_page来把内存页块放回到buddy system中,不过当释放单个页面时,它会把页面放回per-cpu page frame cache。

fastcall void __free_pages(struct page *page, unsigned int order){    if (put_page_testzero(page)) {        if (order == 0)            free_hot_page(page);        else            __free_pages_ok(page, order);    }}

与per-cpu page frame cache打交道的是函数free_hot_page。

void fastcall free_hot_page(struct page *page){    free_hot_cold_page(page, 0);}

/* * Free a 0-order page */static void fastcall free_hot_cold_page(struct page *page, int cold){    struct zone *zone = page_zone(page);    struct per_cpu_pages *pcp;    unsigned long flags;        ...        pcp = &zone_pcp(zone, get_cpu())->pcp[cold];    local_irq_save(flags);    __count_vm_event(PGFREE);    list_add(&page->lru, &pcp->list);    set_page_private(page, get_pageblock_migratetype(page));    pcp->count++;    if (pcp->count >= pcp->high) {        free_pages_bulk(zone, pcp->batch, &pcp->list, 0);        pcp->count -= pcp->batch;    }    local_irq_restore(flags);    put_cpu();}

这个函数逻辑非常简单,把要释放的页面放到cache中。然后检查cache的大小。

如果cache的count值大于等于high watermark, 则利用函数free_pages_bulk来缩减cache。free_pages_bulk利用__free_one_page把batch个单个页面放回到buddy system中。

在操作per-cpu page frame cache时,有个小细节很有意思。在cache的list中拿出和放回一个page frame都是从链表的头部进行的,这样就形成了一个LIFO的stack。而free_pages_bulk缩减cache时,是从链表的尾部开始的,这个很像LRU的思想。这个小的细节可以尽量保证cache中page frame的hot。

《诗经》有云,“迨天之未阴雨,彻彼桑土,绸缪牖户。” 这里讲的per-cpu page frame cache,可以说是“未雨绸缪”的好例子。