2. xv6 源码结构:内核源码目录分析、Makefile 构建系统、关键头文件与数据结构预览
在深入 exec 与 fork 的协同机制之前,必须首先熟悉 xv6 的源码布局。本章将带领你剖析内核源码目录、理解 Makefile 构建系统的核心逻辑,并预览后续章节频繁使用的关键头文件与数据结构。掌握这些基础,是阅读和修改 xv6 源码的必备前提。
2.1 内核源码目录结构分析
xv6 的源码结构非常紧凑,所有核心代码均位于根目录下。下表列出了最重要的源文件及其职责:
| 文件名 | 类别 | 核心职责 |
|---|---|---|
kernel/ |
目录 | 内核核心代码,包含进程管理、内存管理、文件系统、设备驱动等 |
kernel/proc.c |
进程管理 | 实现 fork、exec、scheduler、sleep、wakeup 等核心进程控制函数 |
kernel/exec.c |
进程加载 | 实现 exec 系统调用,负责解析 ELF 文件格式并加载用户程序 |
kernel/syscall.c |
系统调用 | 系统调用分发器,将用户态的系统调用号映射到对应的内核处理函数 |
kernel/sysproc.c |
系统调用实现 | 实现与进程相关的系统调用,如 sys_fork、sys_exec、sys_exit 等 |
kernel/vm.c |
内存管理 | 实现页表管理、虚拟地址空间布局、uvmalloc、uvmcopy 等内存操作 |
kernel/fs.c |
文件系统 | 实现文件系统核心逻辑,包括 inode 管理、路径解析、文件读写 |
kernel/file.c |
文件描述符 | 管理文件描述符表,实现 open、close、read、write 等文件操作 |
kernel/defs.h |
头文件 | 全局函数声明,所有内核模块的公共接口定义 |
kernel/param.h |
头文件 | 系统参数常量,如最大进程数 NPROC、最大文件数 NFILE 等 |
学习提示: 在后续章节中,你将频繁在
kernel/proc.c、kernel/exec.c 和 kernel/vm.c 之间跳转。建议提前打开这三个文件,熟悉它们的函数列表。
2.2 Makefile 构建系统解析
xv6 使用 GNU Make 作为构建系统。其 Makefile 设计精巧,能够自动检测源文件变化并增量编译。核心构建流程如下:
- 编译内核:将
kernel/目录下的所有.c和.S文件编译为.o目标文件。 - 链接内核:使用链接脚本
kernel/kernel.ld将所有目标文件链接为 ELF 格式的内核镜像kernel/kernel。 - 编译用户程序:将
user/目录下的用户程序编译为独立的 ELF 可执行文件。 - 生成文件系统镜像:将所有用户程序打包进
fs.img文件系统镜像中。 - 启动模拟器:通过
make qemu启动 QEMU 模拟器,加载内核镜像和文件系统镜像。
以下 SVG 图展示了 Makefile 构建系统的完整流程:
关键命令速查:
make:编译内核和所有用户程序,生成fs.img。make qemu:编译并启动 QEMU 模拟器。make clean:清理所有编译产物。make CPUS=1 qemu:使用单核 CPU 启动,便于调试。
2.3 关键头文件与数据结构预览
在 exec 与 fork 的协同过程中,以下几个头文件和数据结构是理解代码的关键:
2.3.1 kernel/proc.h — 进程控制块
进程控制块 struct proc 是 xv6 中最重要的数据结构之一。它描述了进程的所有状态信息:
// kernel/proc.h
struct proc {
struct spinlock lock; // 进程锁,保护以下字段
enum procstate state; // 进程状态:UNUSED, USED, SLEEPING, RUNNABLE, RUNNING, ZOMBIE
struct proc *parent; // 父进程
void *chan; // 如果非0,进程正在chan上睡眠
int killed; // 如果非0,进程已被标记为终止
int xstate; // 进程退出状态
int pid; // 进程ID
struct runqueue *runq; // 运行队列(xv6-riscv 中为 struct cpu *cpu)
struct file *ofile[NOFILE]; // 打开的文件描述符表
struct inode *cwd; // 当前工作目录
pagetable_t pagetable; // 用户页表
uint64 sz; // 进程虚拟地址空间大小
uint64 trapframe_phys; // trapframe 的物理地址
struct trapframe *trapframe;// 保存用户态寄存器
struct context context; // 内核线程上下文(用于切换)
};
注意:
struct proc 中的 pagetable 字段是 fork 和 exec 协同工作的核心。在 fork 中,子进程会复制父进程的页表;在 exec 中,进程会丢弃旧页表并创建新页表。
2.3.2 kernel/elf.h — ELF 文件格式
ELF(Executable and Linkable Format)是 xv6 用户程序的文件格式。exec 系统调用需要解析 ELF 头来加载程序:
// kernel/elf.h
#define ELF_MAGIC 0x464C457FU // "\x7FELF" 魔数
struct elfhdr {
uint32 magic; // 魔数,必须等于 ELF_MAGIC
uint8 elf[12]; // ELF 标识信息
uint16 type; // 文件类型(ET_EXEC = 2)
uint16 machine; // 目标架构(RISC-V = 243)
uint32 version; // 版本号
uint64 entry; // 程序入口点
uint64 phoff; // 程序头表偏移
uint64 shoff; // 节头表偏移
uint32 flags; // 标志位
uint16 ehsize; // ELF 头大小
uint16 phentsize; // 程序头表项大小
uint16 phnum; // 程序头表项数量
uint16 shentsize; // 节头表项大小
uint16 shnum; // 节头表项数量
uint16 shstrndx; // 节名字符串表索引
};
struct proghdr {
uint32 type; // 段类型(PT_LOAD = 1)
uint32 flags; // 段标志(可读、可写、可执行)
uint64 off; // 段在文件中的偏移
uint64 vaddr; // 段在虚拟地址中的起始地址
uint64 paddr; // 段在物理地址中的起始地址(通常与vaddr相同)
uint64 filesz; // 段在文件中的大小
uint64 memsz; // 段在内存中的大小
uint64 align; // 对齐要求
};
2.3.3 kernel/vm.c — 虚拟内存核心函数
虚拟内存管理是 fork 和 exec 的底层支撑。以下函数将在后续章节中频繁出现:
| 函数名 | 功能描述 | 在 fork/exec 中的角色 |
|---|---|---|
uvmcopy |
复制父进程的页表到子进程 | fork 的核心步骤,实现写时复制(COW)的基础 |
uvmalloc |
分配用户虚拟内存 | exec 加载程序段时分配内存 |
uvmdealloc |
释放用户虚拟内存 | exec 释放旧进程地址空间 |
walkaddr |
根据虚拟地址查找物理地址 | 用于 exec 加载数据到用户空间 |
mappages |
建立虚拟地址到物理地址的映射 | 页表操作的基础函数 |
以下 SVG 图展示了 fork 与 exec 协同工作时涉及的核心数据结构关系:
2.4 本章小结
通过本章的学习,你已经掌握了 xv6 源码的整体布局:
- 目录结构:
kernel/目录包含所有内核代码,user/目录包含用户程序。 - 构建系统:Makefile 自动管理编译、链接和镜像生成,
make qemu一键启动。 - 关键数据结构:
struct proc是进程的“身份证”,elfhdr和proghdr是 ELF 文件的“地图”,pagetable_t是虚拟内存的“翻译官”。