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 进程与 struct proc / xv6 Process And struct proc ​

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

proc 是 xv6 学习中最重要的词之一。

你会在很多地方看到:

c
struct proc *p;
1

也会看到:

c
myproc()
allocproc()
fork()
exit()
wait()
scheduler()
sleep()
wakeup()
1
2
3
4
5
6
7
8

这些函数都围绕进程展开。

如果 struct proc 没理解,后面调度、系统调用、文件描述符、虚拟内存都会模糊。

这篇文章的目标是:

  • 解释程序和进程的区别。
  • 解释 struct proc 是什么。
  • 解释 proc[NPROC] 是什么。
  • 逐类解释 struct proc 的关键字段。
  • 解释 trapframe 和 context 的区别。
  • 解释进程状态机。
  • 解释 fork/exit/wait 如何改变进程生命周期。
  • 解释 ofile[]、cwd、pagetable 等指针字段。
  • 帮你建立“进程是资源集合和调度对象”的心智模型。

先给一句话:

text
进程 = 用户地址空间 + 内核进程记录 + 调度状态 + 打开资源 + 父子关系
1

再展开:

text
struct proc
  |
  +-- identity
  +-- lifecycle state
  +-- user memory
  +-- kernel execution support
  +-- syscall/trap support
  +-- open files
  +-- current directory
  +-- parent relation
1
2
3
4
5
6
7
8
9
10

进程不是“正在运行的程序”这么简单。

正在运行只是进程的一种状态。

进程也可能正在睡眠、等待调度、已经退出但尚未被父进程回收。

1.1 本文基础词小注释 ​

proc 是你后面会反复遇到的词。

先从最简单版本开始。

text
process
  |
  +-- 进程
  +-- 一个正在被操作系统管理的程序运行实例
  +-- 不等于磁盘上的程序文件

proc
  |
  +-- process 的缩写
  +-- xv6 中常指 struct proc
  +-- 内核用它记录一个进程的信息

pid
  |
  +-- process id
  +-- 进程编号
  +-- 是数字,不是 struct proc 指针

state
  |
  +-- 进程状态
  +-- 表示进程现在能不能运行、是否睡眠、是否退出

ZOMBIE
  |
  +-- 已退出但还没被父进程 wait 回收的进程记录
  +-- 不是还在运行的进程
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

2. 程序和进程不是一回事 ​

程序是静态的。

它可以是磁盘上的可执行文件。

进程是动态的。

它是操作系统正在管理的执行实例。

text
program
  |
  +-- stored as file
  +-- contains instructions and data
  +-- not necessarily running

process
  |
  +-- has address space
  +-- has pid
  +-- has execution state
  +-- has kernel resources
  +-- may run, sleep, or exit
1
2
3
4
5
6
7
8
9
10
11
12
13

一个程序可以运行多次。

产生多个进程。

text
/bin/sh program
  |
  +-- process pid=3
  +-- process pid=8
  +-- process pid=12
1
2
3
4
5

一个进程也可以通过 exec() 替换自己的程序内容。

text
same process pid=5
  |
  +-- before exec: running old program
  |
  +-- after exec: running new program
1
2
3
4
5

所以不要把程序、进程、线程、CPU 混成一团。

xv6 主要用进程作为用户程序执行单位。

每个进程有自己的用户地址空间和内核记录。

3. 本篇涉及的源码文件 ​

进程相关源码集中在:

text
kernel/proc.h
  |
  +-- enum procstate
  +-- struct context
  +-- struct cpu
  +-- struct trapframe
  +-- struct proc

kernel/proc.c
  |
  +-- proc table
  +-- procinit
  +-- allocproc
  +-- freeproc
  +-- userinit
  +-- growproc
  +-- fork
  +-- exit
  +-- wait
  +-- scheduler
  +-- sched
  +-- yield
  +-- sleep
  +-- wakeup
  +-- kill
  +-- mycpu
  +-- myproc

kernel/swtch.S
  |
  +-- context switch between scheduler and process

kernel/trap.c
  |
  +-- traps interact with current process
  +-- timer interrupt may yield
  +-- syscall returns through trapframe

kernel/vm.c
  |
  +-- process page table creation/copy/free
  +-- copyin/copyout

kernel/file.c
  |
  +-- filedup and fileclose for open files

kernel/fs.c
  |
  +-- idup and iput for current directory inode

kernel/sysproc.c
  |
  +-- syscall wrapper layer for fork/exit/wait/getpid/sbrk/sleep
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54

proc.h 是结构定义。

proc.c 是生命周期和调度实现。

swtch.S 是底层切换。

vm.c、file.c、fs.c 提供进程资源的复制和释放。

4. proc[NPROC]:进程表 ​

xv6 使用一个全局数组保存进程槽位。

概念上是:

text
proc[NPROC]

+---------+---------+---------+---------+---------+
| proc[0] | proc[1] | proc[2] | proc[3] | ...     |
+---------+---------+---------+---------+---------+
1
2
3
4
5

每个槽位是一个 struct proc 对象。

槽位不一定正在运行。

它可能处于不同状态。

text
+----------+----------+----------+----------+----------+
| UNUSED   | RUNNABLE | SLEEPING | RUNNING  | ZOMBIE   |
+----------+----------+----------+----------+----------+
1
2
3

allocproc() 会扫描进程表,寻找 UNUSED 槽位。

找到后把它变成可用进程。

text
proc table scan
  |
  v
find UNUSED slot
  |
  v
initialize fields
  |
  v
state = USED
1
2
3
4
5
6
7
8
9
10

所以进程不是动态 malloc 出一个任意大小对象。

在 xv6 中,进程记录来自固定大小进程表。

这是教学内核的简单设计。

真实系统会更复杂。

但固定数组非常适合学习生命周期。

5. struct proc 总图 ​

可以先把 struct proc 画成这样:

text
+------------------------------------------------------+
| struct proc                                          |
+------------------------------------------------------+
| lock                                                 |
| state                                                |
| chan                                                 |
| killed                                               |
| xstate                                               |
| pid                                                  |
+------------------------------------------------------+
| parent ----------------------------------> proc       |
+------------------------------------------------------+
| kstack                                               |
| sz                                                   |
| pagetable -------------------------------> page table |
| trapframe -------------------------------> trapframe  |
| context                                              |
+------------------------------------------------------+
| ofile[NOFILE] ---------------------------> files      |
| cwd -------------------------------------> inode      |
| name                                                 |
+------------------------------------------------------+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

按类别分:

text
synchronization
  |
  +-- lock

lifecycle
  |
  +-- state
  +-- chan
  +-- killed
  +-- xstate

identity
  |
  +-- pid
  +-- name

family
  |
  +-- parent

memory
  |
  +-- kstack
  +-- sz
  +-- pagetable
  +-- trapframe

scheduling
  |
  +-- context

resources
  |
  +-- ofile[]
  +-- cwd
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35

这样看就比字段列表清楚得多。

每个字段都服务于进程这个内核对象。

6. lock:保护进程状态 ​

每个进程有自己的锁。

text
struct proc
  |
  +-- struct spinlock lock
1
2
3

这个锁保护进程内部的一些状态。

特别是和生命周期、调度、sleep/wakeup 相关的字段。

text
p->lock protects
  |
  +-- state
  +-- chan
  +-- killed
  +-- xstate
  +-- parent interactions in some paths
1
2
3
4
5
6
7

为什么进程需要锁?

因为 xv6 是多核内核。

多个 CPU 可能同时查看或修改进程状态。

例如:

  • 一个 CPU 正在调度。
  • 一个 CPU 正在唤醒进程。
  • 一个 CPU 正在 kill 进程。
  • 一个父进程正在 wait 子进程。

没有锁,状态变化可能乱掉。

text
CPU 0: scheduler sees p->state
CPU 1: wakeup changes p->state
CPU 2: exit changes p->state
1
2
3

锁的意义不是“让代码慢一点”。

锁保护的是状态不变量。

例如:

一个进程不能同时被两个 CPU 运行。

一个睡眠进程不能错过唤醒。

一个退出进程必须被父进程正确回收。

7. state:进程状态机 ​

state 是进程生命周期的核心字段。

典型状态:

text
UNUSED
USED
SLEEPING
RUNNABLE
RUNNING
ZOMBIE
1
2
3
4
5
6

状态机:

text
UNUSED
  |
  | allocproc
  v
USED
  |
  | userinit/fork completes setup
  v
RUNNABLE
  |
  | scheduler chooses it
  v
RUNNING
  |
  +-- yield/timer ------------> RUNNABLE
  |
  +-- sleep ------------------> SLEEPING
  |                              |
  |                              | wakeup
  |                              v
  |                           RUNNABLE
  |
  +-- exit -------------------> ZOMBIE
                                  |
                                  | parent wait
                                  v
                               UNUSED
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

各状态含义:

UNUSED 表示槽位空闲。

USED 表示槽位已分配,但还没准备好运行。

RUNNABLE 表示可以运行,等待调度。

RUNNING 表示正在某个 CPU 上运行。

SLEEPING 表示等待某个事件。

ZOMBIE 表示已经退出,但父进程还没回收。

状态不是注释。

调度器、wait、wakeup 都依赖状态。

所以状态变化通常要持锁。

8. pid 和 name:身份和调试 ​

pid 是进程编号。

它是用户和内核识别进程的重要标识。

text
struct proc
  |
  +-- int pid
1
2
3

pid 和 struct proc * 不一样。

pid 是编号。

struct proc * 是指向进程槽位的指针。

text
pid
  |
  +-- integer identity

struct proc *
  |
  +-- address of kernel process object
1
2
3
4
5
6
7

进程槽位可能复用。

一个 struct proc 对象地址未来可能分配给新的 pid。

所以不要把槽位地址等同于永恒进程身份。

name 主要用于调试。

它帮助输出更易读。

text
pid=1 name=init
pid=2 name=sh
1
2

真正的调度和资源管理不靠 name。

9. parent:父子关系 ​

parent 是指向父进程的指针。

text
struct proc *parent;
1

这不是父 pid。

它是指向另一个 struct proc 的指针。

text
child proc
  |
  +-- parent -----> parent proc
1
2
3

父子关系用于:

  • wait() 查找退出的子进程。
  • exit() 时把孤儿进程交给 init。
  • 维护进程树。

关系图:

text
init
  |
  +-- shell
        |
        +-- child command
1
2
3
4
5

如果父进程退出,而子进程还活着,子进程需要重新挂到 init。

这叫 reparent。

否则子进程退出后没人 wait,会无法被回收。

text
parent exits
  |
  v
children reparented to init
  |
  v
init can wait and reap them
1
2
3
4
5
6
7

这就是为什么 init 很特殊。

它不只是第一个进程。

它还负责收养孤儿进程。

10. pagetable 和 sz:用户地址空间 ​

进程拥有用户地址空间。

pagetable 指向用户页表。

sz 表示用户内存大小。

text
struct proc
  |
  +-- pagetable
  +-- sz
1
2
3
4

关系:

text
proc
  |
  +-- pagetable -------> user page table
  |
  +-- sz --------------> size of user memory
1
2
3
4
5

用户地址空间:

text
high address

+-------------------------------+ TRAMPOLINE
| trampoline                    |
+-------------------------------+ TRAPFRAME
| trapframe                     |
+-------------------------------+
| user stack                    |
+-------------------------------+
| heap                          |
+-------------------------------+
| data                          |
+-------------------------------+
| text                          |
+-------------------------------+ 0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

fork() 会复制用户地址空间。

exec() 会替换用户地址空间。

exit() 会释放用户地址空间。

sbrk() 会增长或缩小用户地址空间。

所以 pagetable 不是附属字段。

它是进程的核心。

没有地址空间,进程就不能承载用户程序。

11. trapframe:用户态寄存器现场 ​

trapframe 是指针字段。

text
struct trapframe *trapframe;
1

它指向保存用户寄存器的内存页。

text
proc
  |
  +-- trapframe ----+
                   |
                   v
           +------------------+
           | user registers   |
           | epc              |
           | a0-a7            |
           | sp               |
           | ...              |
           +------------------+
1
2
3
4
5
6
7
8
9
10
11
12

系统调用和 trap 都依赖它。

系统调用号在 a7。

返回值在 a0。

用户 pc 在 epc。

text
syscall path
  |
  v
trap saves registers
  |
  v
syscall reads trapframe->a7
  |
  v
syscall writes trapframe->a0
  |
  v
userret restores registers
1
2
3
4
5
6
7
8
9
10
11
12
13

trapframe 是用户态和内核态之间的桥。

它不是调度上下文。

这点要和 context 区分。

12. context:内核上下文切换现场 ​

context 是嵌入结构体。

text
struct context context;
1

它保存内核调度切换所需寄存器。

简化:

text
context
  |
  +-- ra
  +-- sp
  +-- s0-s11
1
2
3
4
5

调度时:

text
process kernel context
  |
  v
swtch(&p->context, &cpu->context)
  |
  v
scheduler context
1
2
3
4
5
6
7

再切回:

text
scheduler context
  |
  v
swtch(&cpu->context, &p->context)
  |
  v
process kernel context
1
2
3
4
5
6
7

和 trapframe 对比:

text
trapframe
  |
  +-- saves user registers
  +-- used by trap return

context
  |
  +-- saves kernel registers
  +-- used by scheduler swtch
1
2
3
4
5
6
7
8
9

一句话:

trapframe 是用户态现场。

context 是内核调度现场。

13. kstack:进程的内核栈 ​

每个进程有内核栈。

kstack 保存内核栈地址。

text
struct proc
  |
  +-- kstack
1
2
3

用户态运行时,用用户栈。

进入内核后,用内核栈。

text
user mode
  |
  +-- user stack
  |
  v trap

kernel mode
  |
  +-- process kernel stack
1
2
3
4
5
6
7
8
9

为什么不用用户栈?

因为用户栈不可信。

用户程序可以破坏用户栈。

内核需要自己的可信栈来处理系统调用和中断。

每个进程有自己的内核栈,避免不同进程进入内核时互相覆盖。

text
proc A -> kernel stack A
proc B -> kernel stack B
proc C -> kernel stack C
1
2
3

procinit() 会准备这些内核栈映射。

14. chan:睡眠等待的通道 ​

chan 用于 sleep/wakeup。

text
void *chan;
1

它是一个等待通道。

可以理解成“这个进程正在等待什么事件”。

text
process p
  |
  +-- state = SLEEPING
  +-- chan = some address
1
2
3
4

wakeup(chan) 会唤醒睡在同一通道上的进程。

text
sleep(chan)
  |
  v
p->chan = chan
p->state = SLEEPING

wakeup(chan)
  |
  v
find p where p->chan == chan
set p->state = RUNNABLE
1
2
3
4
5
6
7
8
9
10
11

为什么 chan 是 void *?

因为它只是一个标识。

不同子系统可以用不同地址作为等待通道。

例如管道可以用某个 pipe 对象地址或字段地址。

这是一种内核常见技巧。

15. killed 和 xstate ​

killed 表示进程是否被要求杀死。

text
int killed;
1

有时候不能立刻终止进程。

内核会设置 killed。

进程在合适边界检查后退出。

text
kill(pid)
  |
  v
p->killed = 1
  |
  v
process later notices
  |
  v
exit
1
2
3
4
5
6
7
8
9
10

xstate 是退出状态。

父进程 wait() 时可以拿到子进程退出状态。

text
child exit(status)
  |
  v
child->xstate = status
  |
  v
child becomes ZOMBIE
  |
  v
parent wait reads xstate
1
2
3
4
5
6
7
8
9
10

这两个字段都和生命周期相关。

所以要和锁、wait、exit 一起理解。

16. ofile[]:打开文件表 ​

每个进程有自己的文件描述符表。

text
struct file *ofile[NOFILE];
1

这是一个数组。

数组里的每个元素是 struct file *。

text
proc.ofile

+------+-------+-------+-------+-----+
| fd 0 | fd 1  | fd 2  | fd 3  | ... |
+--+---+---+---+---+---+---+---+-----+
   |       |       |
   v       v       v
 file    file    file
1
2
3
4
5
6
7
8

fd 是用户程序看到的整数。

例如:

c
write(1, "hi", 2);
1

这里 1 是 fd。

内核用它索引当前进程的 ofile[]。

text
fd integer
  |
  v
proc.ofile[fd]
  |
  v
struct file
1
2
3
4
5
6
7

fork() 时,子进程会复制父进程打开文件引用。

不是复制文件内容。

而是让子进程的 ofile[] 指向同一些 struct file,并增加引用计数。

text
parent ofile[3] ----+
                    v
                 struct file ref=2
                    ^
child  ofile[3] ----+
1
2
3
4
5

这就是为什么文件对象需要引用计数。

17. cwd:当前工作目录 ​

cwd 指向当前工作目录 inode。

text
struct inode *cwd;
1

它不是字符串路径。

而是 inode 指针。

text
proc
  |
  +-- cwd ----> inode of current directory
1
2
3

为什么不用字符串?

因为文件系统操作最终要落到 inode。

路径解析会从某个目录 inode 开始。

当前工作目录就是相对路径解析的起点。

text
relative path "a/b"
  |
  v
start from proc->cwd
  |
  v
lookup a
  |
  v
lookup b
1
2
3
4
5
6
7
8
9
10

fork() 时,子进程会通过 idup() 增加 cwd inode 引用。

exit() 时,进程会 iput() 当前目录。

这又是一个引用计数和生命周期问题。

18. allocproc():从空槽位创建进程对象 ​

allocproc() 是进程生命周期起点。

它做的事:

text
allocproc()
  |
  +-- scan proc table
  +-- find UNUSED proc
  +-- set state = USED
  +-- assign pid
  +-- allocate trapframe
  +-- create user page table
  +-- initialize context
  +-- return proc pointer
1
2
3
4
5
6
7
8
9
10

状态变化:

text
UNUSED
  |
  v
USED
1
2
3
4

注意:

allocproc() 之后,进程还不能运行。

它只是分配并初始化了内核进程对象。

要等用户地址空间、trapframe、返回路径等准备好,再变成 RUNNABLE。

如果中途失败,需要释放已经分配的资源。

这就是 freeproc() 的作用。

19. userinit():第一个进程 ​

userinit() 创建第一个用户进程。

路径:

text
main()
  |
  v
userinit()
  |
  v
allocproc()
  |
  v
set up initial user code
  |
  v
state = RUNNABLE
1
2
3
4
5
6
7
8
9
10
11
12
13

第一个进程特殊。

它不是由 fork() 创建。

它由内核手动创建。

它会运行一小段初始用户代码,再执行 /init。

text
kernel-created first proc
  |
  v
initcode
  |
  v
exec("/init")
  |
  v
user/init.c
1
2
3
4
5
6
7
8
9
10

这说明:

进程系统必须先由内核“点火”。

有了第一个进程后,后续用户进程可以通过 fork() 和 exec() 扩展出来。

20. fork():复制当前进程 ​

fork() 创建子进程。

但它不是简单复制 struct proc。

它要复制或共享多种资源。

text
fork()
  |
  +-- allocproc()
  |
  +-- copy user memory with uvmcopy()
  |
  +-- copy trapframe
  |
  +-- set child's trapframe->a0 = 0
  |
  +-- duplicate open files with filedup()
  |
  +-- duplicate cwd with idup()
  |
  +-- set parent
  |
  +-- set child RUNNABLE
  |
  +-- return child pid to parent
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

父子返回值不同。

text
parent fork() returns child pid
child  fork() returns 0
1
2

为什么?

因为子进程的 trapframe 被修改。

text
child->trapframe->a0 = 0
1

这会让子进程返回用户态时看到 fork() 返回 0。

对象关系:

text
parent proc
  |
  +-- pagetable copied to child pagetable
  +-- trapframe copied to child trapframe
  +-- ofile pointers duplicated with ref++
  +-- cwd duplicated with ref++
  |
  v
child proc
1
2
3
4
5
6
7
8
9

这就是 fork() 的精髓。

它既复制,又共享。

内存复制。

文件引用共享。

cwd 引用共享。

21. exit():进程退出 ​

exit() 结束当前进程。

它不能简单把进程删掉。

因为父进程可能还需要通过 wait() 获取退出状态。

exit() 大致做:

text
exit(status)
  |
  +-- close all open files
  +-- release cwd
  +-- reparent children to init
  +-- wake up parent
  +-- set xstate
  +-- state = ZOMBIE
  +-- call sched()
1
2
3
4
5
6
7
8
9

为什么变成 ZOMBIE?

因为进程已经停止运行。

但它的退出状态和部分记录还要留给父进程。

text
running process
  |
  v
exit
  |
  v
ZOMBIE
  |
  v
parent wait
  |
  v
UNUSED
1
2
3
4
5
6
7
8
9
10
11
12
13

ZOMBIE 不是还在执行的进程。

它是等待回收的退出记录。

22. wait():父进程回收子进程 ​

wait() 让父进程等待子进程退出。

它会扫描进程表,找当前进程的子进程。

如果有子进程是 ZOMBIE,就回收它。

text
wait()
  |
  v
scan proc table
  |
  +-- find child of current proc
  |
  +-- if child ZOMBIE
  |      |
  |      +-- copy exit status to user
  |      +-- freeproc(child)
  |      +-- return child pid
  |
  +-- if children exist but none zombie
         |
         +-- sleep waiting for child exit
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

父子关系在这里发挥作用。

text
child->parent == current proc
1

wait() 不是等任意进程。

它等自己的子进程。

如果没有子进程,返回错误。

如果有子进程但都没退出,睡眠等待。

这就是为什么 exit() 要唤醒父进程。

23. scheduler():选择 RUNNABLE 进程 ​

调度器循环扫描进程表。

寻找 RUNNABLE 进程。

找到后把它设置为 RUNNING。

然后通过 swtch() 切过去。

text
scheduler()
  |
  v
for each proc
  |
  +-- acquire p->lock
  |
  +-- if p->state == RUNNABLE
  |      |
  |      +-- p->state = RUNNING
  |      +-- c->proc = p
  |      +-- swtch(&c->context, &p->context)
  |      +-- c->proc = 0
  |
  +-- release p->lock
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

调度器本身也有上下文。

每个 CPU 有 struct cpu。

text
struct cpu
  |
  +-- proc
  +-- context
1
2
3
4

关系:

text
CPU scheduler context
  |
  v
swtch
  |
  v
process kernel context
1
2
3
4
5
6
7

调度器不创建进程。

它只选择能运行的进程。

进程什么时候变成 RUNNABLE,由 userinit()、fork()、wakeup() 等函数决定。

24. mycpu() 和 myproc() ​

内核经常需要知道当前 CPU 和当前进程。

mycpu() 返回当前 hart 对应的 struct cpu。

myproc() 返回当前 CPU 正在运行的进程。

关系:

text
hart id
  |
  v
cpus[hartid]
  |
  v
struct cpu
  |
  v
cpu->proc
  |
  v
current struct proc
1
2
3
4
5
6
7
8
9
10
11
12
13

为什么这事不简单?

因为中断和调度可能改变当前 CPU 状态。

读 mycpu()、myproc() 时要注意中断开关。

内核需要避免在读取当前 CPU/进程过程中被打断并迁移状态。

初学时先记住:

myproc() 是很多系统调用找到当前进程的入口。

例如 getpid()。

text
sys_getpid()
  |
  v
myproc()
  |
  v
p->pid
1
2
3
4
5
6
7

25. 进程对象关系总图 ​

现在把所有关系合起来。

text
CPU / hart
  |
  v
struct cpu
  |
  +-- proc -----------------------------+
  |                                     |
  +-- scheduler context                 |
                                        v
                              +-------------------+
                              | struct proc       |
                              +-------------------+
                              | lock              |
                              | state             |
                              | pid               |
                              | parent ----------+----> parent proc
                              | pagetable -------+----> user page table
                              | trapframe -------+----> saved user regs
                              | context          |
                              | kstack           |
                              | ofile[] --------+----> struct file
                              | cwd ------------+----> struct inode
                              +-------------------+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

生命周期和资源:

text
allocproc
  |
  +-- allocate trapframe
  +-- create pagetable
  +-- initialize context
  |
  v
RUNNABLE
  |
  v
scheduler runs process
  |
  +-- syscall
  +-- trap
  +-- sleep
  +-- yield
  +-- exit
  |
  v
ZOMBIE
  |
  v
wait/freeproc
  |
  +-- free trapframe
  +-- free pagetable
  +-- close/release resources already handled
  |
  v
UNUSED
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

如果你能看懂这两张图,proc 的大半就通了。

26. 常见误解 ​

误解一:进程就是程序。

不是。

程序是静态代码和数据。

进程是操作系统管理的执行实例。

误解二:struct proc *p 里的 p 就是 pid。

不是。

p 是指向进程对象的指针。

p->pid 才是编号。

误解三:fork() 只是复制 struct proc。

不是。

它复制用户内存和 trapframe,也复制文件引用和 cwd 引用。

误解四:ZOMBIE 是还在运行的进程。

不是。

它已经退出,只是在等父进程回收。

误解五:trapframe 和 context 一样。

不是。

trapframe 用于用户态返回。

context 用于内核调度切换。

误解六:fd 是全局的。

不是。

fd 是进程局部编号。

它通过当前进程的 ofile[] 找到 struct file。

误解七:cwd 是字符串。

不是。

它是当前目录 inode 指针。

误解八:调度器会运行所有进程。

不准确。

调度器只选择 RUNNABLE 进程。

睡眠和僵尸进程不会被调度运行。

27. 小实验:打印进程生命周期 ​

实验目标:

观察 allocproc()、fork()、exit()、wait() 中的状态变化。

可以打印:

text
allocproc: p=... pid=... state=USED
fork: parent=... child=...
exit: pid=... status=...
wait: parent=... reaped=...
freeproc: pid=...
1
2
3
4
5

观察问题:

  • 进程槽位地址是否可能复用?
  • child 的 parent 指向谁?
  • exit() 后为什么不是立刻 UNUSED?
  • wait() 回收了什么?

实验用户程序:

text
parent starts
  |
  v
fork child
  |
  +-- child exits
  |
  v
parent waits
1
2
3
4
5
6
7
8
9

把输出和状态机对照。

text
UNUSED -> USED -> RUNNABLE -> RUNNING -> ZOMBIE -> UNUSED
1

28. 小实验:观察 fd 和 ofile ​

实验目标:

理解进程的 fd 表。

写一个用户程序:

text
open a file
write data
fork
child writes
parent writes
close
1
2
3
4
5
6

在内核中观察:

  • proc.ofile[fd]。
  • struct file * 地址。
  • file->ref。

预期:

text
parent fd -> file object
child  fd -> same file object after fork
file ref increases
1
2
3

这能证明:

fd 是进程局部整数。

struct file 是内核对象。

fork 后文件引用被共享。

29. 小实验:观察 trapframe 返回值 ​

实验目标:

理解 fork() 为什么父子返回值不同。

在 fork() 中观察:

text
parent return value
child->trapframe->a0
1
2

路径:

text
parent calls fork
  |
  v
kernel creates child
  |
  v
child trapframe copied from parent
  |
  v
child->trapframe->a0 = 0
  |
  v
parent returns child pid
  |
  v
child later returns 0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

这是 trapframe 和进程结合的经典例子。

它能帮助你真正理解:

系统调用返回值不是魔法。

它就是写回将来恢复到用户态的寄存器位置。

30. 本篇总结 ​

struct proc 是 xv6 中最重要的结构之一。

它把操作系统多个机制绑在一起。

text
process identity
  |
  +-- pid
  +-- name

lifecycle
  |
  +-- state
  +-- killed
  +-- xstate

memory
  |
  +-- pagetable
  +-- sz
  +-- trapframe
  +-- kstack

scheduling
  |
  +-- context
  +-- chan

resources
  |
  +-- ofile[]
  +-- cwd

family
  |
  +-- parent
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

理解 proc 后,很多 xv6 代码会连起来:

  • 系统调用通过当前 proc 找到 trapframe。
  • 调度器通过 proc 状态选择进程。
  • fork 复制 proc 相关资源。
  • exit 把 proc 变成 ZOMBIE。
  • wait 回收子 proc。
  • 文件系统通过 proc 的 ofile 和 cwd 连接用户请求。
  • 虚拟内存通过 proc 的 pagetable 连接地址空间。

下一篇进入:

text
09-context-switch-and-scheduler.md
1

那里会继续深入 scheduler()、sched()、yield()、swtch.S,解释 CPU 如何从一个进程切到另一个进程。

最后更新于:

Pager
上一篇8. xv6 trap、中断与系统调用 / xv6 Traps Interrupts And System Calls
下一篇10. xv6 上下文切换与调度器 / xv6 Context Switch And Scheduler

持续记录,持续成长

Copyright © Tidenflow