Linux内存管理之CPU本地页帧缓存

电子说

1.2w人已加入

描述

0 CPU页帧缓存概念

在前一节中,我们学习了buddy伙伴关系系统,它适用于申请连续的大块物理内存;而有些时候,经常需要申请和释放单个页帧。但是,如果使用伙伴关系系统,需要查表、进行合并等操作,效率不高。为了提高性能,每个内存ZONE区都提供了一个per-CPU变量,CPU页帧缓存。每个CPU页帧缓存都包含一些预分配好的页帧,满足本地CPU发起的单个页帧请求。

实际上,每个内存ZONE区和每个CPU都有2个缓存:一个是热缓存,它存储页帧,其内容可能包含在CPU的硬件缓存中;另一个是冷缓存。

如果内核或用户进程在分配后立即写入页帧,那么从热缓存中获取页帧将有利于系统性能。实际上,每次访问页帧的某个内存位置,都会导致硬件Cache中替换其它页帧的某一行(Cache-line),当然,除非硬件Cache已经包含刚刚访问的“热”页帧中内存位置的一行。

相反,如果要用DMA操作填充页帧,则从冷缓存中取页帧是很方便的。在这种情况下,不涉及CPU,也不会修改硬件Cache的任何行。从冷缓存中取页帧可以为其他类型的内存分配请求保留热页帧。

CPU页帧缓存的数据结构是per_cpu_pageset类型的数组,其存储在内存ZONE描述符中的pageset成员中,如下面的代码所示:

 

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

 

数组个数与CPU个数相关,其中的每个数组元素又包含2个per_cpu_pages描述符成员:一个是热缓存;另一个是冷缓存。而per_cpu_pages数据类型的成员如下表所示:

 

struct per_cpu_pages {
    int                 count;      /* 缓存中的页帧数量 */
    int                 low;        /* 阈值下限,用于缓存补充 */
    int                 high;       /* 阈值上限,需要清空缓存 */
    int                 batch;      /* 需从缓存中添加或减少的页帧数 */
    struct list_head    list;       /* 缓存中页帧描述符列表,即内存页列表 */
};

 

内核使用两个阈值(low和high)监控冷/热缓存的大小:如果页帧数量低于阈值,则内核使用伙伴系统分配一定数量的单个页帧(batch);否则,页帧数量超过阈值上限,内核将缓存中的页帧释放到伙伴系统中(batch)。batch、low和high的值,具体依赖于内存ZONE区的页帧数量。

1 通过CPU页帧缓存分配页帧

buffered_rmqueue()函数在给定的内存ZONE区中分配页帧。它利用CPU页帧缓存来处理单个页帧请求。

Linux v2.6.11内核源码实现如下所示(文件位置:/mm/page_alloc.c):

 

static struct page *
buffered_rmqueue(struct zone *zone, int order, int gfp_flags)
{
    unsigned long flags;
    struct page *page = NULL;
    int cold = !!(gfp_flags & __GFP_COLD);

    if (order == 0) {
        struct per_cpu_pages *pcp;

        pcp = &zone->pageset[get_cpu()].pcp[cold];
        local_irq_save(flags);
        if (pcp->count <= pcp->low)
            pcp->count += rmqueue_bulk(zone, 0,
                        pcp->batch, &pcp->list);
        if (pcp->count) {
            page = list_entry(pcp->list.next, struct page, lru);
            list_del(&page->lru);
            pcp->count--;
        }
        local_irq_restore(flags);
        put_cpu();
    }

    if (page == NULL) {
        spin_lock_irqsave(&zone->lock, flags);
        page = __rmqueue(zone, order);
        spin_unlock_irqrestore(&zone->lock, flags);
    }

    if (page != NULL) {
        BUG_ON(bad_range(zone, page));
        mod_page_state_zone(zone, pgalloc, 1 << order);
        prep_new_page(page, order);

        if (gfp_flags & __GFP_ZERO)
            prep_zero_page(page, order, gfp_flags);

        if (order && (gfp_flags & __GFP_COMP))
            prep_compound_page(page, order);
    }
    return page;
}

 

输入参数分别是内存ZONE区的描述符的地址(zone)、内存分配请求大小(2^order)和分配标志gfp_flags。如果在gfp_flags中设置了__GFP_COLD标志,则应从冷缓存中获取页帧,否则应从热缓存中获取页帧(此标志仅对单个页帧请求有意义)。该函数基本上执行以下操作:

如果order不等于0,则页帧缓存不能使用,函数直接跳转到第4步。

检查由__GFP_COLD标志标识的内存ZONE区域的CPU缓存是否必须被补充(per_cpu_pages的count ≤ low)。在本例中,它执行以下子步骤:

重复调用__rmqueue()函数,从伙伴系统中分配batch个页帧。

将分配的页帧描述符插入到缓存的列表中。

更新count变量(将新分配的页帧数量加上)。

如果count > 0,从缓存列表中取一个页帧,然后跳转到第5步。(CPU页帧缓存可能是空的,在第2步的__rmqueue()没有申请到页帧时就会发生)

到这儿,如果内存请求没有被满足,调用__rmqueue()申请从伙伴系统中分配所请求页帧。

如果内存请求被满足,初始化该页帧(第1个)的页描述符:清除某些标志、设置private为0,设置页帧引用计数器为1。另外,如果设置了__GPF_ZERO,将申请的内存清零。

返回页帧(第1个)的描述符,失败返回NULL。

2 通过CPU页帧缓存释放页帧

从CPU页帧缓存中释放页帧,使用free_hot_page()和free_cold_page()函数。它们都是free_hot_cold_page()的封装函数,如下所示(文件位置:/mm/page_alloc.c):

 

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;

    arch_free_page(page, 0);

    kernel_map_pages(page, 1, 0);
    inc_page_state(pgfree);
    if (PageAnon(page))
        page->mapping = NULL;
    free_pages_check(__FUNCTION__, page);
    pcp = &zone->pageset[get_cpu()].pcp[cold];
    local_irq_save(flags);
    if (pcp->count >= pcp->high)
        pcp->count -= free_pages_bulk(zone, pcp->batch, &pcp->list, 0);
    list_add(&page->lru, &pcp->list);
    pcp->count++;
    local_irq_restore(flags);
    put_cpu();
}

 

free_hot_cold_page()接受的参数是待释放页帧的描述符地址page,表示热缓存还是冷缓存的标志cold。

执行的步骤如下:

根据页帧,获取page->flags标志。

根据cold标志获取对应页帧缓存的描述符per_cpu_pages地址。

检查缓存是否不足:如果count ≥ high,调用free_pages_bulk()函数。该函数会重复调用__free_pages_bulk()函数释放指定的页帧到伙伴系统中。

将该页帧添加到缓存列表中,增加count计数。

应该注意的是,在Linux v2.6内核中,没有任何页帧被释放到冷缓存中:内核总是假设释放的页帧相对于硬件缓存来说是热的。当然,这并不意味着冷缓存是空的:当达到低阈值时,缓存由buffered_rmqueue()补充。

3 移除__GFP_COLD

虽然我们前边分析了基于冷热缓存的CPU页帧缓存,但是,从v4.14版本以后的内核中已经移除,参考patch。Patches 1-4是与移除冷缓存最相关的部分;Patches 5-8是可选的,因为它们都是删除无用但也不影响性能的代码。

free_hot_cold_page的大多数调用者用户都声称被释放的页是热缓存的。唯一的例外是页回收代码,因为在不久的将来可能会释放足够多的页,因此CPU的本地页帧缓存列表将被回收,热缓存信息将丢失。由于没有人真正关心被释放到分配器的页的热信息,所以省略该参数即可。




审核编辑:刘清

打开APP阅读更多精彩内容
声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉

全部0条评论

快来发表一下你的评论吧 !

×
20
完善资料,
赚取积分