内存学习(七):伙伴分配器(正式版)1

简介: 内存学习(七):伙伴分配器(正式版)1

伙伴分配器

内核初始化完毕后使用页分配器管理物理页,当前使用的页分配器是伙伴分配器,伙伴分配器的特点是算法简单且效率高。

(接力棒由之前memblock交给了伙伴分配器)

1-基本的伙伴分配器

1、基本单位-阶和页

连续的物理页称为页块(page block)。

阶(order)是伙伴分配器的一个术语,是页的数量单位,2n个连续页称为n阶页块。

满足以下条件的两个n阶页块称为伙伴(buddy)。

  • (1)两个页块是相邻的,即物理地址是连续的。
  • (2)页块的第一页的物理页号必须是2n的整数倍。
  • (3)如果合并成(n+1)阶页块,第一页的物理页号必须是2n+1的整数倍。

这是伙伴分配器(buddy allocator)这个名字的来源。以单页为例说明,0号页和1号页是伙伴,2号页和3号页是伙伴,1号页和2号页不是伙伴,因为1号页和2号页合并组成一阶页块,第一页的物理页号不是2的整数倍。

伙伴分配器分配和释放物理页的数量单位是阶。

2、分配n阶页块流程

分配n阶页块的过程如下。

  • (1)查看是否有空闲的n阶页块,如果有,直接分配;如果没有,继续执行下一步。
  • (2)查看是否存在空闲的(n+1)阶页块,如果有,把(n+1)阶页块分裂为两个n阶页块,一个插入空闲n阶页块链表,另一个分配出去;如果没有,继续执行下一步。
  • (3)查看是否存在空闲的(n+2)阶页块,如果有,把(n+2)阶页块分裂为两个(n+1)阶页块,一个插入空闲(n+1)阶页块链表,另一个分裂为两个n阶页块,一个插入空闲n阶页块链表,另一个分配出去;如果没有,继续查看更高阶是否存在空闲页块。

3、内核拓展

内核在基本的伙伴分配器的基础上做了一些扩展。

  • (1)支持内存节点和区域**,称为分区的伙伴分配器(zoned buddy allocator)**。
  • (2)为了预防内存碎片,把物理页根据可移动性分组。
  • (3)针对分配单页做了性能优化,为了减少处理器之间的锁竞争,在内存区域增加1个每处理器页集合。

2-分区的伙伴分配器(zoned buddy allocator)

1.数据结构

分区的伙伴分配器专注于某个内存节点的某个区域

  • 内存区域的结构体成员free_area用来维护空闲页块数组下标对应页块的阶数
  • 结构体free_area的成员free_list是空闲页块的链表(暂且忽略它是一个数组,下面小节将介绍),
  • nr_free是空闲页块的数量
  • 内存区域的结构体成员managed_pages是伙伴分配器管理的物理页的数量不包括引导内存分配器分配的物理页。
include/linux/mmzone.h
    struct zone {
        /* 不同长度的空闲区域 */
        struct free_area   free_area[MAX_ORDER];
        unsigned long      managed_pages;
    } ____cacheline_internodealigned_in_smp;
    struct free_area {
          struct list_head  free_list[MIGRATE_TYPES];
          unsigned long     nr_free;
    };

MAX_ORDER是最大阶数,实际上是可分配的最大阶数加1,默认值是11,意味着伙伴分配器一次最多可以分配210页。

可以使用配置宏CONFIG_FORCE_MAX_ZONEORDER指定最大阶数。

include/linux/mmzone.h
    /* 空闲内存管理-分区的伙伴分配器 */
    #ifndef CONFIG_FORCE_MAX_ZONEORDER
    #define MAX_ORDER   11
    #else
    #define MAX_ORDER   CONFIG_FORCE_MAX_ZONEORDER
    #endif

2.根据分配标志得到首选区域类型

申请页时,最低的4个标志位用来指定首选的内存区域类型:

include/linux/gfp.h
    #define ___GFP_DMA      0x01u
    #define ___GFP_HIGHMEM      0x02u
    #define ___GFP_DMA32        0x04u
    #define ___GFP_MOVABLE      0x08u

标志组合和首选的内存区域类型的对应关系如表所示。

图片

为什么要使用OPT_ZONE_DMA,而不使用ZONE_DMA?

因为DMA区域是可选的,如果不存在只能访问16MB以下物理内存的外围设备,那么不需要定义DMA区域,OPT_ZONE_DMA就是ZONE_NORMAL,从普通区域申请页。高端内存区域和DMA32区域也是可选的。

include/linux/gfp.h
    #ifdef CONFIG_HIGHMEM
    #define OPT_ZONE_HIGHMEM ZONE_HIGHMEM
    #else
    #define OPT_ZONE_HIGHMEM ZONE_NORMAL
    #endif
   #ifdef CONFIG_ZONE_DMA
    #define OPT_ZONE_DMA ZONE_DMA
    #else
    #define OPT_ZONE_DMA ZONE_NORMAL
    #endif
   #ifdef CONFIG_ZONE_DMA32
    #define OPT_ZONE_DMA32 ZONE_DMA32
    #else
    #define OPT_ZONE_DMA32 ZONE_NORMAL
    #endif
  • 内核使用宏GFP_ZONE_TABLE定义了标志组合到区域类型的映射表,
  • 其中GFP_ZONES_SHIFT是区域类型占用的位数,
  • GFP_ZONE_TABLE把每种标志组合映射到32位整数的某个位置,偏移是(标志组合 * 区域类型位数),从这个偏移开始的GFP_ZONES_SHIFT个二进制位存放区域类型。
  • 宏GFP_ZONE_TABLE是一个常量,编译器在编译时会进行优化,直接计算出结果,不会等到运行程序的时候才计算数值。
include/linux/gfp.h
    #define GFP_ZONE_TABLE ( \
        (ZONE_NORMAL << 0 * GFP_ZONES_SHIFT)                                    \
        | (OPT_ZONE_DMA << ___GFP_DMA * GFP_ZONES_SHIFT)                        \
        | (OPT_ZONE_HIGHMEM << ___GFP_HIGHMEM * GFP_ZONES_SHIFT)                \
        | (OPT_ZONE_DMA32 << ___GFP_DMA32 * GFP_ZONES_SHIFT)                    \
        | (ZONE_NORMAL << ___GFP_MOVABLE * GFP_ZONES_SHIFT)                     \
        | (OPT_ZONE_DMA << (___GFP_MOVABLE | ___GFP_DMA) * GFP_ZONES_SHIFT)     \
        | (ZONE_MOVABLE << (___GFP_MOVABLE | ___GFP_HIGHMEM) * GFP_ZONES_SHIFT)\
        | (OPT_ZONE_DMA32 << (___GFP_MOVABLE | ___GFP_DMA32) * GFP_ZONES_SHIFT) \
    )
gfp_zone()

内核使用函数gfp_zone()根据分配标志得到首选的区域类型:

  • 先分离出区域标志位,
  • 然后算出在映射表中的偏移(区域标志位 * 区域类型位数),
  • 接着把映射表右移偏移值,
  • 最后取出最低的区域类型位数。
include/linux/gfp.h
    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;
    }

3.备用区域列表

如果首选的内存节点和区域不能满足页分配请求,可以从备用的内存区域借用物理页,借用必须遵守以下原则。

  • (1)一个内存节点的某个区域类型可以从另一个内存节点的相同区域类型借用物理页,例如节点0的普通区域可以从节点1的普通区域借用物理页
  • (2)高区域类型可以从低区域类型借用物理页,例如普通区域可以从DMA区域借用物理页。
  • (3)低区域类型不能从高区域类型借用物理页,例如DMA区域不能从普通区域借用物理页。

内存节点的pg_data_t实例定义了备用区域列表,其代码如下:

include/linux/mmzone.h
    typedef struct pglist_data {
        struct zonelist node_zonelists[MAX_ZONELISTS]; /* 备用区域列表 */
    } pg_data_t;
   enum {
          ZONELIST_FALLBACK,     /* 包含所有内存节点的备用区域列表 */
    #ifdef CONFIG_NUMA
          ZONELIST_NOFALLBACK,   /* 只包含当前内存节点的备用区域列表(__GFP_THISNODE) */
    #endif
          MAX_ZONELISTS
    };
   #define MAX_ZONES_PER_ZONELIST (MAX_NUMNODES * MAX_NR_ZONES)
    struct zonelist {
          struct zoneref _zonerefs[MAX_ZONES_PER_ZONELIST + 1];
    };
   struct zoneref {
          struct zone *zone;   /* 指向内存区域的数据结构 */
          int zone_idx;        /* 成员zone指向的内存区域的类型 */
    };
  • UMA系统只有一个备用区域列表,按区域类型从高到低排序。假设UMA系统包含普通区域和DMA区域,那么备用区域列表是:{普通区域,DMA区域}。
  • NUMA系统的每个内存节点有两个备用区域列表:
  • 一个包含所有内存节点的区域,
  • 另一个只包含当前内存节点的区域。
    如果申请页时指定标志__GFP_THISNODE,要求只能从指定内存节点分配物理页,就需要使用指定内存节点的第二个备用区域列表。

包含所有内存节点的备用区域列表有两种排序方法。

  • (1)节点优先顺序:先根据节点距离从小到大排序,然后在每个节点里面根据区域类型从高到低排序
  • (2)区域优先顺序:先根据区域类型从高到低排序,然后在每个区域类型里面根据节点距离从小到大排序。

节点优先顺序的

  • 优点是优先选择距离近的内存,缺点是在高区域耗尽以前就使用低区域,例如DMA区域一般比较小,节点优先顺序会增大DMA区域耗尽的概率。
  • 区域优先顺序的优点是减小低区域耗尽的概率,缺点是不能保证优先选择距离近的内存。

默认的排序方法是自动选择最优的排序方法:

  • 如果是64位系统,因为需要DMA和DMA32区域的设备相对少,所以选择节点优先顺序;
  • 如果是32位系统,选择区域优先顺序。

可以使用内核参数“numa_zonelist_order”指定排序方法:

  • “d”表示默认排序方法,
  • “n”表示节点优先顺序,
  • “z”表示区域优先顺序,大小写字母都可以。

在运行中可以使用文件“/proc/sys/vm/numa_zonelist_order”修改排序方法。

假设NUMA系统包含节点0和1,节点0包含普通区域和DMA区域,节点1只包含普通区域。如果选择节点优先顺序,两个节点的备用区域列表如图所示。

图片

  • 如果节点0的处理器申请普通区域的物理页,应该依次尝试节点0的普通区域、节点0的DMA区域和节点1的普通区域。
  • 如果节点0的处理器申请DMA区域的物理页,首选区域是节点0的DMA区域,备用区域列表没有其他DMA区域可以选择。

4.区域水线

首选的内存区域在什么情况下从备用区域借用物理页?这个问题要从区域水线开始说起。每个内存区域有3个水线。

  • (1)高水线(high):如果内存区域的空闲页数大于高水线,说明该内存区域的内存充足。
  • (2)低水线(low):如果内存区域的空闲页数小于低水线,说明该内存区域的内存轻微不足。
  • (3)最低水线(min):如果内存区域的空闲页数小于最低水线,说明该内存区域的内存严重不足。
include/linux/mmzone.h
    enum zone_watermarks {
        WMARK_MIN,
        WMARK_LOW,
        WMARK_HIGH,
        NR_WMARK
    };
   struct zone {
          /* 区域水线,使用*_wmark_pages(zone) 宏访问 */
          unsigned long watermark[NR_WMARK];
    } ____cacheline_internodealigned_in_smp;

最低水线以下的内存称为紧急保留内存,在内存严重不足的紧急情况下,给承诺“给我少量紧急保留内存使用,我可以释放更多的内存”的进程使用。

设置了进程标志位PF_MEMALLOC的进程可以使用紧急保留内存,标志位PF_MEMALLOC表示承诺“给我少量紧急保留内存使用,我可以释放更多的内存”。内存管理子系统以外的子系统不应该使用这个标志位,典型的例子是页回收内核线程kswapd,在回收页的过程中可能需要申请内存。

如果申请页时设置了标志位__GFP_MEMALLOC,即调用者承诺“给我少量紧急保留内存使用,我可以释放更多的内存”,那么可以使用紧急保留内存。申请页时,第一次尝试使用低水线,如果首选的内存区域的空闲页数小于低水线,就从备用的内存区域借用物理页。如果第一次分配失败,那么唤醒所有目标内存节点的页回收内核线程kswapd以异步回收页,然后尝试使用最低水线。如果首选的内存区域的空闲页数小于最低水线,就从备用的内存区域借用物理页。

申请页时,第一次尝试使用低水线,如果首选的内存区域的空闲页数小于低水线,就从备用的内存区域借用物理页。如果第一次分配失败,那么唤醒所有目标内存节点的页回收内核线程kswapd以异步回收页,然后尝试使用最低水线。如果首选的内存区域的空闲页数小于最低水线,就从备用的内存区域借用物理页。

1-计算水线时,有两个重要的参数。
  • (1)min_free_kbytes是最小空闲字节数。默认值 = 4 * lowmen_kbytes,并且限制在范围[128,65536]以内。其中lowmem_kbytes是低端内存大小,单位是KB。参考文件“mm/page_alloc.c”中的函数init_per_zone_wmark_min。可以通过文件“/proc/sys/vm/min_free_kbytes”设置最小空闲字节数。
  • (2)watermark_scale_factor是水线缩放因子。默认值是10,可以通过文件“/proc/sys/vm/watermark_scale_factor”修改水线缩放因子,取值范围是[1,1000]。

文件“mm/page_alloc.c”中的函数__setup_per_zone_wmarks()负责计算每个内存区域的最低水线、低水线和高水线。

计算最低水线的方法如下。

  • (1)min_free_pages = min_free_kbytes对应的页数。
  • (2)lowmem_pages = 所有低端内存区域中伙伴分配器管理的页数总和。
  • (3)高端内存区域的最低水线 = zone->managed_pages/1024,并且限制在范围[32, 128]以内(zone->managed_pages是该内存区域中伙伴分配器管理的页数,在内核初始化的过程中引导内存分配器分配出去的物理页,不受伙伴分配器管理)。
  • (4)低端内存区域的最低水线 = min_free_pages * zone->managed_pages /lowmem_pages,即把min_free_pages按比例分配到每个低端内存区域。

计算低水线和高水线的方法如下。

  • (1)增量 = (最低水线 / 4, zone->managed_pages * watermark_scale_factor / 10000)取最大值。
  • (2)低水线 = 最低水线 + 增量。
  • (3)高水线 = 最低水线 + 增量 * 2。

如果(最低水线 / 4)比较大,那么计算公式简化如下。

  • (1)低水线 = 最低水线 * 5/4。
  • (2)高水线 = 最低水线 * 3/2。

5.防止过度借用

和高区域类型相比,低区域类型的内存相对少,是稀缺资源,而且有特殊用途,例如DMA区域用于外围设备和内存之间的数据传输。为了防止高区域类型过度借用低区域类型的物理页,低区域类型需要采取防卫措施,保留一定数量的物理页。

一个内存节点的某个区域类型从另一个内存节点的相同区域类型借用物理页,后者应该毫无保留地借用。

内存区域有一个数组用于存放保留页数:

include/linux/mmzone.h
    struct zone {
          long lowmem_reserve[MAX_NR_ZONES];
    } ____cacheline_internodealigned_in_smp;

zone[i]->lowmem_reserve[j]表示区域类型i应该保留多少页不能借给区域类型j,仅当j大于i时有意义。

zone[i]->lowmem_reserve[j]的计算规则如下:

(i < j):
      zone[i]->lowmem_reserve[j]
      = (当前内存节点上从zone[i + 1] 到zone[j]伙伴分配器管理的页数总和)
        / sysctl_lowmem_reserve_ratio[i]
    (i = j):
      zone[i]->lowmem_reserve[j]= 0(相同的区域类型不应该保留)
    (i > j):
      zone[i]->lowmem_reserve[j]= 0(没意义,不会出现低区域类型从高区域类型借用物理页的情况)

数组sysctl_lowmem_reserve_ratio存放各种区域类型的保留比例,因为内核不允许使用浮点数,所以使用倒数值。DMA区域和DMA32区域的默认保留比例都是256,普通区域和高端内存区域的默认保留比例都是32。

mm/page_alloc.c
    int sysctl_lowmem_reserve_ratio[MAX_NR_ZONES-1] = {
    #ifdef CONFIG_ZONE_DMA
            256,
    #endif
    #ifdef CONFIG_ZONE_DMA32
            256,
    #endif
    #ifdef CONFIG_HIGHMEM
            32,
    #endif
            32,
    };

可以通过文件“/proc/sys/vm/lowmem_reserve_ratio”修改各种区域类型的保留比例。

目录
相关文章
|
1月前
|
存储 C语言
C语言如何使用结构体和指针来操作动态分配的内存
在C语言中,通过定义结构体并使用指向该结构体的指针,可以对动态分配的内存进行操作。首先利用 `malloc` 或 `calloc` 分配内存,然后通过指针访问和修改结构体成员,最后用 `free` 释放内存,实现资源的有效管理。
121 13
|
2月前
|
缓存 算法 Java
JVM知识体系学习六:JVM垃圾是什么、GC常用垃圾清除算法、堆内存逻辑分区、栈上分配、对象何时进入老年代、有关老年代新生代的两个问题、常见的垃圾回收器、CMS
这篇文章详细介绍了Java虚拟机(JVM)中的垃圾回收机制,包括垃圾的定义、垃圾回收算法、堆内存的逻辑分区、对象的内存分配和回收过程,以及不同垃圾回收器的工作原理和参数设置。
89 4
JVM知识体系学习六:JVM垃圾是什么、GC常用垃圾清除算法、堆内存逻辑分区、栈上分配、对象何时进入老年代、有关老年代新生代的两个问题、常见的垃圾回收器、CMS
|
2月前
|
存储 Java
JVM知识体系学习四:排序规范(happens-before原则)、对象创建过程、对象的内存中存储布局、对象的大小、对象头内容、对象如何定位、对象如何分配
这篇文章详细地介绍了Java对象的创建过程、内存布局、对象头的MarkWord、对象的定位方式以及对象的分配策略,并深入探讨了happens-before原则以确保多线程环境下的正确同步。
62 0
JVM知识体系学习四:排序规范(happens-before原则)、对象创建过程、对象的内存中存储布局、对象的大小、对象头内容、对象如何定位、对象如何分配
|
4月前
|
存储 JavaScript 前端开发
学习JavaScript 内存机制
【8月更文挑战第23天】学习JavaScript 内存机制
42 3
|
4月前
|
关系型数据库 MySQL
MySQl优化:使用 jemalloc 分配内存
MySQl优化:使用 jemalloc 分配内存
|
4月前
|
缓存 Java 编译器
Go 中的内存布局和分配原理
Go 中的内存布局和分配原理
|
5月前
|
存储 缓存 算法
(五)JVM成神路之对象内存布局、分配过程、从生至死历程、强弱软虚引用全面剖析
在上篇文章中曾详细谈到了JVM的内存区域,其中也曾提及了:Java程序运行过程中,绝大部分创建的对象都会被分配在堆空间内。而本篇文章则会站在对象实例的角度,阐述一个Java对象从生到死的历程、Java对象在内存中的布局以及对象引用类型。
140 8
|
5月前
|
NoSQL Redis C++
c++开发redis module问题之在复杂的Redis模块中,特别是使用第三方库或C++开发时,接管内存统计有哪些困难
c++开发redis module问题之在复杂的Redis模块中,特别是使用第三方库或C++开发时,接管内存统计有哪些困难
|
5月前
|
Java 运维
开发与运维内存问题之在堆内存中新创建的对象通常首先分配如何解决
开发与运维内存问题之在堆内存中新创建的对象通常首先分配如何解决
27 1
|
4月前
|
存储 NoSQL Java
Tair的发展问题之Tair对于不同存储介质(如内存和磁盘)的线程分配是如何处理的
Tair的发展问题之Tair对于不同存储介质(如内存和磁盘)的线程分配是如何处理的

热门文章

最新文章