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 trap、中断与系统调用 / xv6 Traps Interrupts And System Calls ​

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

系统调用是理解 xv6 的最佳入口之一。

因为它把用户程序、CPU、汇编、trapframe、内核 C 函数、进程结构全部连起来。

你在用户程序里写:

c
getpid();
1

看起来像普通函数调用。

但真实路径不是:

text
user getpid -> kernel sys_getpid
1

真实路径更像:

text
user getpid wrapper
  |
  v
ecall instruction
  |
  v
hardware trap
  |
  v
trampoline.S
  |
  v
trap.c:usertrap()
  |
  v
syscall.c:syscall()
  |
  v
sysproc.c:sys_getpid()
  |
  v
trapframe->a0
  |
  v
usertrapret()
  |
  v
trampoline.S:userret
  |
  v
sret back to user
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

这篇文章要解决:

  • trap 是什么。
  • 系统调用、中断、异常有什么区别。
  • 用户态如何进入内核。
  • 内核如何保存用户寄存器。
  • trapframe 为什么存在。
  • syscall.c 如何分发系统调用。
  • sysproc.c 和 sysfile.c 分别做什么。
  • 返回值如何回到用户程序。
  • 为什么 trampoline 必须存在。

这篇是后面学习 proc、调度、文件系统的关键前置。

1.1 本文基础词小注释 ​

这一篇绝不能默认你知道 trap。

先把最基础词讲清。

text
trap
  |
  +-- CPU 转去内核处理特殊事件
  +-- 事件可能是系统调用、中断、异常
  +-- 可以理解成“执行流被带到内核处理”

syscall
  |
  +-- 系统调用
  +-- 用户程序主动请求内核服务
  +-- 例如 fork、read、write

interrupt
  |
  +-- 中断
  +-- 外部设备或定时器打断当前执行
  +-- 例如键盘输入、磁盘完成、时钟到点

exception
  |
  +-- 异常
  +-- 当前指令出问题导致进入内核
  +-- 例如访问非法地址

trapframe
  |
  +-- 保存用户寄存器的一块内存
  +-- 内核处理完 trap 后靠它恢复用户程序
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

2. trap、interrupt、exception、syscall 的区别 ​

先把几个词分清。

trap 是总称。

它表示 CPU 从当前执行流转入内核处理特殊事件。

特殊事件包括:

  • 系统调用。
  • 中断。
  • 异常。
text
trap
  |
  +-- syscall
  |     |
  |     +-- user program intentionally asks kernel for service
  |
  +-- interrupt
  |     |
  |     +-- external or timer event interrupts current flow
  |
  +-- exception
        |
        +-- current instruction causes fault or illegal condition
1
2
3
4
5
6
7
8
9
10
11
12
13

系统调用是主动的。

用户程序执行 ecall,请求内核服务。

中断通常是异步的。

例如定时器到了,磁盘完成了,UART 收到输入了。

异常通常是当前指令引起的。

例如访问非法地址、执行非法指令。

这三种事件都会让 CPU 进入 trap 处理路径。

但内核要根据原因做不同处理。

这就是 scause 的作用。

3. 用户态和内核态边界 ​

用户程序运行在 user mode。

内核运行在 supervisor mode。

用户程序不能直接执行内核内部函数。

它必须通过受控入口进入内核。

text
User mode
  |
  | ecall / interrupt / exception
  v
Supervisor mode
  |
  | sret
  v
User mode
1
2
3
4
5
6
7
8
9

这个边界不是 C 语言边界。

而是硬件特权级边界。

普通函数调用:

text
caller
  |
  v
callee
  |
  v
return
1
2
3
4
5
6
7

系统调用:

text
user code
  |
  v
ecall
  |
  v
trap entry
  |
  v
kernel dispatch
  |
  v
sret
  |
  v
user code
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

这就是为什么系统调用要处理:

  • 寄存器保存。
  • 地址空间切换。
  • 权限检查。
  • 用户参数读取。
  • 返回值写回。

普通函数调用不需要这些。

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

系统调用和 trap 路径涉及这些文件:

text
user/user.h
  |
  +-- user-visible syscall declarations

user/usys.pl
  |
  +-- generates user/usys.S syscall wrappers

kernel/syscall.h
  |
  +-- syscall number definitions

kernel/syscall.c
  |
  +-- syscall dispatch table
  +-- argument fetching helpers
  +-- syscall()

kernel/sysproc.c
  |
  +-- process-related syscalls
  +-- fork, exit, wait, getpid, sbrk, sleep, uptime

kernel/sysfile.c
  |
  +-- file-related syscalls
  +-- open, read, write, close, exec, chdir, link, unlink

kernel/trap.c
  |
  +-- usertrap()
  +-- usertrapret()
  +-- kerneltrap()
  +-- devintr()

kernel/trampoline.S
  |
  +-- uservec
  +-- userret
  +-- save/restore user registers
  +-- switch page tables

kernel/kernelvec.S
  |
  +-- kernel-mode trap vector

kernel/proc.h
  |
  +-- struct trapframe
  +-- struct proc

kernel/proc.c
  |
  +-- myproc()
  +-- yield()
  +-- sleep/wakeup interactions
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
55
56

注意:

系统调用不是只看 syscall.c。

syscall.c 只是分发中心。

完整路径跨越用户态 wrapper、trap 汇编、trap C 处理、具体系统调用实现和返回路径。

5. 一次 getpid() 的完整路径 ​

getpid() 是最适合入门的系统调用。

因为它没有复杂参数。

它只返回当前进程 pid。

完整路径:

text
user program
  |
  | getpid()
  v
user syscall wrapper
  |
  | a7 = SYS_getpid
  | ecall
  v
hardware trap
  |
  | sepc = user pc
  | scause = syscall cause
  | jump to stvec
  v
trampoline.S:uservec
  |
  | save user registers into trapframe
  | switch to kernel page table
  v
trap.c:usertrap()
  |
  | detects syscall
  | advances epc
  v
syscall.c:syscall()
  |
  | num = trapframe->a7
  | call syscalls[num]
  v
sysproc.c:sys_getpid()
  |
  | return myproc()->pid
  v
syscall()
  |
  | trapframe->a0 = return value
  v
usertrapret()
  |
  | prepare return to user
  v
trampoline.S:userret
  |
  | switch to user page table
  | restore user registers
  | sret
  v
user program receives pid in a0
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

这条路径非常重要。

后面 fork()、read()、write() 都是在这条骨架上增加复杂度。

6. 用户态 syscall wrapper ​

用户程序里调用的 getpid(),不是内核里的 sys_getpid()。

用户态有一层 wrapper。

它负责:

  • 把系统调用号放进寄存器。
  • 执行 ecall。
  • 返回结果。

抽象图:

text
user C call
  |
  v
assembly wrapper
  |
  +-- load syscall number into a7
  +-- ecall
  +-- return to caller
1
2
3
4
5
6
7
8

user/usys.pl 会生成 user/usys.S。

这说明有些源码是生成的。

不要只找手写 .S 文件。

你要知道生成关系。

text
user/usys.pl
  |
  v
user/usys.S
  |
  v
user program links syscall wrappers
1
2
3
4
5
6
7

系统调用号来自 kernel/syscall.h。

用户态 wrapper 和内核分发表必须对同一个编号达成一致。

否则用户请求的编号和内核解释会错位。

7. ecall 发生后硬件做什么 ​

ecall 是 RISC-V 指令。

它会触发环境调用异常。

硬件会进入 trap 机制。

简化来说:

text
execute ecall in user mode
  |
  v
hardware records trap info
  |
  +-- sepc records user pc
  +-- scause records cause
  |
  v
jump to trap vector
1
2
3
4
5
6
7
8
9
10

trap vector 地址由 stvec 决定。

用户态 trap 时,xv6 会让它进入 trampoline。

为什么不能直接跳到普通 C 函数?

因为这时状态很特殊:

  • 当前仍处于用户页表相关环境。
  • 用户寄存器还没保存。
  • 内核栈需要切换。
  • 后续要进入内核页表。

所以先进入汇编桥接代码。

text
hardware trap
  |
  v
trampoline assembly
  |
  v
C trap handler
1
2
3
4
5
6
7

这就是 trampoline.S 的价值。

8. trampoline.S:uservec:保存用户现场 ​

用户态 trap 进入后,uservec 负责保存现场并切入内核。

它的核心任务:

text
uservec
  |
  +-- save user registers to trapframe
  +-- load kernel stack
  +-- load kernel page table
  +-- jump to usertrap()
1
2
3
4
5
6

为什么要保存用户寄存器?

因为内核运行也要用寄存器。

如果不保存,用户程序的寄存器值会被内核覆盖。

返回用户态时就无法继续。

text
before trap
  |
  +-- user registers contain user computation state

during kernel
  |
  +-- kernel needs registers

after trap
  |
  +-- user registers must be restored
1
2
3
4
5
6
7
8
9
10
11

trapframe 就是保存位置。

text
struct proc
  |
  +-- trapframe
        |
        v
   saved user registers
1
2
3
4
5
6

这里一定要记住:

trapframe 是当前进程的一部分。

不同进程有不同 trapframe。

9. struct trapframe ​

struct trapframe 定义在 proc.h。

它保存用户态寄存器和内核返回所需信息。

简化结构:

text
struct trapframe
  |
  +-- kernel_satp
  +-- kernel_sp
  +-- kernel_trap
  +-- epc
  +-- kernel_hartid
  |
  +-- ra
  +-- sp
  +-- gp
  +-- tp
  +-- t0-t6
  +-- s0-s11
  +-- a0-a7
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

分两组看。

第一组:返回内核时需要的辅助字段。

text
kernel_satp
kernel_sp
kernel_trap
kernel_hartid
1
2
3
4

第二组:用户寄存器保存区。

text
ra sp gp tp
t0-t6
s0-s11
a0-a7
epc
1
2
3
4
5

epc 保存用户程序返回位置。

a7 保存系统调用号。

a0 保存参数,也保存返回值。

系统调用返回值写入:

text
trapframe->a0
1

这就是用户程序最终拿到返回值的原因。

10. usertrap():进入内核后的 C 层处理 ​

usertrap() 在 trap.c 中。

它处理来自用户态的 trap。

它要先判断 trap 原因。

text
usertrap()
  |
  v
read scause
  |
  +-- syscall?
  |
  +-- device interrupt?
  |
  +-- unexpected exception?
1
2
3
4
5
6
7
8
9
10

如果是系统调用:

text
if killed
  |
  +-- exit

advance epc
  |
  +-- skip ecall instruction

enable interrupts
  |
  +-- allow devices while syscall runs

syscall()
  |
  +-- dispatch
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

为什么要 advance epc?

因为 sepc/epc 指向触发 trap 的那条 ecall。

如果不前进,返回用户态后会再次执行 ecall。

于是无限系统调用。

text
pc points to ecall
  |
  v
trap
  |
  v
epc must become next instruction
1
2
3
4
5
6
7

这是一处非常经典的源码点。

11. syscall.c:系统调用分发器 ​

syscall.c 做两类事。

第一类,取参数。

第二类,按系统调用号分发。

分发表模型:

text
syscall number
  |
  v
syscalls[num]
  |
  v
sys_* function
1
2
3
4
5
6
7

例如:

text
SYS_fork   -> sys_fork
SYS_exit   -> sys_exit
SYS_wait   -> sys_wait
SYS_getpid -> sys_getpid
SYS_read   -> sys_read
SYS_write  -> sys_write
1
2
3
4
5
6

系统调用号来自哪里?

用户 wrapper 放进 a7。

trap 保存到 trapframe。

内核读取:

text
myproc()->trapframe->a7
1

返回值写到:

text
myproc()->trapframe->a0
1

图:

text
trapframe
  |
  +-- a7: syscall number
  |
  +-- a0: return value
1
2
3
4
5

这就是寄存器约定和 C 结构体的结合点。

12. 参数获取:为什么不能直接读用户指针 ​

系统调用可能带参数。

例如:

c
write(fd, buf, n);
1

用户态寄存器大致:

text
a0 = fd
a1 = buf
a2 = n
a7 = SYS_write
1
2
3
4

fd 和 n 是普通整数。

buf 是用户虚拟地址。

内核不能直接把它当 char * 解引用。

所以参数获取分两层。

第一层,从 trapframe 取寄存器值。

text
argraw(n)
  |
  v
trapframe->a0/a1/a2/...
1
2
3
4

第二层,如果参数代表用户地址或字符串,再通过 copyin/copyout/copyinstr。

text
user pointer value
  |
  v
copyin/copyout/copyinstr
  |
  v
safe kernel buffer or user write
1
2
3
4
5
6
7

这就是系统调用安全的基础。

用户传什么地址,内核都要验证。

13. sysproc.c:进程相关系统调用 ​

sysproc.c 处理进程类系统调用。

常见:

text
sys_fork
sys_exit
sys_wait
sys_pipe
sys_kill
sys_getpid
sys_sbrk
sys_sleep
sys_uptime
1
2
3
4
5
6
7
8
9

这些函数通常比较薄。

它们会:

  • 取参数。
  • 调用内核核心函数。
  • 返回结果。

例如:

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

sys_fork:

text
sys_fork
  |
  v
fork()
1
2
3
4

sys_exit:

text
sys_exit
  |
  v
argint status
  |
  v
exit(status)
1
2
3
4
5
6
7

不要把 sys_* 当成全部实现。

它们经常是系统调用层 wrapper。

核心逻辑在 proc.c、vm.c、file.c 等文件中。

14. sysfile.c:文件相关系统调用 ​

sysfile.c 处理文件和路径相关系统调用。

常见:

text
sys_read
sys_write
sys_open
sys_close
sys_fstat
sys_link
sys_unlink
sys_mkdir
sys_mknod
sys_chdir
sys_exec
1
2
3
4
5
6
7
8
9
10
11

它比 sysproc.c 更复杂。

因为文件系统参数涉及:

  • fd。
  • 用户 buffer。
  • 路径字符串。
  • inode。
  • file。
  • pipe。

典型 read() 路径:

text
sys_read
  |
  +-- fetch fd
  +-- fetch user buffer address
  +-- fetch size
  v
fileread
  |
  +-- pipe read
  +-- inode read
  +-- device read
1
2
3
4
5
6
7
8
9
10
11

典型 open() 路径:

text
sys_open
  |
  +-- copy path string from user
  +-- begin filesystem op
  +-- name lookup or create
  +-- allocate file object
  +-- allocate fd
  +-- return fd
1
2
3
4
5
6
7
8

这里你会看到用户地址边界。

路径字符串来自用户空间。

需要安全复制到内核。

15. 设备中断路径 ​

系统调用是主动 trap。

设备中断是异步 trap。

例如 UART 收到字符。

或者 VirtIO 磁盘完成请求。

路径大致是:

text
device event
  |
  v
PLIC
  |
  v
CPU trap
  |
  v
trap.c:devintr()
  |
  +-- identify device
  +-- call driver handler
  +-- complete interrupt
1
2
3
4
5
6
7
8
9
10
11
12
13
14

UART 输入可能走:

text
UART interrupt
  |
  v
devintr
  |
  v
uartintr
  |
  v
consoleintr
  |
  v
wakeup readers
1
2
3
4
5
6
7
8
9
10
11
12
13

磁盘中断可能走:

text
VirtIO disk interrupt
  |
  v
devintr
  |
  v
virtio_disk_intr
  |
  v
wakeup waiting process
1
2
3
4
5
6
7
8
9
10

这连接到 sleep/wakeup。

进程等待设备时可能睡眠。

设备完成后中断唤醒它。

16. 定时器中断和调度 ​

定时器中断让内核获得周期性控制权。

如果当前进程一直运行,定时器中断可以让内核进入 trap。

然后决定是否 yield()。

text
timer interrupt
  |
  v
trap
  |
  v
usertrap or kerneltrap
  |
  v
yield
  |
  v
scheduler
1
2
3
4
5
6
7
8
9
10
11
12
13

这就是分时调度的基础。

用户程序不需要主动配合。

硬件定时器会制造进入内核的机会。

text
running user process
  |
  | timer interrupt
  v
kernel gets control
  |
  v
may switch process
1
2
3
4
5
6
7
8

所以 trap 和调度关系很近。

学调度前必须理解定时器中断。

17. kerneltrap():内核态 trap ​

并不是所有 trap 都来自用户态。

内核执行时也可能发生 trap。

这时走 kerneltrap()。

路径:

text
kernel code running
  |
  v
interrupt / exception
  |
  v
kernelvec.S
  |
  v
trap.c:kerneltrap()
  |
  v
handle or panic
1
2
3
4
5
6
7
8
9
10
11
12
13

和 usertrap() 的区别:

text
usertrap
  |
  +-- came from user mode
  +-- can kill current process
  +-- returns through usertrapret

kerneltrap
  |
  +-- came from kernel mode
  +-- unexpected exceptions are serious
  +-- returns to kernel code
1
2
3
4
5
6
7
8
9
10
11

如果用户程序非法访问,内核可以杀掉用户进程。

如果内核自己非法访问,通常说明内核 bug。

这就是二者处理差异的根本原因。

18. usertrapret():准备返回用户态 ​

系统调用处理完后,不能直接 C return 回用户程序。

因为之前跨越了特权级和页表边界。

必须准备返回环境。

usertrapret() 做这些事情:

text
usertrapret()
  |
  +-- disable interrupts while preparing
  +-- set stvec to user trap vector
  +-- fill trapframe kernel fields
  +-- set sepc to user pc
  +-- set sstatus for returning to user
  +-- jump to trampoline userret
1
2
3
4
5
6
7
8

然后 userret:

text
userret
  |
  +-- switch to user page table
  +-- restore user registers
  +-- execute sret
1
2
3
4
5

返回图:

text
kernel C
  |
  v
usertrapret
  |
  v
trampoline userret
  |
  v
sret
  |
  v
user code
1
2
3
4
5
6
7
8
9
10
11
12
13

这条路径体现了:

普通函数返回不够。

必须用硬件定义的 trap return 机制。

19. 系统调用返回值怎么回到用户程序 ​

系统调用返回值通常通过寄存器 a0。

进入 trap 时,用户寄存器被保存到 trapframe。

内核处理后,把返回值写到 trapframe->a0。

返回用户态时,trampoline 恢复寄存器。

于是用户态 a0 就是返回值。

text
sys_* returns value
  |
  v
syscall()
  |
  v
p->trapframe->a0 = value
  |
  v
userret restores a0
  |
  v
user function returns value
1
2
3
4
5
6
7
8
9
10
11
12
13

这解释了为什么 trapframe 是系统调用路径的关键对象。

它不是抽象概念。

它是用户寄存器和内核 C 代码之间的桥。

20. 系统调用、proc 和 trapframe 的关系 ​

当前进程通过 myproc() 找到。

当前进程里有 trapframe。

系统调用从 trapframe 读参数、写返回值。

text
current CPU
  |
  v
struct cpu
  |
  v
current proc pointer
  |
  v
struct proc
  |
  v
trapframe
  |
  +-- syscall number in a7
  +-- args in a0-a5
  +-- return value in a0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

所以读 syscall.c 时,必须知道 proc。

读 proc 时,也要知道 trapframe。

它们互相依赖。

这就是为什么本专题把 trap 放在 proc 前面。

否则 p->trapframe 会很抽象。

21. 系统调用和文件系统的连接 ​

以 write() 为例。

路径:

text
user write(fd, buf, n)
  |
  v
ecall
  |
  v
syscall
  |
  v
sys_write
  |
  +-- fdarg finds struct file
  +-- fetch user buffer address
  v
filewrite
  |
  +-- console/device
  +-- pipe
  +-- inode file
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

这里有三个层次:

text
syscall layer
  |
  +-- convert user request to kernel objects

file layer
  |
  +-- operate on struct file

filesystem/device layer
  |
  +-- inode, pipe, console, disk
1
2
3
4
5
6
7
8
9
10
11

系统调用层的任务不是自己完成所有工作。

它把用户态请求安全地转成内核对象操作。

22. 系统调用和虚拟内存的连接 ​

系统调用参数经常包含用户地址。

例如:

text
read(fd, user_buf, n)
write(fd, user_buf, n)
exec(user_path, user_argv)
stat(user_path, user_stat_addr)
1
2
3
4

这些都需要跨用户/内核地址边界。

text
trapframe contains user virtual address
  |
  v
syscall implementation
  |
  v
copyin/copyout/copyinstr
  |
  v
vm.c walks process page table
1
2
3
4
5
6
7
8
9
10

这就是 trap 和 vm.c 的连接。

系统调用不是只和 syscall.c 有关。

它经常调用虚拟内存辅助函数。

如果用户地址非法,系统调用应该失败,而不是让内核崩溃。

23. 系统调用和调度的连接 ​

有些系统调用会导致当前进程不再继续运行。

例如:

text
sleep
wait
read from empty pipe
exit
1
2
3
4

这些系统调用会改变进程状态。

text
RUNNING
  |
  +-- sleep syscall
  v
SLEEPING

RUNNING
  |
  +-- exit syscall
  v
ZOMBIE
1
2
3
4
5
6
7
8
9
10
11

然后调度器会选择其他进程。

所以系统调用也可能是调度入口。

text
user syscall
  |
  v
kernel changes proc state
  |
  v
sched / scheduler
1
2
3
4
5
6
7

这就是为什么不能把系统调用理解成普通函数。

普通函数调用不会让当前执行流变成睡眠进程。

系统调用可以。

24. 初学者常见误解 ​

误解一:trap 等于中断。

不准确。

trap 是总称,中断只是其中一种。

误解二:系统调用是普通函数调用。

不是。

它跨越用户态和内核态。

误解三:sys_getpid() 是用户程序直接调用的。

不是。

用户程序通过 wrapper 和 ecall 进入内核,再由分发表调用 sys_getpid()。

误解四:系统调用参数都安全。

不是。

用户传入的地址必须验证和复制。

误解五:trapframe 只是调试用结构。

不是。

它保存用户寄存器,是返回用户态的关键。

误解六:返回用户态就是 C 函数 return。

不是。

需要 usertrapret、trampoline 和 sret。

误解七:syscall.c 包含所有系统调用逻辑。

不是。

它主要分发,具体逻辑在 sysproc.c、sysfile.c 和更底层模块。

误解八:中断只在用户态发生。

不是。

内核态也可能发生 trap,对应 kerneltrap()。

25. 小实验:追踪 getpid() ​

实验目标:

观察一次最简单系统调用。

可以在这些位置加打印或断点:

text
syscall()
sys_getpid()
usertrap()
usertrapret()
1
2
3
4

建议先打印:

text
pid
syscall number
return value
trapframe address
1
2
3
4

示意:

text
usertrap: pid=3
syscall: pid=3 num=SYS_getpid
sys_getpid: pid=3
syscall return: a0=3
1
2
3
4

观察问题:

  • 系统调用号从哪里来?
  • 返回值写到哪里?
  • 当前进程怎么找到?
  • 用户程序为什么能拿到返回值?

不要在所有系统调用里长期打印。

输出会很多。

实验结束后撤掉打印。

26. 小实验:新增一个简单系统调用 ​

实验目标:

通过新增系统调用理解完整链路。

可以新增一个返回固定值的系统调用。

路径会涉及:

text
kernel/syscall.h
  |
  +-- add syscall number

kernel/syscall.c
  |
  +-- add extern declaration
  +-- add dispatch table entry

kernel/sysproc.c
  |
  +-- implement sys_hello or similar

user/user.h
  |
  +-- add user declaration

user/usys.pl
  |
  +-- add wrapper entry

user test program
  |
  +-- call new syscall
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

这个实验很适合巩固:

  • 用户态声明。
  • wrapper 生成。
  • syscall number。
  • 内核分发表。
  • sys_* 实现。
  • 返回值路径。

完成后,你会真正理解系统调用不是一个文件里的东西。

它是一条跨越用户态和内核态的协议。

27. 本篇总结 ​

trap 是 xv6 中用户态和内核态的桥。

系统调用是用户程序主动进入内核的方式。

中断是设备或定时器让内核获得控制权的方式。

异常是当前指令出问题时进入内核的方式。

核心路径:

text
user wrapper
  |
  v
ecall
  |
  v
trampoline uservec
  |
  v
trapframe
  |
  v
usertrap
  |
  v
syscall
  |
  v
sysproc/sysfile
  |
  v
trapframe->a0
  |
  v
usertrapret
  |
  v
trampoline userret
  |
  v
sret
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 就不再只是一个结构体。

你会知道:

proc 保存 trapframe。

trapframe 保存用户寄存器。

系统调用通过当前 proc 找到 trapframe。

返回值通过 trapframe 回到用户程序。

下一篇进入:

text
08-process-and-proc-structure.md
1

这是整个 xv6 专题的核心之一。

我们会把 struct proc、proc[NPROC]、进程状态、父子关系、trapframe、context、pagetable、ofile、cwd 全部拆开。

最后更新于:

Pager
上一篇7. xv6 内存布局与页表 / xv6 Memory Layout And Page Tables
下一篇9. xv6 进程与 struct proc / xv6 Process And struct proc

持续记录,持续成长

Copyright © Tidenflow