0%

Linux内存管理(四)页面分配器

原图

分配物理页面是内存管理中最复杂的部分,它涉及页面回收、内存规整、直接回收内存等相当错综复杂的机制。本节关注在内存充足的情况下如何分配连续物理内存。

page数据结构

Linux内核内存管理的实现以page数据结构为核心,其他的内存管理设施都基于page数据结构,如VMA管理、缺页中断、RMAP、页面分配与回收等。page数据结构定义在include/linux/mm_types.h头文件中。

_refcount

_refcount和__mapcount是page数据结构中非常重要的两个引用计数,且都是atomic_t类型的变量。 _refcount表示内核中引用该页面的次数。

  • 当_refcount 的值为0时,表示该页面为空闲页面或即将要被释放的页面
  • 当_refcount 的值大于0时,表示该页面已经分配给内核正在使用,暂时不会被释放

_mapcount

_mapcount表示这个页面被进程映射的个数,即已经映射了多少个用户PTE。每个用户进程都拥有各自独立的虚拟空间(256TB)和一份独立的页表,所以可能出现多个用户进程地址空间同时映射到一个物理页面的情况,RMAP系统就是利用这个特性来实现的。_mapcount主要用于RMAP系统中。

  • 若_mapcount等于-1,表示没有PTE映射到页面
  • 若_mapcount等于O,表示只有父进程映射到页面。匿名页面刚分配时,_mapcount初始化为O。例如,当do_anonymous_page()产生的匿名页面通过page_add_new_anonrmap()添加到rmap系统中时,会设置_mapcount为O,这表明匿名页面当前只有父进程的PTE映射到页面

RMAP

用户进程在使用虚拟内存过程中,从虚拟内存页面映射到物理内存页面时,PTE保留了这个记录。page数据结构中的_mapcount记录有多少个用户PTE映射到物理页面。有的页面需要迁移,有的页面需要交换到磁盘,在交换之前,必须找出哪些进程使用这个页面,然后解除这些映射的用户PTE。

RMAP的主要目的是从物理页面的page数据结构中找到有哪些映射的用户PTE,这样页面回收模块就可以很快速和高效地把这个物理页面映射的所有用户PTE正都解除并回收这个页面。

RMAP的典型应用场景如下。

  • kswapd内核线程为了回收页面,需要断开所有映射到该匿名页面的用户PTE
  • 页面迁移时,需要断开所有映射到匿名页面的用户PTE

分配物理页面的接口函数(扩展🙈)

内核中分配物理内存页面的常用接口函数是alloc_pages(),它用于分配一个或者多个连续的物理页面,分配的页面个数只能是2的整数次幂。

分配物理页面的核心接口函数

aloc_pages()函数的参数有两个,gfp_mask表示分配掩码,order表示分配级数。

1
2
3
<include/linux/gfp.h>
struct page *__alloc_pages(gfp_t gfp, unsigned int order, int preferred_nid,
nodemask_t *nodemask);

alloc_paes()函数用来分配2的order次幂个连续的物理页面,返回值是第一个物理页面page数据结构。第一个参数是gfp_mask:第二个参数是order,请求的order需要小于MAX_ORDER,MAX_ORDER通常默认是11。 另一个很常见的接口函数是__get_free_pages(),其定义如下。

1
2
3
4
5
6
7
8
9
10
<mm/page_alloc.c>
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);
}

__get_free_pages()函数返回的是所分配内存的内核空间虚拟地址。如果所分配内存是线性映射的物理内存,则直接返回线性映射区域的内核空间虚拟地址。

1
static inline void *page_address(const struct page *page)

分配一个物理页面

如果需要分配一个物理页面,可以使用如下两个封装好的接口函数,它们最后仍调用alloc_pages(),只是order的值为0.

1
2
3
4
5
#define alloc_pages_vma(gfp_mask, order, vma, addr, node, false)\
alloc_pages(gfp_mask, order)

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

如果需要返回一个全填充为0的页面,可以使用如下接口函数。

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

使用alloc_page()分配的物理页面理论上可能被随机地填充了某些垃圾信息,因此在有些敏感的场合下需要先把分配的内存清零再使用,这样可以减少不必要的麻烦。

页面释放函数

页面释放函数主要有如下几个。

1
2
3
4
5
extern void __free_pages(struct page *page, unsigned int order);
extern void free_pages(unsigned long addr, unsigned int order);

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

释放时需要特别注意参数,传递错误的page指针或者错误的order值会引起系统崩溃。

分配掩码

分配掩码是描述页面分配方法的标志,它影响着页面分配的整个流程。因为Linux内核是一个通用的操作系统,所以页面分配器被设计成一个复杂的系统。它既要高效,又要兼顾很多种情况,特别是在内存紧张的情况下的内存分配。gfp_mask其实被定义成一个unsigned类型的变量。

1
typedef unsigned __bitwise__ gfp_t;

gfp_mask定义在include/inux/gfp.h文件中。修饰符在Linux4.4内核中被重新归类,大致可以分成如下几类。

  • 内存管理区修饰符(zone modifier)
  • 移动修饰符(mobility and placement modifier)
  • 水位修饰符(watermark modifier)
  • 页面回收修饰符(page reclaim modifier)
  • 行为修饰符(action modifier)

下面详细介绍各种修饰符的标志。

内存管理区修饰符的标志

内存管理区修饰符主要用于表示应当从哪些内存管理区中来分配物理内存。内存管理区修饰符使用gfp_mask的低4位来表示,其标志如下所示。

  • __GFP_DMA: 从ZONE_DMA中分配内存
  • __GFP_DMA32: 从ZONE DMA32中分配内存
  • __GFP_HIGHMEM: 优先从ZONE_HIGHMEM中分配内存
  • __GFP_MOVABLE: 页面可以被迁移或者回收,如用于内存规整机制

移动修饰符的标志

移动修饰符主要用于指示分配出来的页面具有的迁移属性,其标志如下所示。在Linux2.6.24内核中,为了解决外碎片化的问题,引入了迁移类型,因此在分配内存时需要指定所分配的页面具有哪些迁移属性。

  • __GFP_RECLAIMABLE: 在slab分配器中指定了SLAB_RECLAIM_ACCOUNT标志位,表示slab分配器中使用的页面可以通过收割机来回收
  • __GFP_HARDWALL: 使能cpuset内存分配策略
  • __GFP_THISNODE: 从指定的内存节点中分配内存,并且没有回退机制
  • __GFP_ACCOUNT: 分配过程会被kmemcg记录

水位修饰符的标志

水位修饰符用于控制是否可以访问系统预留的内存。所谓系统预留内存指的是最低警戒水位以下的内存,一般优先级的分配请求是不能访问它们的,只有高优先级的分配请求才能访问,如__GFP_HIGH、__GFP_ATOMIC等。水位修饰符的标志如下所示。

  • __GFP_HIGH: 表示分配内存具有高优先级,并且这个分配请求是很有必要的,分配器可以使用系统预留的内存(即最低警戒水位线下的预留内存)
  • __GFP_ATOMIC: 表示分配内存的过程不能执行页面回收或者睡眠动作,并且具有很高的优先级,可以访问系统预留的内存。常见的一个场景是在中断上下文中分配内存
  • __GFP_MEMALLOC: 分配过程中允许访问所有的内存,包括系统预留的内存。分配内存进程通常要保证在分配内存过程中很快会有内存被释放,如进程退出或者页面回收
  • __GFP_NOMEMALLOC: 分配过程不允许访问系统预留的内存

页面回收修饰符的标志

页面回收修饰符的常用标志如下所示。

  • __GFP_IO: 允许开启I/O传输
  • __GFP_FS: 允许调用底层的文件系统。这个标志清零通常是为了避免死锁的发生,如果相应的文件系统操作路径上已经持有了锁,分配内存过程又递归地调用这个文件系统的相应操作路径,可能会产生死锁
  • __GFP_DIRECT_RECLAIM: 分配内存的过程中允许使用页面直接回收机制
  • __GFP_KSWAPD_RECLAIM: 表示当到达内存管理区的低水位时会唤醒kswapd内核线程,以异步地回收内存,直到内存管理区恢复到了高水位为止
  • __GFP_RECLAIM: 用于允许或者禁止直接页面回收和kswapd内核线程
  • __GFP_REPEAT: 当分配失败时会继续尝试
  • __GFP_NOFAIL: 当分配失败时会无限地尝试下去,直到分配成功为止。当分配者希望分配内存不失败时,应该使用这个标志位,而不是自己写一个while循环来不断地调用页面分配接口函数
  • __GFP_NORETRY: 当使用了直接页面回收和内存规整等机制还无法分配内存时,最好不要重复尝试分配了,直接返回NULL

行为修饰符的标志

行为修饰符的常见标志如下所示。

  • __GFP_COLD: 分配的内存不会马上被使用。通常会返回一个空的高速缓存页面
  • __GFP_NOWARN: 关闭分配过程中的一些错误报告
  • __GFP_ZERO: 返回一个全部填充为0的页面
  • __GFP_NOTRACK: 不被kmemcheck机制跟踪
  • __GFP_OTHER_NODE: 在远端的一个内存节点上分配。通常在 khugepaged内核线程中使用

前文列出了5大类修饰符的标志,对于内核开发者或者驱动开发者来说,要正确使用这些标志是一件很困难的事情,因此定义了一些常用的标志的组合类型标志(type flag),如下所示。类型标志提供了内核开发中常用的标志的组合,推荐开发者使用这些类型标志。

  • GFP_KERNEL: 内核分配内存常用的标志之一。它可能会被阻塞,即分配过程中可能会睡眠
  • GFP_ATOMIC: 调用者不能睡眠并且保证分配会成功。它可以访问系统预留的内存
  • GFP_NOWAIT: 分配中不允许睡眠等待
  • GFP_NOFS: 不会访问任何的文件系统的接口和操作
  • GFP_NOIO: 不需要启动任何的I/O操作。如使用直接回收机制丢弃干净的页面或者为slab分配的页面
  • GFP_USER: 通常用户空间的进程用来分配内存,这些内存可以被内核或者硬件使用。常用的一个场景是硬件使用的DMA缓冲器要映射到用户空间,如显卡的缓冲器
  • GFP_DMA/ GFP_DMA32: 使用ZONE_DMA或者ZONE_DMA32来分配内存
  • GFP_HIGHUSER: 用户空间进程用来分配内存,优先使用ZONE_HIGHMEM,这些内存可以映射到用户空间,内核空间不会直接访问这些内存。另外,这些内存不能迁移
  • GFP_HIGHUSER_MOVABLE: 类似于GFP_HIGHUSER,但是页面可以迁移
  • GFP_TRANSHUGE / GFP_TRANSHUGE_LIGHT: 通常用于透明页面分配

上面这些都是常用的类型标志,在实际使用过程中需要注意以下事项。

  • GFP_KERNEL主要用于分配内核使用的内存,在分配过程中会引起睡眠,在中断上下文和不能睡眠的内核路径里使用该类型标志需要特别警惕,因为这会引起死锁或者其他系统异常
  • GFP_ATOMIC正好和GFP_KERNEL相反,它可以使用在不能睡眠的内存分配路径(如中断处理程序、软中断以及tasklet等)中。GFP_KERNEL可以让调用者睡眠等待系统页面回收来释放一此内存,但GFP_ATOMIC不可以,所以可能会分配失败
  • GFP_USER、 GFP_HIGHUSER和GFP_HIGHUSER_MOVABLE这几个类型标志都是为用户空间进程分配内存的。不同之处在于,GFP_HIGHUSER首先使用高端内存,GFP_HIGHUSER MOVABLE优先使用高端内存并且分配的内存具有可移动属性
  • GFP_NOIO和GFP_NOFS都会产生阻塞,它们用于避免某些其他的操作。GFPNOIO表示分配过程中绝不会启动任何磁盘I/O的操作。GFP_NOFS表示可以启动磁盘I/O,但是不会启动文件系统的相关操作。举一个例子,假设进程A在执行打开文件的操作中需要分配内存但内存短缺,那么进程A会睡眠等待,系统的OOM Killer机制会选择一个进程来终止。假设选择了进程B,而进程B退出时需要执行一些文件系统的操作,这些操作可能会申请锁,而恰巧进程A持有这个锁,所以死锁就发生了
  • GFP_KERNEL分配的页面通常是不可迁移的,GFP_HIGHUSER_MOVABLE分配的页面是可迁移的

alloc_pages()函数

下面以GFP_KERNEL为例,看在理想情况下alloc_pages()函数是如何分配物理内存的。

1
2
《分配物理内存的例子》
page = alloc_pages(GFP_KERNEL, order);

GFP_KERNEL类型标志定义在gfp.h头文件中,是一个标志组合。

1
#define GFP_KERNEL	(__GFP_RECLAIM | __GFP_IO | __GFP_FS)

所以GFP_KERNEL类型标志包含了__GFP_RECLAIM、__GFP_IO和__GFP_FS这3个标志,其地址换算成十六进制是0x60 00 C0。

其函数流程如下:

小结

页面分配器是Linux内核内存管理中最基本的分配器,伙伴系统分配页面就是使用该方法,理解页面分配器需要关注如下几个方面。

  • 从分配掩码中知道可以从哪些zone中分配内存,分配内存的属性属于哪些MIGRATE_TYPES类型
  • 页面分配时应从哪个方向来扫描zone
  • zone水位的判断

本节介绍了理想情况下页面分配器如何分配物理页面,但是在Linux内核处于内存短缺的情况下又该如何分配内存呢?这涉及内存管理中最难的几个主题,如页面回收、直接内存回收内存规整和OOM Killer机制等。

参考文献

《奔跑吧Linux内核》