|
| 1 | +--- |
| 2 | +marp: false |
| 3 | +theme: default |
| 4 | +paginate: true |
| 5 | +_paginate: false |
| 6 | +header: '' |
| 7 | +footer: '' |
| 8 | +backgroundColor: white |
| 9 | +--- |
| 10 | + |
| 11 | +<!-- theme: gaia --> |
| 12 | +<!-- page_number: true --> |
| 13 | +<!-- _class: lead --> |
| 14 | + |
| 15 | +## 第一讲 操作系统概述 |
| 16 | + |
| 17 | +### 第六节 Linux 内核服务总体架构 |
| 18 | + |
| 19 | +<br> |
| 20 | +<br> |
| 21 | + |
| 22 | +向勇 陈渝 李国良 任炬 |
| 23 | + |
| 24 | +<br> |
| 25 | +<br> |
| 26 | + |
| 27 | +2026年春季 |
| 28 | + |
| 29 | +--- |
| 30 | + |
| 31 | +## 问题 |
| 32 | + |
| 33 | +- `helloworld` 为什么不能直接把字符写到硬件上? |
| 34 | +- `open`、`copy`、`fork`、`redirect`、`pipe` 为什么都要经过内核? |
| 35 | +- Linux 内核到底提供了哪些服务?这些服务如何组合起来? |
| 36 | + |
| 37 | +--- |
| 38 | + |
| 39 | +## 总体主线 |
| 40 | + |
| 41 | +```mermaid |
| 42 | +flowchart TB |
| 43 | + subgraph userSpace["用户态"] |
| 44 | + shellProg["Shell / 用户程序"] |
| 45 | + appCode["helloworld / open / copy / forkexec"] |
| 46 | + libc["C 库封装\nprintf open read write fork exec"] |
| 47 | + end |
| 48 | +
|
| 49 | + subgraph kernelSpace["内核态"] |
| 50 | + syscallEntry["系统调用入口\ntrap / syscall"] |
| 51 | + procSvc["进程管理"] |
| 52 | + memSvc["内存管理"] |
| 53 | + fsSvc["文件系统与 VFS"] |
| 54 | + ioSvc["I/O 与设备驱动"] |
| 55 | + ipcSvc["IPC / pipe / socket"] |
| 56 | + end |
| 57 | +
|
| 58 | + hw["终端 / 磁盘 / 网卡 / 时钟 / CPU"] |
| 59 | +
|
| 60 | + shellProg --> appCode |
| 61 | + appCode --> libc |
| 62 | + libc --> syscallEntry |
| 63 | + syscallEntry --> procSvc |
| 64 | + syscallEntry --> memSvc |
| 65 | + syscallEntry --> fsSvc |
| 66 | + syscallEntry --> ioSvc |
| 67 | + syscallEntry --> ipcSvc |
| 68 | + ioSvc --> hw |
| 69 | + fsSvc --> hw |
| 70 | + procSvc --> hw |
| 71 | +``` |
| 72 | + |
| 73 | +- 用户程序看到的是函数和文件描述符 |
| 74 | +- 内核看到的是进程、地址空间、文件对象、设备对象和缓冲区 |
| 75 | +- 硬件访问被统一封装成受保护的内核服务 |
| 76 | + |
| 77 | +--- |
| 78 | + |
| 79 | +## `helloworld` 如何得到输出服务 |
| 80 | + |
| 81 | +```mermaid |
| 82 | +sequenceDiagram |
| 83 | + participant shell as Shell |
| 84 | + participant app as hello |
| 85 | + participant libc as libc |
| 86 | + participant kernel as LinuxKernel |
| 87 | + participant tty as TTYConsole |
| 88 | + participant device as UARTOrDisplay |
| 89 | +
|
| 90 | + shell->>kernel: fork + execve ./hello |
| 91 | + kernel-->>app: 开始执行 main |
| 92 | + app->>libc: printf hello_world newline |
| 93 | + libc->>kernel: write fd1 buf n |
| 94 | + kernel->>tty: 根据 fd=1 找到标准输出 |
| 95 | + tty->>device: 输出字符流 |
| 96 | + device-->>tty: 写入完成 |
| 97 | + tty-->>kernel: 返回状态 |
| 98 | + kernel-->>libc: write 返回 |
| 99 | + libc-->>app: printf 返回 |
| 100 | + app->>kernel: exit status 0 |
| 101 | + kernel-->>shell: wait 返回退出状态 |
| 102 | +``` |
| 103 | + |
| 104 | +- `helloworld` 并不知道屏幕或串口的寄存器地址 |
| 105 | +- 它只知道调用 `printf` 或 `write` |
| 106 | +- 内核负责把“标准输出”翻译成真正的终端或串口设备 |
| 107 | + |
| 108 | +--- |
| 109 | + |
| 110 | +## 一次系统调用内部发生了什么 |
| 111 | + |
| 112 | +```mermaid |
| 113 | +flowchart TD |
| 114 | + userCall["用户态调用\nwrite fd buf n"] --> trap["执行 syscall 指令"] |
| 115 | + trap --> switchMode["硬件切换到内核态"] |
| 116 | + switchMode --> saveCtx["保存用户态上下文"] |
| 117 | + saveCtx --> dispatch["按 syscall 号分发"] |
| 118 | + dispatch --> sysWrite["执行 sys_write"] |
| 119 | + sysWrite --> kernelWork["检查参数\n查找 fd\n调用 VFS / 驱动"] |
| 120 | + kernelWork --> setRet["写回返回值"] |
| 121 | + setRet --> restoreCtx["恢复用户态上下文"] |
| 122 | + restoreCtx --> returnUser["返回用户态继续执行"] |
| 123 | +``` |
| 124 | + |
| 125 | +- 看起来像函数调用,本质上是受保护的内核入口 |
| 126 | +- 应用不能直接跳进任意内核代码,只能通过规定好的系统调用接口 |
| 127 | +- 这保证了安全性,也让不同程序共享通用服务 |
| 128 | + |
| 129 | +--- |
| 130 | + |
| 131 | +## Linux 内核服务的总体分工 |
| 132 | + |
| 133 | +```mermaid |
| 134 | +flowchart LR |
| 135 | + appReq["应用请求"] --> proc["进程管理\nfork exec wait exit"] |
| 136 | + appReq --> mem["内存管理\nmmap brk page fault"] |
| 137 | + appReq --> fs["文件系统\nopen read write close stat"] |
| 138 | + appReq --> io["设备与 I/O\nconsole disk net"] |
| 139 | + appReq --> ipc["进程间通信\npipe signal socket"] |
| 140 | +
|
| 141 | + proc --> cpu["CPU / 调度"] |
| 142 | + mem --> ram["内存 / 页表"] |
| 143 | + fs --> disk["磁盘 / 目录 / inode"] |
| 144 | + io --> terminal["终端 / 串口 / 网卡"] |
| 145 | + ipc --> buffer["内核缓冲区 / 事件"] |
| 146 | +``` |
| 147 | + |
| 148 | +- `fork/exec/wait/exit` 对应进程管理服务 |
| 149 | +- `open/read/write/close/stat` 对应文件系统与 I/O 服务 |
| 150 | +- `pipe`、`signal`、`socket` 对应通信服务 |
| 151 | +- 各类服务共用同一个系统调用边界 |
| 152 | + |
| 153 | +--- |
| 154 | + |
| 155 | +## 文件与 I/O:`open / read / write / close` |
| 156 | + |
| 157 | +```mermaid |
| 158 | +flowchart LR |
| 159 | + app["应用程序"] -->|"open out"| fdTable["进程文件描述符表"] |
| 160 | + fdTable --> fd3["fd 3"] |
| 161 | + fdTable --> fd1["fd 1 标准输出"] |
| 162 | +
|
| 163 | + fd3 --> fileObj["内核打开文件对象"] |
| 164 | + fd1 --> stdoutObj["终端文件对象"] |
| 165 | +
|
| 166 | + fileObj --> vfs["VFS / 文件系统"] |
| 167 | + stdoutObj --> vfs |
| 168 | + vfs --> inode["目录项 / inode / 页缓存"] |
| 169 | + vfs --> driver["终端驱动 / 块设备驱动"] |
| 170 | +``` |
| 171 | + |
| 172 | +- `open()` 返回的是文件描述符,不是直接返回“磁盘块” |
| 173 | +- `read()` 和 `write()` 总是先根据 `fd` 找到内核中的打开文件对象 |
| 174 | +- 这样同一套接口既能访问普通文件,也能访问终端、管道、设备 |
| 175 | + |
| 176 | +--- |
| 177 | + |
| 178 | +## 进程服务:`fork / exec / wait / exit` |
| 179 | + |
| 180 | +```mermaid |
| 181 | +flowchart TD |
| 182 | + shell["Shell"] --> forkStep["fork\n复制进程执行上下文"] |
| 183 | + forkStep --> parent["父进程\n得到子进程 pid"] |
| 184 | + forkStep --> child["子进程\nfork 返回 0"] |
| 185 | + child --> execStep["execve\n装入新程序映像"] |
| 186 | + execStep --> runProg["运行 hello 或其他程序"] |
| 187 | + runProg --> exitStep["exit\n返回状态"] |
| 188 | + parent --> waitStep["wait\n等待子进程"] |
| 189 | + exitStep --> waitStep |
| 190 | + waitStep --> prompt["Shell 打印下一个提示符"] |
| 191 | +``` |
| 192 | + |
| 193 | +- Shell 本身并不实现“运行别的程序”的全部细节 |
| 194 | +- 它依赖内核的进程创建、程序装载和等待回收服务 |
| 195 | +- `fork + exec + wait` 是 UNIX/Linux 非常经典的组合方式 |
| 196 | + |
| 197 | +--- |
| 198 | + |
| 199 | +## 重定向:为什么改 `fd=1` 就够了 |
| 200 | + |
| 201 | +```mermaid |
| 202 | +flowchart LR |
| 203 | + subgraph beforeRun["默认情况"] |
| 204 | + fd0a["fd 0"] --> stdinA["键盘 / 输入"] |
| 205 | + fd1a["fd 1"] --> stdoutA["终端 / 屏幕"] |
| 206 | + fd2a["fd 2"] --> stderrA["终端 / 屏幕"] |
| 207 | + end |
| 208 | +
|
| 209 | + subgraph redirected["执行 close 1 加 open output.txt 之后"] |
| 210 | + fd0b["fd 0"] --> stdinB["键盘 / 输入"] |
| 211 | + fd1b["fd 1"] --> fileOut["output.txt"] |
| 212 | + fd2b["fd 2"] --> stderrB["终端 / 屏幕"] |
| 213 | + end |
| 214 | +``` |
| 215 | + |
| 216 | +- 程序只会向 `fd=1` 写数据,不需要知道它后面连的是屏幕还是文件 |
| 217 | +- 重定向的关键不在应用本身,而在 Shell 和内核共同维护的文件描述符绑定 |
| 218 | +- 这说明内核抽象强调“统一接口、可替换后端” |
| 219 | + |
| 220 | +--- |
| 221 | + |
| 222 | +## 管道:`pipe` 如何提供进程间通信 |
| 223 | + |
| 224 | +```mermaid |
| 225 | +flowchart LR |
| 226 | + procA["进程 A\nwrite 到 p1"] --> writeEnd["管道写端 p1"] |
| 227 | + writeEnd --> pipeBuf["内核管道缓冲区"] |
| 228 | + pipeBuf --> readEnd["管道读端 p0"] |
| 229 | + readEnd --> procB["进程 B\nread p0"] |
| 230 | +``` |
| 231 | + |
| 232 | +- `pipe()` 创建两个文件描述符:一个读端,一个写端 |
| 233 | +- 数据并不是直接从一个用户进程内存拷到另一个用户进程内存 |
| 234 | +- 内核提供缓冲、同步和阻塞唤醒机制,因此管道能安全工作 |
| 235 | + |
| 236 | +--- |
| 237 | + |
| 238 | +## 从 `p5-tryunix` 的例子看内核服务 |
| 239 | + |
| 240 | +```mermaid |
| 241 | +flowchart TB |
| 242 | + examples["用户态小例子"] --> openEx["open.c"] |
| 243 | + examples --> copyEx["copy.c"] |
| 244 | + examples --> forkEx["fork.c"] |
| 245 | + examples --> execEx["exec.c"] |
| 246 | + examples --> forkexecEx["forkexec.c"] |
| 247 | + examples --> redirectEx["redirect.c"] |
| 248 | + examples --> pipeEx["pipe2.c"] |
| 249 | +
|
| 250 | + openEx --> fsSvc["文件系统 / fd 分配"] |
| 251 | + copyEx --> ioSvc["read write / 缓冲区 I/O"] |
| 252 | + forkEx --> procSvc["进程复制 / 调度"] |
| 253 | + execEx --> loadSvc["程序装载 / 地址空间重建"] |
| 254 | + forkexecEx --> procSvc |
| 255 | + forkexecEx --> loadSvc |
| 256 | + redirectEx --> fdSvc["fd 重绑定 / 文件对象继承"] |
| 257 | + pipeEx --> ipcSvc["管道缓冲区 / 同步"] |
| 258 | +``` |
| 259 | + |
| 260 | +- `open.c` 强调“名字 -> 文件对象 -> 文件描述符” |
| 261 | +- `copy.c` 强调“文件访问其实统一成 `read/write`” |
| 262 | +- `fork/exec/forkexec` 强调程序执行依赖进程管理服务 |
| 263 | +- `redirect` 和 `pipe2` 强调 UNIX 抽象具有很强的组合能力 |
| 264 | + |
| 265 | +--- |
| 266 | + |
| 267 | +## `helloworld` 到各个服务的联系 |
| 268 | + |
| 269 | +- `printf("hello world\\n")` |
| 270 | + - 常经过 libc,最终调用 `write` |
| 271 | +- `write(fd=1, buf, n)` |
| 272 | + - 用到文件描述符、VFS、终端驱动 |
| 273 | +- Shell 启动 `./hello` |
| 274 | + - 用到 `fork + execve + wait` |
| 275 | +- 程序运行本身 |
| 276 | + - 用到调度器、地址空间、栈和代码段 |
| 277 | +- 最终看到字符输出 |
| 278 | + - 依赖终端设备和驱动程序 |
| 279 | + |
| 280 | +--- |
| 281 | + |
| 282 | +## 为什么由内核统一提供这些服务 |
| 283 | + |
| 284 | +- 硬件访问需要保护,不能让任意应用直接操作设备和内存 |
| 285 | +- 不同程序都需要进程、文件、通信等共性能力,适合由内核统一实现 |
| 286 | +- 内核提供统一抽象后,应用只需面对少量简单接口 |
| 287 | +- 这些接口还能组合出重定向、管道、Shell 执行等强大机制 |
| 288 | + |
| 289 | +--- |
| 290 | + |
| 291 | +## 小结 |
| 292 | + |
| 293 | +- 用户程序通过 `C 库 -> 系统调用 -> 内核服务` 获得能力 |
| 294 | +- Linux 内核把硬件访问封装成进程、内存、文件、I/O、IPC 等抽象 |
| 295 | +- `helloworld`、`open`、`copy`、`forkexec`、`redirect`、`pipe2` 都只是这些抽象的不同组合 |
| 296 | +- UNIX/Linux 接口数量不多,但表达力很强,这正是其经典之处 |
0 commit comments