linux 源码学习 - 内存管理

linux 版本为 v6.18

结构体定义

主要涉及 include/linux/mm_types.h,定义了内存管理中所需的基本数据结构

空间优化

在内核代码的结构体定义中,有一种广泛使用的设计是这样的:

1
2
3
4
5
6
7
8
9
struct Foo {
...
union {
struct {...};
struct {...};
...
};
...
};

也就是利用匿名结构体和匿名联合体的结合,这样写不仅可以利用顶层 struct 的变量直接访问底层变量,还能够充分发挥 union 的空间优势

以内存页元数据结构体为例,通常情况下物理页的大小是固定的 4KB,但同一个物理页可以有不同的用途,例如文件缓存、匿名内存(进程堆栈)、DMA 缓冲区、swap 缓存、网络栈页面池等,不同用途下页面所需要的元数据也是不同的,因此一种直接的思路是为每一种用途设计一个对应的结构体,例如 struct file_cache_pagestruct anomyous_page 等等

然而,在内核开发中,时间和空间复杂度的优先级是高于代码简洁性与易读性的,如果按照上面的思路来实现,那就无法用一个数组来存储所有页的元数据,从 pfn 映射到元数据的开销会有所上升;并且当有一个物理页的用途发生变化的时候,就需要有结构体的析构和构造,这种开销在内核中是无法接受的

linux 采用的这种实现实际上是用 C 语言实现了一种特殊的“多态”,对于内存空间进行了极致压缩,在同一块内存空间中存储逻辑上互斥访问的多条数据

page 和 folio

struct page 是 linux 内核最基础的内存描述单元,一个 page 对应一个物理页帧(大小通常为 4KB)

现如今,由于内存变得越来越大,使用 4KB 作为页大小很多时候会导致性能问题,例如内核或用户进程需要 2MB 的物理内存时,4KB 的页会导致至少要 512 次 TLB miss 和缺页异常才能够完全建立页表

为了解决这个问题,内核引入了 compound_page 设计,一个 compound_page 代表一组连续的物理页面,由一个 head_page 和若干 tail_page 组成,其元数据由 head_page 对应的 struct page 管理,具体的实现方法仍然是在 struct page 中添加 union,这就导致了 struct page 语义严重过载,在使用的时候往往要增加一系列繁杂的判断

因此,在 linux 5.16 中引入了 struct folio,它代表一个高阶连续物理内存单元(大小为 2n2^n 个页),将传统上分散在多个 page 结构体中的逻辑聚合为一个统一对象

下面来看二者的具体实现:

page 实现
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
struct page {
memdesc_flags_t flags;
/*
* WARNING: bit 0 of the first word is used for PageTail().
*/
union {
struct { /* Page cache and anonymous pages */
union {
struct list_head lru;
struct list_head buddy_list;
struct list_head pcp_list;
struct llist_node pcp_llist;
};
struct address_space *mapping;
...
};
struct { /* page_pool used by netstack */
...
};
struct { /* Tail pages of compound page */
unsigned long compound_head; /* Bit zero is set */
};
struct { /* ZONE_DEVICE pages */
...
};
/** @rcu_head: You can use this to free a page by RCU. */
struct rcu_head rcu_head;
};
union {
unsigned int page_type;
atomic_t _mapcount;
};

/* Usage count. *DO NOT USE DIRECTLY*. See page_ref.h */
atomic_t _refcount;
...
#if defined(WANT_PAGE_VIRTUAL)
void *virtual; /* Kernel virtual address (NULL if
not kmapped, ie. highmem) */
#endif
...
} _struct_page_alignment;

只保留了一些很基础的字段,例如第一个 union 中的若干个匿名 struct,实际上就表示了这一个 page 的不同用途,在不通过用途下按照不同的方式来解释数据,比如如果访问了 page.buddy_list,就表示了这一个 page 已经不再被使用,并且位于伙伴系统;而访问 page.lru 则代表它正在被使用,并且使用 LRU 算法决定要不要被置换;当访问 page.compound_head 的时候就说明这一页是隶属于某个复合页,可以通过这个变量访问对应的 head_page

mapping 这个字段可以用于区分页缓存和匿名页,这里 mapping 的最低位如果是 1 的话,就代表这是一个匿名页,mapping 指向的地址是一个 struct anon_vma,这个的具体原因后面 vma 部分会说。由于内核中的结构体至少需要按照字节对齐,因此结构体指针最后的几位 bit 通常情况下并未被使用,因此用来存储这个信息是安全的

结构体中有两个易混的变量,_mapcount_refcount,可以看出 _refcount 并不在 union 中,因此所有页面都有这个属性,它代表的是内核中有多少个地方正在引用这个物理页(例如一页刚被 alloc 的时候就会增加这个值);而 _mapcount 代表的是一个数据页被多少个进程映射了,非数据页(例如页表页、伙伴系统空闲页)则不需要这个字段,而是被 page_type 代替

这里埋个坑,这两个计数器的具体加减操作什么时候执行要等看到页面分配和回收之后才能知道了

最后是 virtual 字段,这个字段是为了解决 32 位机器上内核虚拟地址无法和物理地址一一映射的问题。在 32 位机器上,虚拟地址空间共有 4GB,通常会分配 1GB 给内核地址空间,因此如果物理内存大于 1GB,则无法在内核启动时就建立好一一映射的静态页表,因此 linux 将物理内存分为了两个部分,第一部分是低内存(通常 896MB),这部分物理地址和内核虚拟地址之间是线性映射的关系;第二部分则是高内存 highmem,当内核想要访问高内存的时候,就需要找到一个空闲的虚拟地址,并且建立该虚拟地址和要访问的物理页之间的临时映射,这个虚拟地址就存在 virtual 里面。由于 64 位机器的虚拟地址空间高达 16 EB,因此不需要考虑这个问题了

folio 实现
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
struct folio {
union {
struct {
memdesc_flags_t flags;
union {
struct list_head lru;
struct {
void *__filler;
unsigned int mlock_count;
};
struct dev_pagemap *pgmap;
};
struct address_space *mapping;
union {
pgoff_t index;
unsigned long share;
};
union {
void *private;
swp_entry_t swap;
};
atomic_t _mapcount;
atomic_t _refcount;
...
};
struct page page;
};
union {
struct {...};
struct page __page_1;
};
union {
struct {...};
struct page __page_2;
};
union {
struct {...};
struct page __page_3;
};
};

struct folio 是设计用于解决复合页和大页的开销,简单来看它的内存布局似乎是 4 个 struct page 的组合,并且在后续利用了大量的 assert 来保证内存结构的一致性,这样保证的是在出现 struct folio 的时候,它一定对应着 head page,因此可以显著减少分支判断,而内存布局的一致性则保证了可以将 struct page*struct folio* 之间灵活的转换,转换代码在 include/linux/page-flags.h 中,具体来说:

1
2
3
4
5
#define page_folio(p)		(_Generic((p),				\
const struct page *: (const struct folio *)_compound_head(p), \
struct page *: (struct folio *)_compound_head(p)))

#define folio_page(folio, n) (&(folio)->page + (n))

其中利用了泛型语法以保持常量属性

此外还有 struct ptdesc,这是将 struct page 描述页表页的功能独立出来,这里就不说了

pglist_data 和 zone

这两个结构体是对于 numa 架构中物理内存的抽象。在 numa 架构中,完整的物理内存被划分为了多个 node(每个 node 有若干个本地 cpu,本地访问的性能很高),而每个 node 又被划分为若干个 zone,struct pglist_data 记录了 numa node 的元数据,而 struct zone 则是 zone 的元数据

pglist_data 实现
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
typedef struct pglist_data {	
/*
* node_zones contains just the zones for THIS node. Not all of the
* zones may be populated, but it is the full list. It is referenced by
* this node's node_zonelists as well as other node's node_zonelists.
*/
struct zone node_zones[MAX_NR_ZONES];

/*
* node_zonelists contains references to all zones in all nodes.
* Generally the first zones will be references to this node's
* node_zones.
*/
struct zonelist node_zonelists[MAX_ZONELISTS];
int nr_zones; /* number of populated zones in this node */
...
unsigned long node_start_pfn;
unsigned long node_present_pages; /* total number of physical pages */
unsigned long node_spanned_pages; /* total size of physical page range, including holes */
int node_id;
wait_queue_head_t kswapd_wait;
...
#ifdef CONFIG_MEMORY_HOTPLUG
struct mutex kswapd_lock;
#endif
struct task_struct *kswapd; /* Protected by kswapd_lock */
int kswapd_order;
enum zone_type kswapd_highest_zoneidx;
atomic_t kswapd_failures; /* Number of 'reclaimed == 0' runs */

} pg_data_t;

struct pgdata_list 是 numa node 的元数据,由于这些元数据比较确定,所以这里面没有使用 union,里面的变量大部分都有很清晰的注释,这里再做一些记录:

  • node_zonelists 记录的是所有 node 的 zones 引用,其中每一个元素对应一个 node 的 node_zones,数组的顺序是内存分配的优先级,也即先从 node_zonelists[0] 中开始分配,之后是 node_zonelists[1],以此类推
  • node_start_pfn 记录的是这个 node 的起始物理地址页帧号,它和 node_present_pages 以及 node_spanned_pages 一起决定了 node 所对应的物理地址区间
  • kswapd 指向一个内核进程,这个进程会在其他进程发现内存不足时被唤醒,将未使用的页写回到磁盘的 swap 分区,其他的 kswapd_ 变量都是配合这个线程的,例如 kswapd_wait 是这个进程的等待队列,虽然队列中只有这一个内核进程,但是其中包含一个自旋锁保证操作原子性
zone 内核实现
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
struct zone {
/* Read-mostly fields */

/* zone watermarks, access with *_wmark_pages(zone) macros */
unsigned long _watermark[NR_WMARK];
...
struct per_cpu_pages __percpu *per_cpu_pageset;
struct per_cpu_zonestat __percpu *per_cpu_zonestats;
...

/* zone_start_pfn == zone_start_paddr >> PAGE_SHIFT */
unsigned long zone_start_pfn;

/*
* spanned_pages is the total pages spanned by the zone, including
* holes, which is calculated as:
* spanned_pages = zone_end_pfn - zone_start_pfn;
*
* present_pages is physical pages existing within the zone, which
* is calculated as:
* present_pages = spanned_pages - absent_pages(pages in holes);
*
* present_early_pages is present pages existing within the zone
* located on memory available since early boot, excluding hotplugged
* memory.
*
* managed_pages is present pages managed by the buddy system, which
* is calculated as (reserved_pages includes pages allocated by the
* bootmem allocator):
* managed_pages = present_pages - reserved_pages;
*
* cma pages is present pages that are assigned for CMA use
* (MIGRATE_CMA).
*
* So present_pages may be used by memory hotplug or memory power
* management logic to figure out unmanaged pages by checking
* (present_pages - managed_pages). And managed_pages should be used
* by page allocator and vm scanner to calculate all kinds of watermarks
* and thresholds.
*/
atomic_long_t managed_pages;
unsigned long spanned_pages;
unsigned long present_pages;
#if defined(CONFIG_MEMORY_HOTPLUG)
unsigned long present_early_pages;
#endif
#ifdef CONFIG_CMA
unsigned long cma_pages;
#endif
...

/* Write-intensive fields used from the page allocator */
CACHELINE_PADDING(_pad1_);

/* free areas of different sizes */
struct free_area free_area[NR_PAGE_ORDERS];
...
/* Primarily protects free_area */
spinlock_t lock;
...
};

这里面最核心的成员是 free_area,这也是伙伴系统的核心结构体,它保存的是不同 order 的空闲页面链表,具体来说,struct free_head 的定义为:

1
2
3
4
struct free_area {
struct list_head free_list[MIGRATE_TYPES];
unsigned long nr_free;
};

下标 order 对应的链表中,空闲块的大小为 1 << order 个页,即 4×2orderKB4\times 2^\text{order}\rm\,\, KB

可以看到,struct page 中有一个 struct list_head buddy_list,当页面空闲的时候,这个 buddy_list 变量就会被挂到这里对应阶数的 free_list 链表下,也即 free_list -> next = buddy_list,之后通过 container_of 宏来找到对应的页

  • _watermark 记录了这个 zone 的剩余内存“水位线”,数组中的值升序排列,当 zone 中的剩余内存低于某一个值的时候,会采取对应的方式回收内存(例如唤醒 kswapd、阻塞进程同步回收)
  • per_cpu_ 这两个变量是为了性能提升用的,由于 cpu 分配页面的时候需要锁住 zone.lock,这导致多 cpu 频繁分配页面时性能会很差,因此给每个 cpu 分一小块缓存,尽量先从本地的 pageset 中分配页面,修改本地的 zonestat,迫不得已时才访问全局变量
  • _pages 这几个变量详细记录了 zone 中不同属性的页面,这个注释写的太清晰了所以忍不住放上来!

在内核中,zone 被分为了若干类型,分别代表了不同的寻址方式,使用 enum zone_type 描述,常见的包括:

  • ZONE_DMA:用于兼容无法访问整个地址空间的老式外围设备
  • ZONE_NORMAL:直接线性映射到物理地址的内核虚拟地址空间,即前面 struct page 中所说的低内存
  • ZONE_HIGHMEM:需要临时映射的高内存,在 64 位机器上并不存在

vm_area_struct

vm_area (VMA) 描述的是进程地址空间中一段连续的、具有相同属性的虚拟内存区间,也即对于进程来说,整个虚拟地址空间并不是一个整体,而是被分成了若干连续的 vma,相邻的 vma 之间有着不同的属性(例如读写权限、映射来源等)

在 linux 中,可以使用 cat /proc/xxx/maps 来访问某一进程的内存映射表,其中第一列就是不同 vma 的地址区间

struct vm_area_struct 记录了 vma 的元数据,包括但不限于该区间的起止地址、权限、以及缺页中断处理方式等

vm_area_struct 内核实现
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
struct vm_area_struct {
union {
struct {
/* VMA covers [vm_start; vm_end) addresses within mm */
unsigned long vm_start;
unsigned long vm_end;
};
freeptr_t vm_freeptr;
};
struct mm_struct *vm_mm;
pgprot_t vm_page_prot;
union {
const vm_flags_t vm_flags;
vm_flags_t __private __vm_flags;
};
...
/*
* A file's MAP_PRIVATE vma can be in both i_mmap tree and anon_vma
* list, after a COW of one of the file pages. A MAP_SHARED vma
* can only be in the i_mmap tree. An anonymous MAP_PRIVATE, stack
* or brk vma (with NULL file) can only be in an anon_vma list.
*/
struct list_head anon_vma_chain;
struct anon_vma *anon_vma;

/* Function pointers to deal with this struct. */
const struct vm_operations_struct *vm_ops;

unsigned long vm_pgoff;
struct file * vm_file;
void * vm_private_data;
...
/*
* For areas with an address space and backing store,
* linkage into the address_space->i_mmap interval tree.
*/
struct {
struct rb_node rb;
unsigned long rb_subtree_last;
} shared;
...
}

当 vma 正在被使用的时候,开头的 union 中记录了这个区域在内存空间中所管理的地址区间,即 area 管理的是 [vm_start, vm_end) 这个区间;当 vma 空闲的时候,其会被加入空闲链表中,链表指针就是 union 中的 vm_freeptr

对于一个 vma 来说,其权限管理由 vm_page_protvm_flags 这二者共同控制,其中 vm_page_prot 是硬件权限,也即执行权限,而 vm_flags 是软件权限,也即逻辑权限,这么设计主要是因为:

  • 不同的硬件架构的权限位规范是有区别的,而 vm_page_prot 的值最终会被写入到 MMU 的页表项中,因此它的具体值需要适配底层架构。例如同样是可读可写的一片 vma,它的 vm_page_prot 在 x86 平台上的值和在 arm 平台上的值就是不一样的
  • 在一些特殊情况下,会出现逻辑可行但实际不可执行的情况,例如进程 A fork 出进程 B,二者共享一片可写的 vma,此时在逻辑上,两个进程都会认为这片内存是可写的,因此在 vm_flags 中会标记可写;而由于二者是共享这一片内存,因此为了防止写入竞争,二者的 vm_page_prot 中都不会标记可写。这样当某一个进程写入这片内存的时候,就会触发 COW 机制

至于为什么 vm_flags 要设计一个 union,在神秘魔法一章分析

anon_vmavm_file 这二者则决定了该 vma 的映射来源,也即到底是一个匿名映射还是一个文件映射,这决定了当发生缺页异常的时候内核应该从何处去加载所需要的页

vm_flags 中,记录了这个 vma 的映射类型,分为共享映射 MAP_SHARED 和私有映射 MAP_PRIVATE 及其他一些类型,核心点在于不同进程对于这片物理内存的修改是共享的还是私有的,注释中比较详细的说明了不同映射来源与不同映射类型的 vma 所存在于的数据结构,下面来稍微详细解释一下:

内核中有一个结构体 struct address_space,它是管理页缓存的核心架构,每个 inode 会有一个对应的 address_space,其中主要记录的是这个文件有哪些页在内存中被缓存了。结构体中有一个重要的成员变量是 struct rb_root_cached i_mmap,它对应着一棵红黑树,树中保存着所有通过 mmap() 映射了该文件的 vma,这样内核就可以在文件发生变动的时候,快速遍历所有可能需要改动的 vma 及其所属的进程。当一个 vma 是文件映射的时候,它就会被插入到 i_mmap 这棵树中,对应的 struct vm_area_struct 中的 vm_file 变量也会是非空

而当一个 vma 是匿名映射的时候,其中的 struct anon_vma *anon_vma 会被赋值。而 struct anon_vma 是连接匿名物理页与 vma 的桥梁,其中保存着一棵区间树struct rb_root_cached rb_root(基于红黑树实现),这棵树中保存着所有相关的 vma,这里说的“相关”指的是树中的 vma 都映射了同一片匿名内存。前面说过当物理页是一个匿名页的时候,其 mapping 变量会指向一个 struct anon_vma,这样当物理页需要知道自己被哪些进程映射时,就可以通过 page->mapping 来访问其中的区间树,进而快速遍历。同时还有一个成员变量 struct list_head anon_vma_chain,这是一个链表头,通过这个链表,vma 可以知道自己属于哪些 struct anon_vma,也即知道自己存在于哪些树中,这样当 vma 被破坏的时候,也能够通知所有的 struct anon_vma 进行对应的修改

这样我们就能解释 A file’s MAP_PRIVATE vma can be in both i_mmap tree and anon_vma list… 这段注释了:

  • 当 vma 是一个文件映射的时候,首先它一定会存在于 i_mmap 树上
    • 当映射类型是 MAP_SHARED 的时候,其实是多个进程映射同一个文件(例如进程间通信),此时不会发生 COW,没有匿名页产生,这个 vma 也就不会存在于 anon_vma 的区间树中
    • 当映射类型是 MAP_PRIVATE 时,多个进程以私有映射的形式共享同一个文件(例如 fork() 调用),当有进程尝试写入该页的时候会触发 COW,赋值出一个新的匿名页,该 vma 就会被插入对应 anon_vma 的区间树里面,并且此时不会把 vma 从 i_mmap 树中删掉(也不会清除结构体中的 vm_file 等变量),这是因为 COW 机制只会复制需要被写入的页,其它页仍然是共享的,需要通过文件来访问
  • 当 vma 是一个 MAP_PRIVATE 的匿名映射、堆栈指针等情况时,它就只能存在于对应 anon_vma 的区间树中,不会存在于 i_mmap 树上

对于不同类型的映射,vma 提供了统一的内存操作接口,例如 open(), close() 等,这部分被定义在成员变量 const struct vm_operations_struct *vm_ops 中,这个结构体里面是一堆函数指针,注意这个写法是常量指针,所以 vm_ops 可修改而 *vm_ops 不可修改

anon_vma_chain
1
2
3
4
5
6
7
struct anon_vma_chain {
struct vm_area_struct *vma;
struct anon_vma *anon_vma;
struct list_head same_vma; /* locked by mmap_lock & page_table_lock */
struct rb_node rb; /* locked by anon_vma->rwsem */
...
};

这是一个精妙的数据结构设计,通过 struct anon_vma_chain 可以维护 vma 和 anon_vma 之间的多对多关系,来捋一下:

  • 一个 vma 可以属于多个 anon_vma,通过 vm_area_struct -> anon_vma_chain 这个链表来访问
  • 一个 anon_vma 中可以包含着多个 vma,通过 anon_vma -> rb_root 这棵树来访问

但是,链表的节点并不是 anon_vma,红黑树的节点也不是 vma,它们的节点都是 struct anon_vma_chain ,这个结构体中最重要的四个变量是:

  • vmaanon_vma:指向连接的双方,也即有对应关系的两个结构体
  • same_vma:一个链表节点,被挂载在 vma -> anon_vma_chain 这个链表中
  • rb:一个红黑树节点,被挂载在 anon_vma -> rb_root 这棵红黑树中

也就是说,这个结构体中有一个成员变量被挂在链表上,有一个成员变量被挂在红黑树上,因此利用 container_of 宏,这个结构体本身就既能通过链表访问,又能通过红黑树访问,实现了双向连接

这么做的一个好处是能绕过红黑树节点的唯一性,也即一个 vma 属于多个 anon_vma 时,它需要存在于多棵红黑树中,而一个 rb_node 只能存在于一颗红黑树中,因此如果在 vma 里面静态添加 struct rb_node 是非常困难的,一个直观的想法是在 vma 里面加一个链表头,链表的每一个节点里面存一个红黑树节点,这其实就是 struct anon_vma_chain 的实现思路了

mm_struct

struct mm_struct 是描述进程整个内存布局的结构体,等看进程管理的时候再来补

内存分配算法

这一章主要关注的话题是,当用户在 C 语言中写下 ptr = malloc(1 << 24); memset(ptr, 0, 1 << 24) 这行代码之后,执行的过程中的会发生什么?

首先介绍一下 linux 的延迟分配机制,当用户进程的堆内存不够的时候,库函数会调用 brkmmap 这两个系统调用。brk 系统调用是移动进程堆的结束地址来分配或释放堆内存,通常是在 malloc 一些较小的区域的时候被调用,而当所申请的大小超过系统阈值的时候,就会调用 mmap 系统调用,内核会为此划分一块新的 vma 并记录,但是并不会建立页表映射。而当进程访问这块内存的时候,会触发缺页异常,进而分配物理页

如题,如果申请的空间是 1 << 24 = 16MB,那基本上会超过阈值,调用 mmap,对于 x86 系统上的用户态程序来说,调用流程是:

  1. arch/x86/um/syscalls_64.c 中提供了 sys_mmap 的接口:SYSCALL_DEFINE6(mmap, ...)
  2. 调用 mm/mmap.c 中的 ksys_mmap_pgoff()
  3. 调用 mm/util.c 中的 vm_mmap_pgoff()
  4. 调用 mm/mmap.c 中的 do_mmap(),这里会做大量的边界检查与权限控制
  5. 调用 mm/vma.c 中的 mmap_region(),其中会进一步调用 static 函数 __mmap_region(),这其中会首先尝试将 vma 与相邻的区域合并,如果无法合并则会调用 __mmap_new_vma() 分配一块新的 vma

分配完 vma 之后 mmap 的工作就完成了,接下来当访问这片内存的时候会触发缺页异常,还是以 x86 系统为例,这个流程是:

  1. 触发缺页异常时,错误码和触发异常的线性地址压栈,并跳转到内核入口 exc_page_fault
  2. 调用 mm/fault.c 中的 handle_page_fault(),进而调用 do_user_addr_fault()
  3. 调用 mm/memory.c 中的 handle_mm_fault(),其中会调用 static 函数 __handle_mm_fault(),落到 handle_pte_fault(),这个函数中当发现虚拟地址不存在页表项的时候,会调用 do_pte_missing(),对于堆内存(属于匿名内存),会进一步调用 do_anonymous_page(),当没有错误发生的时候,会进入 alloc_anon_folio(),最终会进入 folio_prealloc()
  4. 调用 include/linux/gfp.h 中的 vma_alloc_folio(),此时会经过一系列映射与转换:vma_alloc_folio() -> vma_alloc_folio_noprof() -> folio_alloc_noprof() -> __folio_alloc_node_noprof()
  5. 调用 mm/memory.c 中的 __folio_alloc_noprof(),然后进入 __alloc_pages_noprof(),最终到达 __alloc_frozen_pages_noprof(),这里是伙伴系统分配器的最终实现!

至于缺页异常的时候为什么会跳转到 exc_page_fault,这涉及到了更底层的硬件处理 所以再埋个坑

伙伴分配算法

TODO


© 2024 本网站由 Ywang22 使用 Stellar主题 创建
总访问 次 | 本页访问
共发表 82 篇 Blog(s) · 总计 192.9k 字