从 xv6 过渡到 Linux 0.11 / Bridge From xv6 To Linux 0.11
1. 这篇文章解决什么问题
学完 xv6 后,很多人会转向 Linux 0.11。
这个过渡很自然。
xv6 小而清晰。
Linux 0.11 更接近真实 Unix 历史。
但直接读 Linux 0.11 容易迷路。
原因不是 Linux 0.11 太大。
原因是它的抽象更拥挤。
xv6 的 struct proc 很集中。
Linux 0.11 的 task_struct 包含更多体系结构细节。
xv6 的调度器直接扫描进程表。
Linux 0.11 的 schedule() 带有时间片和信号处理。
xv6 的系统调用路径基于 RISC-V ecall。
Linux 0.11 的系统调用路径基于 x86 int 0x80。
本篇的目标不是完整讲 Linux 0.11。
目标是搭桥。
桥的意思是:用 xv6 里已经理解的概念去定位 Linux 0.11 中的对应物。
1.1 本文基础词小注释
Linux 0.11:早期 Linux 内核版本,代码量比现代 Linux 小得多,但比 xv6 更接近真实历史系统。学习它适合放在 xv6 之后。
task_struct:Linux 0.11 里的进程描述结构。你可以先把它粗略对照 xv6 的 struct proc,但它包含更多 x86 相关字段和调度字段。
schedule:Linux 0.11 的调度函数。它和 xv6 的 scheduler() 都负责选择下一个任务,但 Linux 0.11 的策略和历史包袱更多。
x86:Linux 0.11 面向的 CPU 架构。xv6-riscv 用 RISC-V。不同架构会影响 trap、系统调用、页表、特权级切换的具体代码。
xv6 concept Linux 0.11 entry
----------- ----------------
struct proc ---> task_struct
scheduler ---> schedule
ecall/trap ---> int 0x80 / trap gate
Sv39 page table ---> x86 segmentation + paging对照学习:不是机械找“一模一样”。更好的方法是先找相似职责,再承认差异。例如二者都有进程结构,但字段布局、调度策略、硬件机制都不同。
桥接 bridge:本篇所谓桥接,是帮你从 xv6 的清晰模型迁移到 Linux 0.11 的复杂模型。它不是替代 Linux 0.11 源码阅读。
2. 最简单的对照图
先看一张粗图:
+----------------------+ +--------------------------+
| xv6-riscv | | Linux 0.11 |
+----------------------+ +--------------------------+
| struct proc | <----> | struct task_struct |
| scheduler() | <----> | schedule() |
| ecall/usertrap | <----> | int 0x80/system_call |
| bio.c buffer cache | <----> | buffer.c buffer cache |
| fs.c inode/log | <----> | inode/super/block logic |
| RISC-V Sv39 | <----> | x86 segmentation+paging |
+----------------------+ +--------------------------+这张图不是说它们完全相同。
它只是建立入口。
学习迁移时,先找相似处。
再看差异处。
最后回到各自的设计约束。
3. 本篇涉及的 xv6 源码文件
kernel/proc.h 定义 struct proc 和进程状态。
kernel/proc.c 定义进程创建、退出、等待、调度。
kernel/swtch.S 保存和恢复内核上下文。
kernel/trap.c 处理系统调用和中断。
kernel/trampoline.S 处理用户态和内核态切换。
kernel/syscall.c 分派系统调用。
kernel/sysproc.c、kernel/sysfile.c 提供具体系统调用入口。
kernel/bio.c 实现 buffer cache。
kernel/buf.h 定义 struct buf。
kernel/fs.c 实现 inode 和目录。
kernel/file.c 实现文件对象和引用计数。
kernel/virtio_disk.c 连接块设备。
kernel/vm.c 实现 RISC-V 页表。
本篇会提到 Linux 0.11 的典型文件。
例如 include/linux/sched.h。
例如 kernel/sched.c。
例如 kernel/system_call.s。
例如 fs/buffer.c。
例如 fs/inode.c。
具体路径可能随整理版本略有差异。
请以你手边 Linux 0.11 源码为准。
4. 从 proc 到 task_struct
xv6 的进程结构叫 struct proc。
Linux 0.11 的进程结构叫 task_struct。
两者都是进程控制块。
进程控制块保存内核管理进程所需的信息。
xv6 的 struct proc 包含状态。
它包含 pid。
它包含父进程。
它包含页表。
它包含 trapframe。
它包含内核上下文。
它包含打开文件。
它包含当前目录。
Linux 0.11 的 task_struct 也包含类似信息。
但它还明显包含 x86 相关内容。
例如 TSS。
TSS 是 Task State Segment。
它是 x86 架构用于任务状态和特权级栈切换的结构。
Linux 0.11 还包含 LDT。
LDT 是 Local Descriptor Table。
它是 x86 分段机制的一部分。
这就是从 xv6 到 Linux 0.11 的第一个差异。
xv6-riscv 的进程结构偏“操作系统概念”。
Linux 0.11 的进程结构同时暴露了更多 x86 机制。
5. 进程状态对照
xv6 常见状态:
UNUSED
USED
SLEEPING
RUNNABLE
RUNNING
ZOMBIELinux 0.11 常见状态包括:
TASK_RUNNING
TASK_INTERRUPTIBLE
TASK_UNINTERRUPTIBLE
TASK_ZOMBIE
TASK_STOPPEDTASK_RUNNING 不完全等于“正在 CPU 上运行”。
它更接近“可以运行或正在运行”。
xv6 则把 RUNNABLE 和 RUNNING 分开。
这是阅读差异之一。
不要机械翻译状态名。
要问调度器如何解释这个状态。
状态含义由调度代码定义。
状态不是孤立枚举。
6. 进程表组织方式
xv6 使用固定大小数组:
struct proc proc[NPROC]调度器扫描这个数组。
Linux 0.11 也有任务数组的味道。
它使用 task[NR_TASKS] 这样的结构组织任务。
这与现代 Linux 的链表、红黑树、CFS 差别很大。
所以 Linux 0.11 对 xv6 学习者很友好。
它仍然能看见简单扫描。
但 Linux 0.11 已经加入时间片、优先级、信号等因素。
因此它不是 xv6 的简单放大版。
它是更真实的早期 Unix-like 内核。
7. 从 scheduler() 到 schedule()
xv6 的 scheduler() 是每个 CPU 的调度循环。
它不断扫描进程表。
找到 RUNNABLE 进程。
设置为 RUNNING。
通过 swtch 切换到进程上下文。
进程让出 CPU 后再切回来。
Linux 0.11 的 schedule() 更像一个被调用的调度函数。
它选择下一个可运行任务。
它考虑 counter。
counter 可以理解为剩余时间片。
时间片是进程本轮还能运行多久的预算。
Linux 0.11 还考虑 priority。
priority 是静态优先级或权重。
当所有 counter 都耗尽时,Linux 0.11 会重新计算 counter。
粗略图:
xv6:
scheduler loop -> find RUNNABLE -> swtch
Linux 0.11:
schedule -> scan task[] -> choose max counter runnable task -> switch_to8. 调度策略的差异
xv6 调度策略非常朴素。
它接近轮询式。
它主要用于教学。
Linux 0.11 已经体现时间片调度。
时间片让 CPU 分配更可控。
counter 大的任务更可能被选中。
counter 用完后会衰减并补充。
这带来两个学习点。
第一,调度器不是只处理“能不能运行”。
它还处理“谁更应该运行”。
第二,调度策略和进程状态不同。
状态回答资格。
策略回答选择。
9. 上下文切换对照
xv6 的上下文结构在 kernel/proc.h。
它保存 ra、sp 和 callee-saved registers。
callee-saved registers 是调用约定要求被调用者保持的寄存器。
swtch.S 保存旧上下文并恢复新上下文。
Linux 0.11 在 x86 上使用 switch_to。
它涉及寄存器保存。
它也涉及 TSS/LDT 等 x86 机制。
x86 的历史包袱更多。
RISC-V 的路径更直。
对照思路:
What must survive a context switch?
|
+-- kernel stack position
+-- return address or instruction point
+-- callee-saved registers
+-- address space related state
+-- architecture-specific task state用这个问题读两边,比逐行硬背更稳。
10. 系统调用路径对照
xv6-riscv 使用 ecall。
Linux 0.11 on x86 使用 int 0x80。
两者都是从用户态进入内核的受控入口。
RISC-V 路径:
user stub
|
v
ecall
|
v
trampoline.S uservec
|
v
trap.c usertrap
|
v
syscall.c syscallx86 Linux 0.11 路径:
user library
|
v
int 0x80
|
v
system_call.s
|
v
sys_call_table
|
v
sys_* function共同点:
用户态不能直接调用内核 C 函数。
必须通过特权级入口。
系统调用号决定分派目标。
参数按 ABI 约定传递。
返回值再按 ABI 约定放回寄存器。
11. trapframe 与寄存器保存
xv6 有显式 struct trapframe。
它保存用户寄存器。
系统调用参数保存在 a0 到 a5。
系统调用号保存在 a7。
Linux 0.11 的 x86 入口会在汇编中保存寄存器。
x86 传统上用 eax 放系统调用号。
参数可通过 ebx、ecx、edx 等寄存器传递。
不要死记某个寄存器。
要理解入口汇编保存了什么。
要理解 C 函数从哪里取参数。
要理解返回值写回哪里。
这是跨架构阅读系统调用的稳定方法。
12. x86 与 RISC-V 的大差异
RISC-V 是相对现代、简洁的 RISC 架构。
x86 是历史很长、兼容负担较重的 CISC 架构。
CISC 表示复杂指令集计算机。
RISC 表示精简指令集计算机。
Linux 0.11 的 x86 代码会出现分段。
分段是 x86 早期内存保护和地址转换机制。
它使用段选择子和描述符。
xv6-riscv 基本不让你处理这种历史复杂性。
RISC-V xv6 主要围绕页表和特权级。
x86 Linux 0.11 则同时涉及分段和分页。
这会让新读者觉得“多出来很多奇怪结构”。
不要恐慌。
先把它们归类为架构适配层。
13. 地址空间对照
xv6 每个进程有 pagetable。
exec 创建新用户页表。
fork 复制父进程地址空间。
Linux 0.11 也要管理进程地址空间。
但它受 x86 80386 分段分页模型影响。
你会看到基址、限长、页目录、页表等概念。
页目录是 x86 两级分页中的顶层结构。
页表是下一级结构。
xv6 RISC-V Sv39 是三级页表。
Linux 0.11 x86 是两级分页。
对照图:
xv6 RISC-V Sv39:
VA -> L2 -> L1 -> L0 -> PA
Linux 0.11 x86:
logical address -> segment translation -> linear address -> page directory -> page table -> physical address这张图是过渡关键。
Linux 0.11 不是只多一级页表。
它还多了分段到线性地址的阶段。
14. fork 对照
xv6 的 fork 做几件事。
分配 struct proc。
复制用户内存。
复制 trapframe。
复制打开文件引用。
复制当前目录引用。
设置父子关系。
让子进程 RUNNABLE。
Linux 0.11 的 fork 也要创建任务。
它会复制 task_struct。
它会设置 TSS/LDT。
它会复制或共享部分资源。
它会处理页表。
Linux 0.11 已经有写时复制的影子或实现。
写时复制叫 copy-on-write。
它表示 fork 时先共享物理页。
当某一方写入时再复制。
xv6 标准实现通常直接复制用户内存。
所以 xv6 更易懂。
Linux 0.11 更接近性能现实。
15. exec 对照
xv6 的 exec 读取 ELF。
Linux 0.11 读取的是当时支持的可执行格式。
早期 Linux 有 a.out 相关历史。
现代读者熟悉 ELF,但 Linux 0.11 的历史格式不同。
不要因为格式不同就迷路。
exec 的核心语义不变。
它替换当前进程的用户程序。
它通常保留 pid。
它通常保留部分进程属性。
它重建用户地址空间。
它设置入口地址和用户栈。
所以读 Linux 0.11 exec 时,先问:
程序文件怎么验证?
代码和数据怎么装入?
栈怎么布置?
入口地址写到哪里?
旧地址空间怎么释放?
16. wait/exit 对照
xv6 的 exit 把进程变成 ZOMBIE。
xv6 的 wait 回收 ZOMBIE 子进程。
Linux 0.11 也有退出和等待。
但它还涉及信号。
信号是内核通知进程事件的一种机制。
例如子进程退出可以通知父进程。
Linux 的父子关系和会话、进程组等概念更丰富。
xv6 只保留最核心模型。
过渡时要把基本模型抓牢:
child exits
|
v
exit status remains
|
v
parent waits
|
v
kernel releases task record然后再加 Linux 的信号和作业控制细节。
17. buffer cache 对照
xv6 的 buffer cache 在 kernel/bio.c。
它缓存磁盘块。
它保证同一块在缓存中只有一个有效 buffer。
它用引用计数和锁管理并发。
Linux 0.11 也有 buffer cache。
它同样缓存块设备数据。
但 Linux 0.11 的 buffer 管理更复杂。
它有 hash 队列。
它有 free list。
它有脏块。
脏块是内存中已修改但尚未写回磁盘的块。
xv6 的日志系统让写入路径更教学化。
Linux 0.11 的缓存更接近实际块设备性能需求。
对照问题:
如何按设备号和块号查找 buffer?
如何避免同一块出现两个缓存副本?
如何标记数据是否有效?
如何标记数据是否脏?
何时写回磁盘?
谁在等待 I/O 完成?
18. 文件系统对照
xv6 文件系统包含 inode、目录、日志、buffer cache。
inode 是文件元数据对象。
它记录类型、大小、链接数和数据块地址。
目录是特殊文件。
目录内容是名字到 inode 号的映射。
Linux 0.11 文件系统也围绕 inode。
但它的 VFS 抽象还很早期。
现代 Linux 的 VFS 更复杂。
Linux 0.11 适合作为中间阶段。
它比 xv6 更真实。
但还没有现代 Linux 那么庞大。
对照图:
pathname
|
v
directory lookup
|
v
inode
|
v
block mapping
|
v
buffer cache
|
v
disk driver这条链在 xv6 和 Linux 0.11 中都成立。
不同的是结构层数和缓存策略。
19. 文件描述符对照
xv6 进程有 ofile[NOFILE]。
每个元素指向 struct file。
struct file 再指向 inode、pipe 或 device。
Linux 0.11 也有进程打开文件表。
它会关联 file 结构和 inode。
核心思想一致:
文件描述符是进程局部的小整数。
file 对象是内核中的打开文件状态。
inode 是文件本体的元数据。
不要把 fd、file、inode 混成一个概念。
这三个层次在两个系统里都很重要。
20. 锁与并发对照
xv6-riscv 明确展示 spinlock 和 sleeplock。
它是多核教学系统。
Linux 0.11 诞生于更早的单处理器时代背景。
很多并发问题表现不同。
早期 Linux 代码中常见的是关中断、睡眠队列、引用计数等机制。
不要用现代 SMP Linux 的锁模型套 Linux 0.11。
也不要用 xv6 的锁命名机械套过去。
过渡时问同一个问题:
共享对象是什么?
不变量是什么?
谁可能并发访问?
等待条件是什么?
谁负责唤醒?
这个问题框架比术语更稳定。
21. 中断与异常对照
xv6 的 trap 统一处理系统调用、中断、异常。
scause 表明原因。
sepc 表明返回位置。
stval 提供异常相关地址或值。
Linux 0.11 x86 使用 IDT。
IDT 是 Interrupt Descriptor Table。
它保存中断和异常入口描述符。
系统调用通过 int 0x80 进入。
页错误通过 page fault 异常进入。
时钟中断通过中断入口进入。
概念桥:
RISC-V stvec <-> x86 IDT entry
RISC-V scause <-> x86 vector/error code
RISC-V sepc <-> x86 saved EIP
RISC-V sret <-> x86 iret名字不同。
角色相似。
22. 为什么不能逐字对应
桥不是翻译表。
桥只是帮助定位。
有些概念在一个系统中很显式。
在另一个系统中可能被架构机制隐藏。
有些概念在 xv6 中为了教学被简化。
在 Linux 0.11 中为了实际运行被扩展。
有些概念在 Linux 0.11 中存在历史限制。
现代系统已经换了实现。
所以读 Linux 0.11 时要避免两个极端。
不要觉得“这和 xv6 不一样,所以我全不懂”。
也不要觉得“这和 xv6 名字像,所以完全一样”。
正确姿势是相似定位,差异追问。
23. 建议阅读顺序
先用 xv6 复习进程生命周期。
再打开 Linux 0.11 的 sched.h。
找到 task_struct。
只标注字段类别。
不要一开始背字段。
再读 sched.c 的 schedule()。
重点看任务选择条件。
然后读系统调用入口汇编。
找到系统调用号如何分派。
再读 buffer cache。
观察块号查找和引用计数。
最后读文件系统路径。
把 pathname、inode、buffer 连接起来。
每一步都拿 xv6 做参照。
24. 对照阅读表:进程
问题:进程状态保存在哪里?
xv6:struct proc。
Linux 0.11:task_struct。
问题:当前进程如何表示?
xv6:每 CPU struct cpu 中的 proc。
Linux 0.11:有 current 相关机制。
问题:父子关系在哪里?
xv6:p->parent。
Linux 0.11:father 等字段。
问题:内核栈在哪里?
xv6:每进程 kernel stack。
Linux 0.11:与 task 结构和页布局关系紧密。
问题:地址空间在哪里?
xv6:pagetable。
Linux 0.11:页目录、LDT、内存管理字段共同参与。
25. 对照阅读表:调度
问题:谁能被选中?
xv6:RUNNABLE。
Linux 0.11:TASK_RUNNING 且 counter 等条件合适。
问题:选择标准是什么?
xv6:简单扫描。
Linux 0.11:选择 counter 最大的任务。
问题:何时重新计算预算?
xv6:没有复杂时间片预算。
Linux 0.11:counter 用尽后按 priority 重新计算。
问题:切换动作在哪里?
xv6:swtch。
Linux 0.11:switch_to。
问题:体系结构状态在哪里暴露?
xv6:较少。
Linux 0.11:TSS/LDT 更显眼。
26. 对照阅读表:系统调用
问题:用户态入口指令是什么?
xv6:ecall。
Linux 0.11:int 0x80。
问题:系统调用号在哪里?
xv6:a7。
Linux 0.11:通常在 eax。
问题:入口汇编做什么?
xv6:trampoline.S 保存寄存器并切页表。
Linux 0.11:system_call.s 保存寄存器并分派。
问题:分派表在哪里?
xv6:syscalls[]。
Linux 0.11:sys_call_table。
问题:返回值在哪里?
xv6:a0。
Linux 0.11:通常在 eax。
27. 对照阅读表:文件系统
问题:路径名怎么解析?
xv6:namei、namex。
Linux 0.11:路径解析函数和 inode 查找。
问题:文件元数据在哪里?
xv6:struct inode。
Linux 0.11:struct m_inode 等。
问题:磁盘块怎么缓存?
xv6:struct buf 和 bcache。
Linux 0.11:buffer_head 和 buffer cache。
问题:写入如何保证一致性?
xv6:日志系统。
Linux 0.11:buffer 写回和文件系统自身规则。
问题:打开文件状态在哪里?
xv6:struct file。
Linux 0.11:file table / file structure。
28. 常见误解
误解一:Linux 0.11 是 xv6 的复杂版。
不准确。
它们目标不同,架构不同,历史语境不同。
误解二:task_struct 字段都必须先背下来。
不需要。
先按职责分类。
误解三:TASK_RUNNING 等于正在 CPU 上运行。
不总是。
要看调度器如何使用。
误解四:x86 分段可以忽略。
不能完全忽略。
Linux 0.11 的很多初始化和进程切换会碰到它。
误解五:buffer cache 只是性能优化。
不只是。
它还维护块的一致视图和 I/O 同步边界。
误解六:文件描述符就是 inode。
不是。
fd、file、inode 是三层概念。
29. 小实验:画进程字段映射
实验目标:建立 proc 到 task_struct 的地图。
先列 xv6 struct proc 字段。
给每个字段标注职责。
再列 Linux 0.11 task_struct 字段。
尝试归类:
identity
state
scheduling
parent-child
address space
open files
signal
architecture state不要追求一一对应。
追求知道字段属于哪类问题。
30. 小实验:比较调度选择
实验目标:理解 scheduler 和 schedule 的差异。
在 xv6 中阅读 scheduler()。
写出它选进程的条件。
在 Linux 0.11 中阅读 schedule()。
写出它选任务的条件。
比较:
是否考虑时间片。
是否考虑优先级。
是否处理信号。
是否扫描固定数组。
最后写一句总结:哪个系统更教学,哪个系统更实际。
31. 小实验:追踪系统调用号
实验目标:跨架构比较系统调用入口。
在 xv6 中追踪 write。
确认 a7 是系统调用号。
在 Linux 0.11 中追踪一个系统调用宏或汇编入口。
确认 eax 或相应寄存器如何放调用号。
画出两条路径。
把共同点标为“机制”。
把寄存器和入口指令标为“架构细节”。
这能训练你区分本质和实现。
32. 小实验:比较 buffer cache
实验目标:理解缓存块的不变量。
先在 xv6 中找到 bget。
写下它如何查找已有 buffer。
写下它如何回收空闲 buffer。
再在 Linux 0.11 中找到 buffer 获取函数。
观察是否有 hash。
观察是否有 free list。
观察脏块如何表示。
核心问题:
同一磁盘块是否可能有两个内存副本?
如果不能,代码如何保证?
33. 小实验:文件系统路径追踪
实验目标:从文件名走到磁盘块。
在 xv6 中追踪 open("x")。
路径大致经过 sys_open、namei、ilock、readi、bmap、bread。
在 Linux 0.11 中追踪打开文件路径。
找出路径解析。
找出 inode 获取。
找出 block read。
最后画图:
name -> inode -> block number -> buffer -> disk如果能画出来,就说明桥搭起来了。
34. 学习迁移策略
第一步,用 xv6 给概念命名。
第二步,在 Linux 0.11 找对应入口。
第三步,列出相似职责。
第四步,列出架构差异。
第五步,列出历史差异。
第六步,用小实验验证一个路径。
第七步,回到源码补细节。
不要试图一口气读完整个 Linux 0.11。
系统学习最怕“全部都重要”。
先选一条路径读穿。
再横向扩展。
35. 本篇总结
xv6 给你的是清晰骨架。
Linux 0.11 给你的是更真实的早期 Unix-like 内核。
从 xv6 到 Linux 0.11 的桥不是名字映射。
它是问题映射。
进程问题映射到 task_struct。
调度问题映射到 schedule()。
系统调用问题映射到 system_call。
块 I/O 问题映射到 buffer cache。
文件名问题映射到 inode 和目录。
架构问题映射到 x86 分段、分页、TSS、LDT。
最终要记住:
same operating-system questions,
different historical and architectural answers.带着这个心智模型读 Linux 0.11,会比从第一行硬啃轻松很多。