CH4 补充读物:Threads(先看当前代码里“执行流”的基础设施)¶
1. 这份读物怎么用¶
这份文档是给你课后独立看进程代码用的,不是课堂提纲。
读完后,你应该能回答下面 7 个问题:
- 当前仓库真正被调度的对象是什么?
- 一个“执行流”至少需要保存哪些现场?
- 为什么
context、trapframe和栈都不能少? - 如果以后把这套内核扩展为线程内核,哪些资源应该共享,哪些应该私有?
- 为什么
CPUS=1和CPUS=2能帮助你区分并发与并行? - 为什么线程章节还必须读
sleep/wakeup与锁? - 当前仓库为什么还不能算“完整线程实现”?
2. 先把边界说清:当前仓库还没有完整线程实现¶
直接先说结论:
- 当前用户态没有
pthread_*线程库接口。 kernel/core/proc.c的scheduler()目前按struct proc调度。- 当前也没有“同一地址空间内多个执行流分别拥有独立用户栈和
trapframe”的实现。
这一章的正确读法
读 CH4 时,不要去代码里硬找一个现成的“线程子系统”。更有价值的问题是:PPT 里说线程需要哪些底层能力,而当前仓库已经在哪些文件里把这些能力的雏形实现出来了。
也就是说,这一章的目标不是证明“仓库已经实现了 pthread”,而是帮你看清:
- 线程为什么比进程更轻。
- 一个可调度执行流至少需要什么状态。
- 当前代码如果将来要支持线程,最可能从哪里改起。
3. 线程的最小抽象:一个执行流至少要有什么¶
PPT 里说线程至少要有:
- 程序计数器(program counter)。
- 一组寄存器。
- 一段自己的栈。
- 可被调度、阻塞和恢复执行的状态。
把这几项落到当前代码里,对应关系很清楚:
kernel/include/proc.h:struct context:保存内核态调度切换时的寄存器现场。kernel/include/proc.h:struct trapframe:保存用户态陷入内核时的寄存器现场与epc。kstack_base/kstack_top:每个当前可调度实体自己的内核栈。state/chan:执行流当前是否RUNNABLE、RUNNING、SLEEPING等。
所以,虽然当前仓库主要以“进程”为单位调度,但线程所需的最核心底座——执行现场 + 栈 + 状态机——你已经能直接在代码里看到。
4. struct proc 现在扮演了“可调度执行实体”的角色¶
理解 CH4 时,最重要的一步是接受一个现实:
- 理论上,thread 是调度单位。
- 在当前仓库里,真正进入
scheduler()的对象是struct proc。 - 因此
struct proc现在同时承载了“进程资源容器”和“执行流描述符”两部分职责。
你可以直接看这些字段:
pagetable、sz:地址空间。ofile[]、cwd:打开文件与当前目录。context:调度切换上下文。trapframe:用户态返回现场。state、chan、killed:调度与阻塞状态。
为什么这和线程章节有关
在线程系统里,往往需要把“进程共享资源”和“线程私有执行现场”拆开;而当前 struct proc 恰好把这两类信息放在了一起。看懂这一点,你就知道未来若要实现线程,第一步往往就是“把该共享的和该私有的拆分出来”。
5. 如果把当前代码扩展成线程系统,哪些东西该共享¶
你可以把下面这张表当作“CH4 理论和当前代码的对照表”:
| 当前代码里的对象/字段 | 在当前仓库中的归属 | 如果扩展为同进程多线程,通常应如何理解 |
|---|---|---|
pagetable、sz |
每个 struct proc 一份 |
同一进程内多个线程通常共享 |
ofile[]、cwd |
每个 struct proc 一份 |
同一进程内多个线程通常共享 |
trapframe |
每个 struct proc 一份 |
每个线程应各自拥有 |
context |
每个 struct proc 一份 |
每个线程应各自拥有 |
内核栈 kstack_* |
每个 struct proc 一份 |
每个线程应各自拥有 |
state、chan |
每个 struct proc 一份 |
调度时仍应按线程分别维护 |
这张表能帮你把 PPT 中“线程共享什么、私有什么”的理论,真正落到当前代码字段上。
6. 并发与并行:先用当前调度器看懂,再回头理解线程¶
虽然当前调度的是进程,不是线程,但 CH4 里的 concurrency / parallelism 区分依然可以直接观察。
你可以尝试:
进入系统后运行:
再用多 hart 运行:
理解重点:
CPUS=1时,多个执行流只能交错推进,这更接近并发。CPUS=2时,不同 hart 可以同时执行不同RUNNABLE实体,这才接近并行。kernel/core/proc.c:scheduler()是每个 hart 自己在跑的调度循环。
所以 CH4 里“多线程能利用多核”这句话,在当前仓库里虽然还要先借助多进程来观察,但硬件层和调度层的区别已经能看清楚了。
7. 上下文切换:这是理解线程的第一现场¶
建议你直接连读下面 4 个位置:
kernel/include/proc.h中的struct contextkernel/arch/riscv/swtch.Skernel/core/proc.c:sched()kernel/core/proc.c:scheduler()
你要看明白的是:
scheduler()选中一个RUNNABLE实体。swtch()保存当前内核上下文,恢复目标实体的上下文。- 目标实体继续在自己的内核栈上执行。
- 若该实体之后
yield()或sleep(),又会切回调度器。
这正是 PPT 中“线程切换比进程切换轻”的前置知识。即使你现在看到的还是进程级切换,也已经能理解执行流切换的本质。
8. 阻塞与唤醒:线程系统必须依赖的另一半基础设施¶
线程章节不能只看“创建”,还必须看“何时阻塞、如何恢复”。
当前仓库最值得读的接口是:
kernel/core/proc.c:sleep(chan, lk)kernel/core/proc.c:wakeup(chan)kernel/core/trap.c:ticks_sleep()kernel/core/pipe.c:pipewrite()/piperead()kernel/core/sleeplock.c:acquiresleep()
它们共同说明了一件事:
- 执行流并不是一直忙等。
- 条件不满足时,可以把自己置为
SLEEPING并让出 CPU。 - 条件满足后,再由别人把它唤醒回
RUNNABLE。
为什么 CH4 一定要读这部分
无论你以后实现的是内核线程、用户线程还是线程池,只要系统里存在多个执行流,就一定绕不开阻塞、唤醒和同步。sleep/wakeup 正是这套机制在当前仓库里的最小核心。
9. 锁为什么在 CH4 里反而更重要¶
线程比进程更容易共享状态,所以线程章节和锁天然绑在一起。
在当前仓库里:
kernel/core/spinlock.c提供短临界区的自旋锁。kernel/core/sleeplock.c提供会阻塞等待的睡眠锁。pipe.c、proc.c、文件系统路径里都在依赖这些锁保护共享状态。
你至少要建立两个判断:
- 共享状态越多,同步压力越大。
- 线程“轻”并不代表并发程序“简单”;恰恰相反,共享越方便,越容易出错。
10. 把 Pthreads 和当前仓库放在一起时,应该怎么类比¶
理论课会讲 pthread_create()、pthread_join() 等 API,但当前仓库没有现成用户线程库,所以正确姿势是“类比,不等同”。
可以先这样记:
| 线程世界的概念 | 当前仓库里最接近的类比物 | 关键差别 |
|---|---|---|
pthread_create() |
fork() |
fork() 会复制出新地址空间,不是同进程线程 |
pthread_join() |
wait() |
wait() 回收的是子进程,不是同进程线程 |
| 线程私有执行现场 | context + trapframe + 栈 |
当前这些都仍挂在 struct proc 上 |
| 线程共享进程资源 | pagetable、ofile[]、cwd |
当前没有“多个执行流共享这些字段”的实现 |
所以读 CH4 时,真正应该建立的是“线程语义与当前进程语义的边界感”。
11. 线程模型在当前仓库里应该怎么理解¶
PPT 里的 many-to-one、one-to-one、many-to-many,本质上都在讨论:
- 用户线程由谁管理。
- 内核调度器到底看见谁。
- 阻塞语义最终落在哪一层。
而当前仓库的状态是:
- 没有用户线程库,因此谈不上现成的
many-to-one。 - 调度器直接面向
struct proc,也没有“一个进程内多个内核线程共享地址空间”的现成实现。 - 所以它更适合作为“线程底层机制的准备阶段”来理解,而不是某种完整线程模型的成品。
如果以后要把它扩展成接近 one-to-one 的线程实现,最可能的改动点会是:
- 让多个执行实体共享同一个
pagetable和大部分进程资源。 - 为每个线程单独分配
context、trapframe、用户栈和内核栈。 - 重新设计
exit()/wait()一类接口,区分“线程退出”和“进程整体退出”。
12. 建议的代码阅读主线¶
按下面顺序读,会最顺:
kernel/include/proc.hkernel/core/proc.c中的yield()、sleep()、wakeup()、scheduler()kernel/arch/riscv/swtch.Skernel/core/trap.ckernel/core/spinlock.ckernel/core/sleeplock.c
如果你已经读过 CH3,再把 fork()、wait() 和这里的“线程类比”对照起来看,效果会更好。
13. 最常见误解¶
- 以为 CH4 就一定能在当前仓库里找到完整
pthread实现。 - 把“进程是资源容器”和“线程是执行流”这两个层次混为一谈。
- 把
context和trapframe当成同一种现场。 - 以为“多执行流”只需要创建接口,不需要阻塞、唤醒和锁。
- 以为“线程更轻”就等于“并发程序更容易写对”。
14. 最小自测(仅自检)¶
你可以问自己这 7 个问题:
- 当前仓库真正被
scheduler()调度的对象是什么? context和trapframe分别保存哪一层的现场?- 为什么每个可调度实体都需要自己的栈?
- 如果多个线程共享同一地址空间,
pagetable应该归谁? sleep/wakeup为什么是线程系统的底层基础设施?- 当前仓库为什么还不能算完整的线程内核?
- 如果以后要实现线程,
struct proc里哪些字段最可能被拆成“共享部分”和“线程私有部分”?
如果这些问题都能结合源码回答,CH4 的核心就真正掌握了。