上次讲的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,可以说是“未雨绸缪”的好例子。