关于linux-kernel:深入理解-Linux-物理内存分配全链路实现

1次阅读

共计 40375 个字符,预计需要花费 101 分钟才能阅读完成。

前文回顾

在上篇文章《深刻了解 Linux 物理内存治理》中,笔者具体的为大家介绍了 Linux 内核如何对物理内存进行治理以及相干的一些内核数据结构。

在介绍物理内存治理之前,笔者先从 CPU 的角度开始,介绍了三种 Linux 物理内存模型:FLATMEM 平坦内存模型,DISCONTIGMEM 非间断内存模型,SPARSEMEM 稠密内存模型。

随后笔者又带大家站在一个新的视角上,把物理内存看做成一个整体,从 CPU 拜访物理内存以及 CPU 与物理内存的绝对地位变动的角度介绍了两种物理内存架构:一致性内存拜访 UMA 架构,非一致性内存拜访 NUMA 架构。

在 NUMA 架构下,只有 DISCONTIGMEM 非间断内存模型和 SPARSEMEM 稠密内存模型是可用的。而 UMA 架构下,后面介绍的三种内存模型能够配置应用。

无论是 NUMA 架构还是 UMA 架构在内核中都是应用雷同的数据结构来组织治理的,在内核的内存治理模块中会把 UMA 架构当做只有一个 NUMA 节点的伪 NUMA 架构。

这样一来这两种架构模式就在内核中被对立治理起来,咱们基于这个事实,深刻分析了内核针对 NUMA 架构下用于物理内存治理的相干数据结构:struct pglist_data(NUMA 节点),struct zone(物理内存区域),struct page(物理页)。

上图展现的是在 NUMA 架构下,NUMA 节点与物理内存区域 zone 以及物理内存页 page 之间的档次关系。

物理内存被划分成了一个一个的内存节点(NUMA 节点),在每个 NUMA 节点外部又将其所治理的物理内存依照性能不同划分成了不同的内存区域 zone,每个内存区域 zone 治理一片用于具体性能的物理内存页 page,而内核会为每一个内存区域调配一个搭档零碎用于治理该内存区域下物理内存页 page 的调配和开释。

物理内存在内核中治理的层级关系为:None -> Zone -> page

在上篇文章的最初,笔者又花了大量的篇幅来为大家介绍了 struct page 构造,咱们理解了内核如何通过 struct page 构造来形容物理内存页,这个构造是内核中最为简单的一个构造体,因为它是物理内存治理的最小单位,被频繁利用在内核中的各种简单机制下。

通过以上内容的介绍,笔者感觉大家曾经在架构层面上对 Linux 物理内存治理有了一个较为粗浅的意识,当初物理内存治理的架构咱们曾经建设起来了,那么内核如何依据这个架构档次来调配物理内存呢?

为了给大家梳理分明内核调配物理内存的过程及其波及到的各个重要模块,于是就有了本文的内容~~

1. 内核物理内存调配接口

在为大家介绍物理内存调配之前,笔者先来介绍下内核中用于物理内存调配的几个外围接口,这几个物理内存调配接口全副是基于搭档零碎的,搭档零碎有一个特点就是它所调配的物理内存页全部都是物理上间断的,并且只能调配 2 的整数幂个页,这里的整数幂在内核中称之为调配阶。

上面要介绍的这些物理内存调配接口均须要指定这个调配阶,意思就是从搭档零碎申请多少个物理内存页,假如咱们指定调配阶为 order,那么就会从搭档零碎中申请 2 的 order 次幂个物理内存页。

内核中提供了一个 alloc_pages 函数用于调配 2 的 order 次幂个物理内存页,参数中的 unsigned int order 示意向底层搭档零碎指定的调配阶,参数 gfp_t gfp 是内核中定义的一个用于标准物理内存调配行为的修饰符,这里咱们先不开展,前面的大节中笔者会具体为大家介绍。

struct page *alloc_pages(gfp_t gfp, unsigned int order);

alloc_pages 函数用于向底层搭档零碎申请 2 的 order 次幂个物理内存页组成的内存块,该函数返回值是一个 struct page 类型的指针用于指向申请的内存块中第一个物理内存页。

alloc_pages 函数用于调配 多个 间断的物理内存页,在内核的某些内存调配场景中有时候并不需要调配这么多的间断内存页,而是只须要调配一个物理内存页即可,于是内核又提供了 alloc_page 宏,用于这种 单内存页 调配的场景,咱们能够看到其底层还是依赖了 alloc_pages 函数,只不过 order 指定为 0。

#define alloc_page(gfp_mask) alloc_pages(gfp_mask, 0)

当零碎中闲暇的物理内存无奈满足内存调配时,就会导致内存调配失败,alloc_pages,alloc_page 就会返回空指针 NULL。

vmalloc 分配机制底层就是用的 alloc_page

在物理内存调配胜利的状况下,alloc_pages,alloc_page 函数返回的都是指向其申请的物理内存块第一个物理内存页 struct page 指针。

大家能够间接了解成返回的是一块物理内存,而 CPU 能够间接拜访的却是虚拟内存,所以内核又提供了一个函数 __get_free_pages,该函数间接返回物理内存页的虚拟内存地址。用户能够间接应用。

unsigned long __get_free_pages(gfp_t gfp_mask, unsigned int order);

__get_free_pages 函数在应用形式上和 alloc_pages 是一样的,函数参数的含意也是一样,只不过一个是返回物理内存页的虚拟内存地址,一个是间接返回物理内存页。

事实上 __get_free_pages 函数的底层也是基于 alloc_pages 实现的,只不过多了一层虚拟地址转换的工作。

unsigned long __get_free_pages(gfp_t gfp_mask, unsigned int order)
{
    struct page *page;
    // 不能在高端内存中调配物理页,因为无奈间接映射获取虚拟内存地址
    page = alloc_pages(gfp_mask & ~__GFP_HIGHMEM, order);
    if (!page)
        return 0;
    // 将间接映射区中的物理内存页转换为虚拟内存地址
    return (unsigned long) page_address(page);
}

page_address 函数用于将给定的物理内存页 page 转换为它的虚拟内存地址,不过这里只实用于内核虚拟内存空间中的间接映射区,因为在间接映射区中虚拟内存地址到物理内存地址是间接映射的,虚拟内存地址减去一个固定的偏移就能够间接失去物理内存地址。

如果物理内存页处于高端内存中,则不能这样间接进行转换,在通过 alloc_pages 函数获取物理内存页 page 之后,须要调用 kmap 映射将 page 映射到内核虚拟地址空间中。

遗记这块内容的同学,能够在回看下笔者之前的文章《深刻了解虚拟内存治理》中的“7.1.4 永恒映射区”大节。

同 alloc_page 函数一样,内核也提供了 __get_free_page 用于只调配单个物理内存页的场景,底层还是依赖于 __get_free_pages 函数,参数 order 指定为 0。

#define __get_free_page(gfp_mask) \
        __get_free_pages((gfp_mask), 0)

无论是 alloc_pages 也好还是 __get_free_pages 也好,它们申请到的内存页中蕴含的数据在一开始都不是空白的,而是内核随机产生的一些垃圾信息,但其实这些信息可能并不都是齐全随机的,很有可能随机的蕴含一些敏感的信息。

这些敏感的信息可能会被一些黑客所利用,并对计算机系统产生一些危害行为,所以从应用平安的角度思考,内核又提供了一个函数 get_zeroed_page,顾名思义,这个函数会将从搭档零碎中申请到内存页全副初始化填充为 0,这在调配物理内存页给用户空间应用的时候十分有用。

unsigned long get_zeroed_page(gfp_t gfp_mask)
{return __get_free_pages(gfp_mask | __GFP_ZERO, 0);
}

get_zeroed_page 函数底层也依赖于 __get_free_pages,指定的调配阶 order 也是 0,示意从搭档零碎中只申请一个物理内存页并初始化填充 0。

除此之外,内核还提供了一个 __get_dma_pages 函数,专门用于从 DMA 内存区域调配实用于 DMA 的物理内存页。其底层也是依赖于 __get_free_pages 函数。

unsigned long __get_dma_pages(gfp_t gfp_mask, unsigned int order);

这些底层依赖于 __get_free_pages 的物理内存调配函数,在遇到内存调配失败的状况下都会返回 0。

以上介绍的物理内存调配函数,调配的均是在物理上间断的内存页。

当然了,有内存的调配就会有内存的开释,所以内核还提供了两个用于开释物理内存页的函数:

void __free_pages(struct page *page, unsigned int order);
void free_pages(unsigned long addr, unsigned int order);
  • __free_pages:同 alloc_pages 函数对应,用于开释一个或者 2 的 order 次幂个内存页,开释的物理内存区域起始地址由该区域中的第一个 page 实例指针示意,也就是参数里的 struct page *page 指针。
  • free_pages:同 __get_free_pages 函数对应,与 __free_pages 函数的区别是在开释物理内存时,应用了虚拟内存地址而不是 page 指针。

在开释内存时须要十分审慎小心,咱们只能开释属于你本人的内存页,传递了谬误的 struct page 指针或者谬误的虚拟内存地址,或者传递错了 order 值,都可能会导致系统的解体。在内核空间中,内核是齐全信赖本人的,这点和用户空间不同。

另外内核也提供了 __free_page 和 free_page 两个宏,专门用于开释单个物理内存页。

#define __free_page(page) __free_pages((page), 0)
#define free_page(addr) free_pages((addr), 0)

到这里,对于内核中对于物理内存调配和开释的接口,笔者就为大家交代完了,然而大家可能会有一个疑难,就是咱们在介绍 alloc_pages 和 __get_free_pages 函数的时候,它们的参数中都有 gfp_t gfp_mask,之前笔者简略的提过这个 gfp_mask 掩码:它是内核中定义的一个用于标准物理内存调配行为的掩码。

那么这个掩码到底标准了哪些物理内存的调配行为?并对物理内存的调配有哪些影响呢?大家跟着笔者的节奏持续往下看~~~

2. 标准物理内存调配行为的掩码 gfp_mask

笔者在《深刻了解 Linux 物理内存治理》一文中的“4.3 NUMA 节点物理内存区域的划分”大节中已经为大家具体的介绍了 NUMA 节点中物理内存区域 zone 的划分。

笔者在文章中提到,因为理论的计算机体系结构受到硬件方面的制约,间接限度了页框的应用形式。于是内核会依据不同的物理内存区域的性能不同,将 NUMA 节点内的物理内存划分为:ZONE_DMA,ZONE_DMA32,ZONE_NORMAL,ZONE_HIGHMEM 这几个物理内存区域。

ZONE_MOVABLE 区域是内核从逻辑上的划分,该区域中的物理内存页面来自于上述几个内存区域,目标是防止内存碎片和反对内存热插拔

当咱们调用上大节中介绍的那几个物理内存调配接口时,比方:alloc_pages 和 __get_free_pages。就会遇到一个问题,就是咱们申请的这些物理内存到底来自于哪个物理内存区域 zone,如果咱们想要从指定的物理内存区域中申请内存,咱们该如何通知内核呢 ?

struct page *alloc_pages(gfp_t gfp, unsigned int order);
unsigned long __get_free_pages(gfp_t gfp_mask, unsigned int order);

这时,这些物理内存调配接口中的 gfp_t 参数就派上用场了,前缀 gfp 是 get free page 的缩写,意思是在获取闲暇物理内存页的时候须要指定的调配掩码 gfp_mask。

gfp_mask 中的低 4 位用来示意应该从哪个物理内存区域 zone 中获取内存页 page。

gfp_mask 掩码中这些区域修饰符 zone modifiers 定义在内核 /include/linux/gfp.h 文件中:

#define ___GFP_DMA            0x01u
#define ___GFP_HIGHMEM        0x02u
#define ___GFP_DMA32        0x04u
#define ___GFP_MOVABLE        0x08u

大家这里可能会感到好奇,为什么没有定义 ___GFP_NORMAL 的掩码呢?

这是因为内核对物理内存的调配次要是落在 ZONE_NORMAL 区域中,如果咱们不指定物理内存的调配区域,那么内核会默认从 ZONE_NORMAL 区域中分配内存,如果 ZONE_NORMAL 区域中的闲暇内存不够,内核则会降级到 ZONE_DMA 区域中调配。

对于物理内存调配的区域降级策略,笔者在后面的文章《深刻了解 Linux 物理内存治理》的“5.1 物理内存区域中的预留内存”大节中曾经具体地为大家介绍过了,然而之前的介绍只是停留在实践层面,那么这个物理内存区域降级策略是在哪里实现的呢?接下来的内容笔者就为大家揭晓~~~

内核在 /include/linux/gfp.h 文件中定义了一个叫做 gfp_zone 的函数,这个函数用于将咱们在物理内存调配接口中指定的 gfp_mask 掩码转换为物理内存区域,返回的这个物理内存区域是内存调配的最高级内存区域,如果这个最高级内存区域不足以满足内存调配的需要,则依照 ZONE_HIGHMEM -> ZONE_NORMAL -> ZONE_DMA 的程序顺次降级。

static inline enum zone_type gfp_zone(gfp_t flags)
{
    enum zone_type z;
    int bit = (__force int) (flags & GFP_ZONEMASK);

    z = (GFP_ZONE_TABLE >> (bit * GFP_ZONES_SHIFT)) &
                     ((1 << GFP_ZONES_SHIFT) - 1);
    VM_BUG_ON((GFP_ZONE_BAD >> bit) & 1);
    return z;
}

下面的这个 gfp_zone 函数是在内核 5.19 版本中的实现,在高版本的实现中用大量的移位操作替换了低版本中的实现,目标是为了进步程序的性能,然而带来的却是可读性的大幅降落。

笔者写到这里感觉给大家剖析分明每一步移位操作的实现对大家了解这个函数的骨干逻辑并没有什么本质意义上的帮忙,并且和本文主题偏离太远,所以咱们退回到低版本 2.6.24 中的实现,在这一版中直击 gfp_zone 函数本来的风貌。

static inline enum zone_type gfp_zone(gfp_t flags)
{
    int base = 0;

#ifdef CONFIG_NUMA
    if (flags & __GFP_THISNODE)
        base = MAX_NR_ZONES;
#endif

#ifdef CONFIG_ZONE_DMA
    if (flags & __GFP_DMA)
        return base + ZONE_DMA;
#endif
#ifdef CONFIG_ZONE_DMA32
    if (flags & __GFP_DMA32)
        return base + ZONE_DMA32;
#endif
    if ((flags & (__GFP_HIGHMEM | __GFP_MOVABLE)) ==
            (__GFP_HIGHMEM | __GFP_MOVABLE))
        return base + ZONE_MOVABLE;
#ifdef CONFIG_HIGHMEM
    if (flags & __GFP_HIGHMEM)
        return base + ZONE_HIGHMEM;
#endif
    // 默认从 normal 区域中分配内存
    return base + ZONE_NORMAL;
}

咱们看到在内核 2.6.24 版本中的 gfp_zone 函数实现逻辑就十分的清晰了,外围逻辑次要如下:

  • 只有掩码 flags 中设置了 __GFP_DMA,则不论 __GFP_HIGHMEM 有没有设置,内存调配都只会在 ZONE_DMA 区域中调配。
  • 如果掩码只设置了 ZONE_HIGHMEM,则在物理内存调配时,优先在 ZONE_HIGHMEM 区域中进行调配,如果容量不够则降级到 ZONE_NORMAL 中,如果还是不够则进一步降级至 ZONE_DMA 中调配。
  • 如果掩码既没有设置 ZONE_HIGHMEM 也没有设置 __GFP_DMA,则走到最初的分支,默认优先从 ZONE_NORMAL 区域中进行内存调配,如果容量不够则降级至 ZONE_DMA 区域中调配。
  • 独自设置 __GFP_MOVABLE 其实并不会影响内核的调配策略,咱们如果想要让内核在 ZONE_MOVABLE 区域中分配内存须要同时指定 __GFP_MOVABLE 和 __GFP_HIGHMEM。

ZONE_MOVABLE 只是内核定义的一个虚拟内存区域,目标是防止内存碎片和反对内存热插拔。上述介绍的 ZONE_HIGHMEM,ZONE_NORMAL,ZONE_DMA 才是真正的物理内存区域,ZONE_MOVABLE 虚拟内存区域中的物理内存来自于上述三个物理内存区域。

在 32 位零碎中 ZONE_MOVABLE 虚拟内存区域中的物理内存页来自于 ZONE_HIGHMEM。

在 64 位零碎中 ZONE_MOVABLE 虚拟内存区域中的物理内存页来自于 ZONE_NORMAL 或者 ZONE_DMA 区域。

上面是不同的 gfp_t 掩码设置形式与其对应的内存区域降级策略汇总列表:

| gfp_t 掩码 | 内存区域降级策略 |
| :————: | :———————-: |
| 什么都没有设置 | ZONE_NORMAL -> ZONE_DMA |
| __GFP_DMA | ZONE_DMA |
| __GFP_DMA & __GFP_HIGHMEM | ZONE_DMA |
| __GFP_HIGHMEM | ZONE_HIGHMEM -> ZONE_NORMAL -> ZONE_DMA |

除了上述介绍 gfp_t 掩码中的这四个物理内存区域修饰符之外,内核还定义了一些标准内存调配行为的修饰符,这些行为修饰符并不会限度内核从哪个物理内存区域中分配内存,而是会限度物理内存调配的行为,那么具体会限度哪些内存调配的行为呢?让咱们接着往下看~~~

这些内存调配行为修饰符同样也是定义在 /include/linux/gfp.h 文件中:

#define ___GFP_RECLAIMABLE    0x10u
#define ___GFP_HIGH        0x20u
#define ___GFP_IO        0x40u
#define ___GFP_FS        0x80u
#define ___GFP_ZERO        0x100u
#define ___GFP_ATOMIC        0x200u
#define ___GFP_DIRECT_RECLAIM    0x400u
#define ___GFP_KSWAPD_RECLAIM    0x800u
#define ___GFP_NOWARN        0x2000u
#define ___GFP_RETRY_MAYFAIL    0x4000u
#define ___GFP_NOFAIL        0x8000u
#define ___GFP_NORETRY        0x10000u
#define ___GFP_HARDWALL        0x100000u
#define ___GFP_THISNODE        0x200000u
#define ___GFP_MEMALLOC        0x20000u
#define ___GFP_NOMEMALLOC    0x80000u
  • ___GFP_RECLAIMABLE 用于指定调配的页面是能够回收的,___GFP_MOVABLE 则是用于指定调配的页面是能够挪动的,这两个标记会影响底层的搭档零碎从哪个区域中去获取闲暇内存页,这块内容咱们会在前面解说搭档零碎的时候具体介绍。
  • ___GFP_HIGH 示意该内存调配申请是高优先级的,内核急迫的须要内存,如果内存调配失败则会给零碎带来十分重大的结果,设置该标记通常内存是不容许调配失败的,如果闲暇内存不足,则会从紧急预留内存中调配。

对于物理内存区域中的紧急预留内存相干内容,笔者在之前文章《深刻了解 Linux 物理内存治理》一文中的“5.1 物理内存区域中的预留内存”大节中曾经具体介绍过了。

  • ___GFP_IO 示意内核在调配物理内存的时候能够发动磁盘 IO 操作。什么意思呢?比方当内核在进行内存调配的时候,发现物理内存不足,这时须要将不常常应用的内存页置换到 SWAP 分区或者 SWAP 文件中,这时就波及到了 IO 操作,如果设置了该标记,示意容许内核将不罕用的内存页置换进来。
  • ___GFP_FS 容许内核执行底层文件系统操作,在与 VFS 虚构文件系统层相关联的内核子系统中必须禁用该标记,否则可能会引起文件系统操作的循环递归调用,因为在设置 ___GFP_FS 标记分配内存的状况下,可能会引起更多的文件系统操作,而这些文件系统的操作可能又会进一步产生内存调配行为,这样始终递归继续上来。
  • ___GFP_ZERO 在内核分配内存胜利之后,将内存页初始化填充字节 0。
  • ___GFP_ATOMIC 该标记的设置示意内存在调配物理内存的时候不容许睡眠必须是原子性地进行内存调配。比方在中断处理程序中,就不能睡眠,因为中断程序不能被从新调度。同时也不能在持有自旋锁的过程上下文中睡眠,因为可能导致死锁。综上所述这个标记只能用在不能被从新平安调度的过程上下文中
  • ___GFP_DIRECT_RECLAIM 示意内核在进行内存调配的时候,能够进行间接内存回收。当残余内存容量低于水位线 _watermark[WMARK_MIN] 时,阐明此时的内存容量曾经十分危险了,如果过程在这时申请内存调配,内核就会进行间接内存回收,直到内存水位线复原到 _watermark[WMARK_HIGH] 之上。
  • ___GFP_KSWAPD_RECLAIM 示意内核在分配内存的时候,如果残余内存容量在 _watermark[WMARK_MIN] 与 _watermark[WMARK_LOW] 之间时,内核就会唤醒 kswapd 过程开始异步内存回收,直到残余内存高于 _watermark[WMARK_HIGH] 为止。
  • ___GFP_NOWARN 示意当内核分配内存失败时,克制内核的调配失败错误报告。
  • ___GFP_RETRY_MAYFAIL 在内核分配内存失败的时候,容许重试,但重试依然可能失败,重试若干次后进行。与其对应的是 ___GFP_NORETRY 标记示意分配内存失败时不容许重试。
  • ___GFP_NOFAIL 在内核调配失败时始终重试直到胜利为止。
  • ___GFP_HARDWALL 该标记限度了内核分配内存的行为只能在以后过程调配到的 CPU 所关联的 NUMA 节点上进行调配,当过程能够运行的 CPU 受限时,该标记才会有意义,如果过程容许在所有 CPU 上运行则该标记没有意义。
  • ___GFP_THISNODE 该标记限度了内核分配内存的行为只能在以后 NUMA 节点或者在指定 NUMA 节点中分配内存,如果内存调配失败不容许从其余备用 NUMA 节点中分配内存。
  • ___GFP_MEMALLOC 容许内核在分配内存时能够从所有内存区域中获取内存,包含从紧急预留内存中获取。但应用该标示时须要保障过程在取得内存之后会很快的开释掉内存不会过长时间的占用,尤其要警觉防止过多的耗费紧急预留内存区域中的内存。
  • ___GFP_NOMEMALLOC 标记用于明确禁止内核从紧急预留内存中获取内存。___GFP_NOMEMALLOC 标识的优先级要高于 ___GFP_MEMALLOC

好了到当初为止,咱们曾经晓得了 gfp_t 掩码中蕴含的内存区域修饰符以及内存调配行为修饰符,是不是感觉头有点大了,事实上的确很让人头大,因为内核在不同场景下会应用不同的组合,这么多的修饰符总是以组合的模式呈现,如果咱们每次应用的时候都须要独自指定,那就会十分繁冗也很容易出错。

于是内核将各种规范情景下用到的 gfp_t 掩码组合,提前为大家定义了一些规范的分组,不便大家间接应用。

#define GFP_ATOMIC    (__GFP_HIGH|__GFP_ATOMIC|__GFP_KSWAPD_RECLAIM)
#define GFP_KERNEL    (__GFP_RECLAIM | __GFP_IO | __GFP_FS)
#define GFP_NOWAIT    (__GFP_KSWAPD_RECLAIM)
#define GFP_NOIO    (__GFP_RECLAIM)
#define GFP_NOFS    (__GFP_RECLAIM | __GFP_IO)
#define GFP_USER    (__GFP_RECLAIM | __GFP_IO | __GFP_FS | __GFP_HARDWALL)
#define GFP_DMA        __GFP_DMA
#define GFP_DMA32    __GFP_DMA32
#define GFP_HIGHUSER    (GFP_USER | __GFP_HIGHMEM)
  • GFP_ATOMIC 是掩码 __GFP_HIGH,__GFP_ATOMIC,__GFP_KSWAPD_RECLAIM 的组合,示意内存调配行为必须是原子的,是高优先级的。在任何状况下都不容许睡眠,如果闲暇内存不够,则会从紧急预留内存中调配。该标记实用于中断程序,以及持有自旋锁的过程上下文中。
  • GFP_KERNEL 是内核中最罕用的标记,该标记设置之后内核的分配内存行为可能会阻塞睡眠,能够容许内核置换出一些不沉闷的内存页到磁盘中。实用于能够从新平安调度的过程上下文中。
  • GFP_NOIO 和 GFP_NOFS 别离禁止内核在分配内存时进行磁盘 IO 和 文件系统 IO 操作。
  • GFP_USER 用于映射到用户空间的内存调配,通常这些内存能够被内核或者硬件间接拜访,比方硬件设施会将 Buffer 间接映射到用户空间中
  • GFP_DMA 和 GFP_DMA32 示意须要从 ZONE_DMA 和 ZONE_DMA32 内存区域中获取实用于 DMA 的内存页。
  • GFP_HIGHUSER 用于给用户空间调配高端内存,因为在用户虚拟内存空间中,都是通过页表来拜访非间接映射的高端内存区域,所以用户空间个别应用的是高端内存区域 ZONE_HIGHMEM。

当初咱们算是真正了解了,在本大节开始时,介绍的那几个内存调配接口函数中对于内存调配掩码 gfp_mask 的所有内容,其中包含用于限度内核从哪个内存区域中分配内存,内核在分配内存过程中的行为,以及内核在各种规范调配场景下事后定义的掩码组合。

这时咱们在回过头来看内核中对于物理内存调配的这些接口函数是不是感觉一目了然了:

struct page *alloc_pages(gfp_t gfp, unsigned int order)
unsigned long __get_free_pages(gfp_t gfp_mask, unsigned int order)
unsigned long get_zeroed_page(gfp_t gfp_mask)
unsigned long __get_dma_pages(gfp_t gfp_mask, unsigned int order)

好了,当初咱们曾经分明了这些内存调配接口的应用,那么这些接口又是如何实现的呢?让咱们再一次深刻到内核源码中去摸索内核到底是如何调配物理内存的~~

3. 物理内存调配内核源码实现

本文基于内核 5.19 版本探讨

在介绍 Linux 内核对于内存调配的源码实现之前,咱们须要先找到内存调配的入口函数在哪里,在上大节中为大家介绍的泛滥内存调配接口的依赖层级关系如下图所示:

咱们看到内存调配的工作最终会落在 alloc_pages 这个接口函数中,在 alloc_pages 中会调用 alloc_pages_node 进而调用 __alloc_pages_node 函数,最终通过 __alloc_pages 函数正式进入内核内存调配的世界~~

__alloc_pages 函数为 Linux 内核内存调配的外围入口函数

static inline struct page *alloc_pages(gfp_t gfp_mask, unsigned int order)
{return alloc_pages_node(numa_node_id(), gfp_mask, order);
}
static inline struct page *
__alloc_pages_node(int nid, gfp_t gfp_mask, unsigned int order)
{
    // 校验指定的 NUMA 节点 ID 是否非法,不要越界
    VM_BUG_ON(nid < 0 || nid >= MAX_NUMNODES);
    // 指定节点必须是无效在线的
    VM_WARN_ON((gfp_mask & __GFP_THISNODE) && !node_online(nid));

    return __alloc_pages(gfp_mask, order, nid, NULL);
}

__alloc_pages_node 函数参数中的 nid 就是咱们在上篇文章《深刻了解 Linux 物理内存治理》的“4.1 内核如何对立组织 NUMA 节点”大节介绍的 NUMA 节点 id。

内核应用了一个大小为 MAX_NUMNODES 的全局数组 node_data[] 来治理所有的 NUMA 节点,数组的下标即为 NUMA 节点 Id。

#ifdef CONFIG_NUMA
extern struct pglist_data *node_data[];
#define NODE_DATA(nid)        (node_data[(nid)])

这里指定 nid 是为了通知内核应该在哪个 NUMA 节点上分配内存,咱们看到在
alloc_pages 函数中通过 numa_node_id() 获取运行以后过程的 CPU 所在的 NUMA 节点。并通过 !node_online(nid) 确保指定的 NUMA 节点是无效在线的。

对于 NUMA 节点的状态信息,大家可回看上篇文章的《4.5 NUMA 节点的状态 node_states》大节。

3.1 内存调配行为标识掩码 ALLOC_*

在咱们进入 __alloc_pages 函数之前,笔者先来为大家介绍几个影响内核分配内存行为的标识,这些重要标识定义在内核文件 /mm/internal.h 中:

#define ALLOC_WMARK_MIN     WMARK_MIN
#define ALLOC_WMARK_LOW     WMARK_LOW
#define ALLOC_WMARK_HIGH    WMARK_HIGH
#define ALLOC_NO_WATERMARKS 0x04 /* don't check watermarks at all */

#define ALLOC_HARDER         0x10 /* try to alloc harder */
#define ALLOC_HIGH       0x20 /* __GFP_HIGH set */
#define ALLOC_CPUSET         0x40 /* check for correct cpuset */

#define ALLOC_KSWAPD        0x800 /* allow waking of kswapd, __GFP_KSWAPD_RECLAIM set */

咱们先来看前四个标识内存水位线的常量含意,这四个内存水位线标识示意内核在分配内存时必须思考内存的水位线,在不同的水位线下内存的调配行为也会有所不同。

笔者在上篇文章《深刻了解 Linux 物理内存治理》的“5.2 物理内存区域中的水位线”大节中曾具体地介绍了各个水位线的含意以及在不同水位线下内存调配的不同体现。

上篇文章中咱们提到,内核会为 NUMA 节点中的每个物理内存区域 zone 定制三条用于批示内存容量的水位线,它们别离是:WMARK_MIN(页最小阈值),WMARK_LOW(页低阈值),WMARK_HIGH(页高阈值)。

这三个水位线定义在 /include/linux/mmzone.h 文件中:

enum zone_watermarks {
    WMARK_MIN,
    WMARK_LOW,
    WMARK_HIGH,
    NR_WMARK
};

三条水位线对应的 watermark 具体数值存储在每个物理内存区域 struct zone 构造中的 _watermark[NR_WMARK] 数组中。

struct zone {
    // 物理内存区域中的水位线
    unsigned long _watermark[NR_WMARK];
}

物理内存区域中不同水位线的含意以及内存调配在不同水位线下的行为如下图所示:

  • 当该物理内存区域的残余内存容量高于 _watermark[WMARK_HIGH] 时,阐明此时该物理内存区域中的内存容量十分短缺,内存调配齐全没有压力。
  • 当残余内存容量在 _watermark[WMARK_LOW] 与_watermark[WMARK_HIGH] 之间时,阐明此时内存有肯定的耗费然而还能够承受,可能持续满足过程的内存调配需要。
  • 当残余内存容量在 _watermark[WMARK_MIN] 与 _watermark[WMARK_LOW] 之间时,阐明此时内存容量曾经有点危险了,内存调配面临肯定的压力,然而还能够满足过程此时的内存调配要求,当给过程调配完内存之后,就会唤醒 kswapd 过程开始内存回收,直到残余内存高于 _watermark[WMARK_HIGH] 为止。

在这种状况下,过程的内存调配会触发内存回收,但申请过程自身不会被阻塞,由内核的 kswapd 过程异步回收内存。

  • 当残余内存容量低于 _watermark[WMARK_MIN] 时,阐明此时的内存容量曾经十分危险了,如果过程在这时申请内存调配,内核就会进行 间接内存回收,这时内存回收的工作将会由申请进程同步实现。

留神:下面提到的物理内存区域 zone 的残余内存是须要刨去 lowmem_reserve 预留内存大小(用于紧急内存调配)。也就是说 zone 里被搭档零碎所治理的内存并不蕴含 lowmem_reserve 预留内存。

好了,在咱们从新回顾了内存调配行为在这三条水位线:_watermark[WMARK_HIGH],_watermark[WMARK_LOW],_watermark[WMARK_MIN] 下的不同体现之后,咱们在回过来看本大节开始处提到的那几个 ALLOC_* 内存调配标识。

ALLOC_NO_WATERMARKS 示意在内存调配过程中齐全不会思考上述三个水位线的影响。

ALLOC_WMARK_HIGH 示意在内存调配的时候,以后物理内存区域 zone 中残余内存页的数量至多要达到 _watermark[WMARK_HIGH] 水位线,能力进行内存的调配。

ALLOC_WMARK_LOW 和 ALLOC_WMARK_MIN 要表白的内存调配语义也是一样,以后物理内存区域 zone 中残余内存页的数量至多要达到水位线 _watermark[WMARK_LOW] 或者 _watermark[WMARK_MIN],能力进行内存的调配。

ALLOC_HARDER 示意在内存调配的时候,会放宽内存调配规定的限度,所谓的放宽规定就是升高 _watermark[WMARK_MIN] 水位线,致力使内存调配最大可能胜利。

当咱们在 gfp_t 掩码中设置了 ___GFP_HIGH 时,ALLOC_HIGH 标识才起作用,该标识示意以后内存调配申请是高优先级的,内核急迫的须要内存,如果内存调配失败则会给零碎带来十分重大的结果,设置该标记通常内存是不容许调配失败的,如果闲暇内存不足,则会从紧急预留内存中调配。

ALLOC_CPUSET 示意内存只能在以后过程所容许运行的 CPU 所关联的 NUMA 节点中进行调配。比方应用 cgroup 限度过程只能在某些特定的 CPU 上运行,那么过程所发动的内存调配申请,只能在这些特定 CPU 所在的 NUMA 节点中进行。

ALLOC_KSWAPD 示意容许唤醒 NUMA 节点中的 KSWAPD 过程,异步进行内存回收。

内核会为每个 NUMA 节点调配一个 kswapd 过程用于回收不常常应用的页面。

typedef struct pglist_data {
        .........
    // 页面回收过程
    struct task_struct *kswapd;
        ..........
} pg_data_t;

3.2 内存调配的心脏 __alloc_pages

好了,在为大家介绍完这些影响内存调配行为的相干标识掩码:GFP_*ALLOC_* 之后,上面就该来介绍本文的主题——物理内存调配的外围函数 __alloc_pages,从上面内核源码的正文中咱们能够看出,这个函数正是搭档零碎的外围心脏,它是内核内存调配的外围入口函数,整个内存调配的残缺过程全副封装在这里。

该函数的逻辑比较复杂,因为在内存调配过程中须要波及解决各种 GFP_*ALLOC_* 标识,而后根据上述各种标识的含意来决定内存调配该如何进行。所以大家须要多点急躁,一步一步跟着笔者的思路往下走~~~

/*
 * This is the 'heart' of the zoned buddy allocator.
 */
struct page *__alloc_pages(gfp_t gfp, unsigned int order, int preferred_nid,
                            nodemask_t *nodemask)
{
    // 用于指向调配胜利的内存
    struct page *page;
    // 内存区域中的残余内存须要在 WMARK_LOW 水位线之上能力进行内存调配,否则失败(首次尝试疾速内存调配)unsigned int alloc_flags = ALLOC_WMARK_LOW;
    // 之前大节中介绍的内存调配掩码汇合
    gfp_t alloc_gfp; 
    // 用于在不同内存调配辅助函数中传递参数
    struct alloc_context ac = { };

    // 查看用于向搭档零碎申请内存容量的调配阶 order 的合法性
    // 内核定义最大调配阶 MAX_ORDER -1 = 10,也就是说一次最多只能从搭档零碎中申请 1024 个内存页。if (WARN_ON_ONCE_GFP(order >= MAX_ORDER, gfp))
        return NULL;
    // 示意在内存调配期间过程能够休眠阻塞
    gfp &= gfp_allowed_mask;

    alloc_gfp = gfp;
    // 初始化 alloc_context,并为接下来的疾速内存调配设置相干 gfp
    if (!prepare_alloc_pages(gfp, order, preferred_nid, nodemask, &ac,
            &alloc_gfp, &alloc_flags))
        // 提前判断本次内存调配是否可能胜利,如果不能则尽早失败
        return NULL;

    // 防止内存碎片化的相干调配标识设置,可临时疏忽
    alloc_flags |= alloc_flags_nofragment(ac.preferred_zoneref->zone, gfp);

    // 内存调配疾速门路:第一次尝试从底层搭档零碎分配内存,留神此时是在 WMARK_LOW 水位线之上分配内存
    page = get_page_from_freelist(alloc_gfp, order, alloc_flags, &ac);
    if (likely(page))
        // 如果内存调配胜利则间接返回
        goto out;
    // 流程走到这里示意内存调配在疾速门路下失败
    // 这里须要复原最后的内存调配标识设置,后续会尝试更加激进的内存调配策略
    alloc_gfp = gfp;
    // 复原最后的 node mask 因为它可能在第一次内存调配的过程中被扭转
    // 本函数中 nodemask 起初被设置为 null
    ac.nodemask = nodemask;

    // 在第一次疾速内存调配失败之后,阐明内存曾经有余了,内核须要做更多的工作
    // 比方通过 kswap 回收内存,或者间接内存回收等形式获取更多的闲暇内存以满足内存调配的需要
    // 所以上面的过程称之为慢速调配门路
    page = __alloc_pages_slowpath(alloc_gfp, order, &ac);

out:
    // 内存调配胜利,间接返回 page。否则返回 NULL
    return page;
}

__alloc_pages 函数中的内存调配整体逻辑如下:

  • 首先内核会尝试在内存水位线 WMARK_LOW 之上疾速的进行一次内存调配。这一点咱们从开始的 unsigned int alloc_flags = ALLOC_WMARK_LOW 语句中能够看得出来。
  • 校验本次内存调配指定搭档零碎的调配阶 order 的有效性,搭档零碎在内核中的最大调配阶定义在 /include/linux/mmzone.h 文件中,最大调配阶 MAX_ORDER -1 = 10,也就是说一次最多只能从搭档零碎中申请 1024 个内存页,对应 4M 大小的间断物理内存。
/* Free memory management - zoned buddy allocator.  */
#ifndef CONFIG_FORCE_MAX_ZONEORDER
#define MAX_ORDER 11
  • 调用 prepare_alloc_pages 初始化 alloc_context,用于在不同内存调配辅助函数中传递内存调配参数。为接下来行将进行的疾速内存调配做筹备。
struct alloc_context {
    // 运行过程 CPU 所在 NUMA  节点以及其所有备用 NUMA 节点中容许内存调配的内存区域
    struct zonelist *zonelist;
    // NUMA  节点状态掩码
    nodemask_t *nodemask;
    // 内存调配优先级最高的内存区域 zone
    struct zoneref *preferred_zoneref;
    // 物理内存页的迁徙类型分为:不可迁徙,可回收,可迁徙类型,避免内存碎片
    int migratetype;

    // 内存调配最高优先级的内存区域 zone
    enum zone_type highest_zoneidx;
    // 是否容许以后 NUMA 节点中的脏页平衡扩散迁徙至其余 NUMA 节点
    bool spread_dirty_pages;
};
  • 调用 get_page_from_freelist 办法首次尝试在搭档零碎中进行内存调配,这次内存调配比拟疾速,只是疾速的扫描一下各个内存区域中是否有足够的闲暇内存可能满足本次内存调配,如果有则立马从搭档零碎中申请,如果没有立刻返回,page 设置为 null,进行后续慢速内存调配解决。

这里须要留神的是:首次尝试的疾速内存调配是在 WMARK_LOW 水位线之上进行的。

  • 当疾速内存调配失败之后,状况就会变得非常复杂,内核将不得不做更多的工作,比方开启 kswapd 过程异步内存回收,更极其的状况则须要进行间接内存回收,或者间接内存整理以获取更多的闲暇间断内存。这所有的简单逻辑全副封装在 __alloc_pages_slowpath 函数中。

__alloc_pages_slowpath 函数简单在于须要联合前边大节中介绍的 GFP_,ALLOC_ 这些内存调配标识,依据不同的标识进入不同的内存调配逻辑分支,波及到的状况比拟繁冗。这里大家只须要简略理解,前面笔者会具体介绍~~~

以上介绍的 __alloc_pages 函数内存调配逻辑以及与对应的内存水位线之间的关系如下图所示:

总体流程介绍完之后,咱们接着来看一下以上内存调配过程波及到的三个重要内存调配辅助函数:prepare_alloc_pages,__alloc_pages_slowpath,get_page_from_freelist。

3.3 prepare_alloc_pages

prepare_alloc_pages 初始化 alloc_context,用于在不同内存调配辅助函数中传递内存调配参数,为接下来行将进行的疾速内存调配做筹备。

static inline bool prepare_alloc_pages(gfp_t gfp_mask, unsigned int order,
        int preferred_nid, nodemask_t *nodemask,
        struct alloc_context *ac, gfp_t *alloc_gfp,
        unsigned int *alloc_flags)
{
    // 依据 gfp_mask 掩码中的内存区域修饰符获取内存调配最高优先级的内存区域 zone
    ac->highest_zoneidx = gfp_zone(gfp_mask);
    // 从 NUMA 节点的备用节点链表中一次性获取容许进行内存调配的所有内存区域
    ac->zonelist = node_zonelist(preferred_nid, gfp_mask);
    ac->nodemask = nodemask;
    // 从 gfp_mask 掩码中获取页面迁徙属性,迁徙属性分为:不可迁徙,可回收,可迁徙。这里只须要简略晓得,前面在相干章节会细讲
    ac->migratetype = gfp_migratetype(gfp_mask);

   // 如果应用 cgroup 将过程绑定限度在了某些 CPU 上,那么内存调配只能在
   // 这些绑定的 CPU 相关联的 NUMA 节点中进行
    if (cpusets_enabled()) {
        *alloc_gfp |= __GFP_HARDWALL;
        if (in_task() && !ac->nodemask)
            ac->nodemask = &cpuset_current_mems_allowed;
        else
            *alloc_flags |= ALLOC_CPUSET;
    }
      
    // 如果设置了容许间接内存回收,那么内存调配过程则可能会导致休眠被从新调度 
    might_sleep_if(gfp_mask & __GFP_DIRECT_RECLAIM);
    // 提前判断本次内存调配是否可能胜利,如果不能则尽早失败
    if (should_fail_alloc_page(gfp_mask, order))
        return false;
    // 获取最高优先级的内存区域 zone
    // 后续内存调配则首先会在该内存区域中进行调配
    ac->preferred_zoneref = first_zones_zonelist(ac->zonelist,
                    ac->highest_zoneidx, ac->nodemask);

    return true;
}

prepare_alloc_pages 次要的工作就是在疾速内存调配开始之前,做一些筹备初始化的工作,其中最外围的就是从指定 NUMA 节点中,依据 gfp_mask 掩码中的内存区域修饰符获取能够进行内存调配的所有内存区域 zone(包含其余备用 NUMA 节点中蕴含的内存区域)。

之前笔者曾经在《深刻了解 Linux 物理内存治理》一文中的“4.3 NUMA 节点物理内存区域的划分”大节为大家曾经具体介绍了 NUMA 节点的数据结构 struct pglist_data。

struct pglist_data 构造中不仅蕴含了本 NUMA 节点中的所有内存区域,还包含了其余备用 NUMA 节点中的物理内存区域,当本节点中内存不足的状况下,内核会从备用 NUMA 节点中的内存区域进行跨节点内存调配。

typedef struct pglist_data {
    // NUMA 节点中的物理内存区域个数
    int nr_zones; 
    // NUMA 节点中的物理内存区域
    struct zone node_zones[MAX_NR_ZONES];
    // NUMA 节点的备用列表,其中蕴含了所有 NUMA 节点中的所有物理内存区域 zone,依照拜访间隔由近到远程序顺次排列
    struct zonelist node_zonelists[MAX_ZONELISTS];
} pg_data_t;

咱们能够依据 nid 和 gfp_mask 掩码中的物理内存区域描述符利用 node_zonelist 函数一次性获取容许进行内存调配的所有内存区域(所有 NUMA 节点)。

static inline struct zonelist *node_zonelist(int nid, gfp_t flags)
{return NODE_DATA(nid)->node_zonelists + gfp_zonelist(flags);
}

4. 内存慢速调配入口 alloc_pages_slowpath

正如前边大节咱们提到的那样,alloc_pages_slowpath 函数十分的简单,其中蕴含了内存调配的各种异常情况的解决,并且会依据前边介绍的 GFP_,ALLOC_ 等各种内存调配策略掩码进行不同分支的解决,这样就变得十分的宏大而繁冗。

alloc_pages_slowpath 函数蕴含了整个内存调配的外围流程,自身十分的繁冗宏大,为了可能给大家清晰的梳理分明这些简单的内存调配流程,所以笔者决定还是以 总 - 分 - 总 的构造来给大家出现。

上面这段伪代码是笔者提取进去的 alloc_pages_slowpath 函数的骨干框架,其中蕴含的一些外围分支以及外围步骤笔者都通过正文的模式为大家标注进去了,这里我先从总体上大略浏览下 alloc_pages_slowpath 次要分为哪几个逻辑解决模块,它们别离解决了哪些事件。

还是那句话,这里大家只须要总体把握,不须要把握每个细节,对于细节的局部,笔者前面会带大家一一击破!!!

static inline struct page *
__alloc_pages_slowpath(gfp_t gfp_mask, unsigned int order,
                        struct alloc_context *ac)
{
        ......... 初始化慢速内存调配门路下的相干参数 .......

retry_cpuset:

        ......... 调整内存调配策略 alloc_flags 采纳更加激进形式获取内存 ......
        ......... 此时内存调配次要是在过程所容许运行的 CPU 相关联的 NUMA 节点上 ......
        ......... 内存水位线下调至 WMARK_MIN ...........
        ......... 唤醒所有 kswapd 过程进行异步内存回收  ...........
        ......... 触发间接内存整理 direct_compact 来获取更多的间断闲暇内存 ......

retry:

        ......... 进一步调整内存调配策略 alloc_flags 应用更加激进的十分伎俩进行内存调配 ...........
        ......... 在内存调配时疏忽内存水位线 ...........
        ......... 触发间接内存回收 direct_reclaim ...........
        ......... 再次触发间接内存整理 direct_compact ...........
        ......... 最初的杀手锏触发 OOM 机制  ...........

nopage:
        ......... 通过以上激进的内存调配伎俩依然无奈满足内存调配就会来到这里 ......
        ......... 如果设置了 __GFP_NOFAIL 不容许内存调配失败,则不停重试上述内存调配过程 ......

fail:
        ......... 内存调配失败,输入告警信息 ........

      warn_alloc(gfp_mask, ac->nodemask,
            "page allocation failure: order:%u", order);
got_pg:
        ......... 内存调配胜利,返回新申请的内存块 ........

      return page;
}

4.1 初始化内存调配慢速门路下的相干参数

static inline struct page *
__alloc_pages_slowpath(gfp_t gfp_mask, unsigned int order,
                        struct alloc_context *ac)
{
    // 在慢速内存调配门路中可能会导致内核进行间接内存回收
    // 这里设置 __GFP_DIRECT_RECLAIM 示意容许内核进行间接内存回收
    bool can_direct_reclaim = gfp_mask & __GFP_DIRECT_RECLAIM;
    // 本次内存调配是否是针对大量内存页的调配,内核定义 PAGE_ALLOC_COSTLY_ORDER = 3
    // 也就是说内存申请内存页的数量大于 2 ^ 3 = 8 个内存页时,costly_order = true,后续会影响是否进行 OOM
    const bool costly_order = order > PAGE_ALLOC_COSTLY_ORDER;
    // 用于指向胜利申请的内存
    struct page *page = NULL;
    // 内存调配标识,后续会依据不同标识进入到不同的内存调配逻辑解决分支
    unsigned int alloc_flags;
    // 后续用于记录间接内存回收了多少内存页
    unsigned long did_some_progress;
    // 对于内存整理相干参数
    enum compact_priority compact_priority;
    enum compact_result compact_result;
    int compaction_retries;
    // 记录重试的次数,超过肯定的次数(16 次)则内存调配失败
    int no_progress_loops;
    // 长期保留调整后的内存调配策略
    int reserve_flags;

    // 流程当初来到了慢速内存调配这里,阐明疾速调配门路曾经失败了
    // 内核须要对 gfp_mask 调配行为掩码做一些批改,批改为一些更可能导致内存调配胜利的标识
    // 因为接下来的间接内存回收十分耗时可能会导致过程阻塞睡眠,不实用原子 __GFP_ATOMIC 内存调配的上下文。if (WARN_ON_ONCE((gfp_mask & (__GFP_ATOMIC|__GFP_DIRECT_RECLAIM)) ==
                (__GFP_ATOMIC|__GFP_DIRECT_RECLAIM)))
        gfp_mask &= ~__GFP_ATOMIC;

retry_cpuset:

retry:

nopage:

fail:

got_pg:

}

在内核进入慢速内存调配门路之前,首先会在这里初始化后续内存调配须要的参数,因为笔者曾经在各个字段上标注了丰盛的正文,所以这里笔者只对那些难以了解的外围参数为大家进行相干细节的铺垫,这里大家对这些参数有个大略印象即可,后续在应用到的时候,笔者还会再次提起~~~

首先咱们看 costly_order 参数,order 示意底层搭档零碎的调配阶,内核只能向搭档零碎申请 2 的 order 次幂个内存页,costly 从字面意思上来说示意有肯定代价和耗费的,costly_order 连起来就示意在内核中 order 调配阶达到多少,在内核看来就是代价比拟大的内存调配行为。

这个临界值就是 PAGE_ALLOC_COSTLY_ORDER 定义在 /include/linux/mmzone.h 文件中:

#define PAGE_ALLOC_COSTLY_ORDER 3

也就是说在内核看来,当申请内存页的数量大于 2 ^ 3 = 8 个内存页时,costly_order = true,内核就认为本次内存调配是一次老本比拟大的行为。后续会依据这个参数 costly_order 来决定是否触发 OOM。

    const bool costly_order = order > PAGE_ALLOC_COSTLY_ORDER;

当内存严重不足的时候,内核会开启间接内存回收 direct_reclaim,参数 did_some_progress 示意通过一次间接内存回收之后,内核回收了多少个内存页。这个参数后续会影响是否须要进行内存调配重试。

no_progress_loops 用于记录内存调配重试的次数,如果内存调配重试的次数超过最大限度 MAX_RECLAIM_RETRIES,则进行重试,开启 OOM。

MAX_RECLAIM_RETRIES 定义在 /mm/internal.h 文件中:

#define MAX_RECLAIM_RETRIES 16

compact_* 相干的参数用于间接内存整理 direct_compact,内核通常会在间接内存回收 direct_reclaim 之前进行一次 direct_compact,如果通过 direct_compact 整顿之后有了足够多的空间内存就不须要进行 direct_reclaim 了。

那么这个 direct_compact 到底是干什么的呢?它在慢速内存调配过程起了什么作用?

随着零碎的长时间运行通常会随同着不同大小的物理内存页的调配和开释,这种不规则的调配开释,随着零碎的长时间运行就会导致内存碎片,内存碎片会使得零碎在明明有足够内存的状况下,仍然无奈为过程调配适合的内存。

如上图所示,如果当初零碎一共有 16 个物理内存页,以后零碎只是调配了 3 个物理页,那么在以后零碎中还残余 13 个物理内存页的状况下,如果内核想要调配 8 个间断的物理页因为内存碎片的存在则会调配失败。(只能调配最多 4 个间断的物理页)

内核中申请调配的物理页面数只能是 2 的次幂!!

为了解决内存碎片化的问题,内核将内存页面分为了:可挪动的,可回收的,不可挪动的三种类型。

可挪动的页面汇集在一起,可回收的的页面汇集在一起,不可挪动的的页面汇集也在一起。从而作为去碎片化的根底,而后进行成块回收。

在回收时把可回收的一起回收,把可挪动的一起挪动,从而能空出大量间断物理页面。direct_compact 会扫描内存区域 zone 里的页面,把已调配的页记录下来,而后把所有已调配的页挪动到 zone 的一端,这样就会把一个曾经充斥碎片的 zone 整顿成一段齐全未调配的区间和一段曾经调配的区间,从而腾出大块间断的物理页面供内核调配。

4.2 retry_cpuset

在介绍完了内存调配在慢速门路下所须要的相干参数之后,上面就正式来到了 alloc_pages_slowpath 的内存调配逻辑:

static inline struct page *
__alloc_pages_slowpath(gfp_t gfp_mask, unsigned int order,
                        struct alloc_context *ac)
{
        ......... 初始化慢速内存调配门路下的相干参数 .......

retry_cpuset:

    // 在之前的疾速内存调配门路下设置的相干调配策略比拟激进,不是很激进,用于在 WMARK_LOW 水位线之上进行疾速内存调配
    // 走到这里示意疾速内存调配失败,此时闲暇内存严重不足了
    // 所以在慢速内存调配门路下须要从新设置更加激进的内存调配策略,采纳更大的代价来分配内存
    alloc_flags = gfp_to_alloc_flags(gfp_mask);

    // 从新依照新的设置依照内存区域优先级计算 zonelist 的迭代终点(最高优先级的 zone)// fast path 和 slow path 的设置不同所以这里须要从新计算
    ac->preferred_zoneref = first_zones_zonelist(ac->zonelist,
                    ac->highest_zoneidx, ac->nodemask);
    // 如果没有适合的内存调配区域,则跳转到 nopage , 内存调配失败
    if (!ac->preferred_zoneref->zone)
        goto nopage;
    // 唤醒所有的 kswapd 过程异步回收内存
    if (alloc_flags & ALLOC_KSWAPD)
        wake_all_kswapds(order, gfp_mask, ac);

    // 此时所有的 kswapd 过程曾经被唤醒,正在异步进行内存回收
    // 之前咱们曾经在 gfp_to_alloc_flags 办法中从新调整了 alloc_flags
    // 换成了一套更加激进的内存调配策略,留神此时是在 WMARK_MIN 水位线之上进行内存调配
    // 调整后的 alloc_flags 很可能会立刻胜利,因而这里先尝试一下
    page = get_page_from_freelist(gfp_mask, order, alloc_flags, ac);
    if (page)
        // 内存调配胜利,跳转到 got_pg 间接返回 page
        goto got_pg;

    // 对于调配大内存来说 costly_order = true (超过 8 个内存页),须要首先进行内存整理,这样内核能够防止间接内存回收从而获取更多的间断闲暇内存页
    // 对于须要调配不可挪动的高阶内存的状况,也须要先进行内存整理,避免永恒内存碎片
    if (can_direct_reclaim &&
            (costly_order ||
               (order > 0 && ac->migratetype != MIGRATE_MOVABLE))
            && !gfp_pfmemalloc_allowed(gfp_mask)) {
        // 进行间接内存整理,获取更多的间断闲暇内存避免内存碎片
        page = __alloc_pages_direct_compact(gfp_mask, order,
                        alloc_flags, ac,
                        INIT_COMPACT_PRIORITY,
                        &compact_result);
        if (page)
            goto got_pg;

        if (costly_order && (gfp_mask & __GFP_NORETRY)) {
            // 流程走到这里示意通过内存整理之后仍然没有足够的内存供调配
            // 然而设置了 NORETRY 标识不容许重试,那么就间接失败,跳转到 nopage
            if (compact_result == COMPACT_SKIPPED ||
                compact_result == COMPACT_DEFERRED)
                goto nopage;
            // 同步内存整理开销太大,后续开启异步内存整理
            compact_priority = INIT_COMPACT_PRIORITY;
        }
    }

retry:

nopage:

fail:

got_pg:
    return page;
}

流程走到这里,阐明内核在《3.2 内存调配的心脏 __alloc_pages》大节中介绍的疾速门路下尝试的内存调配曾经失败了,所以才会走到慢速调配门路这里来。

之前咱们介绍到疾速调配门路是在 WMARK_LOW 水位线之上进行内存调配,与其相配套的内存调配策略比拟激进,目标是疾速的在各个内存区域 zone 之间搜寻可供调配的闲暇内存。

疾速调配门路下的失败意味着此时零碎中的闲暇内存曾经有余了,所以在慢速调配门路下内核须要扭转内存调配策略,采纳更加激进的形式来进行内存调配,首先会把内存调配水位线升高到 WMARK_MIN 之上,而后将内存调配策略调整为更加容易促使内存调配胜利的策略。

而内存调配策略相干的调整逻辑,内核定义在 gfp_to_alloc_flags 函数中:

static inline unsigned int gfp_to_alloc_flags(gfp_t gfp_mask)
{
    // 在慢速内存调配门路中,会进一步放宽对内存调配的限度,将内存调配水位线调低到 WMARK_MIN
    // 也就是说内存区域中的残余内存须要在 WMARK_MIN 水位线之上才能够进行内存调配
    unsigned int alloc_flags = ALLOC_WMARK_MIN | ALLOC_CPUSET;
    
    // 如果内存调配申请无奈运行间接内存回收,或者调配申请设置了 __GFP_HIGH 
    // 那么意味着内存调配会更多的应用紧急预留内存
    alloc_flags |= (__force int)
        (gfp_mask & (__GFP_HIGH | __GFP_KSWAPD_RECLAIM));

    if (gfp_mask & __GFP_ATOMIC) {
        //  ___GFP_NOMEMALLOC 标记用于明确禁止内核从紧急预留内存中获取内存。// ___GFP_NOMEMALLOC 标识的优先级要高于 ___GFP_MEMALLOC
        if (!(gfp_mask & __GFP_NOMEMALLOC))
           // 如果容许从紧急预留内存中调配,则须要进一步放宽内存调配限度
           // 后续依据 ALLOC_HARDER 标识会升高 WMARK_LOW 水位线
            alloc_flags |= ALLOC_HARDER;
        // 在这个分支中示意内存调配申请曾经设置了  __GFP_ATOMIC(十分重要,不容许失败)// 这种状况下为了内存调配的胜利,会去除掉 CPUSET 的限度,能够在所有 NUMA 节点上分配内存
        alloc_flags &= ~ALLOC_CPUSET;
    } else if (unlikely(rt_task(current)) && in_task())
         // 如果以后过程不是 real time task 或者不在 task 上下文中
         // 设置 HARDER 标识
        alloc_flags |= ALLOC_HARDER;

    return alloc_flags;
}

在调整好的新的内存调配策略 alloc_flags 之后,就须要依据新的策略来从新获取可供调配的内存区域 zone。

  ac->preferred_zoneref = first_zones_zonelist(ac->zonelist,
                    ac->highest_zoneidx, ac->nodemask);

从上图中咱们能够看出,当残余内存处于 WMARK_MIN 与 WMARK_LOW 之间时,内核会唤醒所有 kswapd 过程来异步回收内存,直到残余内存从新回到水位线 WMARK_HIGH 之上。

    if (alloc_flags & ALLOC_KSWAPD)
        wake_all_kswapds(order, gfp_mask, ac);

到目前为止,内核曾经在慢速调配门路下通过 gfp_to_alloc_flags 调整为更加激进的内存调配策略,并将水位线升高到 WMARK_MIN,同时也唤醒了 kswapd 过程来异步回收内存。

此时在新的内存调配策略下进行内存调配很可能会一次性胜利,所以内核会首先尝试进行一次内存调配。

page = get_page_from_freelist(gfp_mask, order, alloc_flags, ac);

如果首次尝试分配内存失败之后,内核就须要进行间接内存整理 direct_compact 来获取更多的可供调配的间断内存页。

如果通过 direct_compact 之后仍然没有足够的内存可供调配,那么就会进入 retry 分支采纳更加激进的形式来分配内存。如果内存调配策略设置了 __GFP_NORETRY 示意不容许重试,那么就会间接失败,流程跳转到 nopage 分支进行解决。

4.3 retry

内存调配流程来到 retry 分支这里阐明状况曾经变得十分危急了,在通过 retry_cpuset 分支的解决,内核将内存水位线下调至 WMARK_MIN,并开启了 kswapd 过程进行异步内存回收,触发间接内存整理 direct_compact,在采取了这些措施之后,仍然无奈满足内存调配的需要。

所以在接下来的调配逻辑中,内核会近一步采取更加激进的十分伎俩来获取间断的闲暇内存,上面咱们来一起看下这部分激进的内容:

static inline struct page *
__alloc_pages_slowpath(gfp_t gfp_mask, unsigned int order,
                        struct alloc_context *ac)
{
        ......... 初始化慢速内存调配门路下的相干参数 .......

retry_cpuset:

        ......... 调整内存调配策略 alloc_flags 采纳更加激进形式获取内存 ......
        ......... 此时内存调配次要是在过程所容许运行的 CPU 相关联的 NUMA 节点上 ......
        ......... 内存水位线下调至 WMARK_MIN ...........
        ......... 唤醒所有 kswapd 过程进行异步内存回收  ...........
        ......... 触发间接内存整理 direct_compact 来获取更多的间断闲暇内存 ......

retry:
    // 确保所有 kswapd 过程不要意外进入睡眠状态
    if (alloc_flags & ALLOC_KSWAPD)
        wake_all_kswapds(order, gfp_mask, ac);

    // 流程走到这里,阐明在 WMARK_MIN 水位线之上也分配内存失败了
    // 并且通过内存整理之后,内存调配依然失败,阐明以后内存容量曾经严重不足
    // 接下来就须要应用更加激进的十分伎俩来尝试内存调配(疏忽掉内存水位线),持续批改 alloc_flags 保留在 reserve_flags 中
    reserve_flags = __gfp_pfmemalloc_flags(gfp_mask);
    if (reserve_flags)
        alloc_flags = gfp_to_alloc_flags_cma(gfp_mask, reserve_flags);

    // 如果内存调配能够任意跨节点调配(疏忽内存调配策略),这里须要重置 nodemask 以及 zonelist。if (!(alloc_flags & ALLOC_CPUSET) || reserve_flags) {
        // 这里的内存调配是高优先级零碎级别的内存调配,不是面向用户的
        ac->nodemask = NULL;
        ac->preferred_zoneref = first_zones_zonelist(ac->zonelist,
                    ac->highest_zoneidx, ac->nodemask);
    }

    // 这里应用从新调整的 zonelist 和 alloc_flags 在尝试进行一次内存调配
    // 留神此次的内存调配是疏忽内存水位线的 ALLOC_NO_WATERMARKS
    page = get_page_from_freelist(gfp_mask, order, alloc_flags, ac);
    if (page)
        goto got_pg;

    // 在疏忽内存水位线的状况下依然调配失败,当初内核就须要进行间接内存回收了
    if (!can_direct_reclaim)
        // 如果过程不容许进行间接内存回收,则只能调配失败
        goto nopage;

    // 开始间接内存回收
    page = __alloc_pages_direct_reclaim(gfp_mask, order, alloc_flags, ac,
                            &did_some_progress);
    if (page)
        goto got_pg;

    // 间接内存回收之后依然无奈满足调配需要,则再次进行间接内存整理
    page = __alloc_pages_direct_compact(gfp_mask, order, alloc_flags, ac,
                    compact_priority, &compact_result);
    if (page)
        goto got_pg;

    // 在内存间接回收和整顿全副失败之后,如果不容许重试,则只能失败
    if (gfp_mask & __GFP_NORETRY)
        goto nopage;

    // 后续会触发 OOM 来开释更多的内存,这里须要判断本次内存调配是否须要调配大量的内存页(大于 8)costly_order = true
    // 如果是的话则内核认为即便执行 OOM 也未必会满足这么多的内存页调配需要.
    // 所以还是间接失败比拟好,不再执行 OOM,除非设置 __GFP_RETRY_MAYFAIL
    if (costly_order && !(gfp_mask & __GFP_RETRY_MAYFAIL))
        goto nopage;

    // 流程走到这里阐明咱们曾经尝试了所有措施内存仍然调配失败了,此时内存曾经十分危急了。// 走到这里阐明过程容许内核进行重试流程,但在开始重试之前,内核须要判断是否应该进行重试, 重试规范:// 1 如果内核曾经重试了 MAX_RECLAIM_RETRIES (16) 次依然失败,则放弃重试执行后续 OOM。// 2 如果内核将所有可选内存区域中的所有可回收页面全副回收之后,依然无奈满足内存的调配,那么放弃重试执行后续 OOM
    if (should_reclaim_retry(gfp_mask, order, ac, alloc_flags,
                 did_some_progress > 0, &no_progress_loops))
        goto retry;

    // 如果内核判断不应进行间接内存回收的重试,这里还须要判断下是否应该进行内存整理的重试。// did_some_progress 示意上次间接内存回收,具体回收了多少内存页
    // 如果 did_some_progress = 0 则没有必要在进行内存整理重试了,因为内存整理的实现依赖于足够的闲暇内存量
    if (did_some_progress > 0 &&
            should_compact_retry(ac, order, alloc_flags,
                compact_result, &compact_priority,
                &compaction_retries))
        goto retry;


    // 依据 nodemask 中的内存调配策略判断是否应该在过程所容许运行的所有 CPU 关联的 NUMA 节点上重试
    if (check_retry_cpuset(cpuset_mems_cookie, ac))
        goto retry_cpuset;

    // 最初的杀手锏,进行 OOM,抉择一个得分最高的过程,开释其占用的内存 
    page = __alloc_pages_may_oom(gfp_mask, order, ac, &did_some_progress);
    if (page)
        goto got_pg;

    // 只有 oom 产生了作用并开释了内存 did_some_progress > 0 就一直的进行重试
    if (did_some_progress) {
        no_progress_loops = 0;
        goto retry;
    }

nopage:

fail:  
      warn_alloc(gfp_mask, ac->nodemask,
            "page allocation failure: order:%u", order);
got_pg:
      return page;
}

retry 分支蕴含的是更加激进的内存调配逻辑,所以在一开始须要调用 __gfp_pfmemalloc_flags 函数来从新调整内存调配策略,调整后的策略为:后续内存调配会疏忽水位线的影响,并且容许内核从紧急预留内存中获取内存。

static inline int __gfp_pfmemalloc_flags(gfp_t gfp_mask)
{
    // 如果不容许从紧急预留内存中调配,则不扭转 alloc_flags
    if (unlikely(gfp_mask & __GFP_NOMEMALLOC))
        return 0;
    // 如果容许从紧急预留内存中调配,则前面的内存调配会疏忽内存水位线的限度
    if (gfp_mask & __GFP_MEMALLOC)
        return ALLOC_NO_WATERMARKS;
    // 以后过程处于软中断上下文并且过程设置了 PF_MEMALLOC 标识
    // 则疏忽内存水位线
    if (in_serving_softirq() && (current->flags & PF_MEMALLOC))
        return ALLOC_NO_WATERMARKS;
    // 以后过程不在任何中断上下文中
    if (!in_interrupt()) {if (current->flags & PF_MEMALLOC)
            // 疏忽内存水位线
            return ALLOC_NO_WATERMARKS;
        else if (oom_reserves_allowed(current))
            // 以后过程容许进行 OOM
            return ALLOC_OOM;
    }
    // alloc_flags 不做任何批改
    return 0;
}

在调整好更加激进的内存调配策略 alloc_flags 之后,内核会首先尝试从搭档零碎中进行一次内存调配,这时会有很大概率促使内存调配胜利。

留神:此次尝试进行的内存调配会疏忽内存水位线:ALLOC_NO_WATERMARKS

   page = get_page_from_freelist(gfp_mask, order, alloc_flags, ac);

如果在疏忽内存水位线的状况下,内存仍然调配失败,则进行间接内存回收 direct_reclaim。

   page = __alloc_pages_direct_reclaim(gfp_mask, order, alloc_flags, ac,
                            &did_some_progress);

通过 direct_reclaim 之后,依然没有足够的内存可供调配的话,那么内核会再次进行间接内存整理 direct_compact。

    page = __alloc_pages_direct_compact(gfp_mask, order, alloc_flags, ac,
                    compact_priority, &compact_result);

如果 direct_compact 之后还是没有足够的内存,那么当初内核曾经处于绝境了,是时候应用杀手锏:触发 OOM 机制杀死得分最高的过程以获取更多的闲暇内存。

然而在进行 OOM 之前,内核还是须要通过一系列的判断,这时就用到了咱们在《4.1 初始化内存调配慢速门路下的相干参数》大节中介绍的 costly_order 参数了,它会影响内核是否触发 OOM。

如果 costly_order = true,示意此次内存调配的内存页大于 8 个页,内核会认为这是一次代价比拟大的调配行为,况且此时内存曾经十分危急,严重不足。在这种状况下内核认为即便触发了 OOM,也无奈获取这么多的内存,仍然无奈满足内存调配。

所以当 costly_order = true 时,内核不会触发 OOM,间接跳转到 nopage 分支,除非设置了 __GFP_RETRY_MAYFAIL 内存调配策略:

    if (costly_order && !(gfp_mask & __GFP_RETRY_MAYFAIL))
        goto nopage;

上面内核也不会间接开始 OOM,而是进入到重试流程,在重试流程开始之前内核须要调用 should_reclaim_retry 判断是否应该进行重试,重试规范:

  1. 如果内核曾经重试了 MAX_RECLAIM_RETRIES (16) 次依然失败,则放弃重试执行后续 OOM。
  2. 如果内核将所有可选内存区域中的所有可回收页面全副回收之后,依然无奈满足内存的调配,那么放弃重试执行后续 OOM。

如果 should_reclaim_retry = false,前面会进一步判断是否应该进行 direct_compact 的重试。

    if (did_some_progress > 0 &&
            should_compact_retry(ac, order, alloc_flags,
                compact_result, &compact_priority,
                &compaction_retries))
        goto retry;

did_some_progress 示意上次间接内存回收具体回收了多少内存页, 如果 did_some_progress = 0 则没有必要在进行内存整理重试了,因为内存整理的实现依赖于足够的闲暇内存量。

当这些所有的重试申请都被回绝时,杀手锏 OOM 就开始退场了:

   page = __alloc_pages_may_oom(gfp_mask, order, ac, &did_some_progress);
    if (page)
        goto got_pg;

如果 OOM 之后并没有开释内存,那么就来到 nopage 分支解决。

然而如果 did_some_progress > 0 示意 OOM 产生了作用,至多开释了一些内存那么就再次进行重试。

4.4 nopage

到当初为止,内核曾经尝试了包含 OOM 在内的所有回收内存的措施,然而依然没有足够的内存来满足调配要求,看上去此次内存调配就要宣告失败了。

然而这里还有肯定的回旋余地,如果内存调配策略中配置了 __GFP_NOFAIL,则示意此次内存调配十分的重要,不容许失败。内核会在这里不停的重试直到调配胜利为止。

咱们在《深刻了解 Linux 物理内存治理》一文中的“3.2 非一致性内存拜访 NUMA 架构”大节,介绍 NUMA 内存架构的时候已经提到:当 CPU 本人所在的本地 NUMA 节点内存不足时,CPU 就须要跨 NUMA 节点去拜访其余内存节点,这种跨 NUMA 节点分配内存的行为就产生在这里,这种状况下 CPU 拜访内存就会慢很多

static inline struct page *
__alloc_pages_slowpath(gfp_t gfp_mask, unsigned int order,
                        struct alloc_context *ac)
{
        ......... 初始化慢速内存调配门路下的相干参数 .......

retry_cpuset:

        ......... 调整内存调配策略 alloc_flags 采纳更加激进形式获取内存 ......
        ......... 此时内存调配次要是在过程所容许运行的 CPU 相关联的 NUMA 节点上 ......
        ......... 内存水位线下调至 WMARK_MIN ...........
        ......... 唤醒所有 kswapd 过程进行异步内存回收  ...........
        ......... 触发间接内存整理 direct_compact 来获取更多的间断闲暇内存 ......

retry:

        ......... 进一步调整内存调配策略 alloc_flags 应用更加激进的十分伎俩尽心内存调配 ...........
        ......... 在内存调配时疏忽内存水位线 ...........
        ......... 触发间接内存回收 direct_reclaim ...........
        ......... 再次触发间接内存整理 direct_compact ...........
        ......... 最初的杀手锏触发 OOM 机制  ...........

nopage:
    // 流程走到这里表明内核曾经尝试了包含 OOM 在内的所有回收内存的动作。// 然而这些措施仍然无奈满足内存调配的需要,看上去内存调配到这里就应该失败了。// 然而如果设置了 __GFP_NOFAIL 示意不容许内存调配失败,那么接下来就会进入 if 分支进行解决
    if (gfp_mask & __GFP_NOFAIL) {
        // 如果不容许进行间接内存回收,则跳转至 fail 分支宣告失败
        if (WARN_ON_ONCE_GFP(!can_direct_reclaim, gfp_mask))
            goto fail;

        // 此时内核曾经无奈通过回收内存来获取可供调配的闲暇内存了
        // 对于 PF_MEMALLOC 类型的内存调配申请,内核当初无能为力,只能不停的进行 retry 重试。WARN_ON_ONCE_GFP(current->flags & PF_MEMALLOC, gfp_mask);

        // 对于须要调配 8 个内存页以上的大内存调配,并且设置了不可失败标识 __GFP_NOFAIL
        // 内核当初也无能为力,毕竟事实是曾经没有闲暇内存了,只是给出一些告警信息
        WARN_ON_ONCE_GFP(order > PAGE_ALLOC_COSTLY_ORDER, gfp_mask);

       // 在 __GFP_NOFAIL 状况下,尝试进行跨 NUMA 节点内存调配
        page = __alloc_pages_cpuset_fallback(gfp_mask, order, ALLOC_HARDER, ac);
        if (page)
            goto got_pg;
        // 在进行内存调配重试流程之前,须要让 CPU 从新调度到其余过程上
        // 运行一会其余过程,因为毕竟此时内存曾经严重不足
        // 立马重试的话只能节约过多工夫在搜寻闲暇内存上,导致其余过程处于饥饿状态。cond_resched();
        // 跳转到 retry 分支,重试内存调配流程
        goto retry;
    }

fail:
      warn_alloc(gfp_mask, ac->nodemask,
            "page allocation failure: order:%u", order);
got_pg:
      return page;
}

这里笔者须要着重强调的一点就是,在 nopage 分支中决定开始重试之前,内核不能立刻进行重试流程,因为之前曾经经验过那么多严格激进的内存回收策略依然没有足够的内存,内存现状十分紧急。

所以咱们有理由置信,如果内核立刻开始重试的话,仍然没有什么成果,反而会节约过多工夫在搜寻闲暇内存上,导致其余过程处于饥饿状态。

所以在开始重试之前,内核会调用 cond_resched() 让 CPU 从新调度到其余过程上,让其余过程也运行一会,与此同时 kswapd 过程始终在后盾异步回收着内存。

当 CPU 从新调度回以后过程时,说不定 kswapd 过程曾经回收了足够多的内存,重试胜利的概率会大大增加同时又防止了资源的无谓耗费。

5. __alloc_pages 内存调配流程总览

到这里为止,笔者就为大家残缺地介绍完内核分配内存的整个流程,当初笔者再把内存调配的残缺流程图放进去,咱们在联合残缺的内存调配相干源码,整体在领会一下:

static inline struct page *
__alloc_pages_slowpath(gfp_t gfp_mask, unsigned int order,
                        struct alloc_context *ac)
{
    // 在慢速内存调配门路中可能会导致内核进行间接内存回收
    // 这里设置 __GFP_DIRECT_RECLAIM 示意容许内核进行间接内存回收
    bool can_direct_reclaim = gfp_mask & __GFP_DIRECT_RECLAIM;
    // 本次内存调配是否是针对大量内存页的调配,内核定义 PAGE_ALLOC_COSTLY_ORDER = 3
    // 也就是说内存申请内存页的数量大于 2 ^ 3 = 8 个内存页时,costly_order = true,后续会影响是否进行 OOM
    const bool costly_order = order > PAGE_ALLOC_COSTLY_ORDER;
    // 用于指向胜利申请的内存
    struct page *page = NULL;
    // 内存调配标识,后续会依据不同标识进入到不同的内存调配逻辑解决分支
    unsigned int alloc_flags;
    // 后续用于记录间接内存回收了多少内存页
    unsigned long did_some_progress;
    // 对于内存整理相干参数
    enum compact_priority compact_priority;
    enum compact_result compact_result;
    int compaction_retries;
    int no_progress_loops;
    unsigned int cpuset_mems_cookie;
    int reserve_flags;

    // 流程当初来到了慢速内存调配这里,阐明疾速调配门路曾经失败了
    // 内核须要对 gfp_mask 调配行为掩码做一些批改,批改为一些更可能导致内存调配胜利的标识
    // 因为接下来的间接内存回收十分耗时可能会导致过程阻塞睡眠,不实用原子 __GFP_ATOMIC 内存调配的上下文。if (WARN_ON_ONCE((gfp_mask & (__GFP_ATOMIC|__GFP_DIRECT_RECLAIM)) ==
                (__GFP_ATOMIC|__GFP_DIRECT_RECLAIM)))
        gfp_mask &= ~__GFP_ATOMIC;

retry_cpuset:

    // 在之前的疾速内存调配门路下设置的相干调配策略比拟激进,不是很激进,用于在 WMARK_LOW 水位线之上进行疾速内存调配
    // 走到这里示意疾速内存调配失败,此时闲暇内存严重不足了
    // 所以在慢速内存调配门路下须要从新设置更加激进的内存调配策略,采纳更大的代价来分配内存
    alloc_flags = gfp_to_alloc_flags(gfp_mask);

    // 从新依照新的设置依照内存区域优先级计算 zonelist 的迭代终点(最高优先级的 zone)// fast path 和 slow path 的设置不同所以这里须要从新计算
    ac->preferred_zoneref = first_zones_zonelist(ac->zonelist,
                    ac->highest_zoneidx, ac->nodemask);
    // 如果没有适合的内存调配区域,则跳转到 nopage , 内存调配失败
    if (!ac->preferred_zoneref->zone)
        goto nopage;
    // 唤醒所有的 kswapd 过程异步回收内存
    if (alloc_flags & ALLOC_KSWAPD)
        wake_all_kswapds(order, gfp_mask, ac);

    // 此时所有的 kswapd 过程曾经被唤醒,正在异步进行内存回收
    // 之前咱们曾经在 gfp_to_alloc_flags 办法中从新调整了 alloc_flags
    // 换成了一套更加激进的内存调配策略,留神此时是在 WMARK_MIN 水位线之上进行内存调配
    // 调整后的 alloc_flags 很可能会立刻胜利,因而这里先尝试一下
    page = get_page_from_freelist(gfp_mask, order, alloc_flags, ac);
    if (page)
        // 内存调配胜利,跳转到 got_pg 间接返回 page
        goto got_pg;

    // 对于调配大内存来说 costly_order = true (超过 8 个内存页),须要首先进行内存整理,这样内核能够防止间接内存回收从而获取更多的间断闲暇内存页
    // 对于须要调配不可挪动的高阶内存的状况,也须要先进行内存整理,避免永恒内存碎片
    if (can_direct_reclaim &&
            (costly_order ||
               (order > 0 && ac->migratetype != MIGRATE_MOVABLE))
            && !gfp_pfmemalloc_allowed(gfp_mask)) {
        // 进行间接内存整理,获取更多的间断闲暇内存避免内存碎片
        page = __alloc_pages_direct_compact(gfp_mask, order,
                        alloc_flags, ac,
                        INIT_COMPACT_PRIORITY,
                        &compact_result);
        if (page)
            goto got_pg;

        if (costly_order && (gfp_mask & __GFP_NORETRY)) {
            // 流程走到这里示意通过内存整理之后仍然没有足够的内存供调配
            // 然而设置了 NORETRY 标识不容许重试,那么就间接失败,跳转到 nopage
            if (compact_result == COMPACT_SKIPPED ||
                compact_result == COMPACT_DEFERRED)
                goto nopage;
            // 同步内存整理开销太大,后续开启异步内存整理
            compact_priority = INIT_COMPACT_PRIORITY;
        }
    }

retry:
    // 确保所有 kswapd 过程不要意外进入睡眠状态
    if (alloc_flags & ALLOC_KSWAPD)
        wake_all_kswapds(order, gfp_mask, ac);

    // 流程走到这里,阐明在 WMARK_MIN 水位线之上也分配内存失败了
    // 并且通过内存整理之后,内存调配依然失败,阐明以后内存容量曾经严重不足
    // 接下来就须要应用更加激进的十分伎俩来尝试内存调配(疏忽掉内存水位线),持续批改 alloc_flags 保留在 reserve_flags 中
    reserve_flags = __gfp_pfmemalloc_flags(gfp_mask);
    if (reserve_flags)
        alloc_flags = gfp_to_alloc_flags_cma(gfp_mask, reserve_flags);

    // 如果内存调配能够任意跨节点调配(疏忽内存调配策略),这里须要重置 nodemask 以及 zonelist。if (!(alloc_flags & ALLOC_CPUSET) || reserve_flags) {
        // 这里的内存调配是高优先级零碎级别的内存调配,不是面向用户的
        ac->nodemask = NULL;
        ac->preferred_zoneref = first_zones_zonelist(ac->zonelist,
                    ac->highest_zoneidx, ac->nodemask);
    }

    // 这里应用从新调整的 zonelist 和 alloc_flags 在尝试进行一次内存调配
    // 留神此次的内存调配是疏忽内存水位线的 ALLOC_NO_WATERMARKS
    page = get_page_from_freelist(gfp_mask, order, alloc_flags, ac);
    if (page)
        goto got_pg;

    // 在疏忽内存水位线的状况下依然调配失败,当初内核就须要进行间接内存回收了
    if (!can_direct_reclaim)
        // 如果过程不容许进行间接内存回收,则只能调配失败
        goto nopage;

    // 开始间接内存回收
    page = __alloc_pages_direct_reclaim(gfp_mask, order, alloc_flags, ac,
                            &did_some_progress);
    if (page)
        goto got_pg;

    // 间接内存回收之后依然无奈满足调配需要,则再次进行间接内存整理
    page = __alloc_pages_direct_compact(gfp_mask, order, alloc_flags, ac,
                    compact_priority, &compact_result);
    if (page)
        goto got_pg;

    // 在内存间接回收和整顿全副失败之后,如果不容许重试,则只能失败
    if (gfp_mask & __GFP_NORETRY)
        goto nopage;

    // 后续会触发 OOM 来开释更多的内存,这里须要判断本次内存调配是否须要调配大量的内存页(大于 8)costly_order = true
    // 如果是的话则内核认为即便执行 OOM 也未必会满足这么多的内存页调配需要.
    // 所以还是间接失败比拟好,不再执行 OOM,除非设置 __GFP_RETRY_MAYFAIL
    if (costly_order && !(gfp_mask & __GFP_RETRY_MAYFAIL))
        goto nopage;

    // 流程走到这里阐明咱们曾经尝试了所有措施内存仍然调配失败了,此时内存曾经十分危急了。// 走到这里阐明过程容许内核进行重试流程,但在开始重试之前,内核须要判断是否应该进行重试, 重试规范:// 1 如果内核曾经重试了 MAX_RECLAIM_RETRIES (16) 次依然失败,则放弃重试执行后续 OOM。// 2 如果内核将所有可选内存区域中的所有可回收页面全副回收之后,依然无奈满足内存的调配,那么放弃重试执行后续 OOM
    if (should_reclaim_retry(gfp_mask, order, ac, alloc_flags,
                 did_some_progress > 0, &no_progress_loops))
        goto retry;

    // 如果内核判断不应进行间接内存回收的重试,这里还须要判断下是否应该进行内存整理的重试。// did_some_progress 示意上次间接内存回收具体回收了多少内存页
    // 如果 did_some_progress = 0 则没有必要在进行内存整理重试了,因为内存整理的实现依赖于足够的闲暇内存量
    if (did_some_progress > 0 &&
            should_compact_retry(ac, order, alloc_flags,
                compact_result, &compact_priority,
                &compaction_retries))
        goto retry;


    // 依据 nodemask 中的内存调配策略判断是否应该在过程所容许运行的所有 CPU 关联的 NUMA 节点上重试
    if (check_retry_cpuset(cpuset_mems_cookie, ac))
        goto retry_cpuset;

    // 最初的杀手锏,进行 OOM,抉择一个得分最高的过程,开释其占用的内存 
    page = __alloc_pages_may_oom(gfp_mask, order, ac, &did_some_progress);
    if (page)
        goto got_pg;

    // 只有 oom 产生了作用并开释了内存 did_some_progress > 0 就一直的进行重试
    if (did_some_progress) {
        no_progress_loops = 0;
        goto retry;
    }

nopage:
    // 流程走到这里表明内核曾经尝试了包含 OOM 在内的所有回收内存的动作。// 然而这些措施仍然无奈满足内存调配的需要,看上去内存调配到这里就应该失败了。// 然而如果设置了 __GFP_NOFAIL 示意不容许内存调配失败,那么接下来就会进入 if 分支进行解决
    if (gfp_mask & __GFP_NOFAIL) {
        // 如果不容许进行间接内存回收,则跳转至 fail 分支宣告失败
        if (WARN_ON_ONCE_GFP(!can_direct_reclaim, gfp_mask))
            goto fail;

        // 此时内核曾经无奈通过回收内存来获取可供调配的闲暇内存了
        // 对于 PF_MEMALLOC 类型的内存调配申请,内核当初无能为力,只能不停的进行 retry 重试。WARN_ON_ONCE_GFP(current->flags & PF_MEMALLOC, gfp_mask);

        // 对于须要调配 8 个内存页以上的大内存调配,并且设置了不可失败标识 __GFP_NOFAIL
        // 内核当初也无能为力,毕竟事实是曾经没有闲暇内存了,只是给出一些告警信息
        WARN_ON_ONCE_GFP(order > PAGE_ALLOC_COSTLY_ORDER, gfp_mask);

       // 在 __GFP_NOFAIL 状况下,尝试进行跨 NUMA 节点内存调配
        page = __alloc_pages_cpuset_fallback(gfp_mask, order, ALLOC_HARDER, ac);
        if (page)
            goto got_pg;
        // 在进行内存调配重试流程之前,须要让 CPU 从新调度到其余过程上
        // 运行一会其余过程,因为毕竟此时内存曾经严重不足
        // 立马重试的话只能节约过多工夫在搜寻闲暇内存上,导致其余过程处于饥饿状态。cond_resched();
        // 跳转到 retry 分支,重试内存调配流程
        goto retry;
    }
fail:
    warn_alloc(gfp_mask, ac->nodemask,
            "page allocation failure: order:%u", order);
got_pg:
    return page;
}

当初内存调配流程中波及到的三个重要辅助函数:prepare_alloc_pages,__alloc_pages_slowpath,get_page_from_freelist。笔者曾经为大家介绍了两个了。prepare_alloc_pages,__alloc_pages_slowpath 函数次要是依据不同的闲暇内存残余容量调整内存的调配策略,尽量使内存调配行为尽最大可能胜利。

了解了以上两个辅助函数的逻辑,咱们就相当于梳理分明了整个内存调配的链路流程。但目前咱们还没有波及到具体内存调配的真正逻辑,而内核中执行具体内存调配动作是在 get_page_from_freelist 函数中,这也是把握内存调配的最初一道关卡。

因为 get_page_from_freelist 函数执行的是具体的内存调配动作,所以它和内核中的搭档零碎有着千头万绪的分割,而本文的主题更加偏重形容整个物理内存调配的链路流程,思考到文章篇幅的关系,笔者把搭档零碎这部分的内容放在下篇文章为大家解说。

总结

本文首先从 Linux 内核中常见的几个物理内存调配接口开始,介绍了这些内存调配接口的各自的应用场景,以及接口函数中参数的含意。

并以此为终点,联合 Linux 内核 5.19 版本源码具体探讨了物理内存调配在内核中的整个链路实现。在整个链路中,内存的调配整体分为了两个门路:

  1. 疾速门路 fast path:该门路的下,内存调配的逻辑比较简单,次要是在 WMARK_LOW 水位线之上疾速的扫描一下各个内存区域中是否有足够的闲暇内存可能满足本次内存调配,如果有则立马从搭档零碎中申请,如果没有立刻返回。
  2. 慢速门路 slow path:慢速门路下的内存调配逻辑就变的非常复杂了,其中蕴含了内存调配的各种异常情况的解决,并且会依据文中介绍的 GFP_,ALLOC_ 等各种内存调配策略掩码进行不同分支的解决,整个链路十分宏大且繁冗。

本文铺垫了大量的内存调配细节,然而整个内存调配链路流程的精华,笔者绘制在了上面这副流程图中,不便大家遗记的时候回顾。

正文完
 0