0%

Linux内存管理(三)基础知识

原图

内存管理发展史

内存管理的“远古时代”

在分页机制出现之前,操作系统有很多不同的内存管理机制,如动态分区法。如图(a)所示。剩余的4MB内存不足以装载进程D,如图(b)所示,因为进程D需要5MB内存,这个内存末尾就成了第一个空洞(内存碎片)。假设某个时刻,操作系统需要运行进程D,因为系统中没有足够的内存,所以需要选择一个进程来换出,为进程D腾出足够的空间。假设操作系统选择进程B来换出,这样进程D就装载到了原来进程B的地址空间里,于是产生了第二个空洞,如图(c)所示。假设操作系统某个时刻需要运行进程B,也需要选择一个进程来换出,假设进程A被换出,那么操作系统中又产生了第三个空洞,如图(d)所示。

这种动态分区法在开始时是很好的,但是随着时间的推移会出现很多内存空洞,内存的利用率随之下降,这些内存空洞便是我们常说的内存碎片,动态分区法依然存在以下问题。

  • 进程地址空间保护问题。所有的进程都可以访问全部的物理内存,所以恶意的程序可以修改其他程序的内存数据,这使进程一直处于危险的状态下
  • 内存使用效率低。如果即将运行的进程所需要的内存空间不足,就需要选择一个进程以进行整体换出,这种机制导致大量的数据需要换出和换入,效率非常低下
  • 程序运行地址重定位问题。从上图中看到,进程在每次换出、换入时使用的地址都是不固定的,这给程序的编写带来一定的麻烦,因为访问数据和指令跳转时的目标地址通常是固定的,这就需要重定位技术

因此产生了分段机制和分页机制。

分段机制

  • 把程序所需的内存空间的虚拟地址映射到某个物理地址空间中,解决地址空间保护问题
  • 分段机制把进程分成若干段(代码段、数据段栈段与堆段等),每个段的大小是不固定的,有点类似于动态分区法,这些段的物理地址可以不连续这样可以一定程度上解决内存碎片问题

分段机制是一个比较明显的改进,但是它的内存使用效率依然比较低。分段机制对虚拟内存到物理内存的映射依然以进程为单位。进程在运行时,根据局部性原理,只有一部分数据是一直在使用的,若把那些不常用的数据交换出磁盘,就可以节省很多系统带宽。

分页机制

分页机制把分段机制的单位继续细分成固定大小的页面(page),进程的虚拟地址空间也按照页面来分割,这样常用的数据和代码就可以以页面为单位驻留在内存中,而那些不常用的页面可以交换到磁盘中。物理内存也以页面为单位来管理,这些物理内存称为物理页面(physical page)或者页帧(page frame)。进程的虚拟地址空间中的页面称为虚拟页面(virtualpage)。操作系统为了管理这些页帧需要按照物理地址给每个页帧编号,这个编号称为页帧号(Page Frame Number,PFN)。

从进程的角度看内存管理

在Linux系统中,应用程序常用的可执行文件格式是可执行与可链接格式(Executable Linkable Format,ELF)。ELF最开始的部分是ELF文件头(ELF Header),它包含了描述整个文件的基本属性,如ELF文件版本、目标计算机型号、程序入口地址等信息。ELF文件头后面是程序的各个段,包括代码段、数据段、未初始化数据段等。后面是段头表,用于描述ELF文件中包含的所有段的信息,如每个段的名字、段的长度、在文件中的偏移量、读写权限以及段的其他属性等,后面紧跟着是字符串表和符号表等。ELF结构如下图所示:

下面介绍常见的几个段,这些段与内核映像中的段也是基本类似的。

  • 代码段:存放程序源代码编译后的机器指令
  • 数据段:存放已初始化的全局变量和已初始化的局部静态变量
  • 未初始化数据段:存放未初始化的全局变量以及未初始化的局部静态变量。下面编写一个简单的C程序
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#define SIZE (100*1024)
int main()
{
char * buf = malloc(SIZE);
memset(buf, 0x58, SIZE);
printf("malloc buffer 0x%p\n", buf);
while(1)
{
sleep(100);
}
}

这个C程序很简单,首先通过malloc()函数来分配100KB的内存,然后通过memset()函写入这块内存,最后用while循环是为了不让这个程序退出。我们通过如下命令来把它编成ELF文件。

1
$ aarch64-linux-gnu-gcc-static test.c -o test.elf

可以使用objdump或者readelf工具来查看ELF文件包含哪些段。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
vooxle@liushuai:~$ aarch64-linux-gnu-readelf -S test.elf
There are 29 section headers, starting at offset 0x92548:

Section Headers:
[Nr] Name Type Address Offset
Size EntSize Flags Link Info Align
[ 0] NULL 0000000000000000 00000000
0000000000000000 0000000000000000 0 0 0
[ 1] .note.gnu.build-i NOTE 0000000000400190 00000190
0000000000000024 0000000000000000 A 0 0 4
[ 2] .note.ABI-tag NOTE 00000000004001b4 000001b4
0000000000000020 0000000000000000 A 0 0 4
[ 3] .rela.plt RELA 00000000004001d8 000001d8
00000000000000a8 0000000000000018 AI 0 21 8
[ 4] .init PROGBITS 0000000000400280 00000280
0000000000000014 0000000000000000 AX 0 0 4
[ 5] .plt PROGBITS 00000000004002a0 000002a0
0000000000000070 0000000000000000 AX 0 0 16
[ 6] .text PROGBITS 0000000000400340 00000340
00000000000516ac 0000000000000000 AX 0 0 64
[ 7] __libc_freeres_fn PROGBITS 00000000004519f0 000519f0
0000000000000b44 0000000000000000 AX 0 0 8
[ 8] .fini PROGBITS 0000000000452534 00052534
0000000000000010 0000000000000000 AX 0 0 4
[ 9] .rodata PROGBITS 0000000000452550 00052550
000000000001be28 0000000000000000 A 0 0 16
[10] __libc_subfreeres PROGBITS 000000000046e378 0006e378
0000000000000048 0000000000000000 A 0 0 8
[11] __libc_IO_vtables PROGBITS 000000000046e3c0 0006e3c0
00000000000005e8 0000000000000000 A 0 0 8
[12] __libc_atexit PROGBITS 000000000046e9a8 0006e9a8
0000000000000008 0000000000000000 A 0 0 8
[13] .eh_frame PROGBITS 000000000046e9b0 0006e9b0
000000000000a158 0000000000000000 A 0 0 8
[14] .gcc_except_table PROGBITS 0000000000478b08 00078b08
000000000000009e 0000000000000000 A 0 0 1
[15] .tdata PROGBITS 0000000000489978 00079978
0000000000000020 0000000000000000 WAT 0 0 8
[16] .tbss NOBITS 0000000000489998 00079998
0000000000000040 0000000000000000 WAT 0 0 8
[17] .init_array INIT_ARRAY 0000000000489998 00079998
0000000000000008 0000000000000008 WA 0 0 8
[18] .fini_array FINI_ARRAY 00000000004899a0 000799a0
0000000000000010 0000000000000008 WA 0 0 8
[19] .data.rel.ro PROGBITS 00000000004899b0 000799b0
0000000000000594 0000000000000000 WA 0 0 8
[20] .got PROGBITS 0000000000489f48 00079f48
00000000000000a0 0000000000000008 WA 0 0 8
[21] .got.plt PROGBITS 0000000000489fe8 00079fe8
0000000000000050 0000000000000008 WA 0 0 8
[22] .data PROGBITS 000000000048a038 0007a038
0000000000001950 0000000000000000 WA 0 0 8
[23] .bss NOBITS 000000000048b988 0007b988
00000000000015d0 0000000000000000 WA 0 0 8
[24] __libc_freeres_pt NOBITS 000000000048cf58 0007b988
0000000000000028 0000000000000000 WA 0 0 8
[25] .comment PROGBITS 0000000000000000 0007b988
000000000000002a 0000000000000001 MS 0 0 1
[26] .symtab SYMTAB 0000000000000000 0007b9b8
0000000000010110 0000000000000018 27 1726 8
[27] .strtab STRTAB 0000000000000000 0008bac8
0000000000006954 0000000000000000 0 0 1
[28] .shstrtab STRTAB 0000000000000000 0009241c
0000000000000128 0000000000000000 0 0 1

可以看到刚才编译的test.elf文件一共有28个段,除了常见的代码段、数据段之外,还有一些其他的段,这些段在进程加载时起辅助作用,暂时先不用关注它们。程序在编译、链接时会尽量把相同权限属性的段分配在同一个空间里,如把可读、可执行的段放在一起,包括代码段、init段等;把可读、可写的段放在一起,包括数据段和未初始化数据段等。ELF把这些属性相似并且链接在一起的段叫作分段(segment),进程在加载时是按照这些分段来映射可执行文件的。描述这些分段的结构叫作程序头(program header),它描述了ELF文件是如何映射到进程地址空间的,这是我们比较关心的。我们可以通过readelf -l命今来查看这些程序头。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
vooxle@liushuai:~$ aarch64-linux-gnu-readelf -l test.elf

Elf file type is EXEC (Executable file)
Entry point 0x400558
There are 6 program headers, starting at offset 64

Program Headers:
Type Offset VirtAddr PhysAddr
FileSiz MemSiz Flags Align
LOAD 0x0000000000000000 0x0000000000400000 0x0000000000400000
0x0000000000078ba6 0x0000000000078ba6 R E 0x10000
LOAD 0x0000000000079978 0x0000000000489978 0x0000000000489978
0x0000000000002010 0x0000000000003608 RW 0x10000
NOTE 0x0000000000000190 0x0000000000400190 0x0000000000400190
0x0000000000000044 0x0000000000000044 R 0x4
TLS 0x0000000000079978 0x0000000000489978 0x0000000000489978
0x0000000000000020 0x0000000000000060 R 0x8
GNU_STACK 0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 RW 0x10
GNU_RELRO 0x0000000000079978 0x0000000000489978 0x0000000000489978
0x0000000000000688 0x0000000000000688 R 0x1

Section to Segment mapping:
Segment Sections...
00 .note.gnu.build-id .note.ABI-tag .rela.plt .init .plt .text __libc_freeres_fn .fini .rodata __libc_subfreeres __libc_IO_vtables __libc_atexit .eh_frame .gcc_except_table
01 .tdata .init_array .fini_array .data.rel.ro .got .got.plt .data .bss __libc_freeres_ptrs
02 .note.gnu.build-id .note.ABI-tag
03 .tdata .tbss
04
05 .tdata .init_array .fini_array .data.rel.ro .got

从上面可以看到之前的28个段被分成了6个分段,我们只关注其中两个LOAD类型的分段。因为在加载时需要映射它,其他的分段在加载时起辅助作用。 先看第一个LOAD类型的分段,它是具有只读和可执行的权限,包含.init段、代码段、只读数据段等常见的段,它映射的虚拟地址是0x400000,长度是0x78ba6。 第二个LOAD类型的分段具有可读和可写的权限,包含数据段和未初始化数据段等常见的段,它映射的虚拟地址是0x489978,长度是0x3608 上面从静态的角度来看进程的内存管理,我们还可以从动态的角度来看。Linux操作系统提供了“proc”文件系统来窥探Linux内核的执行情况,每个进程执行之后,在/proc/pid/maps节点会列出当前进程的地址映射情况。

1
2
3
4
5
6
7
# cat /proc/721/maps
00400000-0046000 r-xp 00000000 00:26 52559883 test.elf
0047e000-00481000 rw-p 0006e000 00:26 52559883 test.elf
272dd000-272ff000 rw-p 00000000 00:00 0 [heap]
ffffa97ea000-ffffa97eb000 r--p 00000000 00:00 0 [vvar]
ffffa97eb000-ffffa97ec000 r-xp 00000000 00:00 0 [vdso]
ffffcb6c6000-ffffcb6e7000 rw-p 00000000 00:00 0 [stack]

第1行显示了地址0x400000~0x46f000,这段进程地址空间的属性是只读和可执行的,由此我们知道它是代码段,也就是之前看到的代码段的程序头。
第2行显示了地址0x47e000~0x48100,这段进程地址空间的属性是可读和可写的,也就是我们之前看到的数据段的程序头。
第3行显示了地址0x272dd00~0x272ff000,这段进程地址空间叫作堆(heap)空间,也就是通常使用malloc()分配的内存,大小是140KB。test进程主要使用malloc()分配100KB的内存,这里看到Linux内核会分配比100KB稍微大一点的内存空间。
第4行显示了名为vvar的特殊映射。
第5行显示了名为vdso的特殊映射,VDSO指Virtual Dynamic Shared Object,用于解决内核和libc之间的版本问题。
第6行显示了test进程的栈(stack)空间。
对于这里的进程地址空间,在Linux内核中使用一个叫作VMA的术语来描述它,它是vm_area_struct数据结构的简称。 另外,/proc/pid/smaps节点会提供更多地址映射的细节.

物理内存管理之预备知识

内存架构之UMA和NUMA

  • UMA统一内存访问架构:内存有统一的结构并且可以统一寻址。目前大部分嵌入式系统、手机操作系统以及台式机操作系统等采用UMA架构

  • NUMA非统一内存访问架构:系统中有多个内存节点和多个CPU簇,CPU访问本地内存节点的速度最快,访问远端的内存节点的速度要慢一点

基本概念

从Linux内核的角度来看,DDR存储设备其实就是一段物理内存空间。在Linux内核中,和内存硬件物理特性相关的一些数据结构主要集中在MMU(如页表、高速缓存/TLB操作等)中。因此大部分的Linux内核中关于内存管理的相关数据结构是软件层面的概念,如mm、VMA、内存管理区(zone)、页面、pg_data等。Linux内核内存管理中的数据结构错综复杂,如下图所示。

和物理内存管理相关的数据结构有内存节点(pglist data)、内存管理区、物理页面(page)、mem_map[]数组、页表项(PTE)、页帧号(PFN)、物理地址(paddress)。

其中,pglist data数据结构用来描述一个内存节点的所有资源。在UMA架构中,只有一个内存节点,即系统有一个全局的变量contig_page_data来描述这个内存节点。在NUMA架构中,整个系统的内存由一个pglist_data*的指针数组node_data[]来管理,在系统初始化枚举BIOS固件(ACPI)来完成。

Linux内核用内存管理区来划分物理内存是有以下历史原因的。

  • 由于地址数据线位宽的限制,32位处理器通常最多支持4GB的物理内存,如果打开了LPAE特性,可以支持更大的物理内存。在4Gb的地址空间中,通常内核空间只有1GB大小,因此对于大小为4GB的物理内存是无法进行--线性映射的。Linux内核的做法是把物理内存分成两部分,其中一部分是线性映射的。如果用一个内存管理区来描述它,那就是ZONE_NORMAL。剩余的部分叫从高端内存(high memory)同样使用一个内存管理区来描述它,称为ZONE_HIGHMEM
  • 内存管理区的分布和架构相关,如在x86架构中,ISA设备只能访问物理内存的前 16MB,所以在x86架构中会有一个名为ZONE_DMA的管理区域。在x86_64架构中,由于有足够大的内核空间可以线性映射物理内存,因此就不需要ZONE_HIGHMEM这个管理区域了

在Linux操作系统中常见的内存管理区可以分为以下几种:

  • ZONE_DMA:用于ISA设备的DMA操作,范围是0~16MB,只适用于Intel x86架构,ARM架构没有这个内存管理区
  • ZONE_DMA32:用于最低4GB的内存访问的设备,如只支持32位的DMA设备
  • ZONE_NORMAL:4GB以后的物理内存,用于线性映射物理内存。若系统内存小于4GB,则没有这个内存管理区
  • ZONE_HIGHMEM:用于管理高端内存,这些高端内存是不能线性映射到内核地址空间的。注意,在64位Linux操作系统中没有这个内存管理区

Linux内核中使用一个page数据结构来描述一个物理页面。Linux内核为每个物理页面都分配了一个page数据结构,采用mem_map[]数组来存放这些page数据结构,并且它们和物理页面是一对一的映射关系,如下图所示

每个node由一个或多个zone组成,每个zone又由若干page frames组成(一般page frame都是指物理页面)。

Page Frame

虽然内存访问的最小单位是byte或者word,但MMU是以page为单位来查找页表的,page也就成了Linux中内存管理的重要单位。包括换出(swap out)、回收(relcaim)、映射等操作,都是以page为粒度的。

因此,描述page frame的struct page自然成为了内核中一个使用频率极高,非常重要的结构体,来看下它是怎样构成的:

1
2
3
4
5
6
7
8
9
struct page {
unsigned long flags;
atomic_t count;
atomic_t _mapcount;
struct list_head lru;
struct address_space *mapping;
unsigned long index;
...
}
  • flags表示page frame的状态或者属性,包括和内存回收相关的PG_active, PG_dirty, PG_writeback, PG_reserved, PG_locked, PG_highmem等。其实flags是身兼多职的,它还有其他用途,这将在下文中介绍到
  • count表示引用计数。当count值为0时,该page frame可被free掉;如果不为0,说明该page正在被某个进程或者内核使用,调用page_count()可获得count值
  • _mapcount表示该page frame被映射的个数,也就是多少个page table entry中含有这个page frame的PFN
  • lru是"least recently used"的缩写,根据page frame的活跃程度(使用频率),一个可回收的page frame要么挂在active_list双向链表上,要么挂在inactive_list双向链表上,以作为页面回收的选择依据,lru中包含的就是指向所在链表中前后节点的指针
  • 如果一个page是属于某个文件的(也就是在page cache中),则mapping指向文件inode对应的address_space(这个结构体虽然叫address_space,但并不是进程地址空间里的那个address space),index表示该page在文件内的offset(以page size为单位)

Zone

因为硬件的限制,内核不能对所有的page frames采用同样的处理方法,因此它将属性相同的page frames归到一个zone中。

比如在i386中,一些使用DMA的设备只能访问0 ~ 16MB的物理空间,因此将0 ~ 16MB划分为了ZONE_DMA。ZONE_HIGHMEM则是适用于要访问的物理地址空间大于虚拟地址空间,不能建立直接映射的场景。除开这两个特殊的zone,物理内存中剩余的部分就是ZONE_NORMAL了。

ZONE_HIGHMEM也可能没有。比如在64位的x64中,因为内核虚拟地址空间足够大,不再需要ZONE_HIGH映射。为了区分使用32位地址的DMA应用和使用64位地址的DMA应用,64位系统中设置了ZONE_DMA32和ZONE_DMA。

所以,同样的ZONE_DMA,对于32位系统和64位系统表达的意义是不同的,ZONE_DMA32则只对64位系统有意义,对32位系统就等同于ZONE_DMA,没有单独存在的意义。

此外,还有防止内存碎片化的ZONE_MOVABLE和支持设备热插拔的ZONE_DEVICE。可通过“cat /proc/zoneinfo |grep Node”命令查看系统中包含的zones的种类。

Zone虽然是用于管理物理内存的,但zone与zone之间并没有任何的物理分割,它只是Linux为了便于管理进行的一种逻辑意义上的划分。Zone在Linux中用struct zone表示(以下为了讲解需要,调整了结构体中元素的顺序):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
struct zone {
spinlock_t lock;

unsigned long spanned_pages;
unsigned long present_pages;
unsigned long nr_reserved_highatomic;
atomic_long_t managed_pages;

struct free_area free_area[MAX_ORDER];
unsigned long _watermark[NR_WMARK];
long lowmem_reserve[MAX_NR_ZONES];
atomic_long_t vm_stat[NR_VM_ZONE_STAT_ITEMS];

unsigned long zone_start_pfn;
struct pglist_data *zone_pgdat;
struct page *zone_mem_map;
...
}
  • lock是用来防止并行访问struct zone的spin lock,它只能保护struct zone这个结构体哈,可不能保护整个zone里的所有pages
  • spanned_pages是这个zone含有的总的page frames数目。在某些体系结构(比如Sparc)中,zone中可能存在没有物理页面的"holes",spanned_pages减去这些holes里的absent pages就是present_pages
  • nr_reserved_highatomic是为某些场景预留的内存,managed_pages是由buddy内存分配系统管理的page frames数目,其实也就是present_pages减去reserved pages
  • free_area由free list空闲链表构成,表示zone中还有多少空余可供分配的page frames。_watermark有min(mininum), low, high三种,可作为启动内存回收的判断标准(详情请参考这篇文章)
  • lowmem_reserve是给更高位的zones预留的内存(更详细的介绍请参考这篇文章)。vm_stat作为zone的内存使用情况的统计信息,是“/proc/zoneinfo”的数据来源
  • zone_start_pfn是zone的起始物理页面号,zone_start_pfn+spanned_pages就是该zone的结束物理页面号。zone_pgdat是指向这个zone所属的node的。zone_mem_map指向由struct page构成的mem_map数组

因为内核对zone的访问是很频繁的,为了更好的利用硬件cache来提高访问速度,struct zone中还有一些填充位,用于帮助结构体元素的cache line对齐。这和struct page对内存精打细算的使用形成了鲜明的对比,因为zone的种类很有限,一个系统中一共也不会有多少个zones,struct zone这个结构体的体积大点也没有什么关系。

Node

介绍完了page和zone,沿着自底向上的顺序,最后就是表示node的结构体pglist_data了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
typedef struct pglist_data {
int nr_zones;
struct zone node_zones[MAX_NR_ZONES];
struct zonelist node_zonelists[MAX_ZONELIST];

unsigned long node_size;
struct page *node_mem_map;

int node_id;
unsigned long node_start_paddr;
struct pglist_data *node_next;

spinlock_t lru_lock;
...
} pg_data_t;
  • nr_zones表示这个node含有多少个zones,node_zones[]则是一个包含各个zone结构体的数组
  • node_zonelists[]包含了2个zonelist,一个是由本node的zones组成,另一个是由从本node分配不到内存时可选的备用zones组成,相当于是选择了一个退路,所以叫fallback
1
2
3
4
5
6
7
enum {
ZONELIST_FALLBACK, /* zonelist with fallback */
#ifdef CONFIG_NUMA
ZONELIST_NOFALLBACK, /* zonelist without fallback (__GFP_THISNODE) */
#endif
MAX_ZONELISTS
};

如果能从指定的目标node获得内存,则为NUMA hit;只能从备选node中获取,则为NUMA miss,可通过"/sys/devices/system/node/node*/numastat"查看。

  • node_size是指这个node含有多少个page frames,node_mem_map指向node中所有struct page构成的mem_map数组
  • node_id是这个node的逻辑ID,也就是在NUMA系统中的编号。现在Linux中的内存分配函数区分不同的node,靠的就是这个node_id,类似于文件描述符fd
  • node_start_paddr(在2.6内核中被换成了node_start_pfn)是该node的起始物理地址。node_next指向由多个node构成的NUMA单向链表pgdat_list中的下一个节点。如果是UMA系统,只有一个node,则node_start_pfn为0,node_next为NULL

你看,表示node的结构体pglist_data通过"node_zones"包含了它的下一级,也就是表示zone的结构体zone_struct,zone_struct又通过"zone_pgdat"指向包含它的node。zone_struct中"zone_mem_map"指向它的下一级,也就是表示page frame的struct page,按理struct page中也应该有一个元素是指向包含它的zone,可是好像没看到对不对?

并不是在那个省略号里,而是……就在struct page的"flags"里,它用flags的高8位存储了它所属的zone和node。这是通过page flags找到page所属的node的方法:

1
2
3
4
5
static inline int page_to_nid(const struct page *page)
{
struct page *p = (struct page *)page;
return (PF_POISONED_CHECK(p)->flags >> NODES_PGSHIFT) & NODES_MASK;
}

这是通过page flags找到page所属的zone的:

1
2
3
4
5
6
7
8
9
static inline enum zone_type page_zonenum(const struct page *page)
{
return (page->flags >> ZONES_PGSHIFT) & ZONES_MASK;
}

static inline struct zone *page_zone(const struct page *page)
{
return &NODE_DATA(page_to_nid(page))->node_zones[page_zonenum(page)];
}

事实上,page flags完整的组织是这样的:

只有低位的bits才是上文提到的那些表示page frame属性和状态的标志位。在32位系统中,只有32个bits的flags的资源已经非常紧张了。然而,为了避免在struct page中单独开辟内存空间引发反对的声浪,ZONE, NODE和SECTION这三个家伙还硬挤进了flags里,占去flags那么多宝贵的bits,以至于现在要再添加一个标志位都变得极其困难。

参考文献

https://zhuanlan.zhihu.com/p/68465952
https://zhuanlan.zhihu.com/p/68473428
《奔跑吧Linux内核》