一些概念:COW、Lazy Allocation 和 Demand Paging
好的,我来帮你把这三个特性 分别是什么、解决什么问题、以及实现思路 梳理清楚,形成一个更清晰的结构。
1. 写时复制 (Copy-on-Write, COW)
是什么
写时复制是一种优化 fork() 的技术。正常情况下,父进程在调用 fork() 时会把整个用户内存空间都复制一份给子进程。但在大多数情况下,子进程很快就会调用 exec() 替换自己的地址空间,这导致之前的复制工作大部分浪费。
COW 的思想是:父子进程先共享同一份物理内存,只有在某一方真的要写这块内存时,才复制出一个新的物理页。
作用
- 避免了无谓的内存复制,减少
fork()的开销。 - 节省物理内存(父子进程在读的情况下共享数据)。
实现思路
-
修改
fork()- 不再复制物理页,而是让父子进程的页表都指向同一份物理页。
- 把这些页的 写权限去掉(PTE_W 清零),并设置一个自定义标志(如 PTE_COW)。
-
增加物理页引用计数
- 每个物理页维护一个
ref_count。 fork()时,父子进程共享页,引用计数加一。
- 每个物理页维护一个
-
在缺页异常中处理 COW
- 如果进程写一个只读的 COW 页,触发缺页异常。
- 检查 PTE 是否带有 PTE_COW。
- 如果是:
- 如果引用计数 > 1:分配一个新物理页,复制内容,更新页表。
- 如果引用计数 == 1:直接恢复写权限即可。
-
释放内存时配合引用计数
- 在
kfree()中,只有当引用计数为 0 时,才真正释放物理页。
- 在
2. 懒加载 (Lazy Allocation)
是什么
正常的 sbrk() / malloc() 在用户申请内存时,内核会立刻分配物理页并映射。
懒加载的思想是:当用户申请时,不立即分配物理内存,只更新进程的地址空间大小。真正访问这块地址时(触发缺页异常),才去分配物理页。
作用
- 避免申请了大量内存但未使用时浪费物理内存。
- 内存使用更高效。
实现思路
-
修改
sbrk()/growproc()- 当增加内存时,只修改
p->sz,不调用uvmalloc()。
- 当增加内存时,只修改
-
缺页中断中处理
- 当访问到尚未分配物理页的地址,会触发缺页异常。
- 检查异常地址
va:- 是否在
p->sz范围内。 - 是否尚未映射。
- 是否在
- 如果合法:
- 调用
kalloc()分配物理页,并清零。 - 在页表中建立映射,赋予
PTE_R|PTE_W|PTE_U。
- 调用
3. 按需分页 (Demand Paging)
是什么
在 xv6 的默认实现里,exec() 会一次性把程序的所有代码段和数据段加载到内存中,即使程序只用到其中一小部分。
按需分页的思想是:只在需要的时候才把文件内容加载进内存。启动时只建立“占位”的页表项,等访问到那一页时触发缺页,再从文件中读取对应的内容。
作用
- 加快程序启动速度(不用一次性加载整个程序)。
- 减少内存使用(只加载真正用到的部分)。
实现思路
-
修改
exec()/loadseg()- 不实际分配物理页,也不把程序段内容立即读入内存。
- 只为段建立 PTE,但:
- PTE_V 清零(标记为无效)。
- 设置一个自定义标志(如 PTE_DEMAND)。
- 在 PTE 中保存该页在可执行文件中的偏移信息。
-
缺页中断中处理
- 当访问无效页时,检查 PTE 是否带有 PTE_DEMAND。
- 如果是:
- 分配一个物理页。
- 根据偏移量,用
readi()从文件中把数据加载到物理页。 - 更新 PTE,设为有效并加上正确权限(如代码段 PTE_X,数据段 PTE_W)。
PTE 结构
1. 代码定义(kernel/riscv.h)
// --- In kernel/riscv.h ---
// Standard RISC-V PTE flags
#define PTE_V (1L << 0) // Valid: 1 if the entry is valid
#define PTE_R (1L << 1) // Read: 1 if reading is allowed
#define PTE_W (1L << 2) // Write: 1 if writing is allowed
#define PTE_X (1L << 3) // Execute: 1 if execution is allowed
#define PTE_U (1L << 4) // User: 1 if user mode can access this page
#define PTE_G (1L << 5) // Global: mapping visible across address spaces
#define PTE_A (1L << 6) // Accessed: set by hardware on any access
#define PTE_D (1L << 7) // Dirty: set by hardware on write
// Custom flags (use software-reserved bits)
#define PTE_COW (1L << 8) // Copy-On-Write page
#define PTE_DEMAND (1L << 9) // Demand paging: page not in memory yet
// Address translation helpers
#define PTE2PA(pte) (((pte) >> 10) << 12) // extract physical addr
#define PA2PTE(pa) (((pa) >> 12) << 10) // build PTE from PA
2. Sv39 PTE 结构表格(含自定义位)
| 比特位 (Bit) | 宏定义/名称 | 含义 (说明) |
|---|---|---|
| 0 | PTE_V | 有效位 (Valid)。1 = 此 PTE 有效,可被硬件用于地址翻译;0 = 无效,访问会触发异常。 |
| 1 | PTE_R | 可读位 (Read)。1 = 页面可读。 |
| 2 | PTE_W | 可写位 (Write)。1 = 页面可写。 |
| 3 | PTE_X | 可执行位 (Execute)。1 = 页面可执行。 |
| 4 | PTE_U | 用户位 (User)。1 = 用户态可访问;0 = 仅内核可访问。 |
| 5 | PTE_G | 全局位 (Global)。1 = 全局映射,不因地址空间切换而刷新 TLB。 |
| 6 | PTE_A | 已访问位 (Accessed)。硬件在页面被访问(读/写/执行)时置 1。 |
| 7 | PTE_D | 脏位 (Dirty)。硬件在页面被写入时置 1。 |
| 8 | PTE_COW | 写时复制标志 (Copy-on-Write)。1 = 此页是 COW 页,写入需要触发缺页并复制。 |
| 9 | 还未被使用 | 按需分页标志 (Demand Paging)。1 = 此页尚未加载到内存,访问时需要从磁盘加载。 |
| 10–53 | PPN | 物理页号 (Physical Page Number)。44 位,指定物理内存页的地址(4KB 对齐)。 |
| 54–63 | 保留(不使用) | Reserved。在 Sv39 下这些位必须为 0。 |
就是留下来可以使用的只有 8 和 9, 但是已经拿来分配了 8, 还有一个位置没有被使用, 就是如果我有多个需求的时候, 但是没有位置, 该怎么办?
两种可行方案
您的判断是正确的:如果我们把位 8 和 9 分别用作 PTE_COW 和 PTE_DEMAND,那么 RISC-V 架构官方为软件保留的两个 RSW 位就用完了。如果想再加一个全新的标志,比如 PTE_SWAPPED(表示页面被换出到交换分区),确实没有“官方”的空位了。
但是,我们有两种非常常用且实用的方法来解决这个问题。
方法一:利用无效页表项(PTE_V = 0)的 PPN 空间(推荐)
核心思想 / 黄金法则 当 PTE_V(有效位)为 0 时,硬件会完全忽略 PTE 中的物理页号(PPN)部分(位 10 到 53)。这意味着,对于一个不存在于物理内存中的页面,我们有足足 44 个比特位(位 10–53)可以自由使用,用来存储我们想要的任何信息(例如磁盘偏移、swap 槽号、状态位等)。
设计思路(示例)
-
当页面 不存在于内存(
PTE_V == 0)时,使用 PPN 区域的若干位存储元信息,例如PTE_ONDISK、PTE_SWAPPED,以及磁盘偏移或 swap slot 编号等。 -
当页面 存在于内存(
PTE_V == 1)时,仍然使用常规的低位(例如 R/W/X/U 等)以及可用的 RSW 位(如果需要)来表示其他软件标志(例如PTE_COW)。
代码示例(kernel/riscv.h)
// ... Standard flags ...
#define PTE_COW (1L << 8) // For PRESENT pages (V=1)
// --- Flags for NON-PRESENT pages (when V=0) ---
// We can use bits from the PPN space (10 and up) because hardware ignores them.
#define PTE_ONDISK (1L << 10) // Page is on disk (from executable)
#define PTE_SWAPPED (1L << 11) // Page is in the swap file
// other bits in 10. can store disk offset / swap slot, etc.
usertrap() 中的处理逻辑(伪代码)
// In usertrap()
pte_t *pte = walk(p->pagetable, va, 0);
if ((*pte & PTE_V) == 0) { // Page is NOT present in memory
if ((*pte & PTE_SWAPPED) != 0) {
// This is a swapped-out page.
// Handle swapping in from swap file.
} else if ((*pte & PTE_ONDISK) != 0) {
// This is a demand-paging page.
// Handle loading from the executable file.
// The rest of the PPN bits can store the disk offset.
// uint64 disk_offset = (*pte >> 12);
} else {
// This is a true segfault (e.g., lazy allocation fault or invalid access).
// Handle lazy allocation or kill the process.
}
} else { // Page IS present in memory (PTE_V == 1)
if ((*pte & PTE_COW) != 0) {
// This is a COW fault.
// Handle it.
}
}
优点
-
空间巨大(44 位 PPN 可被利用),非常灵活且可扩展。
-
各种状态互不干扰,便于存放偏移量、swap slot 编号等元信息。
-
代码可读性好,便于调试和以后扩展(如增加
PTE_SWAPPED、PTE_FILEBACKED等)。
方法二:复用比特位(按 PTE_V 的值解释同一比特位)
核心思想 分析自定义标志的互斥性,将同一物理比特在不同 PTE_V 情形下赋予不同语义。比如用 同一位(如 bit 8) 表示两种不同含义:
-
当
PTE_V == 1且 bit8 == 1:表示PTE_COW(页面在内存且为 COW)。 -
当
PTE_V == 0且 bit8 == 1:表示PTE_ONDISK(页面不在内存且在磁盘/可执行文件中)。
设计思路
-
只用一个自定义位(例如 bit 8)作为“多态”标志。
-
在处理缺页或页面相关逻辑时,先检查
PTE_V,再根据PTE_V的值解释该位。
usertrap() 中的处理逻辑(伪代码)
// In usertrap()
pte_t *pte = walk(p->pagetable, va, 0);
uint64 custom_flag_bit = *pte & (1L << 8);
if ((*pte & PTE_V) == 0) { // Page is NOT present
if (custom_flag_bit != 0) {
// Bit 8 is 1, and page is not present -> This is an ONDISK page.
// Handle demand paging.
} else {
// Handle lazy allocation or segfault.
}
} else { // Page IS present
if (custom_flag_bit != 0) {
// Bit 8 is 1, and page is present -> This is a COW page.
// Handle COW fault.
}
}
优点与权衡
-
优点:节省比特位,适合比特位极度紧张的场景。
-
缺点:逻辑相对复杂,代码可读性和维护性差于方法一;未来增加状态可能更困难且容易出错。
结论与建议(按您提供的内容)
对于您的项目,方法一(利用无效 PTE 的 PPN 空间)更推荐,因为它:
-
更清晰:每个标志都有自己独立的比特位,代码可读性更高。
-
扩展性好:未来想为无效页面增加更多状态(如
PTE_SWAPPED),只需在 PPN 空间中再找一个比特位即可,互不干扰。