0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

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

jf_0tjVfeJz 来源:嵌入式ARM和Linux 2024-02-21 09:29 次阅读

分配页帧

分配页帧的具体实现

释放页帧

分配页帧

内核中使用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)
returnpage;
}
}

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

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

/*
*如果空闲页帧在阈值之上,则返回1.考虑分配的大小(order密数决定)
*/
intzone_watermark_ok(structzone*z,intorder,unsignedlongmark,
intclasszone_idx,intcan_try_harder,intgfp_high)
{
/*free_pages可能会变成负值,但是没有关系*/
longmin=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])
return0;
/*除了要分配的页帧,
*在`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()函数主要执行以下步骤:

structpage*fastcall
__alloc_pages(unsignedintgfp_mask,unsignedintorder,
structzonelist*zonelist)
{
//...省略

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

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

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)
gotogot_pg;
}
}
/*11.如果在步骤9中没有释放页帧,那么内核就有大麻烦了,
*因为可用内存非常低,无法回收任何页帧。
*也许是时候做出一个关键的决定了:
*如果此时设置了__GFP_FS标志,且清零了__GFP_NORETRY标志
*如果内核控制路径允许执行与文件系统相关的操作来终止进程(gfp_mask中的'__GFP_FS'标志已设置),并且'__GFP_NORETRY'标志已清除,则执行以下子步骤:
*/
elseif((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)
gotogot_pg;
}

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

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

/*如果__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系统中。

审核编辑:汤梓红

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

    关注

    3

    文章

    1309

    浏览量

    39846
  • cpu
    cpu
    +关注

    关注

    68

    文章

    10442

    浏览量

    206560
  • Linux
    +关注

    关注

    87

    文章

    10990

    浏览量

    206734
  • 分配器
    +关注

    关注

    0

    文章

    176

    浏览量

    25293

原文标题:Linux内核8.6-内存管理之ZONE内存分配器

文章出处:【微信号:嵌入式ARM和Linux,微信公众号:嵌入式ARM和Linux】欢迎添加关注!文章转载请注明出处。

收藏 人收藏

    评论

    相关推荐

    Linux内核内存管理内核非连续物理内存分配

    的主要优点是避免了外部碎片,而缺点是需要修改内核页表。显然,非连续内存区域的大小必须是4096的倍数。Linux使用非连续物理内存区的场景有几种:(1)为swap区
    的头像 发表于 02-23 09:44 379次阅读
    <b class='flag-5'>Linux</b><b class='flag-5'>内核</b><b class='flag-5'>内存</b><b class='flag-5'>管理</b>之<b class='flag-5'>内核</b>非连续物理<b class='flag-5'>内存</b><b class='flag-5'>分配</b>

    Linux内存系统: Linux 内存分配算法

    表项也会相应的更新6、slab 算法——基本原理1) 基本概念· Linux 所使用的 slab 分配器的基础是 Jeff Bonwick 为 SunOS 操作系统首次引入的一种算法· 它的基本思想是将
    发表于 08-24 07:44

    如何去制作一个高效的内存分配器

    高效内存分配机制是什么意思?如何去制作一个高效的内存分配器呢?
    发表于 01-20 06:57

    关于RT-Thread的动态内存管理简析

    分配器分配 。每个 zone分配内存块大小是固定的,相同大小内存块的
    发表于 04-06 17:11

    有关RT-Thread操作系统的内存管理模块基本知识简析

    zone达到一定数目后,系统就会把这个全空闲的zone释放到页面分配器中去。  内存管理的应用场景  RT-Threadd操作系统将
    发表于 05-11 15:14

    关于RTT支持的内存分配算法

    的,能够分配相同大小内存块的zone会链接在一个链表中,而72种对象的zone链表则放在一个数组(zone arry)中统一
    发表于 04-27 14:40

    关于RTT支持的内存分配算法

    是固定的,能够分配相同大小内存块的zone会链接在一个链表中,而72种对象的zone链表则放在一个数组(zone arry)中统一
    发表于 04-27 14:42

    linux内存管理中的SLAB分配器详解

    管理区页框分配器,这里我们简称为页框分配器,在页框分配器中主要是管理物理内存,将物理
    发表于 05-17 15:01 1926次阅读
    <b class='flag-5'>linux</b><b class='flag-5'>内存</b><b class='flag-5'>管理</b>中的SLAB<b class='flag-5'>分配器</b>详解

    深入剖析SLUB分配器和SLAB分配器的区别

    首先为什么要说slub分配器内核里小内存分配一共有三种,SLAB/SLUB/SLOB,slub分配器是slab
    发表于 05-17 16:05 871次阅读
    深入剖析SLUB<b class='flag-5'>分配器</b>和SLAB<b class='flag-5'>分配器</b>的区别

    Linux内核深度解析》之内存地址空间

    内核空间提供了把页划分成小内存分配的块分配器,提供分配内存的接口 kmalloc()和释放
    的头像 发表于 07-15 14:22 1919次阅读

    bootmem分配器使用的数据结构

    内核初始化的过程中需要分配内存内核提供了临时的引导内存分配器,在页
    的头像 发表于 07-22 11:18 1145次阅读

    Linux之引导内存分配器

    早期使用的引导内存分配器是 bootmem,目前正在使用 memblock 取代 bootmem。如果开启配置宏 CONFIG_NO_BOOTMEM,memblock 就会取代 bootmem。为了保证兼容性,bootmem 和 memblock 提供了相同的接口。
    的头像 发表于 07-22 11:17 1176次阅读

    Linux内核之伙伴分配器

    内核初始化完毕后,使用页分配器管理物理页,当前使用的页分配器是伙伴分配器,伙伴分配器的特点是算法
    的头像 发表于 07-25 14:06 1328次阅读

    Linux内核之块分配器

    为了解决小块内存分配问题,Linux 内核提供了块分配器,最早实现的块分配器是SLAB
    的头像 发表于 07-27 09:35 1250次阅读

    Linux内核引导内存分配器的原理

    Linux内核引导内存分配器使用的是伙伴系统算法。这种算法是一种用于动态内存分配的高效算法,它将
    发表于 04-03 14:52 246次阅读