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

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

时间: 2024-07-30 10:13:13

Kernel那些事儿之内存管理(4) --- 未雨绸缪的相关文章

Kernel那些事儿之内存管理(1)

有人的地方就有江湖.要介绍内存管理这个江湖,首先还得从这里面的主要人物讲起. 在NUMA结构中,物理内存首先被分成若干nodes.每一个node进一步被分成若干zones.每一个zone又关联了一个描述page frames的数组,该数组包含了属于该zone的所有page frame的描述符. 不难看出,在这个江湖里主要有三位重要人物:nodes, zones 和 page frames.这三者的关系和地位大体可以用下图来描述(该图取自"Professional Linux Kernel Arc

Kernel那些事儿之内存管理(3) --- 久别重逢

上次我们讲到page frame是物理内存的基本组成单位.那Kernel就必须要有一套机制来管理空闲的page frames.这一点不难理解.每个县长必须要把本县可用的劳动力登记在册,这样哪天皇帝要征兵了,你才不至于手忙脚乱. 这个问题看似简单,实则不然.因为这里面有一个外碎片的问题. 在物理内存中,连续的物理内存页有时是很重要的.例如在DMA操作中,由于大部分DMA处理器都没有分页机制,它们会直接访问物理内存地址,因此DMA 所用的缓冲区在物理地址空间必须连续:再例如,使用连续的物理内存页,可

Kernel那些事儿之内存管理(8) --- Slab(中)

上篇讲了Slab中的数据结构,这篇该讲Slab中的操作了. 既然是内存管理,那操作无非就两点:allocate 和 free. 1. 申请一个object 在Slab中,申请一个object是通过函数 kmem_cache_alloc() 来完成的. 3618 void *kmem_cache_alloc(struct kmem_cache *cachep, gfp_t flags) 3619 { 3620     return __cache_alloc(cachep, flags, __bu

Kernel那些事儿之内存管理(7) --- Slab(上)

前面讲的buddy system算法,分配内存的最小单位是一个页面(例如 4K).这对于大的内存申请比较适用.可是实际生活中,Kernel经常需要分配小的内存空间,比如几十个字节,这个时候怎么办呢? 不同的人可能会想到不同的解决办法. 对于财大气粗的富人一族,办法很简单:申请一个页面,只用其中的几十字节,剩下的直接丢掉. 对于锱铢必较的穷人一族,就不敢这么挥霍了:申请一个页面,然后记录好页面内哪些内存区用了,哪些还没用,没用的部分可以用来满足其他的内存分配请求.总之务必做到物尽其用. 很不幸,K

Kernel那些事儿之内存管理(6) --- 衣带渐宽终不悔(下)

接着上篇写,继续介绍zone allocator.上一篇介绍了周边,现在来看看它的全貌 --- 函数__alloc_pages(). Kernel源代码里是这样注释函数__alloc_pages()的.其重要地位可见一斑. 1451 /* 1452  * This is the 'heart' of the zoned buddy allocator. 1453  */ __alloc_pages()的工作模式很清晰:利用函数get_page_from_freelist()多次遍历zonelis

Kernel那些事儿之内存管理(2) --- 百闻不如一见

上次介绍了物理内存管理中三位主要人物中的node 和zone.这两位是当官的,一个是县长,一个是里长,不敢不先介绍啊.接下来出场的就是我们的老百姓了 --- page frame. Page frame是物理内存的基本组成单位,在Kernel中由结构体 struct page 来描述. struct page {     unsigned long flags;          atomic_t _count;          union {         atomic_t _mapcou

Kernel那些事儿之内存管理(13) --- 内核映射(下)

前面讲过,针对于内核地址空间中后面的128MB空间,Kernel提供了三种机制来映射物理内存.之前讲过了两种,即持久内核映射和临时内核映射.这两种机制的目的都是一样的:使Kernel能够访问到高端内存. 今天讲一下第三种机制:非连续内存分配,也就是vmalloc.这个机制同样可以使Kernel能够访问到高端内存,不过这不是该机制的主要目的.该机制的主要目的是:把物理上不连续的页面映射到连续的内核线性地址空间中. 非连续内存区域管理 既然是映射,肯定会涉及到三个元素:集合L,集合P,映射M. 集合

Kernel那些事儿之内存管理(11) --- 内核映射(上)

前面简单地介绍了三种不同的地址空间,接下来重点讲述线性地址空间到物理地址空间的映射. 我们先从32位系统开始. 在32位系统中,线性地址空间的大小为 2^32,即4GB.Kernel一般会按照 3:1 的比例,把线性地址空间分为两部分: 0~3GB 用户地址空间 3GB~4GB 内核地址空间. 用户地址空间的管理和映射是个大的topic.我们后面再详细讲述. 内核地址空间只有1GB大小,最多只能映射1GB的物理内存.那问题来了:1GB之外物理内存怎么办? Kernel 给出的解决办法是,把1GB

Kernel那些事儿之内存管理(5) --- 衣带渐宽终不悔(上)

Kernel中负责分配一个连续内存页块的子系统一般被称为zoned page frame allocator.前面讲了函数 buffered_rmqueue() 是如何从指定zone的buddy system中分配一个连续内存页块的.这个函数貌似完成了内存页块分配相关的所有工作,然而实际上,这个函数只是zone allocator的冰山一角. 记得我刚上大学那会,拥有一个MP3还是一件能够令男生羡慕.令女生着迷的事情.于是我咬咬牙,花了近一个月的生活费买了一个漂亮的MP3,从此过上了非凡的生活.