一些概念:COW、Lazy Allocation 和 Demand Paging

好的,我来帮你把这三个特性 分别是什么、解决什么问题、以及实现思路 梳理清楚,形成一个更清晰的结构。


1. 写时复制 (Copy-on-Write, COW)

是什么

写时复制是一种优化 fork() 的技术。正常情况下,父进程在调用 fork() 时会把整个用户内存空间都复制一份给子进程。但在大多数情况下,子进程很快就会调用 exec() 替换自己的地址空间,这导致之前的复制工作大部分浪费。

COW 的思想是:父子进程先共享同一份物理内存,只有在某一方真的要写这块内存时,才复制出一个新的物理页。

作用

  • 避免了无谓的内存复制,减少 fork() 的开销。
  • 节省物理内存(父子进程在读的情况下共享数据)。

实现思路

  1. 修改 fork()

    • 不再复制物理页,而是让父子进程的页表都指向同一份物理页。
    • 把这些页的 写权限去掉(PTE_W 清零),并设置一个自定义标志(如 PTE_COW)。
  2. 增加物理页引用计数

    • 每个物理页维护一个 ref_count
    • fork() 时,父子进程共享页,引用计数加一。
  3. 在缺页异常中处理 COW

    • 如果进程写一个只读的 COW 页,触发缺页异常。
    • 检查 PTE 是否带有 PTE_COW。
    • 如果是:
      • 如果引用计数 > 1:分配一个新物理页,复制内容,更新页表。
      • 如果引用计数 == 1:直接恢复写权限即可。
  4. 释放内存时配合引用计数

    • kfree() 中,只有当引用计数为 0 时,才真正释放物理页。

2. 懒加载 (Lazy Allocation)

是什么

正常的 sbrk() / malloc() 在用户申请内存时,内核会立刻分配物理页并映射。

懒加载的思想是:当用户申请时,不立即分配物理内存,只更新进程的地址空间大小。真正访问这块地址时(触发缺页异常),才去分配物理页。

作用

  • 避免申请了大量内存但未使用时浪费物理内存。
  • 内存使用更高效。

实现思路

  1. 修改 sbrk() / growproc()

    • 当增加内存时,只修改 p->sz,不调用 uvmalloc()
  2. 缺页中断中处理

    • 当访问到尚未分配物理页的地址,会触发缺页异常。
    • 检查异常地址 va
      • 是否在 p->sz 范围内。
      • 是否尚未映射。
    • 如果合法:
      • 调用 kalloc() 分配物理页,并清零。
      • 在页表中建立映射,赋予 PTE_R|PTE_W|PTE_U

3. 按需分页 (Demand Paging)

是什么

在 xv6 的默认实现里,exec() 会一次性把程序的所有代码段和数据段加载到内存中,即使程序只用到其中一小部分。

按需分页的思想是:只在需要的时候才把文件内容加载进内存。启动时只建立“占位”的页表项,等访问到那一页时触发缺页,再从文件中读取对应的内容。

作用

  • 加快程序启动速度(不用一次性加载整个程序)。
  • 减少内存使用(只加载真正用到的部分)。

实现思路

  1. 修改 exec() / loadseg()

    • 不实际分配物理页,也不把程序段内容立即读入内存。
    • 只为段建立 PTE,但:
      • PTE_V 清零(标记为无效)。
      • 设置一个自定义标志(如 PTE_DEMAND)。
      • 在 PTE 中保存该页在可执行文件中的偏移信息。
  2. 缺页中断中处理

    • 当访问无效页时,检查 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。

就是留下来可以使用的只有 89, 但是已经拿来分配了 8, 还有一个位置没有被使用, 就是如果我有多个需求的时候, 但是没有位置, 该怎么办?

两种可行方案

您的判断是正确的:如果我们把位 8 和 9 分别用作 PTE_COWPTE_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_ONDISKPTE_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_SWAPPEDPTE_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 空间)更推荐,因为它:

  1. 更清晰:每个标志都有自己独立的比特位,代码可读性更高。

  2. 扩展性好:未来想为无效页面增加更多状态(如 PTE_SWAPPED),只需在 PPN 空间中再找一个比特位即可,互不干扰。