3. 进程数据结构:struct proc 详解
在 xv6 内核中,每个进程都由一个 struct proc 结构体表示。该结构体集中管理了进程的所有核心属性:状态、上下文、内存映射、文件描述符 以及 调度信息。理解这个结构体是掌握进程管理的第一步。
核心要点:
struct proc 是 xv6 进程控制块(PCB)的唯一实现。所有进程相关的系统调用(fork、exec、exit、wait)最终都在操作这个结构体的字段。
3.1 结构体全景图
下面这张 SVG 图展示了 struct proc 的完整字段布局,按功能划分为 5 个区域:
3.2 进程状态枚举
xv6 定义了 6 种进程状态,状态转换由调度器和系统调用驱动:
| 状态 | 枚举值 | 含义 | 典型转换 |
|---|---|---|---|
| UNUSED | 0 | 进程槽空闲,可被分配 | allocproc() → USED |
| USED | 1 | 进程已分配,但尚未就绪 | 初始化后 → RUNNABLE |
| SLEEPING | 2 | 进程因等待事件阻塞 | sleep() → RUNNABLE (wakeup) |
| RUNNABLE | 3 | 进程可被调度执行 | scheduler() → RUNNING |
| RUNNING | 4 | 进程正在 CPU 上执行 | yield() → RUNNABLE |
| ZOMBIE | 5 | 进程已退出,等待父进程回收 | exit() → wait() → UNUSED |
面试考点: ZOMBIE 状态是 xv6 进程管理的核心设计。子进程先 exit 变成 ZOMBIE,父进程调用 wait 读取退出状态后,内核才释放 proc 结构体。如果父进程不调用 wait,子进程会一直留在 ZOMBIE 状态,造成“僵尸进程”。
3.3 CPU 上下文:context 与 trapframe
xv6 使用两套寄存器保存机制,职责完全不同:
| 结构体 | 保存位置 | 保存内容 | 使用场景 |
|---|---|---|---|
| struct context | 内核栈底部 | 被调用者保存寄存器:ra, sp, s0-s11 | 进程调度切换(swtch 函数) |
| struct trapframe | 独立物理页 | 所有通用寄存器、CSR、epc、sp等 | 用户态↔内核态切换(中断/系统调用) |
下面这张 SVG 图展示了上下文切换时寄存器的流向:
3.4 内存管理字段
每个进程拥有独立的地址空间,由以下字段管理:
pagetable_t pagetable:指向进程页表的指针(RISC-V 页表基址寄存器 satp 的值)。fork 时子进程复制父进程的页表,exec 时重新创建页表。uint64 sz:进程堆的当前大小(从 0 到 sz 的虚拟地址空间已映射)。sbrk 系统调用通过增减 sz 来分配/释放堆内存。char *kstack:内核栈的虚拟地址。每个进程在内核态执行时使用独立的栈,防止互相干扰。uint64 trapframe_phys:trapframe 页的物理地址(用于内核页表映射)。
常见错误: 在 xv6 中,
kstack 是进程在内核态使用的栈,不是用户态栈。用户态栈位于进程虚拟地址空间的顶部(由 exec 设置)。初学者容易混淆这两个栈的角色。
3.5 文件描述符表
xv6 使用固定大小的文件描述符数组:
// kernel/proc.h
#define NOFILE 16
struct file *ofile[NOFILE]; // 打开文件表
struct inode *cwd; // 当前工作目录
char name[16]; // 进程名(调试用)
文件描述符表的关键行为:
- fork 时共享:子进程复制父进程的 ofile 数组,每个文件引用计数 +1。
- exec 时保留:exec 替换进程内存,但文件描述符表保持不变(实现 shell 重定向的基础)。
- exit 时关闭:进程退出时遍历 ofile,对每个打开的文件调用 fileclose。
- cwd 继承:子进程继承父进程的当前工作目录(inode 引用计数 +1)。
3.6 调度与同步字段
| 字段 | 类型 | 保护对象 | 使用方式 |
|---|---|---|---|
| lock | struct spinlock | 整个 proc 结构体 | 所有对 proc 字段的读写必须先 acquire 锁 |
| parent | struct proc* | 父进程指针 | exit 时通知父进程;wait 时读取子进程状态 |
| chan | int | 睡眠通道 | sleep(chan) 和 wakeup(chan) 的匹配标识 |
锁的粒度分析: xv6 使用粗粒度锁——每个进程一把锁。这意味着访问进程状态、修改文件描述符表、更新调度字段都需要持有同一把锁。优点是实现简单,缺点是并发度低。现代内核(如 Linux)使用细粒度锁,每个字段或每组字段独立加锁。
3.7 进程生命周期与字段变化
下面这张 SVG 图展示了进程从创建到销毁过程中关键字段的变化:
3.8 代码实现:struct proc 完整定义
以下是 xv6 中 struct proc 的完整定义(来自 kernel/proc.h):
// kernel/proc.h
struct proc {
struct spinlock lock;
// p->lock must be held when using these:
enum procstate state; // Process state
int pid; // Process ID
int killed; // If non-zero, have been killed
int xstate; // Exit status to be returned to parent's wait
int exit_code; // Exit code (set by exit syscall)
// p->lock is not held for these; they are exclusive to the process:
pagetable_t pagetable; // User page table
uint64 sz; // Size of process memory (bytes)
char *kstack; // Bottom of kernel stack for this process
uint64 trapframe_phys; // Physical address of trapframe page
// These are private to the process, so p->lock need not be held:
struct context context; // swtch() here to run process
struct trapframe *tf; // Trapframe for current syscall/interrupt
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
char name[16]; // Process name (debugging)
struct proc *parent; // Parent process
int chan; // If non-zero, sleeping on chan
};
代码注释解读: xv6 源码中的注释明确标注了哪些字段受
p->lock 保护,哪些字段是进程私有的。这种注释风格是阅读内核源码的重要线索——看到 "p->lock must be held" 就表示访问该字段前必须先 acquire 锁。