> For the complete documentation index, see [llms.txt](https://lightc.gitbook.io/pwn-gitbook/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://lightc.gitbook.io/pwn-gitbook/kpwn/kpwn-internals/nei-cun-guan-li.md).

# 内存管理

### 内存管理-转换为物理内存

内核中的寻址空间大小是由`CONFIG_ARM64_VA_BITS`控制的 这里以48位为例，ARMv8中 `Kernel Space`的页表基地址存放在`TTBR1_EL1`寄存器中，内核地址空间的高位为全1，（`0xFFFF0000_00000000 ~ 0xFFFFFFFF_FFFFFFFF`） `User Space`页表基地址存放在`TTBR0_EL0`寄存器中，用户地址空间的高位为全0，（`0x00000000_00000000 ~ 0x0000FFFF_FFFFFFFF`）

当 CPU 收到一个虚拟地址转换请求时

1. 检查虚拟地址高 16 位是否为全 0 或全 1
2. 若全 0 → 用 `TTBR0_EL1` 的值作为页表基址，开始多级页表遍历
3. 若全 1 → 用 `TTBR1_EL1` 的值作为页表基址，开始多级页表遍历
4. 否则 → 触发地址异常

**TTBR0\_EL1**：存放**用户进程页表的物理基址**

* 每个用户进程有独立的页表 → 进程切换时，内核会更新 TTBR0\_EL1 的值
* 仅用于转换用户空间虚拟地址

**TTBR1\_EL1**：存放**内核全局页表的物理基址**

* 内核页表是全局的 → 所有进程共享同一套内核页表，进程切换时 TTBR1\_EL1 **无需修改**
* 仅用于转换内核空间虚拟地址

### 内存管理-访问内存

TLB 是 **CPU 内核内部的硬件组件**，和 L1 指令 / 数据缓存一样，属于 CPU 片上高速存储，物理上集成在 MMU 模块中。

1. **TLB 的分类**
   * 按地址类型分：**指令 TLB（ITLB）** 缓存指令页的页表项，**数据 TLB（DTLB）** 缓存数据页的页表项
   * 按页大小分：支持大页（如 2MB/1GB）的 TLB 条目会单独划分区域，避免小页条目挤占大页空间
2. **TLB 失效的影响**
   * 若地址转换的页表项不在 TLB 中（**TLB Miss**），CPU 会触发**页表遍历**：去内存中查多级页表（如 x86\_64 的 4 级页表），找到后将页表项填入 TLB，供后续访问复用
   * 频繁的 TLB Miss 会显著降低性能，这也是内核尽量使用大页（HugePage）的原因 —— 一个大页表项能覆盖更大的内存区域，减少 TLB 条目占用，降低 Miss 率
3. **内核对 TLB 的管理**
   * 当内核修改页表（如 `vmalloc` 分配内存、页表映射变更）时，必须主动**刷新 TLB**（如执行 `invlpg` 指令），否则 CPU 会使用旧的 TLB 条目，导致地址转换错误
   * 不同架构的 TLB 刷新指令不同：x86 用 `invlpg`，ARMv8 用 `tlbi` 系列指令

TLB：MMU工作的过程就是查询页表的过程，CPU从虚拟地址中提取虚拟页号，到 TLB 中匹配直接得到对应的物理页号和权限，再通过物理页号 + 页内偏移得到真实的物理地址，发起内存读写请求

对于同一个内存，更大的大页对应的虚拟页号位数更少，可以覆盖更大的内存空间，直接减少 TLB Miss 概率，提升地址转换效率（1 个 1GB 大页 TLB 条目 = 256 个 4MB 页条目 = 1048576 个 4KB 页条目）

### 内存管理-页

通常采用四级页表，页全局目录`(PGD)`，页上级目录`(PUD)`，页中间目录`(PMD)`，页表`(PTE)`

1. 从`CR3`寄存器中读取页目录所在物理页面的基址（即所谓的页目录基址），从线性地址的第一部分获取页目录项的索引，两者相加得到页目录项的物理地址
2. 第一次读取内存得到`pgd_t`结构的目录项，从中取出物理页基址取出，即**页上级页目录的物理基地址**
3. 从线性地址的第二部分中取出页上级目录项的索引，与页上级目录基地址相加得到页上级目录项的物理地址
4. 第二次读取内存得到`pud_t`结构的目录项，从中取出**页中间目录的物理基地址**
5. 从线性地址的第三部分中取出页中间目录项的索引，与页中间目录基址相加得到页中间目录项的物理地址
6. 第三次读取内存得到`pmd_t`结构的目录项，从中取出**页表的物理基地址**
7. 从线性地址的第四部分中取出页表项的索引，与页表基址相加得到页表项的物理地址
8. 第四次读取内存得到`pte_t`结构的目录项，从中取出**物理页的基地址**
9. 从线性地址的第五部分中取出物理页内偏移量，与物理页基址相加得到最终的物理地址
10. 第五次读取内存得到**最终要访问的数据**

### 内存管理-ELF程序内存映射

一、整体概述

ELF 可执行文件从加载到内存映射建立，遵循 **"先虚拟，后物理，按需分配"** 的核心原则。整个过程分为两大阶段：

第一阶段：虚拟地址空间规划（execve 阶段）

1. 解析 ELF 文件格式
2. 为进程创建 `mm_struct` 和页表根节点
3. 划定虚拟内存区域（`vm_area_struct`）

第二阶段：物理内存实际映射（运行时阶段）

4. 首次访问触发缺页异常
5. 动态分配页表中间层级（`PUD/PMD/PTE`）
6. 分配物理内存并建立映射

二、详细流程分解

**步骤 1：执行触发与 ELF 解析**

用户态：用户输入 `./hello` → `Shell` 调用 `execve()`

内核态：`sys_execve() → do_execve() → load_elf_binary()`

**关键操作：**

* 内核读取 ELF 头部 `Elf64_Ehdr`，判断文件类型：
  * `ET_EXEC`：非 PIE，固定基址（如 0x400000）
  * `ET_DYN`：PIE，需要 ASLR 随机化
* 读取程序头表 `Elf64_Phdr`，获取可加载段信息（`.text`、`.data` 等）

**关键数据结构：**

* `linux_binprm`：临时存放加载参数
* `elf64_phdr`：程序段描述符

**步骤 2：进程内存描述符创建**

`load_elf_binary() → mm_alloc() → mm_init()`

**关键操作：**

* 分配 `mm_struct` 结构体
* 调用 `pgd_alloc()` 分配 PGD（页全局目录）物理页
* 初始化 `mm_struct` 核心字段：

```c
    mm->pgd = pgd_alloc(mm);  // 页表根节点
    mm->mmap = NULL;          // 虚拟区域链表
    mm->mm_rb = RB_ROOT;      // 虚拟区域红黑树
    mm->start_code = mm->end_code = 0;
    mm->start_data = mm->end_data = 0;
    mm->start_brk = mm->brk = 0;  // 堆起始/当前指针
```

**关键点：**

* 此时只分配了 **PGD 根页**（4KB 物理页）
* `PUD/PMD/PTE` 均未分配，节省内存
* `TTBR0_EL1` 仍指向原进程页表

**步骤 3：虚拟内存区域创建与基址随机化**

**3.1 基址确定**

`arch_pick_mmap_layout() → randomize_stack_top()`

**PIE vs 非 PIE 差异：**

| 类型    | ELF 类型   | 基址确定方式 | 典型基址                       |
| ----- | -------- | ------ | -------------------------- |
| 非 PIE | ET\_EXEC | 固定值    | 0x400000                   |
| PIE   | ET\_DYN  | 内核随机分配 | 0x5555000000\~0x7777000000 |

**PIE 随机化细节：**

**3.2 vm\_area\_struct 创建**

`load_elf_binary() → elf_map() → mmap_region()`

**各段处理逻辑：**

| 段类型   | 创建时机                | 权限  | 特殊标志          |
| ----- | ------------------- | --- | ------------- |
| 代码段   | ELF 加载时             | r-x | VM\_EXEC      |
| 数据段   | ELF 加载时             | rw- | VM\_WRITE     |
| BSS 段 | ELF 加载时             | rw- | VM\_WRITE     |
| 栈     | setup\_arg\_pages() | rw- | VM\_GROWSDOWN |
| 堆     | set\_brk()          | rw- | VM\_GROWSUP   |

**关键数据结构：** `vm_area_struct`

```c
struct vm_area_struct {
    unsigned long vm_start;     // 虚拟起始地址
    unsigned long vm_end;       // 虚拟结束地址
    pgprot_t vm_page_prot;      // 访问权限
    unsigned long vm_flags;     // VM_READ|VM_WRITE|VM_EXEC
    struct file *vm_file;       // 映射的文件（如有）
    struct rb_node vm_rb;       // 红黑树节点
};
```

**虚拟地址最终布局示例（PIE）：**

```bash
0x5555000000 ~ 0x5555001000   .text (代码段)
0x5555002000 ~ 0x5555003000   .data (数据段) 
0x5555003000 ~ 0x5555004000   .bss  (BSS段)
0x5555004000 ~ 0x5555005000   堆初始预留区
0x7ffffffde000 ~ 0x7ffffffff000  栈
```

**步骤 4：页表框架的惰性建立**

**核心原则：** 页表中间层级（PUD/PMD）按需分配

**4.1 进程创建时的页表状态**

```c
// 只分配 PGD 根页
mm->pgd = pgd_alloc(mm);  // 返回物理地址 0x80001234000

// PGD 项初始化为无效
for (i = 0; i < PTRS_PER_PGD; i++)
    mm->pgd[i] = pgd_none;
```

**4.2 ARMv8 4 级页表结构**

```
虚拟地址 [48位] = [PGD(9)][PUD(9)][PMD(9)][PTE(9)][Offset(12)]
```

各级覆盖范围：

* PGD：512GB（2^39 字节）
* PUD：1GB（2^30 字节）
* PMD：2MB（2^21 字节）
* PTE：4KB（2^12 字节）

**步骤 5：进程切换与页表激活**

`context_switch() → switch_mm() → cpu_switch_mm()`

**ARMv8 硬件操作：**

```assembly
// 将新进程的 PGD 物理地址写入 TTBR0_EL1
msr ttbr0_el1, x0          // x0 = mm->pgd 物理地址
dsb ish                    // 内存屏障
tlbi vmalle1is             // 刷新 TLB
dsb ish
isb
```

**关键点：**

* `TTBR0_EL1` 指向进程私有用户页表
* `TTBR1_EL1` 指向内核全局页表（不变）
* TLB 刷新确保旧进程映射失效

**步骤 6：首次访问触发缺页异常**

**6.1 异常触发流程**

用户态：执行第一条指令（PC = 入口地址） 硬件：`MMU` 遍历页表发现 `PTE` 无效 → 触发 `Data/Instruction Abort` 内核：`el0_sync → do_mem_abort() → do_page_fault()`

**ARMv8 缺页异常原因码（ESR\_EL1）：**

* 0b000000：地址大小故障
* 0b000100：转换故障（页表项无效）
* 0b000101：权限故障

**6.2 缺页处理核心逻辑**

```c
handle_mm_fault()
    → __handle_mm_fault()
        → pud_alloc()      // 按需分配 PUD 页
        → pmd_alloc()      // 按需分配 PMD 页  
        → pte_alloc_map()  // 按需分配 PTE 页
        → handle_pte_fault()
```

**6.3 不同段类型的映射策略**

| 段类型   | 缺页类型 | 处理函数                  | 物理页来源           |
| ----- | ---- | --------------------- | --------------- |
| 代码段   | 文件映射 | do\_read\_fault()     | page cache / 磁盘 |
| 数据段   | 文件映射 | do\_shared\_fault()   | page cache / 磁盘 |
| BSS 段 | 匿名映射 | do\_anonymous\_page() | 零页 → COW        |
| 栈     | 匿名映射 | do\_anonymous\_page() | 零页 → COW        |
| 堆     | 匿名映射 | do\_anonymous\_page() | 新分配物理页          |

**代码段文件映射示例：**

```c
// 物理页分配与映射
page = alloc_page(GFP_KERNEL);          // 分配物理页
copy_page_from_file(page, elf_file);    // 从文件/缓存加载数据
entry = mk_pte(page, PAGE_READONLY_EXEC); // 创建 PTE 项
set_pte_at(mm, address, pte, entry);    // 写入页表
```

**步骤 7：堆的动态扩展机制**

**7.1 堆地址的三阶段确定**

阶段 1：ELF 解析时（虚拟预留） set\_brk() → 创建 vm\_area\_struct 范围：基址+偏移 \~ 基址+偏移+4KB（仅占位）

阶段 2：第一次 malloc() 时（虚拟扩展） brk() 系统调用 → 扩展 vm\_area\_struct 范围 例：0x5555004000 → 0x5555004800

阶段 3：首次访问时（物理映射） 缺页异常 → 分配物理页 → 建立 PTE 映射

**7.2 brk() 系统调用流程**

```c
SYSCALL_DEFINE1(brk, unsigned long, brk)
    → do_brk_flags()
        → find_vma_links()      // 查找扩展位置
        → vma_merge()           // 尝试合并相邻区域
        → vma_link()            // 插入/扩展 vm_area_struct
        // 此时仍未分配物理页！
```

**步骤 8：后续访问与 TLB 加速**

**8.1 TLB 命中流程**

虚拟地址 0x5555000000 → TLB 查询 ↓ 命中 物理地址 0x80005678000 + 偏移 → 访问内存 ↓ 未命中（TLB Miss） 硬件遍历页表：TTBR0\_EL1 → PGD → PUD → PMD → PTE 获取物理地址并填充 TLB

**8.2 多级页表遍历开销**

* **TLB 命中**：1\~2 个时钟周期
* **TLB 未命中 + 页表遍历**：几十到上百时钟周期
* **缺页异常**：数千到数万时钟周期（涉及磁盘 I/O 时更慢）

三、核心数据结构关系图

```
进程控制块 (task_struct)
    ↓
内存描述符 (mm_struct)
    ├── pgd: 0x80001234000  →  PGD 物理页
    ├── mmap: 链表头        →  vm_area_struct 链表
    ├── mm_rb: 红黑树根     →  vm_area_struct 红黑树
    ├── start_code/end_code
    └── start_brk/brk
           ↓
       vm_area_struct
           ├── vm_start/vm_end
           ├── vm_flags (VM_READ|WRITE|EXEC)
           └── vm_page_prot
                 ↓
            页表层级
            PGD → PUD → PMD → PTE
                 ↓
             物理页帧
```

四、PIE 与非 PIE 的完整对比

| 维度         | 非 PIE (ET\_EXEC) | PIE (ET\_DYN)       |
| ---------- | ---------------- | ------------------- |
| **ELF 类型** | ET\_EXEC         | ET\_DYN             |
| **入口地址**   | 绝对地址 (0x400000)  | 相对偏移 (0x1000)       |
| **虚拟基址**   | 固定 0x400000      | 随机 (ASLR)           |
| **页表构建**   | 固定 PGD 偏移        | 随机 PGD 偏移           |
| **安全性**    | 低（地址可预测）         | 高（ASLR 防护）          |
| **兼容性**    | 传统方式             | 现代标准（Android/Linux） |
| **加载器**    | 内核直接加载           | 内核+动态链接器            |

### 内存管理-组织物理内存

* `node` 目前计算机系统有两种体系结构：

1. 非一致性内存访问 `NUMA(Non-Uniform Memory Access)`意思是内存被划分为各个`node`，访问一个`node`花费的时间取决于CPU离这个`node`的距离。每一个cpu内部有一个本地的`node`，访问本地`node`时间比访问其他`node`的速度快
2. 一致性内存访问 `UMA(Uniform Memory Access)`也可以称为`SMP(Symmetric Multi-Process)`对称多处理器。意思是所有的处理器访问内存花费的时间是一样的。也可以理解整个内存只有一个`node`

* `zone`

`ZONE`的意思是把整个物理内存划分为几个区域，每个区域有特殊的含义

* `page`

代表一个物理页，在内核中一个物理页用一个`struct page`表示。

* `page frame`

为了描述一个物理`page`，内核使用`struct page`结构来表示一个物理页。假设一个`page`的大小是4K的，内核会将整个物理内存分割成一个一个4K大小的物理页，而4K大小物理页的区域我们称为`page frame`

### 内存管理-分区页框分配器

有时候目标管理区不一定有足够的页框去满足分配，这时候系统会从另外两个管理区中获取要求的页框，但这是按照一定规则去执行的，如下：

* 如果要求从`DMA`区中获取，就只能从`ZONE_DMA`区中获取
* 如果没有规定从哪个区获取，就按照顺序从`ZONE_NORMAL -> ZONE_DMA`获取
* 如果规定从`HIGHMEM`区获取，就按照顺序从 `ZONE_HIGHMEM -> ZONE_NORMAL -> ZONE_DMA` 获取

```c
struct page *__alloc_frozen_pages_noprof(gfp_t gfp, unsigned int order,
		int preferred_nid, nodemask_t *nodemask)
{
	...
	
	page = get_page_from_freelist(alloc_gfp, order, alloc_flags, &ac);
	if (likely(page))
		goto out;

	...
	
	page = __alloc_pages_slowpath(alloc_gfp, order, &ac);

	...
	
	return page;
}
EXPORT_SYMBOL(__alloc_frozen_pages_noprof);
```

在页面分配时，有两种路径可以选择，如果在快速路径中分配成功了，则直接返回分配的页面；快速路径分配失败则选择慢速路径来进行分配

* 正常分配（或叫快速分配）：

1. 如果分配的是单个页面，考虑从`per CPU`缓存中分配空间，如果缓存中没有页面，从伙伴系统中提取页面做补充。
2. 分配多个页面时，从指定类型中分配，如果指定类型中没有足够的页面，从备用类型链表中分配。最后会试探保留类型链表。

* 慢速（允许等待和页面回收）分配
* 当上面两种分配方案都不能满足要求时，考虑页面回收、杀死进程等操作后在试

### 内存管理-水位

衡量当前系统内存状态，内存`watermark`水位就能很好的衡量系统内存状态，内存状态的划分分三个层次：`HIGH、LOW、MIN`，系统针对内存不同的状态就会做不同的内存行为，对系统内存状态进行管控

* 如果空闲页数目 `< min`值，则该`zone`非常缺页，页面回收压力很大，应用程序写内存操作就会被阻塞，直接在应用程序的进程上下文中进行回收，即`direct reclaim`
* 如果空闲页数目小于 `< low && > min`值，`kswapd`线程将被唤醒，并开始释放回收页面
* 如果空闲页面的值 `> high`值，则该`zone`的状态很完美, `kswapd`线程将重新休眠

通过`init_per_zone_wmark_min 、 __setup_per_zone_wmarks`计算水位

在`boost_watermark`中计算出`watermark_boost`再进入`balance_pgdat`设置`boost` 当`boosted enable`后，`wakeup_kcompactd`进行内存碎片整理，一定程度上会缓解系统内存碎片化问题

```c
	if (boosted) {
		unsigned long flags;

		for (i = 0; i <= highest_zoneidx; i++) {
			if (!zone_boosts[i])
				continue;

			/* Increments are under the zone lock */
			zone = pgdat->node_zones + i;
			spin_lock_irqsave(&zone->lock, flags);
			zone->watermark_boost -= min(zone->watermark_boost, zone_boosts[i]);
			spin_unlock_irqrestore(&zone->lock, flags);
		}

		/*
		 * As there is now likely space, wakeup kcompact to defragment
		 * pageblocks.
		 */
		wakeup_kcompactd(pgdat, pageblock_order, highest_zoneidx);
	}
```

### 内存管理-Buddy分配算法

空闲内存都会交给内核内存管理系统来进行统一管理和分配，内核中会把内存按照页来组织分配，随着进程的对内存的申请和释放，系统的内存会不断的区域碎片化，到最后会发现，明明系统还有很多空闲内存，却无法分配出一块连续的内存，这对于系统来说并不是好事，而伙伴系统算法就可以缓解这种碎片化

把所有的空闲页框分组为11个块链表，每个块链表分别包含大小为1，2，4，8，16，32，64，128，256，512和1024个连续页框的页框块，最大可以申请1024个连续页框，对应4MB大小的连续内存，每个页框块的第一个页框的物理地址是该块大小的整数倍

```c
static struct page *
get_page_from_freelist(gfp_t gfp_mask, unsigned int order, int alloc_flags,
						const struct alloc_context *ac)
{

retry:
	for_next_zone_zonelist_nodemask(zone, z, ac->highest_zoneidx,
					ac->nodemask) {

check_alloc_wmark:
		mark = wmark_pages(zone, alloc_flags & ALLOC_WMARK_MASK);
		if (!zone_watermark_fast(zone, order, mark,
				       ac->highest_zoneidx, alloc_flags,
				       gfp_mask)) {
			...
			ret = node_reclaim(zone->zone_pgdat, gfp_mask, order);
			switch (ret) {
			case NODE_RECLAIM_NOSCAN:
				/* did not scan */
				continue;
			case NODE_RECLAIM_FULL:
				/* scanned but unreclaimable */
				continue;
			default:
				/* did we reclaim enough */
				if (zone_watermark_ok(zone, order, mark,
					ac->highest_zoneidx, alloc_flags))
					goto try_this_zone;

				continue;
			}
		}

try_this_zone: //本zone正常水位
		page = rmqueue(zonelist_zone(ac->preferred_zoneref), zone, order,
				gfp_mask, alloc_flags, ac->migratetype);
		...
	}
	...
	return NULL;
}
```

首先遍历当前`zone`，按照`HIGHMEM->NORMAL`的方向进行遍历，判断当前`zone`是否能够进行内存分配的条件是首先判断`free memory`是否满足`low water mark`水位值，如果不满足则进行一次快速的内存回收操作，然后再次检测是否满足`low water mark`，如果还是不能满足，相同步骤遍历下一个`zone`，满足的话进入正常的分配情况，即`rmqueue`函数，这也是伙伴系统的核心

```c
static inline
struct page *rmqueue(struct zone *preferred_zone,
			struct zone *zone, unsigned int order,
			gfp_t gfp_flags, unsigned int alloc_flags,
			int migratetype)
{
	struct page *page;

	if (likely(pcp_allowed_order(order))) {    //如果order=0则从pcp中分配
		page = rmqueue_pcplist(preferred_zone, zone, order,
				       migratetype, alloc_flags);
		if (likely(page))
			goto out;
	}

	page = rmqueue_buddy(preferred_zone, zone, order, alloc_flags,
							migratetype);

out:
	/* Separate test+clear to avoid unnecessary atomics */
	if ((alloc_flags & ALLOC_KSWAPD) &&
	    unlikely(test_bit(ZONE_BOOSTED_WATERMARK, &zone->flags))) {
		clear_bit(ZONE_BOOSTED_WATERMARK, &zone->flags);
		wakeup_kswapd(zone, 0, 0, zone_idx(zone));
	}

	VM_BUG_ON_PAGE(page && bad_range(zone, page), page);
	return page;
}

static __always_inline
struct page *rmqueue_buddy(struct zone *preferred_zone, struct zone *zone,
			   unsigned int order, unsigned int alloc_flags,
			   int migratetype)
{
	struct page *page;
	unsigned long flags;

	do {
		page = NULL;
		if (unlikely(alloc_flags & ALLOC_TRYLOCK)) {
			if (!spin_trylock_irqsave(&zone->lock, flags))
				return NULL;
		} else {
			spin_lock_irqsave(&zone->lock, flags);
		}
		if (alloc_flags & ALLOC_HIGHATOMIC)
			page = __rmqueue_smallest(zone, order, MIGRATE_HIGHATOMIC);
		if (!page) {
			enum rmqueue_mode rmqm = RMQUEUE_NORMAL;

			page = __rmqueue(zone, order, migratetype, alloc_flags, &rmqm);

			/*
			 * If the allocation fails, allow OOM handling and
			 * order-0 (atomic) allocs access to HIGHATOMIC
			 * reserves as failing now is worse than failing a
			 * high-order atomic allocation in the future.
			 */
			if (!page && (alloc_flags & (ALLOC_OOM|ALLOC_NON_BLOCK)))
				page = __rmqueue_smallest(zone, order, MIGRATE_HIGHATOMIC);

			if (!page) {
				spin_unlock_irqrestore(&zone->lock, flags);
				return NULL;
			}
		}
		spin_unlock_irqrestore(&zone->lock, flags);
	} while (check_new_pages(page, order));

	__count_zid_vm_events(PGALLOC, page_zonenum(page), 1 << order);
	zone_statistics(preferred_zone, zone, 1);

	return page;
}

```

### 内存管理-碎片化整理

主要应用了内核的页面迁移机制，是一种将可移动页面进行迁移后腾出连续物理内存的方法

### 内存管理-slub分配器

伙伴系统是以页为单位分配内存，但是现实中很多时候却以字节为单位，不然申请`10Bytes`内存还要给1页的话就太浪费了，slab分配器就是为小内存分配而生的，`slub`分配器分配内存以`Byte`为单位，基于伙伴系统分配的大内存进一步细分成小内存分配

`slub`把内存分组管理，每个组分别包含`8,16,32,...,2048`个字节，在`4K`页大小的默认情况下，另外还有两个特殊的组，分别是`96B`和`192B`，之所以这样分配是因为如果申请`2^12B`大小的内存，就可以使用伙伴系统提供的接口直接申请一个完整的页面，加上整页分配共`12`组，

可以把一个`kmem_cache`结构体看做是一个特定大小内存的零售商，整个`slub`系统中共有12个这样的零售商，每个零售商只零售特定大小的内存，零售商把这些整页的内存分成许多小内存，然后分别零售出去，一个slab可能包含多个连续的内存页

每个零售商`kmem_cache`有两个部门，一个是仓库：`kmem_cache_node`，一个“营业厅`kmem_cache_cpu`，营业厅里只保留一个`slub`，只有在营业厅`kmem_cache_cpu`中没有空闲内存的情况下才会从仓库中换出其他的`slub`

整个slub系统的框图

物理页按照对象`object`大小组织成单向链表，对象大小由`objsize`指定的。例如16字节的对象大小，每个object就是16字节，每个`object`包含指向下一个`object`的指针，该指针的位置是每个`object`的起始地址`+offset`

`slub`系统刚刚创建出来，这是第一次申请。 此时`slub`系统刚建立起来，营业厅`kmem_cache_cpu`和仓库`kmem_cache_node`中没有任何可用的`slab`可以使用因此只能向伙伴系统申请空闲的内存页，并把这些页面分成很多个`object`，取出其中的一个`object`标志为已被占用，并返回给用户，其余的`object`标志为空闲并放在`kmem_cache_cpu`中保存。`kmem_cache_cpu`的`freelist`变量中保存着下一个空闲`object`的地址，表示申请一个新的`slab`，并把第一个空闲的`object`返回给用户，`freelist`指向下一个空闲的`object`

`slub`的`kmem_cache_cpu`中保存的`slab`上有空闲的`object`可以使用。\
这种情况是最简单的一种，直接把`kmem_cache_cpu`中保存的一个空闲`object`返回给用户，并把`freelist`指向下一个空闲的`object`

`slub`已经连续申请了很多页，现在`kmem_cache_cpu`中已经没有空闲的`object`了，但`kmem_cache_node`的`partial`中有空闲的`object` 。所以从`kmem_cache_node`的`partial`变量中获取有空闲`object`的`slab`，并把一个空闲的`object`返回给用户

`kmem_cache_cpu`中已经都被占用的`slab`放到仓库中，`kmem_cache_node`中有两个双链表，`partial`和`full`，分别盛放不满的`slab`，`slab`中有空闲的`object`和全满的`slab`，`slab`中没有空闲的`object`。然后从`partial`中挑出一个不满的`slab`放到`kmem_cache_cpu`中

`slub`已经连续申请了很多页，现在`kmem_cache_cpu`中保存的物理页上已经没有空闲的`object`可以使用了，而此时`kmem_cache_node`中没有空闲的页面了，只能向内存管理器(伙伴算法)申请`slab`。并把该`slab`初始化，返回第一个空闲的`object`

向`slub`系统释放内存块`object`时，如果`kmem_cache_cpu`中缓存的`slab`就是该`object`所在的`slab`，则把该`object`放在空闲链表中即可，如果`kmem_cache_cpu`中缓存的`slab`不是该`object`所在的`slab`，然后把该`object`释放到该`object`所在的`slab`中。在释放`object`的时候可以分为一下三种情况

`object`在释放之前`slab`是`full`状态的时候（`slab`中的`object`都是被占用的），释放该`object`后，这是该`slab`就是半满（`partial`）的状态了，这时需要把该`slab`添加到`kmem_cache_node`中的`partial`链表中

`slab`是`partial`状态时（`slab`中既有`object`被占用，又有空闲的），直接把该`object`加入到该`slab`的空闲队列中即可

该`object`在释放后，`slab`中的`object`全部是空闲的，还需要把该`slab`释放掉

### 内存管理-vmalloc

随着碎片化的积累，连续物理内存的分配就会变得困难，对于那些非DMA访问（`Direct Memory Access`，直接存储器访问），不一定非要连续物理内存的话完全可以像malloc那样，将不连续的物理内存页框映射到连续的虚拟地址空间中

主要分以下三步：

1. 从`VMALLOC_START`到`VMALLOC_END`查找空闲的虚拟地址空间`(hole)`
2. 根据分配的`size`,调用`alloc_page`依次分配单个页面
3. 把分配的单个页面，映射到第一步中找到的连续的虚拟地址。把分配的单个页面，映射到第一步中找到的连续的虚拟地址

### 内存管理-缺页处理

当进程访问这些还没建立映射关系的虚拟地址时，处理器会自动触发缺页异常

ARM64把异常分为同步异常和异步异常，通常异步异常指的是中断，同步异常指的是异常

当处理器有异常发生时，处理器会先跳转到ARM64的异常向量表中，选择如何处理异常

当触发异常的虚拟地址属于某个`vma`，并且拥有触发页错误异常的权限时，会调用到`__handle_mm_fault`函数来建立`vma`和物理地址的映射

* 查找页全局目录，获取地址对应的表项
* 查找页四级目录表项，没有则创建
* 查找页上级目录表项，没有则创建
* 查找页中级目录表项，没有则创建
* `handle_pte_fault`处理`pte`页表

**do\_anonymous\_page**

匿名页缺页异常，对于匿名映射，映射完成之后，只是获得了一块虚拟内存，并没有分配物理内存，当第一次访问的时候：

1. 如果是读访问，会将虚拟页映射到０页，以减少不必要的内存分配
2. 如果是写访问，用a`lloc_zeroed_user_highpage_movable`分配新的物理页，并用`０`填充，然后映射到虚拟页上去
3. 如果是先读后写访问，则会发生两次缺页异常：第一次是匿名页缺页异常的读的处理（虚拟页到`0`页的映射），第二次是写时复制缺页异常处理。

从上面的总结我们知道，第一次访问匿名页时有三种情况，其中第一种和第三种情况都会涉及到内核共享0页

**do\_fault**

**do\_swap\_page**

`pte`对应的内容不为`0`(页表项存在)，但是`pte`所对应的`page`不在内存中时，表示此时`pte`的内容所对应的页面在`swap`空间中，缺页异常时会通过`do_swap_page()`函数来分配页面。

`do_swap_page`发生在`swap in`的时候，即查找磁盘上的`slot`，并将数据读回。

换入的过程如下：

1. 查找`swap cache`中是否存在所查找的页面，如果存在，则根据`swap cache`引用的内存页，重新映射并更新页表；如果不存在，则分配新的内存页，并添加到`swap cache`的引用中，更新内存页内容完成后，更新页表。
2. 换入操作结束后，对应`swap area`的页引用减`1`，当减少到`0`时，代表没有任何进程引用了该页，可以进行回收

**do\_wp\_page**

走到这里说明页面在内存中，只是PTE只有读权限，而又要写内存的时候就会触发`do_wp_page`
