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 用户程序与 shell / xv6 User Programs And Shell ​

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

读 xv6 时,很容易只盯着内核。

但一个操作系统只有内核还不够。

读者真正能敲命令,是因为还有用户程序。

用户程序是运行在用户态的普通程序。

用户态表示 CPU 权限较低。

用户程序不能直接修改页表。

用户程序不能直接操作磁盘控制器。

用户程序不能直接改内核数据结构。

它必须通过系统调用请求内核服务。

系统调用是用户程序进入内核的受控入口。

本篇要把这条链路讲清楚。

我们会从 init 讲起。

再进入 sh。

然后看 usys.pl 和构建生成的 usys.S。

最后把 fork、exec、wait 串成 shell 的执行模型。

本篇基于本地可读的 E:\Github\xv6-riscv。

该本地树里 user/usys.S 当前未生成。

这符合 xv6 的标准构建方式。

user/usys.S 通常由 user/usys.pl 在 make 时生成。

1.1 本文基础词小注释 ​

用户程序 user program:运行在用户态的程序,比如 sh、cat、echo。它们不能随便访问内核内存,也不能直接控制硬件。

用户态 user mode:CPU 的低权限模式。用户态程序如果想读文件、创建进程、退出进程,必须通过系统调用请求内核。

shell:命令解释器。它读入你敲的一行命令,解析后创建子进程去执行目标程序。xv6 的 sh 很小,但已经包含 shell 的核心骨架。

text
keyboard input
      |
      v
shell parses command
      |
      v
fork child
      |
      v
child exec program
1
2
3
4
5
6
7
8
9
10

init:xv6 启动后第一个用户进程。它负责打开 console,然后启动 shell。没有 init,内核起来了也没有一个正常的用户入口。

fork:复制当前进程,得到一个子进程。父子进程从同一个位置继续执行,但返回值不同,所以程序能分辨自己是父进程还是子进程。

exec:把当前进程的用户地址空间替换成另一个可执行文件。注意 exec 不创建新进程,它是在现有进程壳子里换一套程序内容。

wait:父进程等待子进程退出,并回收子进程资源。shell 通常会 fork 子进程执行命令,然后 wait 它结束。

2. 最简单的心智模型 ​

用户程序不是内核的一部分。

它们只是被放进 xv6 文件系统镜像里的 ELF 可执行文件。

ELF 是一种可执行文件格式。

它描述代码段、数据段、入口地址等信息。

xv6 的 exec 会读取 ELF。

然后为进程建立新的用户地址空间。

shell 的核心不是魔法。

shell 只是一个普通用户程序。

它读入一行命令。

它解析命令。

它 fork 出子进程。

它让子进程 exec 目标程序。

它让父进程 wait 子进程结束。

整体图如下:

text
------------------+
| QEMU starts xv6 |
+------------------+
        |
        v
+------------------+
| kernel userinit  |
+------------------+
        |
        v
+------------------+
| user/init        |
+------------------+
        |
        v
+------------------+
| user/sh          |
+------------------+
        |
        v
+------------------------------+
| fork -> child exec -> wait    |
+------------------------------+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

这张图很重要。

它说明命令行不是内核直接提供的。

命令行是用户程序和系统调用组合出来的。

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

user/init.c 是第一个普通用户程序。

user/sh.c 是 xv6 的 shell。

user/user.h 声明用户态可调用的库函数和系统调用包装函数。

user/ulib.c 提供用户态小型 C 库函数。

user/printf.c 提供用户态格式化输出。

user/umalloc.c 提供用户态简单堆分配。

user/usys.pl 生成系统调用汇编桩。

user/usys.S 是构建产物,不一定在干净源码树中存在。

kernel/syscall.h 定义系统调用号。

kernel/syscall.c 根据系统调用号分派到内核函数。

kernel/sysproc.c 实现进程相关系统调用包装。

kernel/sysfile.c 实现文件相关系统调用包装。

kernel/trap.c 处理从用户态进入内核的 trap。

kernel/trampoline.S 保存和恢复用户寄存器。

kernel/proc.c 实现 fork、wait、exit、调度。

kernel/exec.c 实现 exec 加载 ELF。

Makefile 描述用户程序如何编译进 fs.img。

4. 什么是用户程序 ​

用户程序运行在用户虚拟地址空间。

虚拟地址空间是进程看到的地址集合。

用户程序看到的是自己的地址。

内核通过页表决定这些地址映射到哪里。

xv6 用户程序很小。

它们通常在 user/ 目录下。

例如 cat.c。

例如 echo.c。

例如 grep.c。

例如 ls.c。

例如 init.c。

例如 sh.c。

这些程序使用的 API 看起来像普通 Unix。

例如 open。

例如 read。

例如 write。

例如 fork。

例如 exec。

例如 wait。

但这些名字并不直接等于内核函数。

用户态调用的是一层系统调用桩。

系统调用桩再通过 ecall 进入内核。

5. 用户程序如何被放进系统 ​

xv6 启动后并不会从宿主机文件系统找程序。

它使用自己的磁盘镜像 fs.img。

fs.img 是 xv6 文件系统镜像。

构建时,Makefile 会把用户程序加入镜像。

简化流程:

text
user/*.c
   |
   v
compile and link
   |
   v
user/_echo, user/_sh, user/_init
   |
   v
mkfs
   |
   v
fs.img
   |
   v
xv6 kernel mounts root file system
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

注意 _sh 和 sh 的关系。

宿主机构建产物常带下划线。

放进 xv6 文件系统后,名字通常去掉下划线。

shell 中输入 ls。

内核执行的是 xv6 文件系统里的 ls 文件。

不是宿主机的 /bin/ls。

6. init 为什么是第一个用户程序 ​

内核需要一个用户态起点。

这个起点就是 init。

在 xv6 中,内核创建第一个进程。

这个进程最终执行 user/init.c。

init 的职责非常少。

它确保 console 可用。

它打开标准输入。

它复制标准输出。

它复制标准错误。

它循环启动 shell。

它回收 shell 或孤儿进程。

user/init.c 的核心流程:

text
open console
  |
  v
dup stdin to stdout
  |
  v
dup stdin to stderr
  |
  v
fork
  |
  +-- child exec sh
  |
  +-- parent wait
1
2
3
4
5
6
7
8
9
10
11
12
13
14

console 是控制台设备文件。

设备文件不是普通磁盘文件。

它是文件系统名字和设备驱动的连接点。

mknod("console", CONSOLE, 0) 会创建这个连接点。

open("console", O_RDWR) 返回文件描述符 0。

文件描述符是进程打开文件表中的小整数索引。

dup(0) 复制文件描述符 0。

第一次 dup(0) 通常得到 1。

第二次 dup(0) 通常得到 2。

于是形成标准约定:

text
fd 0 -> stdin
fd 1 -> stdout
fd 2 -> stderr
1
2
3

7. init 为什么要循环启动 shell ​

shell 也是普通进程。

普通进程可以退出。

如果 shell 退出,系统不应该立刻没有命令入口。

所以 init 会重新启动 shell。

init 的外层循环负责重启 shell。

init 的内层循环负责 wait。

内层 wait 有两个可能结果。

一种结果是 shell 退出。

另一种结果是某个孤儿进程退出。

孤儿进程是父进程已经退出但自己还活着的进程。

xv6 会把孤儿进程重新挂到 init 名下。

所以 init 也承担回收孤儿的职责。

这解释了 wait() 可能返回非 shell pid。

init 对这种 pid 不做额外处理。

它只是继续等。

只有 wpid == pid 时,说明 shell 本身结束。

然后 init 重新 fork 一个 shell。

8. shell 是什么 ​

shell 是命令解释器。

解释器表示它读入文本。

然后按语法把文本转换成动作。

xv6 的 shell 很小。

但它包含 Unix shell 的核心骨架。

它支持普通命令。

它支持重定向。

它支持管道。

它支持命令列表。

它支持后台执行。

它支持 cd 这个内建命令。

内建命令是由 shell 自己执行的命令。

普通命令则通过 fork 和 exec 执行。

cd 必须是内建命令。

因为改变工作目录影响的是当前进程。

如果在子进程里 chdir,父 shell 的目录不会改变。

所以 xv6 shell 在父进程中处理 cd。

这是理解 shell 的一个关键点。

9. sh.c 的命令结构 ​

sh.c 把命令解析成一组结构体。

struct cmd 是所有命令的基类式头部。

它只有一个 type。

struct execcmd 表示普通执行命令。

struct redircmd 表示重定向命令。

struct pipecmd 表示管道命令。

struct listcmd 表示分号命令列表。

struct backcmd 表示后台命令。

这些结构不是面向对象。

但它们用 C 模拟了带类型标签的数据结构。

这叫 tagged union 风格。

tagged union 表示用一个字段说明当前对象应该按哪种结构解释。

在 runcmd 中,shell 根据 cmd->type 做类型转换。

结构图:

text
struct cmd
  |
  +-- EXEC  -> struct execcmd
  +-- REDIR -> struct redircmd
  +-- PIPE  -> struct pipecmd
  +-- LIST  -> struct listcmd
  +-- BACK  -> struct backcmd
1
2
3
4
5
6
7

这种设计让解析和执行分离。

解析阶段只建立命令树。

执行阶段递归解释命令树。

10. 普通命令如何执行 ​

普通命令是 EXEC。

例如:

text
$ echo hello
1

shell 解析出:

text
argv[0] = "echo"
argv[1] = "hello"
argv[2] = 0
1
2
3

然后 shell 的父进程 fork。

子进程进入 runcmd。

runcmd 调用 exec("echo", argv)。

如果 exec 成功,它不会返回。

不会返回是 exec 的重要语义。

因为当前进程的用户地址空间已经被新程序替换。

如果 exec 返回,说明失败。

所以 xv6 shell 会打印 exec echo failed。

流程如下:

text
shell parent
  |
  | fork
  v
+-------------------+        +-------------------+
| parent shell      |        | child shell copy   |
| wait child        |        | exec echo          |
+-------------------+        +-------------------+
                                      |
                                      v
                              +-------------------+
                              | echo program      |
                              +-------------------+
1
2
3
4
5
6
7
8
9
10
11
12
13

11. 为什么要先 fork 再 exec ​

fork 创建当前进程的副本。

exec 把当前进程替换成新程序。

如果 shell 直接 exec echo,shell 自己就没了。

所以 shell 必须先 fork。

父进程保留 shell。

子进程变成目标程序。

这就是 Unix 程序启动模型。

它看起来绕。

但它非常灵活。

因为父进程可以在子进程 exec 前修改环境。

例如设置文件描述符。

例如设置管道。

例如设置当前目录。

例如设置后台执行关系。

fork 给了 shell 一个“可改造的子进程副本”。

exec 再把这个副本换成目标程序。

12. wait 在 shell 中的作用 ​

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

没有 wait,shell 会立刻打印下一个提示符。

前台命令通常需要 shell 等它结束。

例如:

text
$ cat README
1

用户期望 cat 输出完,shell 再出现 $ 。

所以父 shell 会 wait。

后台命令则不同。

例如:

text
$ longjob &
1

后台命令不要求 shell 等待。

xv6 的 BACK 分支会 fork 子进程。

然后父流程不 wait 这个后台子进程。

后台进程退出后可能由 init 回收。

这是 shell 简化实现的一部分。

13. 重定向如何工作 ​

重定向改变文件描述符指向。

例如:

text
$ echo hello > out
1

shell 的做法是:

关闭 fd 1。

打开文件 out。

因为 fd 1 空出来了,open 通常返回最小可用 fd,也就是 1。

于是 echo 写 stdout 时,实际上写入文件。

流程:

text
child process before exec
  |
  +-- close(1)
  +-- open("out", O_WRONLY | O_CREATE | O_TRUNC)
  +-- exec("echo", argv)
1
2
3
4
5

重定向必须在子进程中做。

否则父 shell 的 stdout 也会被改掉。

这再次说明 fork 的价值。

它给 shell 一个可安全修改的执行环境。

14. 管道如何工作 ​

管道连接两个进程。

左边命令写入管道。

右边命令读取管道。

例如:

text
$ cat README | grep xv6
1

shell 创建一个 pipe。

pipe 返回两个 fd。

p[0] 是读端。

p[1] 是写端。

左子进程把 stdout 接到写端。

右子进程把 stdin 接到读端。

父 shell 关闭自己持有的两个 pipe fd。

然后 wait 两个子进程。

图:

text
+------------+       pipe write       +------------+
| cat child  | ---------------------> | grep child |
| stdout=p1  |                        | stdin=p0   |
+------------+                        +------------+
       ^                                      ^
       |                                      |
 shell creates pipe and forks both children  |
1
2
3
4
5
6
7

关闭多余 fd 很重要。

如果写端没有全部关闭,读端可能一直等 EOF。

EOF 表示文件结束。

对管道来说,EOF 通常意味着所有写端都关闭。

15. 命令列表如何工作 ​

命令列表用分号表示。

例如:

text
$ echo one; echo two
1

LIST 命令有左命令和右命令。

xv6 shell 先 fork 执行左命令。

父进程 wait 左命令结束。

然后执行右命令。

这提供顺序语义。

图:

text
left command
  |
  v
wait left
  |
  v
right command
1
2
3
4
5
6
7

这里的右命令是递归执行。

所以多个分号可以形成一棵命令树。

16. 解析器的核心思想 ​

sh.c 的解析器是递归下降解析器。

递归下降解析器用函数调用表达语法层次。

parseline 处理 ; 和 &。

parsepipe 处理 |。

parseexec 处理普通命令和参数。

parseredirs 处理 <、>、>>。

parseblock 处理括号。

优先级大致如下:

text
line:      pipe [&] [; line]
pipe:      exec [| pipe]
exec:      words and redirs
block:     ( line ) redirs
1
2
3
4

优先级表示哪个语法先结合。

例如管道比分号结合更紧。

所以 a | b; c 会先组成 a | b。

然后再和 c 组成列表。

17. nulterminate 为什么存在 ​

解析时,shell 不立刻复制字符串。

它只是记录字符串起点和终点。

argv[i] 指向起点。

eargv[i] 指向终点。

终点可能是空格或特殊符号。

C 字符串需要以 \0 结尾。

所以执行前要调用 nulterminate。

它把终点字符改成 0。

这样 exec 能获得标准 C 字符串。

这是一个很朴素的零拷贝解析技巧。

但也带来限制。

命令缓冲区会被原地修改。

解析树中的字符串依赖原始输入缓冲区仍然存在。

18. 用户态系统调用入口 ​

用户程序调用 fork() 时,并不是直接跳到 kernel/proc.c。

路径是:

text
user C code
  |
  v
user syscall stub
  |
  v
ecall
  |
  v
trampoline.S uservec
  |
  v
trap.c usertrap
  |
  v
syscall.c syscall
  |
  v
sysproc.c sys_fork
  |
  v
proc.c kfork
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

stub 是很短的汇编函数。

它设置系统调用号。

它执行 ecall。

它返回到用户代码。

RISC-V 使用寄存器 a7 放系统调用号。

RISC-V 使用 a0 到 a5 传递前几个参数。

RISC-V 使用 a0 接收返回值。

这些约定叫 ABI。

ABI 是二进制接口约定。

它规定编译器、汇编代码、操作系统如何互相传参。

19. usys.pl 做什么 ​

user/usys.pl 是一个 Perl 脚本。

它为每个系统调用生成一段汇编。

例如 fork 的概念形式:

text
.global fork
fork:
  li a7, SYS_fork
  ecall
  ret
1
2
3
4
5

li 把立即数装入寄存器。

ecall 触发从用户态到内核的异常。

ret 返回调用者。

SYS_fork 来自 kernel/syscall.h。

user/usys.S 是生成结果。

Makefile 中有规则:

text
user/usys.S : user/usys.pl
        perl user/usys.pl > user/usys.S
1
2

所以干净仓库里没有 usys.S 并不奇怪。

构建时它会出现。

20. 参数如何进入内核 ​

用户调用:

c
write(1, "hi\n", 3)
1

调用约定会把参数放进寄存器。

a0 放 1。

a1 放字符串地址。

a2 放 3。

stub 把 SYS_write 放入 a7。

然后执行 ecall。

进入内核后,trampoline.S 保存寄存器到 trapframe。

trapframe 是保存用户寄存器的一页内存。

kernel/syscall.c 通过 p->trapframe->a7 取系统调用号。

argraw 通过 p->trapframe->a0 到 a5 取参数。

如果参数是用户指针,内核不能直接信任。

它要通过 copyin 或 copyinstr 从用户页表复制。

返回值写回 p->trapframe->a0。

回到用户态后,用户程序从 a0 看到返回值。

21. shell 执行一条普通命令的完整路径 ​

假设输入:

text
$ echo hello
1

第一步,shell 调用 getcmd 读一行。

第二步,shell 判断不是空行。

第三步,shell 判断不是 cd。

第四步,父 shell 调用 fork1。

第五步,子进程调用 parsecmd。

第六步,子进程调用 runcmd。

第七步,runcmd 看到 EXEC。

第八步,子进程调用 exec("echo", argv)。

第九步,内核加载 echo。

第十步,echo 运行并调用 write。

第十一步,echo 调用 exit。

第十二步,父 shell 的 wait 返回。

第十三步,shell 打印下一个 $ 。

这个过程可以画成时间线:

text
shell parent: read line -> fork -> wait -----------------> prompt again
                           |
child copy:                parse -> runcmd -> exec echo -> exit
1
2
3

22. exec 成功后为什么不返回 ​

exec 的语义是替换当前进程映像。

进程映像包括用户代码。

进程映像包括用户数据。

进程映像包括用户栈。

进程映像包括入口地址。

exec 成功后,旧的 shell 子进程代码不再存在。

CPU 将从新程序入口继续。

所以 runcmd 中 exec 后面的打印只代表失败路径。

这点常让初学者困惑。

fork 返回两次。

exec 成功返回零次。

wait 在父进程返回一次。

这三个语义组合成 Unix 的进程启动模型。

23. 文件描述符为什么能跨 exec 保留 ​

exec 替换用户地址空间。

但它不会清空进程的打开文件表。

这正是重定向和管道能工作的原因。

shell 在子进程 exec 前调整 fd。

exec 后目标程序继承这些 fd。

于是目标程序不需要知道自己被重定向。

它只管写 fd 1。

fd 1 可能是 console。

fd 1 也可能是普通文件。

fd 1 也可能是 pipe 写端。

这叫抽象边界。

程序依赖文件描述符接口。

shell 负责布置文件描述符连接。

24. shell 和内核的责任边界 ​

shell 负责解析命令。

shell 负责决定创建几个进程。

shell 负责连接文件描述符。

shell 负责等待前台命令。

内核负责创建进程。

内核负责加载 ELF。

内核负责管理文件对象。

内核负责调度进程。

内核负责安全地复制用户参数。

这条边界非常重要。

不要把 shell 的语法能力误认为内核功能。

内核并不知道 | 是什么。

内核只知道 pipe、fork、dup、close、exec。

| 是 shell 对这些系统调用的编排。

25. 常见误解 ​

误解一:shell 是内核的一部分。

不是。

shell 是普通用户程序。

误解二:系统调用就是 C 函数调用。

不是。

系统调用通过 ecall 触发特权级切换。

误解三:exec 创建新进程。

不是。

fork 创建进程。

exec 替换当前进程映像。

误解四:cd 可以像 ls 一样 exec。

不行。

如果 cd 在子进程执行,父 shell 目录不变。

误解五:管道是 shell 内存里的字符串。

不是。

管道是内核对象,连接两个文件描述符。

误解六:usys.S 必须手写。

不是。

它由 usys.pl 生成。

误解七:用户指针可以在内核随便解引用。

不能。

内核必须通过用户页表检查和复制。

26. 小实验:追踪 echo ​

实验目标:观察 echo hello 的进程链路。

建议修改点只用于学习分支。

可以在 kernel/sysproc.c 的 sys_fork 打印 pid。

可以在 kernel/sysfile.c 的 sys_exec 打印 path。

可以在 kernel/proc.c 的 kwait 打印回收 pid。

预期现象:

text
shell calls fork
child calls exec echo
echo calls write
echo calls exit
shell wait returns
1
2
3
4
5

验证问题一:fork 后父子哪个先进 runcmd?

验证问题二:exec 成功后是否还会打印失败信息?

验证问题三:wait 返回的是哪个 pid?

27. 小实验:理解 cd ​

实验目标:验证 cd 为什么必须由父 shell 执行。

做法一:保留原始 shell。

运行:

text
$ mkdir d
$ cd d
$ pwd
1
2
3

xv6 标准用户程序未必有完整 pwd。

可以用 ls 和相对路径观察。

做法二:把 cd 改成在子进程中执行。

然后观察 shell 的目录是否改变。

预期结果:父 shell 目录不会改变。

结论:进程资源变化默认不反向影响父进程。

28. 小实验:故意不关闭 pipe fd ​

实验目标:理解管道 EOF。

在 PIPE 分支中,临时注释父进程关闭 pipe fd 的代码。

运行管道命令。

观察右侧命令是否可能等待 EOF。

然后恢复代码。

解释原因:

text
some write end still open
  |
  v
reader cannot observe EOF
  |
  v
pipeline may hang
1
2
3
4
5
6
7

这类实验非常适合理解文件描述符引用。

29. 小实验:新增一个用户程序 ​

实验目标:理解用户程序如何进入 fs.img。

创建 user/hello.c。

在 Makefile 的 UPROGS 添加 _hello。

重新构建并启动 xv6。

在 shell 输入:

text
$ hello
1

如果提示 exec failed,检查程序是否在 fs.img。

如果编译失败,检查是否包含 user/user.h。

如果系统调用未链接,检查 ULIB 是否包含 usys.o。

这能把构建链路和运行链路连起来。

30. 阅读源码的顺序 ​

先读 user/init.c。

它短,而且揭示第一个 shell 怎么来。

再读 user/sh.c 的 main。

不要一开始陷进解析器。

接着读 runcmd。

理解每种命令类型如何 fork、exec、wait。

然后读 user/usys.pl。

把用户函数名和 ecall 联系起来。

再读 kernel/trap.c:usertrap。

观察 ecall 如何进入 syscall()。

最后读 kernel/syscall.c 和具体 sys_*。

这条路线能避免被文件数量吓住。

31. 本篇总结 ​

xv6 的用户世界很小。

但它已经包含 Unix 的关键骨架。

init 负责启动和回收 shell。

sh 负责解析命令和编排系统调用。

usys.pl 负责生成用户态系统调用入口。

ecall 负责跨越用户态和内核态边界。

fork、exec、wait 负责普通命令生命周期。

请记住这一句:

text
shell syntax is user-space policy;
fork/exec/wait/pipe/dup are kernel mechanisms.
1
2

下一篇进入 GDB。

因为理解到这里之后,最好的进阶方式不是继续背流程。

而是用断点验证自己的心智模型。

最后更新于:

Pager
上一篇15. xv6 设备驱动、console、UART 与磁盘 / xv6 Device Driver Console UART And Disk
下一篇17. 用 QEMU/GDB 调试 xv6 / Debugging xv6 With QEMU And GDB

持续记录,持续成长

Copyright © Tidenflow