跳转至

CH2 补充读物:把 OS Structure 和 System Call 读成可运行代码

1. 这份读物怎么用

这份文档不是课件提纲,而是给你课后自己读代码用的“导航图”。

你读完后,应该能独立讲清 3 件事:

  1. 你在 shell 里敲一条命令后,系统到底做了什么。
  2. 一个用户态函数(如 getpid)如何进入内核。
  3. 这套内核在“结构设计”上属于哪种风格,为什么。

2. 从一个真实动作开始:在 shell 里输入 pid

建议你先在系统里实际运行一次:

pid

然后按这条路径阅读代码:

  1. user/init.c:系统启动后为什么会拉起 shell。
  2. user/sh.c:shell 怎么解析命令、怎么 fork + exec
  3. user/pid.cpid 程序本体只做了什么。
  4. user/syscall.cgetpid() 最终如何触发 ecall
  5. kernel/core/trap.c:内核如何识别这是一次用户态系统调用。
  6. kernel/core/syscall.c:如何分发到 sys_getpid

如果你能把上面 6 步串起来,CH2 的主线就已经真正掌握了。

3. OS 服务在这套内核里的具体体现

操作系统课常说“OS 提供服务”,在这套代码里可以落成下面几类:

  1. 程序执行服务:fork/exec/wait/exit
  2. I/O 服务:read/write,以及控制台与磁盘路径。
  3. 文件系统服务:open/close/fstat/mkdir/unlink/link/chdir
  4. 进程通信服务:pipe
  5. 时间和系统信息服务:sleep/uptime/getpid
  6. 保护与隔离:用户态不能直接碰内核对象,必须走系统调用边界。

你可以在 kernel/include/syscall.hkernel/core/syscall.c 中看到这些服务是如何被注册和分发的。

4. CLI 不是“命令合集”,而是“命令调度器”

user/sh.c 很适合反复读,因为它把“用户接口”这个抽象概念写成了很实在的代码:

  1. 读取一行输入。
  2. 解析参数。
  3. 内建命令(如 cd)在 shell 自己内部处理。
  4. 外部命令通过 fork 创建子进程,然后子进程 exec 目标程序。
  5. 父进程 wait 子进程结束。

这就是你日常和 OS 交互的核心机制。

5. API 和系统调用:两层不要混

在这套仓库里,这两层的分工很清楚:

  1. user/user.h:给应用程序调用的函数声明(API 入口)。
  2. user/syscall.c:把 API 变成寄存器约定 + ecall
  3. kernel/core/syscall.c:内核侧分发表和具体实现。

简化理解:

  1. API 是“你写 C 程序时调用的函数名”。
  2. 系统调用是“CPU 进入内核态后的服务分发机制”。

6. 参数是怎么跨边界传进去的

这个项目当前采用寄存器传参路径:

  1. a7 放系统调用号。
  2. a0/a1/a2 放参数。
  3. ecall 触发 trap。
  4. uservec 把寄存器保存进 trapframe。
  5. syscall() 从 trapframe 读取参数。
  6. 返回值再写回 trapframe 的 a0,最终带回用户态。

建议你对照这几个文件连读:

  1. user/syscall.c
  2. kernel/arch/riscv/trampoline.S
  3. kernel/include/proc.hstruct trapframe
  4. kernel/core/trap.c
  5. kernel/core/syscall.c

7. 系统调用类别在代码里的位置

你可以把当前系统调用粗分成:

  1. 进程控制类:fork/exec/wait/exit/kill/yield/getpid/sleep
  2. 文件类:open/read/write/close/dup/fstat/mkdir/unlink/link/chdir
  3. 通信类:pipe
  4. 系统信息类:uptime

做代码阅读时,建议每类至少挑一个函数,做一次“用户态 -> 内核态 -> 返回”的闭环跟踪。

8. 这套内核在结构上属于什么风格

如果你学过“simple / monolithic / layered / microkernel / modules”这些术语,这个项目可这样理解:

  1. 执行模型上是单内核(核心功能都在内核态同一地址空间)。
  2. 工程组织上是模块化目录(arch/core/drivers/fs/lib)。
  3. 不是 microkernel(文件系统与驱动不是用户态服务进程)。

所以最贴切的描述是:单内核实现 + 模块化代码组织。

9. 启动链路:从硬件到调度器

建议你至少完整读一遍这条路径:

  1. kernel/arch/riscv/entry.S:最初入口。
  2. kernel/arch/riscv/start.c:M 态初始化,切换到 S 态。
  3. kernel/core/main.c:初始化内存、进程、中断、文件系统。
  4. scheduler():开始调度。

这能把“操作系统是怎么起来的”从概念变成你能看到的真实代码。

10. 读这章时最容易踩的坑

  1. 以为 API 调用等于系统调用。
  2. 只看 sys_xxx,忽略了 trap 入口和返回路径。
  3. 只看用户程序,不看 shell 如何组织 fork/exec/wait
  4. 只记术语,不把术语映射到具体文件和函数。

11. 最小自测(不交作业,仅自检)

你可以问自己这 5 个问题:

  1. a7 的作用是什么?
  2. ecall 后先到哪个汇编入口?
  3. syscall() 怎么找到 sys_getpid()
  4. 为什么 usertrap() 里要把 epc 前移?
  5. shell 为什么要 fork 后再 exec,而不是直接 exec

如果你能用代码位置回答这些问题,CH2 就已经学到位了。