物理内存组织与架构

源码均参考5.17版本的内核

NUMA与UMA

共享存储型多处理机有两种模型

  • 均匀存储器存取(Uniform-Memory-Access,简称UMA)模型
  • 非均匀存储器存取(Nonuniform-Memory-Access,简称NUMA)模型

UMA模型

物理存储器被所有处理机均匀共享。所有处理机对所有存储字具有相同的存取时间,这就是为什么称它为均匀存储器存取的原因。每台处理机可以有私用高速缓存,外围设备也以一定形式共享。

NUMA模型

NUMA模式下,处理器被划分成多个”节点”(node), 每个节点被分配有的本地存储器空间。 所有节点中的处理器都可以访问全部的系统物理存储器,但是访问本节点内的存储器所需要的时间,比访问某些远程节点内的存储器所花的时间要少得多

造成差异的原因在于内存的组织和接口分布在不同位置

img

Linux 物理内存组织结构

Linux把物理内存划分为三个层次来管理:

    • 存储节点(Node):是每个CPU对应的一个本地内存,在内核中表示为pg_*data_*t的实例。因为CPU被划分为多个节点,内存被划分为簇,每个CPU都对应一个本地物理内存,即一个CPU Node对应一个内存簇bank,即每个内存簇被认为是一个存储节点。在UMA结构下,只存在一个存储节点。
    • 内存域(Zone):每个物理内存节点Node被划分为多个内存域, 用于表示不同范围的内存,内核可以使用不同的映射方式映射物理内存。
    • 页面(Page):各个内存域都关联一个数组,用来组织属于该内存域的物理内存页(页帧)。页面是最基本的页面分配的单位。

img

内核对UMA和NUMA 使用相同的数据结构,因此对不同形式的内存布局,各个算法没什么区别,在UMA系统上,相当于只有一个NUMA节点(只需一个pg_data_t结构体来描述),内存管理的其他代码都将内存统一当成NUMA系统的特例看待。

NODE

在分配一个页面时, Linux采用节点局部分配的策略, 从最靠近运行中的CPU的节点分配内存, 由于进程往往是在同一个CPU上运行, 因此从当前节点得到的内存很可能被用到

一个节点使用 pglist_data 结构进行描述,该结构定义于 /include/linux/mmzone.h 中,在linux系统中可以用numactl命令来查看系统node信息(默认不安装)

sudo apt install numactl安装即可

image-20221103171412918

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
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
typedef struct pglist_data {
/*
比较重要,node_zones 仅包含此节点的区域。并非所有区域都可能被填充,但它是完整列表。它被该节点的 node_zonelists 以及其他节点的 node_zonelists 引用。
*/
struct zone node_zones[MAX_NR_ZONES];

/*
node_zonelists 包含对所有节点中所有区域的引用。
通常,第一个区域将引用此节点的 node_zones。
此数组记录了分配内存时搜索备用内存域的顺序,在本节点分配不成功,可以沿着此数组搜索到其他节点的zone
*/
struct zonelist node_zonelists[MAX_ZONELISTS];

int nr_zones; /* node 中 zone 的数量*/
#ifdef CONFIG_FLATMEM /* means !SPARSEMEM */
struct page *node_mem_map; // 指向本节点的page数组mem_map
#ifdef CONFIG_PAGE_EXTENSION
struct page_ext *node_page_ext;
#endif
#endif
#if defined(CONFIG_MEMORY_HOTPLUG) || defined(CONFIG_DEFERRED_STRUCT_PAGE_INIT)
/*
* 若你期望 node_start_pfn, node_present_pages,
* node_spanned_pages 或 nr_zones 保持不变,
* 必须在任何时刻持有(这个锁)。
* 同时在 deferred page 初始化期间对 pgdat->first_deferred_pfn 进行同步。
*
* (内核)提供了 pgdat_resize_lock() 与 pgdat_resize_unlock()
* 以在没有对 CONFIG_MEMORY_HOTPLUG 或 CONFIG_DEFERRED_STRUCT_PAGE_INIT
* 进行检查的情况下操纵 node_size_lock
*
* 基于 zone->lock 与 zone->span_seqlock
*/
spinlock_t node_size_lock;
#endif
unsigned long node_start_pfn; //node 的起始页框标号
unsigned long node_present_pages; /* node 中物理页的总数量 */
unsigned long node_spanned_pages; /* node 中物理页的总大小 */

int node_id; //node 的标号
wait_queue_head_t kswapd_wait;
wait_queue_head_t pfmemalloc_wait;

wait_queue_head_t reclaim_wait[NR_VMSCAN_THROTTLE];

atomic_t nr_writeback_throttled;/* nr of writeback-throttled tasks */
unsigned long nr_reclaim_start; /* nr pages written while throttled
* when throttling started. */

struct task_struct *kswapd; /* Protected by
mem_hotplug_begin/end() */
int kswapd_order;
enum zone_type kswapd_highest_zoneidx;

int kswapd_failures; /* Number of 'reclaimed == 0' runs */

#ifdef CONFIG_COMPACTION
int kcompactd_max_order;
enum zone_type kcompactd_highest_zoneidx;
wait_queue_head_t kcompactd_wait;
struct task_struct *kcompactd;
bool proactive_compact_trigger;
#endif
/*
这是对用户空间分配不可用的页面的每个节点保留。
*/
unsigned long totalreserve_pages;

#ifdef CONFIG_NUMA
/*
如果存在更多未映射的页面,则节点回收将变为活动状态。
*/
unsigned long min_unmapped_pages;
unsigned long min_slab_pages;
#endif /* CONFIG_NUMA */

/* 页面回收使用的写入密集型字段 */
ZONE_PADDING(_pad1_)

spinlock_t lru_lock; //LRU(最近最少使用算法)的自旋锁

#ifdef CONFIG_DEFERRED_STRUCT_PAGE_INIT
/*
如果大型机器上的内存初始化被推迟,那么这是第一个需要初始化的 PFN。
*/
unsigned long first_deferred_pfn;
#endif /* CONFIG_DEFERRED_STRUCT_PAGE_INIT */

#ifdef CONFIG_TRANSPARENT_HUGEPAGE
struct deferred_split deferred_split_queue;
#endif

/* 页面回收扫描器通常访问的字段 */

/*
* 注意:如果启用了 MEMCG,则此选项未使用。
*
* 使用 mem_cgroup_lruvec() 查找 lruvecs。
*/
struct lruvec __lruvec;

unsigned long flags;

ZONE_PADDING(_pad2_)

/* 每个节点的 vmstats */
struct per_cpu_nodestat __percpu *per_cpu_nodestats;
atomic_long_t vm_stat[NR_VM_NODE_STAT_ITEMS];
} pg_data_t;

  • 首先, 内存被划分为结点. 每个节点关联到系统中的一个处理器, 内核中表示为pg_data_t的实例. 系统中每个节点被链接到一个以NULL结尾的pgdat_list链表中<而其中的每个节点利用pg_data_tnode_next字段链接到下一节.而对于PC这种UMA结构的机器来说, 只使用了一个成为contig_page_data的静态pg_data_t结构.

  • 接着各个节点又被划分为内存管理区域, 一个管理区域通过struct zone_struct描述, 其被定义为zone_t, 用以表示内存的某个范围, 低端范围的16MB被描述为ZONE_DMA, 某些工业标准体系结构中的(ISA)设备需要用到它, 然后是可直接映射到内核的普通内存域ZONE_NORMAL,最后是超出了内核段的物理地址域ZONE_HIGHMEM, 被称为高端内存. 是系统中预留的可用内存空间, 不能被内核直接映射.

ZONE

为什么node要分为多个zone?

NUMA结构下, 每个处理器CPU与一个本地内存直接相连, 而不同处理器之前则通过总线进行进一步的连接, 因此相对于任何一个CPU访问本地内存的速度比访问远程内存的速度要快, 而Linux为了兼容NUMAJ结构, 把物理内存相依照CPU的不同node分成簇, 一个CPU-node对应一个本地内存pgdata_t.

这样已经很好的表示物理内存了, 在一个理想的计算机系统中, 一个页框就是一个内存的分配单元, 可用于任何事情:存放内核数据, 用户数据和缓冲磁盘数据等等. 任何种类的数据页都可以存放在任页框中, 没有任何限制.

但是Linux内核又把各个物理内存节点分成个不同的管理区域zone, 这是为什么呢?

因为实际的计算机体系结构有硬件的诸多限制, 这限制了页框可以使用的方式. 尤其是, Linux内核必须处理80x86体系结构的两种硬件约束.

  • ISA总线的直接内存存储DMA处理器有一个严格的限制 : 他们只能对RAM的前16MB进行寻址
  • 在具有大容量RAM的现代32位计算机中, CPU不能直接访问所有的物理地址, 因为线性地址空间太小, 内核不可能直接映射所有物理内存到线性地址空间, 我们会在后面典型架构(x86)上内存区域划分详细讲解x86_32上的内存区域划分

因此Linux内核对不同区域的内存需要采用不同的管理方式和映射方式, 因此内核将物理地址或者成用zone_t表示的不同地址区域

https://www.cnblogs.com/still-smile/p/11564598.html

对于x86-64架构或MIPS架构,除硬件外设访问的物理区间上的内存域为ZONE_DMA除外,其余都为ZONE_NORMAL类型,每个内存域内部则记录了所覆盖的页帧情况并用buddy system 来管理本内存域内部的空闲页帧,可以通过cat /proc/zoneinfo 命令查看系统的zone相关信息

image-20221103171506734

zone域用zone结构体描述,该结构体定义于 /include/linux/mmzone.h 中,如下:

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
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
struct zone {
/* Read-mostly fields */

/* zone watermarks, access with *_wmark_pages(zone) macros */
unsigned long _watermark[NR_WMARK]; //内存域的三个水位线值:高水线(代表充足),低水线,最低水线
unsigned long watermark_boost;

unsigned long nr_reserved_highatomic;

/*
我们不知道我们要分配的内存是否会被释放或/并且最终会被释放,所以为了避免完全浪费几个 GB 的内存,我们必须保留一些较低的区域
内存(否则我们冒着在较低区域运行 OOM 的风险,尽管较高区域有大量可自由使用的 ram)。如果 sysctl_lowmem_reserve_ratio sysctl 发生变化,我会在运行时重新计算这个数组。
*/
long lowmem_reserve[MAX_NR_ZONES]; //page管理的数据结构对象,内部有一个page的列表(list)来管理。每个CPU维护一个page list,避免自旋锁的冲突。这个数组的大小和NR_CPUS(CPU的数量)有关,这个值是编译的时候确定的

#ifdef CONFIG_NUMA
int node; //所属节点号
#endif
struct pglist_data *zone_pgdat; //zone_pgdat 指向 " 内存节点 " 的 pglist_data 实例
struct per_cpu_pageset __percpu *pageset; // pageset 表示 每个 " 处理页 " 的集合 ;
struct per_cpu_zonestat __percpu *per_cpu_zonestats; // 将高值和批处理值复制到各个页面集以加快访问速度

int pageset_high;
int pageset_batch;

#ifndef CONFIG_SPARSEMEM
/*
* Flags for a pageblock_nr_pages block. See pageblock-flags.h.
* In SPARSEMEM, this map is stored in struct mem_section
*/
unsigned long *pageblock_flags; // pageblock_nr_pages 块的标志
#endif /* CONFIG_SPARSEMEM */

/* zone_start_pfn == zone_start_paddr >> PAGE_SHIFT */
unsigned long zone_start_pfn; //当前zone起始的物理页面号。而通过zone_start_pfn+spanned_pages可获得该zone的结束物理页面号。


atomic_long_t managed_pages;

/*
managed_pages是buddy系统管理的页面数量,计算公式为(reserved_pages包括bootmem分配器分配的pages):managed_pages = present_pages - reserved_pages;
*/

unsigned long spanned_pages;
/*
* spanned_pages 为 zone 跨越的总页数,包括空洞,计算公式为 spanned_pages = zone_end_pfn - zone_start_pfn;
*/


unsigned long present_pages;
/*
present_pages 为当前的 " 内存区域 " 包含的 物理页 个数 , 不包含 " 内存空洞
计算公式为 present_pages = spanned_pages-absent_pages(pages in hole);
present_pages可能会被内存热插拔或内存电源使用,内存热插拔或内存电源管理逻辑可以使用 present_pages 通过检查 (present_pages - managed_pages) 找出未被管理的页面。页面分配器和vm扫描器应该使用 managed_pages 来计算各种水位线和阈值。
*/

/* 锁规则:
*
* zone_start_pfn 与 spanned_pages 由 span_seqlock 保护.
* 这是一个顺序锁(seqlock,写优先锁)因为他得在 zone->lock 之外被读取,
* 在主分配器路径中完成.
* 但他确实不经常被写入。
*
* span_seq lock 随着 zone->lock 被定义,因为相较于 zone->lock,
* 他经常被读取. 让他们有个机会在同一条缓存线(cacheline)上一件好事
*
* 运行时 present_pages 应当由 mem_hotplug_begin/end() 进行保护.
* 任何无法忍受 present_pages 的应当使用 get_online_mems()来获得固定的值.
*/
#if defined(CONFIG_MEMORY_HOTPLUG)
unsigned long present_early_pages;
#ifdef CONFIG_CMA
unsigned long cma_pages;
#endif
const char *name; //name 表示 " 内存区域 " 名称 ;

#ifdef CONFIG_MEMORY_ISOLATION
/*
* 独立的 pageblock 的数量. 用以解决由于对 pagelock
* 的 migratetype 的竞态检索导致的对 freepage 的错误计数.
* 由 zone->lock 保护
*/
unsigned long nr_isolate_pageblock;
#endif

#ifdef CONFIG_MEMORY_HOTPLUG
/* 参见 spanned/present_pages 以获得更多描述 */
seqlock_t span_seqlock;
#endif

int initialized;

/* 页面分配器使用的写入密集型字段 */
ZONE_PADDING(_pad1_) // 由于自旋锁频繁的被使用,因此为了性能上的考虑,将某些成员对齐到cache line中,有助于提高执行的性能。使用这个宏,可以确定zone->lock,zone->lru_lock,zone->pageset这些成员使用不同的cache line.

/* 不同size的闲置区域 */
struct free_area free_area[MAX_ORDER];

/* zone标志位 */
unsigned long flags;


spinlock_t lock; //对zone并发访问的保护的自旋锁,主要保护 free_area

/* ompaction 和 vmstats 使用的写入密集型字段 */
ZONE_PADDING(_pad2_)

/*
* 当闲置页在这一点下时, 在读取闲置页数量时会采取额外的步骤
* 以避免 per-cpu 计数器
* 漂移导致水位线被突破
*/
unsigned long percpu_drift_mark;

#if defined CONFIG_COMPACTION || defined CONFIG_CMA
/* pfn where compaction free scanner should start */
unsigned long compact_cached_free_pfn;
/* pfn where async and sync compaction migration scanner should start */
unsigned long compact_cached_migrate_pfn[2];
unsigned long compact_init_migrate_pfn;
unsigned long compact_init_free_pfn;
#endif

#ifdef CONFIG_COMPACTION
/*
在压缩失败时,在重试之前跳过 1<<compact_defer_shift 压缩。使用 compact_considered 跟踪自上次失败以来尝试的次数。
*/
unsigned int compact_considered;
unsigned int compact_defer_shift;
int compact_order_failed;
#endif

#if defined CONFIG_COMPACTION || defined CONFIG_CMA
/* 当应该清除 PG_migrate_skip 位时设置为 true */
bool compact_blockskip_flush;
#endif

bool contiguous;

ZONE_PADDING(_pad3_)
/* 区域统计 */
atomic_long_t vm_stat[NR_VM_ZONE_STAT_ITEMS];
atomic_long_t vm_numa_stat[NR_VM_NUMA_STAT_ITEMS];
} ____cacheline_internodealigned_in_smp;
  • 由于多cpu多核的发展,当多个cpu需对一个zone操作时,容易造成条件竞争,频繁加解锁操作又过于消耗时间,故引入了per_cpu_pages结构,为每个cpu都准备一个单独的页面仓库

  • ```c
    struct per_cpu_pages {
    int count; /* 链表所包含的页数目 /
    int high; /
    高水位线 /
    int batch; /
    chunk size for buddy add/remove /
    short free_factor; /
    batch scaling factor during free /
    #ifdef CONFIG_NUMA
    short expire; /
    When 0, remote pagesets are drained */
    #endif

    /* Lists of pages, one per migrate type stored on the pcp-lists */
    struct list_head lists[NR_PCP_LISTS];
    

    };

    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

    - > 参考:https://arttnba3.cn/2021/11/28/OS-0X02-LINUX-KERNEL-MEMORY-5.11-PART-I/#0x01-struct-page%EF%BC%9A%E9%A1%B5
    >
    > 该结构体会被存放在每个 CPU 自己独立的 `.data..percpu` 段中,以 CPU0 为例,结构如下图所示
    >
    > [![自己画的图.png](https://blogimg-1314041910.cos.ap-guangzhou.myqcloud.com/1dCZA3IDpUK2xYg.png)](https://i.loli.net/2021/12/03/1dCZA3IDpUK2xYg.png)

    - 所有空闲页帧由buddy system 通过free_area[MAX_ORDER] 来管理,并按照连续的空闲页面区间大小组织成多个队列。





    > 关于zone_padding, 请看这位博主的文章https://www.cnblogs.com/still-smile/p/11564598.html
    >
    > 以下是摘抄:
    >
    > 该结构比较特殊的地方是它由ZONE_PADDING分隔的几个部分. 这是因为堆zone结构的访问非常频繁. 在多处理器系统中, 通常会有不同的CPU试图同时访问结构成员. 因此使用锁可以防止他们彼此干扰, 避免错误和不一致的问题. 由于内核堆该结构的访问非常频繁, 因此会经常性地获取该结构的两个自旋锁zone->lock和zone->lru_lock
    >
    > > 由于 `struct zone` 结构经常被访问到, 因此这个数据结构要求以 `L1 Cache` 对齐. 另外, 这里的 `ZONE_PADDING( )` 让 `zone->lock` 和 `zone_lru_lock` 这两个很热门的锁可以分布在不同的 `Cahe Line` 中. 一个内存 `node` 节点最多也就几个 `zone`, 因此 `zone` 数据结构不需要像 `struct page` 一样关心数据结构的大小, 因此这里的 `ZONE_PADDING( )` 可以理解为用空间换取时间(性能). 在内存管理开发过程中, 内核开发者逐渐发现有一些自选锁竞争会非常厉害, 很难获取. 像 `zone->lock` 和 `zone->lru_lock` 这两个锁有时需要同时获取锁. 因此保证他们使用不同的 `Cache Line` 是内核常用的一种优化技巧.
    >
    > 那么数据保存在CPU高速缓存中, 那么会处理得更快速. 高速缓冲分为行, 每一行负责不同的内存区. 内核使用ZONE_PADDING宏生成"填充"字段添加到结构中, 以确保每个自旋锁处于自身的缓存行中
    >
    > ZONE_PADDING宏定义在[nclude/linux/mmzone.h?v4.7, line 105](http://lxr.free-electrons.com/source/include/linux/mmzone.h?v4.7#L105)
    >
    >
    >
    > ```c
    > /*
    > * zone->lock and zone->lru_lock are two of the hottest locks in the kernel.
    > * So add a wild amount of padding here to ensure that they fall into separate
    > * cachelines. There are very few zone structures in the machine, so space
    > * consumption is not a concern here.
    > */
    > #if defined(CONFIG_SMP)
    > struct zone_padding
    > {
    > char x[0];
    > } ____cacheline_internodealigned_in_smp;
    > #define ZONE_PADDING(name) struct zone_padding name;
    >
    > #else
    > #define ZONE_PADDING(name)
    > #endif

内核还用了____cacheline_internodealigned_in_smp,来实现最优的高速缓存行对其方式.

该宏定义在include/linux/cache.h

1
2
3
4
5
6
7
8
#if !defined(____cacheline_internodealigned_in_smp)
#if defined(CONFIG_SMP)
#define ____cacheline_internodealigned_in_smp \
__attribute__((__aligned__(1 << (INTERNODE_CACHE_SHIFT))))
#else
#define ____cacheline_internodealigned_in_smp
#endifc
#endif

zone flags: 定义于 include/linux/mmzone.h

不知道为啥只有两个…

1
2
3
4
5
6
enum zone_flags {
ZONE_BOOSTED_WATERMARK, /* zone recently boosted watermarks.
* Cleared when kswapd is woken.
*/
ZONE_RECLAIM_ACTIVE, /* kswapd may be scanning the zone. */
};

在一些低的版本是这样

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
enum zone_flags
{
ZONE_RECLAIM_LOCKED, /* prevents concurrent reclaim */
ZONE_OOM_LOCKED, /* zone is in OOM killer zonelist 内存域可被回收*/
ZONE_CONGESTED, /* zone has many dirty pages backed by
* a congested BDI
*/
ZONE_DIRTY, /* reclaim scanning has recently found
* many dirty file pages at the tail
* of the LRU.
*/
ZONE_WRITEBACK, /* reclaim scanning has recently found
* many pages under writeback
*/
ZONE_FAIR_DEPLETED, /* fair zone policy batch depleted */
};
flag标识 描述
ZONE_RECLAIM_LOCKED 防止并发回收, 在SMP上系统, 多个CPU可能试图并发的回收亿i个内存域. ZONE_RECLAIM_LCOKED标志可防止这种情况: 如果一个CPU在回收某个内存域, 则设置该标识. 这防止了其他CPU的尝试
ZONE_OOM_LOCKED 用于某种不走运的情况: 如果进程消耗了大量的内存, 致使必要的操作都无法完成, 那么内核会使徒杀死消耗内存最多的进程, 以获取更多的空闲页, 该标志可以放置多个CPU同时进行这种操作
ZONE_CONGESTED 标识当前区域中有很多脏页
ZONE_DIRTY 用于标识最近的一次页面扫描中, LRU算法发现了很多脏的页面
ZONE_WRITEBACK 最近的回收扫描发现有很多页在写回
ZONE_FAIR_DEPLETED 公平区策略耗尽(没懂)

type:定义于 include/linux/mmzone.h

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
enum zone_type {
/*
* ZONE_DMA and ZONE_DMA32 are used when there are peripherals not able
* to DMA to all of the addressable memory (ZONE_NORMAL).
* On architectures where this area covers the whole 32 bit address
* space ZONE_DMA32 is used. ZONE_DMA is left for the ones with smaller
* DMA addressing constraints. This distinction is important as a 32bit
* DMA mask is assumed when ZONE_DMA32 is defined. Some 64-bit
* platforms may need both zones as they support peripherals with
* different DMA addressing limitations.
*
* Some examples:
*
* - i386 and x86_64 have a fixed 16M ZONE_DMA and ZONE_DMA32 for the
* rest of the lower 4G.
*
* - arm only uses ZONE_DMA, the size, up to 4G, may vary depending on
* the specific device.
*
* - arm64 has a fixed 1G ZONE_DMA and ZONE_DMA32 for the rest of the
* lower 4G.
*
* - powerpc only uses ZONE_DMA, the size, up to 2G, may vary
* depending on the specific device.
*
* - s390 uses ZONE_DMA fixed to the lower 2G.
*
* - ia64 and riscv only use ZONE_DMA32.
*
* - parisc uses neither.
*/
#ifdef CONFIG_ZONE_DMA
ZONE_DMA,
#endif
#ifdef CONFIG_ZONE_DMA32
ZONE_DMA32,
#endif
/*
* Normal addressable memory is in ZONE_NORMAL. DMA operations can be
* performed on pages in ZONE_NORMAL if the DMA devices support
* transfers to all addressable memory.
*/
ZONE_NORMAL,
#ifdef CONFIG_HIGHMEM
/*
* A memory area that is only addressable by the kernel through
* mapping portions into its own address space. This is for example
* used by i386 to allow the kernel to address the memory beyond
* 900MB. The kernel will set up special mappings (page
* table entries on i386) for each page that the kernel needs to
* access.
*/
ZONE_HIGHMEM,
#endif
ZONE_MOVABLE,
#ifdef CONFIG_ZONE_DEVICE
ZONE_DEVICE,
#endif
__MAX_NR_ZONES

};

管理内存域 描述
ZONE_DMA 标记了适合DMA的内存域. 该区域的长度依赖于处理器类型. 这是由于古老的ISA设备强加的边界. 但是为了兼容性, 现代的计算机也可能受此影响
ZONE_DMA32 标记了使用32位地址字可寻址, 适合DMA的内存域. 显然, 只有在53位系统中ZONE_DMA32才和ZONE_DMA有区别, 在32位系统中, 本区域是空的, 即长度为0MB, 在Alpha和AMD64系统上, 该内存的长度可能是从0到4GB
ZONE_NORMAL 标记了可直接映射到内存段的普通内存域. 这是在所有体系结构上保证会存在的唯一内存区域, 但无法保证该地址范围对应了实际的物理地址. 例如, 如果AMD64系统只有两2G内存, 那么所有的内存都属于ZONE_DMA32范围, 而ZONE_NORMAL则为空
ZONE_HIGHMEM 标记了超出内核虚拟地址空间的物理内存段, 因此这段地址不能被内核直接映射
ZONE_MOVABLE 内核定义了一个伪内存域ZONE_MOVABLE, 在防止物理内存碎片的机制memory migration中需要使用该内存域. 供防止物理内存碎片的极致使用
ZONE_DEVICE 为支持热插拔设备而分配的Non Volatile Memory非易失性内存
MAX_NR_ZONES 充当结束标记, 在内核中想要迭代系统中所有内存域, 会用到该常亮

PAGE

分析了节点和内存域后,讨论他们的基本元素-page,每个物理页帧有一个page结构体描述,为了节省内存空间,其定义中使用了大量的联合体。所有的page构成一个全局数组并由node和zone管理,zone中的空闲页帧形成了buddy system。而当页帧用于小数据对象时,由slab/slub 系统所管理,用于文件页缓存时由address_space管理。

必须要理解的是,page结构于物理页相关,而并非与虚拟页相关,故该结构体对页的描述只是短暂的,内核仅仅用这个数据结构来描述当前时刻在相关物理页中所存放的东西,这个数据结构的目的在于描述物理内存本身,而不是描述包含在其中的数据。

page结构体定义于/include/linux/mm_types.h ,如下:

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
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
struct page {
unsigned long flags; /* Atomic flags, some possibly
* updated asynchronously */
/*
* 此联合中有五个字(20/40 字节)。
* 警告:第一个字的位 0 用于 PageTail()。这意味着该联合的其他用户不得使用该位来避免冲突和误报 PageTail()。
*/
union {
struct { /* 页面缓存和匿名页面 */
/**
* @lru:页面输出列表,例如active_list 受 pgdat->lru_lock 保护。
* 有时被页面所有者用作通用列表。
*/
struct list_head lru;
/* PAGE_MAPPING_FLAGS 见 page-flags.h */
struct address_space *mapping;
pgoff_t index; /* 在映射的虚拟空间(vma_area)内的偏移 */
/**
* @private:映射私有不透明数据。
* 如果 PagePrivate,通常用于 buffer_heads。
* 如果 PageSwapCache 用于 swp_entry_t。
* 如果是PageBuddy,则表示buddy system中的顺序。
*/
unsigned long private;
};
struct { /* page_pool used by netstack */
/**
@pp_magic: magic value 避免回收非 page_pool 分配的页面。
*/
unsigned long pp_magic;
struct page_pool *pp;
unsigned long _pp_mapping_pad;
unsigned long dma_addr;
union {
/**
dma_addr_upper:在 32 位架构上可能需要 64 位值。
*/
unsigned long dma_addr_upper;
/**
对于片段页面支持,在具有 64 位 DMA 的 32 位架构中不支持。
*/
atomic_long_t pp_frag_count;
};
};
struct { /* 复合页(多个物理连续页框视作一个大页)的尾页 */
unsigned long compound_head; /* Bit zero is set */

/* 只有第一个尾页 */
unsigned char compound_dtor;
unsigned char compound_order;
atomic_t compound_mapcount;
unsigned int compound_nr; /* 1 << compound_order */
};
struct { /* 复合页的第二个尾页 */
unsigned long _compound_pad_1; /* compound_head */
atomic_t hpage_pinned_refcount;
/* For both global and memcg */
struct list_head deferred_list;
};
struct { /* 页表页面 */
unsigned long _pt_pad_1; /* compound_head */
pgtable_t pmd_huge_pte; /* protected by page->ptl */
unsigned long _pt_pad_2; /* mapping */
union {
struct mm_struct *pt_mm; /* x86 pgds only */
atomic_t pt_frag_refcount; /* powerpc */
};
#if ALLOC_SPLIT_PTLOCKS
spinlock_t *ptl;
#else
spinlock_t ptl;
#endif
};
struct { /* ZONE_DEVICE 页面 */
/** @pgmap: 指向宿主设备的页面映射。 */
struct dev_pagemap *pgmap;
void *zone_device_data;
/*
* ZONE_DEVICE 私有页面被计为被映射,因此接下来的 3 个字包含来自源匿名或页面缓存页面的映射、索引和私有字段,同时页面被迁移到设备私有内存。
* ZONE_DEVICE MEMORY_DEVICE_FS_DAX 页面在映射 pmem 支持的 DAX 文件时也使用映射、索引和私有字段。
*/
};

/** @rcu_head: 你可以通过该成员以通过 RCU 释放内存页 */
struct rcu_head rcu_head;
};

union { /* 这个联合体占用四个字节 */
/*
若是这个页被映射到用户空间, 记录该页被页表引用的次数
*/
atomic_t _mapcount;

/*
* 若是该页既不是 PageSlab 也没有被映射到用户空间,
* 则该值会帮助决定该页的作用。
* 该处的页面类型列表参见 page-flags.h
*/
unsigned int page_type;
};

/* 使用次数。 *不要直接使用*。见 page_ref.h */
atomic_t _refcount;

#ifdef CONFIG_MEMCG
unsigned long memcg_data;
#endif

/*
* 当机器上的所有内存都被映射到内核空间时,
* 我们可以简单地计算其虚拟地址。
* 在有着【高端内存(大于896MB)】的机器上,有的内存被动态地映射到内核
* 虚拟空间中,因此我们需要一个地方来存储这个地址
* 在 x86 机器上这个域可能占 16 bit 的空间 ... ;)
*
* 乘法计算较慢的架构可以在 asm/page.h 中定义 WANT_PAGE_VIRTUAL
*/
#if defined(WANT_PAGE_VIRTUAL)
void *virtual; /* 内核虚拟地址 */
#endif /* WANT_PAGE_VIRTUAL */

#ifdef LAST_CPUPID_NOT_IN_PAGE_FLAGS
int _last_cpupid;
#endif
} _struct_page_alignment;
  • 整页使用方式,该种情况也存在两种页,一种是直接映射虚拟地址空间的匿名页(Anonymous Page),另一种则是用于关联文件、然后再和虚拟地址空间建立映射的页,称之为内存映射文件(Memory-mapped File)。对于该种模式,会使用联合里的以下变量

  • struct address_space *mapping :用于内存映射,如果是匿名页,最低位为 1;如果是映射文件,最低位为 0

  • pgoff_t index :映射区的偏移量

  • atomic_t _mapcount:指向该页的页表数

  • struct list_head lru :表示这一页应该在一个链表上,例如这个页面被换出,就在换出页的链表中;

  • compound 相关的变量用于复合页(Compound Page),就是将物理上连续的两个或多个页看成一个独立的大页。

  • 小块内存使用方式。在很多情况下,我们只需要使用少量内存,因此采用了slab allocator技术用于分配小块内存slab。它的基本原理是从内存管理模块申请一整块页,然后划分成多个小块的存储池,用复杂的队列来维护这些小块的状态(状态包括:被分配了 / 被放回池子 / 应该被回收)。也正是因为 slab allocator 对于队列的维护过于复杂,后来就有了一种不使用队列的分配器 slub allocator,但是里面还是用了很多 带有slab的API ,因为它保留了 slab 的用户接口,可以看成 slab allocator 的另一种实现。该种模式会使用联合里的以下变量

  • s_mem :正在使用的 slab 的第一个对象

  • freelist :池子中的空闲对象

  • rcu_head :需要释放的列表

  • 小块内存分配器slob,非常简单,常用于小型嵌入式系统

    mapping指定了页帧所在的地址空间, index是页帧在映射内部的偏移量. 地址空间是一个非常一般的概念. 例如, 可以用在向内存读取文件时. 地址空间用于将文件的内容与装载数据的内存区关联起来. mapping不仅能够保存一个指针, 而且还能包含一些额外的信息, 用于判断页是否属于未关联到地址空间的某个匿名内存区.

    1. 如果mapping = 0,说明该page属于交换高速缓存页(swap cache);当需要使用地址空间时会指定交换分区的地址空间swapper_space。
    2. 如果mapping != 0,第0位bit[0] = 0,说明该page属于页缓存或文件映射,mapping指向文件的地址空间address_space。
    3. 如果mapping != 0,第0位bit[0] != 0,说明该page为匿名映射,mapping指向struct anon_vma对象。

    通过mapping恢复anon_vma的方法:anon_vma = (struct anon_vma *)(mapping - PAGE_MAPPING_ANON)。

    pgoff_t index是该页描述结构在地址空间radix树page_tree中的对象索引号即页号, 表示该页在vm_file中的偏移页数, 其类型pgoff_t被定义为unsigned long即一个机器字长.

pageflags 位于include/linux/page-flags.h

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
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
enum pageflags {
PG_locked, /* 该页已被上锁,说明此时该页正在被使用 */
PG_referenced, //该页刚刚被访问过;该标志位与 PG_reclaim 标志位共同被用于匿名与文件备份缓存的页面回收
PG_uptodate, //该页处在最新状态(up-to-date);当对该页完成一次读取时,该页便变更为 up-to-date 状态,除非发生了磁盘 IO 错误
PG_dirty, // 该页为脏页,即该页的内容已被修改,应当尽快将内容写回磁盘上
PG_lru, //该页处在一个 LRU 链表上
PG_active, //该页面位于活跃 lru 链表中
PG_workingset, //该页位于某个进程的 working set
PG_waiters, /* 有进程在等待该页面 */
PG_error, //该页在 I/O 过程中出现了差错
PG_slab, // 该页由 slab 使用
PG_owner_priv_1, /* 页由其所有者使用,若是作为 pagecache 页面,则可能是被文件系统使用*/
PG_arch_1, //该标志位与体系结构相关联
PG_reserved, // 该页被保留,不能够被 swap out(内核会将不活跃的页交换到磁盘上)
PG_private, /* 该页拥有私有数据a */
PG_private_2, /* 该页拥有私有数据 */
PG_writeback, /* 该页正在被写到磁盘上 */
PG_head, /* 该页是 compound pages 的第一个页 */
PG_mappedtodisk, /* 该页被映射到硬盘中 */
PG_reclaim, /* 该页可以被回收 */
PG_swapbacked, /* 该页的后备存储器为 swap/RAM */
PG_unevictable, /* PG_unevictable */
#ifdef CONFIG_MMU
PG_mlocked, /* 该页被对应的 vma 上锁(通常是系统调用 mlock) */
#endif
#ifdef CONFIG_ARCH_USES_PG_UNCACHED
PG_uncached, /* 该页被设置为不可缓存 */
#endif
#ifdef CONFIG_MEMORY_FAILURE
PG_hwpoison, /* 硬件相关的标志位 */
#endif
#if defined(CONFIG_PAGE_IDLE_FLAG) && defined(CONFIG_64BIT)
PG_young,
PG_idle,
#endif
#ifdef CONFIG_64BIT
PG_arch_2, //64位下的体系结构相关标志位
#endif
#ifdef CONFIG_KASAN_HW_TAGS
PG_skip_kasan_poison,
#endif
68


__NR_PAGEFLAGS,

PG_readahead = PG_reclaim,

/* Filesystems */
PG_checked = PG_owner_priv_1,

/* SwapBacked */
PG_swapcache = PG_owner_priv_1, /* Swap page: swp_entry_t in private */

/* Two page bits are conscripted by FS-Cache to maintain local caching
* state. These bits are set on pages belonging to the netfs's inodes
* when those inodes are being locally cached.
*/
PG_fscache = PG_private_2, /* page backed by cache */

/* XEN */
/* Pinned in Xen as a read-only pagetable page. */
PG_pinned = PG_owner_priv_1,
/* Pinned as part of domain save (see xen_mm_pin_all()). */
PG_savepinned = PG_dirty,
/* Has a grant mapping of another (foreign) domain's page. */
PG_foreign = PG_owner_priv_1,
/* Remapped by swiotlb-xen. */
PG_xen_remapped = PG_owner_priv_1,

/* SLOB */
PG_slob_free = PG_private,

/* Compound pages. Stored in first tail page's flags */
PG_double_map = PG_workingset,

#ifdef CONFIG_MEMORY_FAILURE
/*
* Compound pages. Stored in first tail page's flags.
* Indicates that at least one subpage is hwpoisoned in the
* THP.
*/
PG_has_hwpoisoned = PG_mappedtodisk,
#endif

/* non-lru isolated movable page */
PG_isolated = PG_reclaim,

/* Only valid for buddy pages. Used to track pages that are reported */
PG_reported = PG_uptodate,
};

Linux 三种内存模型

这里有一篇讲的很好的文章 : https://zhuanlan.zhihu.com/p/503695273

Linux 有三种内存模型,定义于 include/asm-generic/memory_model.h ,一个简单的概览:

image.png

这是早期的,现在有一个SPARSEMEM_VMEMMAP,没有Discontiguous,这玩意很快就被替代了 所以不讲了

2008年以后,SPARSEMEM_VMEMMAP 成为 x86-64 唯一支持的内存模型

为什么要讲到这个,因为page结构体的存储和内存模型十分相关,从源码中就能窥知一二

image-20221107160424880

内存模型在编译时就会被确定下来

从page结构体到物理页框

既然我们用page 结构体描述 物理页框,那我们怎么知道page结构体描述的是哪个物理页呢,我们通过page结构体转化成PFN 来实现。

PFN 即 page frame number 物理页框号,是针对物理内存而言的,将物理内存分成由每个page size页框构成的区域,并给每个page 编号,这个编号就是 PFN。假设物理内存从0地址开始,那么PFN等于0的那个页帧就是0地址(物理地址)开始的那个page。假设物理内存从x地址开始,那么第一个页帧号码就是(x>>PAGE_SHIFT)。

但是由于物理内存映射的关系,物理内存的0地址对应到到系统上不一定是物理地址的0,如果由物理内存基地址(取决于物理内存映射)的话,在系统中 pfn的值 应该等于 (physical address - memory base address) >> 12 。

FlATMEM:

平坦内存模型:由一个全局数组mem_map 存储 struct page,直接线性映射到实际的物理内存

img

page与pfn的转化:

此时pfn直接理解为数组索引值即可

1
2
3
#define __pfn_to_page(pfn)	(mem_map + ((pfn) - ARCH_PFN_OFFSET))
#define __page_to_pfn(page) ((unsigned long)((page) - mem_map) + \
ARCH_PFN_OFFSET)

ARCH_PFN_OFFSET 即上面所说的真实物理内存基地址

SPARSEMEM

离散内存模型

img

内存被分为一个个Section,每个Section包含一个sturct page 数组,这样每个数组就不用顺序存放了,结局了热插拔问题。而所有的section又由一个统一的mem_section 数组管理。

以x86为例,定义于 /arch/x86/include/asm/sparsemem.h

1
2
3
4
5
6
7
8
9
10
11
12
#ifdef CONFIG_X86_32
# ifdef CONFIG_X86_PAE
# define SECTION_SIZE_BITS 29
# define MAX_PHYSMEM_BITS 36
# else
# define SECTION_SIZE_BITS 26
# define MAX_PHYSMEM_BITS 32
# endif
#else /* CONFIG_X86_32 */
# define SECTION_SIZE_BITS 27 /* matt - 128 is convenient right now */
# define MAX_PHYSMEM_BITS (pgtable_l5_enabled() ? 52 : 46)
#endif

可以看到section最大值为128MB,简单计算一下(2 ^ (MAX_PHYSMEM_BITS - SECTION_SIZE_BITS))会发现如果这样的话mem_section的数组大小会非常大

解决方案:mem_section 也是可以动态分配的,给定CONFIG_SPARSEMEM_EXTREME 参数,就可以实现二级数组,整个mem_section都是动态分配的

1
2
3
4
5
6
7
8
9
10
11
12
#define __page_to_pfn(pg)					\
({ const struct page *__pg = (pg); \
int __sec = page_to_section(__pg); \
(unsigned long)(__pg - __section_mem_map_addr(__nr_to_section(__sec))); \
})

#define __pfn_to_page(pfn) \
({ unsigned long __pfn = (pfn); \
struct mem_section *__sec = __pfn_to_section(__pfn); \
__section_mem_map_addr(__sec) + __pfn; \
})
#endif /* CONFIG_FLATMEM/SPARSEMEM */

page_to_section 可以获得page所在section,page在该section的索引放在page.flags中

__nr_to_section(__sec),就是根据section_id 找到从mem_section数组中找到指定的section

__section_mem_map_addr返回mem_section.section_mem_map

mem_section.section_mem_map 存储的为 page 数组与 PFN 的差值

SPARSEMEM_VMEMMAP

SPARSEMEM有个缺点就是 page 结构体需要存储section id ,这会由不晓得内存耗费

偷点图

img

image.png

SPARSEMEM_VMEMMAP是虚拟映射,走页表

将所有的mem_section中page 都抽象到一个虚拟数组vmemmap,这样在进行struct page *和pfn转换时,之间使用vmemmap数组即可,如下转换(位于include\asm-generic\memory_model.h)

1
2
#define __pfn_to_page(pfn)	(vmemmap + (pfn))
#define __page_to_pfn(page) (unsigned long)((page) - vmemmap)

效率高多了

slab相关

在早期的版本中,在 page 结构体中专门有着一个匿名结构体用于存放与 slab 相关的成员

https://arttnba3.cn/2021/11/28/OS-0X02-LINUX-KERNEL-MEMORY-5.11-PART-I/#slab%E7%9B%B8%E5%85%B3%E7%BB%93%E6%9E%84%E4%BD%93

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
struct {    /* 供 slab, slob and slub 使用 */
union {
struct list_head slab_list;
struct { /* Partial pages */
struct page *next;
#ifdef CONFIG_64BIT
int pages; /* 剩余的页数量 */
int pobjects; /* 近似计数 */
#else
short int pages;
short int pobjects;
#endif
};
};
struct kmem_cache *slab_cache; /* 不在 slob 中使用 */
/* 两个 word 的范围 */
void *freelist; /* 第一个空闲对象 */
union {
void *s_mem; /* slab: first object */
unsigned long counters; /* SLUB */
struct { /* SLUB */
unsigned inuse:16;
unsigned objects:15;
unsigned frozen:1;
};
};
};

我阅读的5.17版本的源码中却没有,查看了commit,发现在一月的时候有了这样的改动

image-20221103201437873

1
2
3
4
mm: Remove slab from struct page
All members of struct slab can now be removed from struct page.
This shrinks the definition of struct page by 30 LOC, making
it easier to understand.

image-20221103201511781

相对的 ,slab分配器也有了它单独的结构体type

image-20221103201558075

具体的在 slab.h 中

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
struct slab {
unsigned long __page_flags;
union {
struct list_head slab_list;
struct { /* Partial pages */
struct slab *next;
#ifdef CONFIG_64BIT
int slabs; /* Nr of slabs left */
#else
short int slabs;
#endif
};
struct rcu_head rcu_head;
};
struct kmem_cache *slab_cache; /* not slob */
/* Double-word boundary */
void *freelist; /* first free object */
union {
void *s_mem; /* slab: first object */
unsigned long counters; /* SLUB */
struct { /* SLUB */
unsigned inuse:16;
unsigned objects:15;
unsigned frozen:1;
};
};

union {
unsigned int active; /* SLAB */
int units; /* SLOB */
};
atomic_t __page_refcount;
#ifdef CONFIG_MEMCG
unsigned long memcg_data;
#endif
};

也新增了不少slab操作相关的函数接口

注释也变成了

1
2
3
4
 SLUB uses cmpxchg_double() to atomically update its freelist and counters.That requires that freelist & counters in struct slab be adjacent and double-word aligned. Because struct slab currently just reinterprets the  bits of struct page, we align all struct pages to double-word boundaries,and ensure that 'freelist' is aligned within struct slab.


SLUB 使用 cmpxchg_double() 以原子方式更新其 freelist 和 counters。这要求 struct slab 中的 freelist 和 counters 相邻且双字对齐。 因为 struct slab 目前只是重新解释 struct page 的位,所以我们将所有 struct pages 对齐到双字边界,并确保 'freelist' 在 struct slab 内对齐。

LRU链表

LRU即Least Recently Used,对于整个内存回收而言,LRU是十分关键的数据机构,整个内存回收,实际上就是处理lru链表的收缩。

lru链表并非在系统中只有一个,而是每个zone有一个,每个memcg在每个zone上也有一个,结构为list_head 是内核,是内核通用链表构。

哪些页面可以被回收?

磁盘高速缓存的页面(包括文件映射的页面)都是可以被丢弃并回收的。但是如果页面是脏页面,则丢弃之前必须将其写回磁盘。

匿名映射的页面则都是不可以丢弃的,因为页面里面存有用户程序正在使用的数据,丢弃之后数据就没法还原了。相比之下,磁盘高速缓存页面中的数据本身是保存在磁盘上的,可以复现。

于是,要想回收匿名映射的页面,只好先把页面上的数据转储到磁盘,这就是页面交换(swap)。显然,页面交换的代价相对更高一些。匿名映射的页面可以被交换到磁盘上的交换文件或交换分区上(分区即是设备,设备即也是文件。所以下文统称为交换文件)。

于是,除非页面被保留或被上锁(页面标记PG_reserved/PG_locked被置位。某些情况下,内核需要暂时性地将页面保留,避免被回收),所有的磁盘高速缓存页面都可回收,所有的匿名映射页面都可交换。

image.png

图源:arttnba3’s blog

最后来一个总览图:

image-20221103191403671

参考资料

https://www.cnblogs.com/MrLiuZF/p/15251906.html

https://blog.csdn.net/shulianghan/article/details/124256224

https://www.cnblogs.com/still-smile/p/11564598.html

https://blog.csdn.net/yhb1047818384/article/details/111789736

https://zhuanlan.zhihu.com/p/464770819

https://biscuitos.github.io/blog/NODEMASK/

《Linux技术内幕》- 罗秋明

《Linux内核设计与实现》-Robert Love