Skip to content
Gains Summary
Main Navigation 首页 / Home
C++ 编程 / C++ Programming
系统与高性能 / Systems & Performance
Web 开发 / Web Development
人工智能 / Artificial Intelligence
工业软件 / Industrial Software
其他内容 / Other Topics
C++ 编程 / C++系统与性能 / SystemsWeb 开发 / Web人工智能 / AI工业软件 / Industrial

外观

Sidebar Navigation

← 系统与高性能 / Systems & Performance

xv6 源码学习 / xv6 Source Learning

1. xv6 源码学习专题 / xv6 Source Learning

2. xv6 学习路线:从 C 指针到操作系统源码 / xv6 Learning Roadmap

3. 读 xv6 必备的 C 语言和指针基础 / C And Pointers For xv6

4. 读 xv6 必备的 RISC-V 基础 / RISC-V Basics For xv6

5. 如何阅读 xv6 源码 / How To Read xv6 Source

6. xv6 启动与内核入口 / xv6 Boot And Kernel Entry

7. xv6 内存布局与页表 / xv6 Memory Layout And Page Tables

8. xv6 trap、中断与系统调用 / xv6 Traps Interrupts And System Calls

9. xv6 进程与 struct proc / xv6 Process And struct proc

10. xv6 上下文切换与调度器 / xv6 Context Switch And Scheduler

11. xv6 sleep/wakeup、wait 与 exit / xv6 Sleep Wakeup Wait And Exit

12. xv6 锁与并发 / xv6 Locks And Concurrency

13. xv6 文件描述符与 inode / xv6 File Descriptor And Inode

14. xv6 文件系统与日志 / xv6 File System And Log

15. xv6 设备驱动、console、UART 与磁盘 / xv6 Device Driver Console UART And Disk

16. xv6 用户程序与 shell / xv6 User Programs And Shell

17. 用 QEMU/GDB 调试 xv6 / Debugging xv6 With QEMU And GDB

18. 从 xv6 过渡到 Linux 0.11 / Bridge From xv6 To Linux 0.11

19. xv6 专题总结与实践项目 / xv6 Summary And Practice Projects

本页目录

xv6 sleep/wakeup、wait 与 exit / xv6 Sleep Wakeup Wait And Exit ​

1. 这篇文章解决什么问题 ​

进程不总是在运行。

它可能等待输入。

可能等待子进程退出。

可能等待磁盘 I/O。

也可能已经退出,但父进程还没回收。

xv6 用几组机制处理这些情况:

text
sleep / wakeup
  |
  +-- wait for an event

exit
  |
  +-- terminate current process

wait
  |
  +-- parent waits and reaps child

kill
  |
  +-- mark process as killed and wake if needed
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

这篇文章聚焦:

  • chan 是什么。
  • 为什么睡眠要和锁配合。
  • lost wakeup 是什么。
  • exit() 为什么产生 ZOMBIE。
  • wait() 如何回收子进程。
  • 父进程退出后子进程怎么办。

1.1 本文基础词小注释 ​

sleep:这里不是让 CPU 睡觉,而是让“当前进程”暂停运行。进程进入 SLEEPING 状态后,调度器不会选它,直到某个事件把它唤醒。

wakeup:唤醒不是立刻运行。wakeup(chan) 通常只是把睡在某个 chan 上的进程改成 RUNNABLE,表示它以后可以被调度器选中。

text
----------+      wakeup(chan)      +----------+
|SLEEPING | ---------------------> |RUNNABLE  |
+----------+                       +----------+
      |                                  |
      | not chosen by scheduler          | may be chosen later
1
2
3
4
5

chan:chan 是 channel 的缩写,但 xv6 里它不是 Go 语言那种通道。它本质上是一个“等待原因的标记”,通常用一个地址当标记。睡眠者说“我在等这个地址代表的事件”,唤醒者也用同一个地址去找人。

lost wakeup:丢失唤醒。意思是事件已经发生了,但等待者还没正确进入睡眠状态,结果唤醒信号错过了它,之后它可能永远睡下去。xv6 用锁把“检查条件”和“进入睡眠”连起来,避免这个问题。

wait:父进程等待子进程退出,并回收子进程留下的内核资源。这里的 wait 不只是等待,还包含“收尸式回收”,所以它会处理 ZOMBIE 子进程。

exit:当前进程结束自己。进程调用 exit() 后,不能直接把自己的所有东西立刻清掉,因为父进程还需要通过 wait() 看到它的退出状态。

ZOMBIE:不是正在运行的进程,也不是完全不存在的进程。它已经退出,但还留着一点记录,等父进程 wait() 来回收。

2. 为什么需要 sleep ​

如果进程等待事件时一直占用 CPU,会浪费资源。

例如管道为空。

读进程没有数据可读。

它应该睡眠。

text
pipe empty
  |
  v
reader cannot proceed
  |
  v
reader sleeps
  |
  v
writer writes data
  |
  v
writer wakes reader
1
2
3
4
5
6
7
8
9
10
11
12
13

睡眠不是结束。

睡眠表示进程暂时不可运行。

状态:

text
RUNNING
  |
  | sleep
  v
SLEEPING
  |
  | wakeup
  v
RUNNABLE
1
2
3
4
5
6
7
8
9

调度器不会选择 SLEEPING 进程。

只有变回 RUNNABLE,它才可能再次运行。

3. chan:等待通道 ​

chan 是进程正在等待的通道。

text
struct proc
  |
  +-- void *chan
1
2
3

它通常是某个内核对象地址。

可以理解成等待事件的标签。

text
process p
  |
  +-- state = SLEEPING
  +-- chan = X

wakeup(X)
  |
  +-- find sleeping processes with chan == X
  +-- set them RUNNABLE
1
2
3
4
5
6
7
8
9

图:

text
chan = pipe_read_addr
  |
  +-- reader A sleeps here
  +-- reader B sleeps here

wakeup(pipe_read_addr)
  |
  +-- wake A
  +-- wake B
1
2
3
4
5
6
7
8
9

chan 是 void *,说明它不关心具体类型。

它只用于比较。

这是内核中常见的轻量等待标识方式。

4. sleep(chan, lock) 为什么需要 lock ​

sleep 不只接收通道。

它还接收锁。

text
sleep(chan, lock)
1

这个锁非常重要。

它保护“检查条件”和“进入睡眠”之间的原子性。

否则会出现 lost wakeup。

错误时序:

text
reader checks pipe empty
  |
  | before reader sleeps
  v
writer writes data and wakeup
  |
  v
reader now goes to sleep
  |
  v
no one wakes it again
1
2
3
4
5
6
7
8
9
10
11

这就是 lost wakeup。

正确做法需要锁保护条件。

text
acquire lock
  |
  v
check condition
  |
  v
sleep atomically releases lock and sleeps
  |
  v
wakeup later can see sleeping state
1
2
3
4
5
6
7
8
9
10

所以 sleep/wakeup 的难点不是 C 语法。

而是并发时序。

5. wakeup(chan) 做什么 ​

wakeup(chan) 扫描进程表。

找到睡在同一个通道上的进程。

把它们改成 RUNNABLE。

text
wakeup(chan)
  |
  v
for p in proc table
  |
  +-- if p->state == SLEEPING && p->chan == chan
          |
          v
       p->state = RUNNABLE
1
2
3
4
5
6
7
8
9

它不是立即运行进程。

它只是让进程有资格被调度。

text
SLEEPING
  |
  | wakeup
  v
RUNNABLE
  |
  | scheduler later
  v
RUNNING
1
2
3
4
5
6
7
8
9

这点很重要。

唤醒不是切换。

唤醒只是改变状态。

6. pipe 中的 sleep/wakeup ​

管道是理解 sleep/wakeup 的好例子。

读空管道:

text
pipe empty
  |
  v
reader sleeps
1
2
3
4

写管道:

text
writer adds data
  |
  v
wakeup readers
1
2
3
4

管道满时写进程也可能睡眠。

text
pipe full
  |
  v
writer sleeps

reader consumes data
  |
  v
wakeup writers
1
2
3
4
5
6
7
8
9

图:

text
pipe buffer
  |
  +-- readers wait for data
  |
  +-- writers wait for space
1
2
3
4
5

同一个机制支持不同等待事件。

关键在于使用不同 chan。

7. exit():进程如何结束 ​

进程退出不是简单删除。

exit(status) 会:

text
close open files
  |
  v
release cwd
  |
  v
reparent children
  |
  v
wake parent
  |
  v
set xstate
  |
  v
state = ZOMBIE
  |
  v
sched()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

为什么变成 ZOMBIE?

因为父进程可能要读取退出状态。

如果内核立刻把进程记录清空,父进程就无法 wait() 得到结果。

text
child exits
  |
  v
ZOMBIE record remains
  |
  v
parent wait reads status
  |
  v
freeproc
1
2
3
4
5
6
7
8
9
10

ZOMBIE 是退出但未回收的进程记录。

不是还在运行的进程。

8. wait():父进程如何回收 ​

wait() 查找当前进程的子进程。

如果找到 ZOMBIE 子进程,就回收。

如果有子进程但还没退出,就睡眠。

text
wait()
  |
  v
scan proc table
  |
  +-- child ZOMBIE?
  |      |
  |      +-- copy xstate
  |      +-- freeproc
  |      +-- return pid
  |
  +-- children exist but none zombie?
         |
         +-- sleep waiting
1
2
3
4
5
6
7
8
9
10
11
12
13
14

父进程等待的是子进程。

不是任意进程。

关系:

text
child->parent == current process
1

exit() 会唤醒父进程。

这样父进程如果正在 wait() 睡眠,就能继续扫描并回收子进程。

9. reparent:父进程先退出怎么办 ​

如果父进程先退出,而子进程还活着,子进程会变成孤儿。

xv6 会把它们重新挂到 init。

text
parent exits
  |
  v
children still alive
  |
  v
reparent to init
  |
  v
init will wait and reap
1
2
3
4
5
6
7
8
9
10

为什么需要 init?

因为每个退出子进程都需要有人回收。

init 作为根进程,负责收养孤儿。

图:

text
before

parent
  |
  +-- child

after parent exits

init
  |
  +-- child
1
2
3
4
5
6
7
8
9
10
11

这保证 ZOMBIE 最终有人处理。

10. kill() 和睡眠进程 ​

kill(pid) 通常不会直接强行删除进程。

它设置 killed 标志。

如果目标进程正在睡眠,会把它唤醒。

text
kill(pid)
  |
  v
find process
  |
  v
p->killed = 1
  |
  +-- if SLEEPING, set RUNNABLE
1
2
3
4
5
6
7
8
9

为什么要唤醒?

因为睡眠进程如果永远睡着,就没有机会检查 killed。

唤醒后,它返回到内核路径,检查标志,然后退出。

text
SLEEPING killed process
  |
  v
wakeup
  |
  v
RUNNABLE
  |
  v
runs and notices killed
  |
  v
exit
1
2
3
4
5
6
7
8
9
10
11
12
13

这体现了 xv6 的协作式终止。

11. wait/exit 的锁关系 ​

wait/exit 涉及父子关系和进程表扫描。

所以锁很重要。

关键不变量:

text
child state and parent relation must be observed consistently
1

如果没有锁,可能出现:

  • 父进程错过子进程退出。
  • 子进程状态被同时修改。
  • reparent 和 wait 竞态。

简化关系:

text
exit child
  |
  +-- set xstate
  +-- set ZOMBIE
  +-- wake parent

wait parent
  |
  +-- scan children
  +-- find ZOMBIE
  +-- free child
1
2
3
4
5
6
7
8
9
10
11

wait 和 exit 必须配合,不能各干各的。

12. sleep/wakeup 与调度器 ​

sleep 最终会调用 sched()。

这让当前进程切回调度器。

text
sleep
  |
  +-- p->chan = chan
  +-- p->state = SLEEPING
  +-- sched()
1
2
3
4
5

wakeup 不直接切换。

text
wakeup
  |
  +-- p->state = RUNNABLE
1
2
3

调度器之后选择它。

完整图:

text
process waits
  |
  v
sleep -> SLEEPING -> scheduler
  |
  v
event happens
  |
  v
wakeup -> RUNNABLE
  |
  v
scheduler -> RUNNING
1
2
3
4
5
6
7
8
9
10
11
12
13

这解释了 sleep/wakeup 和 scheduler 的边界。

13. 常见误解 ​

误解一:sleep 会睡固定时间。

不一定。

xv6 的 sleep(chan, lock) 是等待事件通道。

不是普通定时睡眠 API。

误解二:wakeup 立刻运行进程。

不是。

它只是改成 RUNNABLE。

误解三:ZOMBIE 很可怕,说明进程还在运行。

不是。

ZOMBIE 是退出后等待父进程回收的记录。

误解四:exit 可以直接释放 proc。

不能。

父进程还需要 wait 读取退出状态。

误解五:kill 立即销毁进程。

不是。

它设置 killed,并唤醒睡眠进程。

误解六:chan 是数据队列。

不是。

chan 是等待事件的标识。

14. lost wakeup 的完整时序 ​

lost wakeup 是 sleep/wakeup 最重要的陷阱。

它不是“忘记调用 wakeup”这么简单。

它通常发生在检查条件和真正睡眠之间。

错误时序可以画得更细。

text
Reader CPU                         Writer CPU
----------                         ----------
acquire pipe lock
check buffer empty
decide to sleep
                                   acquire pipe lock blocked
release pipe lock
                                   acquire pipe lock
                                   write data
                                   wakeup(reader chan)
                                   release pipe lock
now reader sets SLEEPING
sched()

result:
  reader sleeps after wakeup already happened
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

这个时序的问题是:

reader 还没把自己标记为 SLEEPING,writer 就已经完成 wakeup。

之后 reader 再睡下去,就没人叫醒它。

正确的 sleep 设计要把这些动作变成安全顺序:

text
hold condition lock
  |
  v
sleep obtains p->lock before releasing condition lock
  |
  v
process state and chan become visible safely
  |
  v
wakeup cannot miss the sleeping process
1
2
3
4
5
6
7
8
9
10

所以 sleep(chan, lock) 的参数不是多余设计。

它是为了把外部条件锁和进程状态锁衔接起来。

15. wait 为什么也会 sleep ​

wait() 不只是扫描一次。

如果当前进程有子进程,但它们都还没退出,父进程应该睡眠。

否则父进程会一直循环扫描进程表,浪费 CPU。

text
parent calls wait
  |
  v
has children?
  |
  +-- no -> return -1
  |
  +-- yes
        |
        v
     any ZOMBIE?
        |
        +-- yes -> reap
        |
        +-- no -> sleep
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

子进程退出时,会唤醒父进程。

text
child exit
  |
  v
state = ZOMBIE
  |
  v
wakeup(parent)
1
2
3
4
5
6
7

父进程醒来后,不是直接相信某个子进程已经退出。

它会重新扫描。

这是一种常见模式:

text
while condition not satisfied:
  sleep
after wakeup:
  recheck condition
1
2
3
4

为什么醒来要重新检查?

因为 wakeup 只说明“可能有变化”。

不保证你要的条件一定成立。

16. exit 为什么不能返回 ​

普通函数执行完会 return。

但 exit() 不会返回到用户程序。

进程已经决定结束。

所以 exit() 最终会进入调度路径。

text
process calls exit
  |
  v
cleanup resources
  |
  v
state = ZOMBIE
  |
  v
sched()
  |
  v
never returns to user mode
1
2
3
4
5
6
7
8
9
10
11
12
13

这和 yield() 不同。

yield() 是:

text
RUNNING -> RUNNABLE
later may run again
1
2

exit() 是:

text
RUNNING -> ZOMBIE
will not run user code again
1
2

所以 exit() 后如果还能回到用户程序,就是严重错误。

ZOMBIE 只保留给父进程回收的信息。

17. wait/exit/reparent 的三方关系 ​

wait/exit 不是两方关系。

还要考虑 init。

三方图:

text
parent process
  |
  +-- waits for child

child process
  |
  +-- exits and becomes ZOMBIE

init process
  |
  +-- adopts orphan children
  +-- eventually waits them
1
2
3
4
5
6
7
8
9
10
11
12

如果父进程活着:

text
child exit
  |
  v
parent wait
  |
  v
child reaped
1
2
3
4
5
6
7

如果父进程先退出:

text
parent exit
  |
  v
child reparented to init
  |
  v
child exits
  |
  v
init wait
1
2
3
4
5
6
7
8
9
10

这样保证每个退出子进程最终有人收尸。

否则 ZOMBIE 会永远留在进程表里。

18. 本篇源码阅读清单 ​

建议按这个顺序读:

text
proc.h
  |
  +-- state
  +-- chan
  +-- killed
  +-- xstate
  +-- parent

proc.c
  |
  +-- sleep
  +-- wakeup
  +-- exit
  +-- wait
  +-- kill
  +-- reparent

pipe.c
  |
  +-- pipe read/write sleep/wakeup case
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

每个函数问:

text
what condition is being waited for?
what lock protects that condition?
what chan is used?
who calls wakeup?
what state transition happens?
1
2
3
4
5

这套问题能覆盖大部分 sleep/wakeup 场景。

19. 小实验:观察 wait/exit ​

写用户程序:

text
parent
  |
  +-- fork child
  |
  +-- child exits with status
  |
  +-- parent waits
1
2
3
4
5
6
7

内核打印:

text
exit: pid=child status=...
wait: parent=... found zombie=...
freeproc: pid=...
1
2
3

观察:

  • child 何时变 ZOMBIE。
  • parent 何时回收。
  • 回收后槽位是否 UNUSED。

20. 小实验:观察 pipe sleep/wakeup ​

写一个管道程序。

让 reader 先读,writer 稍后写。

观察:

text
reader sleep on pipe
writer writes data
writer wakeup reader
reader becomes RUNNABLE
1
2
3
4

打印要少。

只在 sleep/wakeup 关键点打印。

21. 本篇总结 ​

sleep/wakeup 解决等待事件。

wait/exit 解决父子进程退出和回收。

核心图:

text
RUNNING
  |
  +-- sleep ------------> SLEEPING
  |                         |
  |                         | wakeup
  |                         v
  |                      RUNNABLE
  |
  +-- exit --------------> ZOMBIE
                            |
                            | wait
                            v
                         UNUSED
1
2
3
4
5
6
7
8
9
10
11
12
13

下一篇进入:

text
11-locks-and-concurrency.md
1

因为 sleep/wakeup、wait/exit 的正确性都依赖锁。

最后更新于:

Pager
上一篇10. xv6 上下文切换与调度器 / xv6 Context Switch And Scheduler
下一篇12. xv6 锁与并发 / xv6 Locks And Concurrency

持续记录,持续成长

Copyright © Tidenflow