Linux内核内存管理之ZONE内存分配器

电子说

1.2w人已加入

描述

分配页帧

分配页帧的具体实现

释放页帧

分配页帧

内核中使用ZONE分配器满足内存分配请求。该分配器必须具有足够的空闲页帧,以便满足各种内存大小请求。为此,ZONE分配器必须能够:

它应该保护预留页帧池;

当内存不足并且允许阻塞当前进程时,能够触发页帧回收机制。一旦某些页帧被释放,ZONE分配器重新分配;

尽可能保留小的、珍贵的ZONE_DMA内存区。如果请求正常内存或高端内存,ZONE分配器不太可能分配ZONE_DMA内存区中的页帧。

对于每次连续页帧的申请,ZONE页帧分配器调用alloc_pages()宏实现。该宏其实是__alloc_pages()的封装,而该函数才是ZONE分配器的核心。它需要三个参数:

gfp_mask

内存分配请求中指定的标志。

order

连续物理页帧的对数。

zonelist

指向zonelist数据结构,按照优先顺序,选择适合内存分配的内存区。

__alloc_pages()扫描zonelist数据结构中每一个内存区,代码大概如下所示:

 

for (i = 0; (z=zonelist->zones[i]) != NULL; i++) {
    if (zone_watermark_ok(z, order, ...)) {
        page = buffered_rmqueue(z, order, gfp_mask);
        if (page)
            return page;
    }
}

 

对于每个内存区域,该函数将空闲页帧的数量与一个阈值进行比较,该阈值取决于内存分配标志、当前进程的类型以及该函数已经检查该区域的次数。实际上,如果可用内存很少,通常会对每个内存区域扫描几次,每次都对分配所需的最小可用内存设置较低的阈值。因此,前面的代码块在__alloc_pages()函数的主体中被复用了几次(只有很小的变化)。buffered_rmqueue()函数已经在前面的“CPU页帧缓存”一节中描述过了:它返回第一个分配的页帧的页描述符,如果内存区域不包含一组请求大小的连续页帧,则返回NULL。

zone_watermark_ok()辅助函数接收几个参数,这些参数决定内存ZONE中可用页帧数量的阈值min。特别是,如果满足以下两个条件,该函数返回值1,也就是具有足够的内存:

 

/*
 * 如果空闲页帧在阈值之上,则返回1.考虑分配的大小(order密数决定)
 */
int zone_watermark_ok(struct zone *z, int order, unsigned long mark,
              int classzone_idx, int can_try_harder, int gfp_high)
{
    /* free_pages可能会变成负值,但是没有关系 */
    long min = mark, free_pages = z->free_pages - (1 << order) + 1;
    int o;

    /* 如果设置了gfp_high标志,则阈值再减少1/2 */
    if (gfp_high)
        min -= min / 2;
    /* 如果设置了can_try_harder标志,则阈值再减少1/4 */
    if (can_try_harder)
        min -= min / 4;

    /* 除了要分配的页帧之外,该内存`ZONE`还至少包含min个页帧,
     * 但是,不包含预留的页帧。
     *(ZONE描述符的`low-on-memory`字段表示)。
     */
    if (free_pages <= min + z->lowmem_reserve[classzone_idx])
        return 0;
    /* 除了要分配的页帧,
     * 在`1`到`order`之间的空闲页帧列表中的每一个`k`,
     * 至少有`min/(2^k)`个空闲页帧。
     * 因此,如果`order`大于0,在大小为`2`的内存块列表中,
     * 至少有`min/2`个空闲页帧;
     * 如果`order`大于0,在大小为`4`的内存块列表中,
     * 至少有`min/4`个空闲页帧;以此类推。
     */
    for (o = 0; o < order; o++) {
        /* At the next order, this order's pages become unavailable */
        free_pages -= z->free_area[o].nr_free << o;

        /* Require fewer higher order pages to be free */
        min >>= 1;

        if (free_pages <= min)
            return 0;
    }
    return 1;

 

阈值min的值由zone_watermark_ok()确定,如下所示:

可以将pages_min,pages_low和pages_high三个内存ZONE区之一作为基本值作为函数的参数(参见本章前面的“预留页帧池”一节)。

如果设置了gfp_high标志,则将基值除以2。通常,如果在gfp_mask中设置了__GFP_HIGHMEM标志,也就是说,如果可以从高端内存中分配页帧的话,则该标志等于1。

如果设置了can_try_harder标志,则阈值将进一步减少四分之一。如果在gfp_mask中设置了__GFP_WAIT标志,或者当前进程是实时进程,并且内存分配是在进程上下文中完成的(在中断处理程序和可延迟函数之外),则该标志通常等于1。

分配页帧的具体实现

__alloc_pages()函数主要执行以下步骤:

 

struct page * fastcall
__alloc_pages(unsigned int gfp_mask, unsigned int order,
        struct zonelist *zonelist)
{
    // ...省略

    /* 如果调用方不能运行直接回收算法,
     * 或者调用方具有实时调度策略,
     * 则调用方可能会更多地使用预留页帧
     */
    can_try_harder = (unlikely(rt_task(p)) && !in_interrupt()) || !wait;
    zones = zonelist->zones;    /* 内存ZONE列表 */
    if (unlikely(zones[0] == NULL)) {
        return NULL;            /* 这应该发生吗? */
    }
    classzone_idx = zone_idx(zones[0]);

 restart:
    /* 1. 执行内存区域的第一次扫描。
     * 在第一次扫描中,min阈值设置为z->pages_low,
     * 其中z指向正在分析的zone描述符
     * (can_try_harder和gfp_high参数设置为零)。
     */
    for (i = 0; (z = zones[i]) != NULL; i++) {

        if (!zone_watermark_ok(z, order, z->pages_low,
                       classzone_idx, 0, 0))
            continue;

        page = buffered_rmqueue(z, order, gfp_mask);
        if (page)
            goto got_pg;
    }

    /* 2. 如果在前一步中没有终止,那么剩余的空闲内存就不多了;
     * 应该唤醒kswapd内核线程,开始异步回收页帧。
     */
    for (i = 0; (z = zones[i]) != NULL; i++)
        wakeup_kswapd(z, order);

    /* 3. 对内存区域执行第二次扫描:
     *    将值z->pages_min作为基本阈值传递。
     *    实际阈值还与can_try_harder和gfp_high标志有关。
     *    (允许内核和实时任务访问预留页帧池)
     *    这一步几乎与步骤1相同,只是函数使用了较低的阈值。
    */
    for (i = 0; (z = zones[i]) != NULL; i++) {
        if (!zone_watermark_ok(z, order, z->pages_min,
                       classzone_idx, can_try_harder,
                       gfp_mask & __GFP_HIGH))
            continue;

        page = buffered_rmqueue(z, order, gfp_mask);
        if (page)
            goto got_pg;
    }

    /* 4. 执行第三次内存区域扫描:
     *    如果前面没有分配到内存页帧,则说明系统内存应该非常低了。
     *    如果内核代码不是中断处理程序或可延迟函数,
     *    且它正在尝试回收页帧(设置了PF_MEMALLOC或PF_MEMDIE标志)。
     *    此时应该进行第3次扫描。
     *    此时应该忽略低内存阈值,即不调用zone_watermark_ok()。
     *    这应该是耗尽低内存预留页帧的唯一情况
     *   (这些页帧由zone描述符的lowmem_reserve字段指定)。
     *    在这种情况下,发送内存请求的内核代码最终通过尝试释放页帧,
     *    获得它想要的内存请求。
     *    如果没有内存ZONE包含足够的页帧,
     *    则函数返回NULL,并通知调用者分配失败。
     */
    if (((p->flags & PF_MEMALLOC) || 
            unlikely(test_thread_flag(TIF_MEMDIE))) &&
            !in_interrupt()) {
        /* 再一次遍历zonelist,忽略min */
        for (i = 0; (z = zones[i]) != NULL; i++) {
            page = buffered_rmqueue(z, order, gfp_mask);
            if (page)
                goto got_pg;
        }
        goto nopage;
    }

    /* 5. 原子分配 - 这种情况我们不能做任何均衡处理
     *    这种情况下,该函数返回NULL以通知内核代码内存分配失败:
     *    这种情况下,没有办法在不阻塞当前进程的情况下满足请求。
     */
    if (!wait)
        goto nopage;

rebalance:
    /* 6. 在这里,当前进程可以被阻塞:
     *    调用cond_resched()来检查其他进程是否需要CPU。
     */
    cond_resched();

    /* 7. 设置当前的PF_MEMALLOC标志,
     *    表示进程已准备好执行异步内存回收。
     */
    p->flags |= PF_MEMALLOC;

    /* 8. reclaim_state只包含一个字段reclaimed_slab,初始化为0 */
    reclaim_state.reclaimed_slab = 0;
    p->reclaim_state = &reclaim_state;

    /* 9. 寻找一些要回收的页帧。
     *    该函数可能会阻塞当前进程。
     *    一旦该函数返回,重置当前的PF_MEMALLOC标志,
     *    并再次调用cond_resched()。
     */
    did_some_progress = try_to_free_pages(zones, gfp_mask, order);

    p->reclaim_state = NULL;
    p->flags &= ~PF_MEMALLOC;

    cond_resched();

    if (likely(did_some_progress)) {
        /* 10. 说明前一步释放了一些页帧,
         *     那么该函数将执行与步骤3中相同的另一次内存区域扫描。
         *     如果内存分配请求不能被满足,
         *     zone_watermark_ok函数决定是否应该继续扫描内存区域。
         *     这儿使用高阈值,仅是为了捕获并行的oom kill;
         *     (也就是说,如果内存压力还是很大,则应该失败)
         */
        for (i = 0; (z = zones[i]) != NULL; i++) {
            if (!zone_watermark_ok(z, order, z->pages_min,
                           classzone_idx, can_try_harder,
                           gfp_mask & __GFP_HIGH))
                continue;

            page = buffered_rmqueue(z, order, gfp_mask);
            if (page)
                goto got_pg;
        }
    }
    /* 11. 如果在步骤9中没有释放页帧,那么内核就有大麻烦了,
     *     因为可用内存非常低,无法回收任何页帧。
     *     也许是时候做出一个关键的决定了:
     *     如果此时设置了__GFP_FS标志,且清零了__GFP_NORETRY标志
     *     如果内核控制路径允许执行与文件系统相关的操作来终止进程(gfp_mask中的' __GFP_FS '标志已设置),并且' __GFP_NORETRY '标志已清除,则执行以下子步骤:
     */ 
    else if ((gfp_mask & __GFP_FS) && !(gfp_mask & __GFP_NORETRY)) {
        /* 11.a zone_watermark_ok函数决定是否应该继续扫描内存区域。
         *      这儿使用高阈值z->pages_high,仅是为了捕获并行的oom kill;
         *      (也就是说,如果内存压力还是很大,则应该失败)
         *      
         *      因为该步使用的阈值比之前的都高,所以大概率会失败。
         *      实际上,只有当内核的其他代码已经杀死了一个进程并回收内存后
         *      该步才能成功。但是,这一步避免了杀死两个进程的情况。
         */
        for (i = 0; (z = zones[i]) != NULL; i++) {
            if (!zone_watermark_ok(z, order, z->pages_high,
                           classzone_idx, 0, 0))
                continue;

            page = buffered_rmqueue(z, order, gfp_mask);
            if (page)
                goto got_pg;
        }

        /* 11.b 杀死一些进程,释放内存 */
        out_of_memory(gfp_mask);

        /* 11.c 跳转回第1步 */
        goto restart;
    }

    /* 如果__GFP_NORETRY标志是清除的,并且内存分配请求跨越最多8页帧
     * 也就是说,尽量不要重复分配大于8个页帧以上的内存。
     * 或者__GFP_REPEAT和__GFP_NOFAIL标志之一被设置,
     * 函数调用blk_congestion_wait使进程休眠一段时间,
     * 然后它跳回步骤6。
     * 否则,该函数返回NULL以通知调用者内存分配失败。
     */
    do_retry = 0;
    if (!(gfp_mask & __GFP_NORETRY)) {
        if ((order <= 3) || (gfp_mask & __GFP_REPEAT))
            do_retry = 1;
        if (gfp_mask & __GFP_NOFAIL)
            do_retry = 1;
    }
    if (do_retry) {
        blk_congestion_wait(WRITE, HZ/50);
        goto rebalance;
    }

nopage:
    if (!(gfp_mask & __GFP_NOWARN) && printk_ratelimit()) {
        // ...省略
    }
    return NULL;
got_pg:
    zone_statistics(zonelist, z);
    return page;
}

 

释放页帧

zone分配器还负责释放页帧,但要比分配页帧简单。

内核中,所有释放页帧的宏和函数,都是基于__free_pages()函数实现的。该函数的参数是page,待要释放的第一个页帧的页描述符的地址;order,要释放的连续页帧组的对数大小。函数执行以下步骤:

检查第1个页帧是否真的属于动态内存(它的PG_reserved标志被清除);如果不是,则终止。

减少page->_count使用计数器;如果仍然大于等于0,终止。

如果order等于零,该函数调用free_hot_page()将页帧释放到相应内存区域的CPU本地热缓存中。

如果order大于0,它将页帧添加到本地列表中,并调用free_pages_bulk()函数将它们释放到适当内存区域的buddy系统中。

 
 审核编辑:汤梓红

 

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

全部0条评论

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

×
20
完善资料,
赚取积分