码迷,mamicode.com
首页 > 其他好文 > 详细

Kernel那些事儿之内存管理(4) --- 未雨绸缪

时间:2015-06-29 22:27:47      阅读:200      评论:0      收藏:0      [点我收藏+]

标签:linux kernel   内存管理   

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


本文出自 “内核部落格” 博客,请务必保留此出处http://richardguo.blog.51cto.com/9343720/1669125

Kernel那些事儿之内存管理(4) --- 未雨绸缪

标签:linux kernel   内存管理   

原文地址:http://richardguo.blog.51cto.com/9343720/1669125

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!