Skip to content

Commit 6e33d35

Browse files
author
Lysssyo
committed
add new documentation 2026-03-18
1 parent 9d1f3d2 commit 6e33d35

6 files changed

Lines changed: 1371 additions & 6 deletions

File tree

docs/.obsidian/plugins/obsidian-icon-folder/data.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,6 @@
5151
"80-人工智能": "LiFolder",
5252
"81-量化": "LiFolder",
5353
"96-杂项": "LiFolder",
54-
"97-运动健身": "LiFolder",
5554
"98-Private": "LiFolder",
5655
"99-关联树": "LiFolder",
5756
"96-杂项/010-建站": "LiFolder",
@@ -62,5 +61,6 @@
6261
"04-中间件/ClickHouse": "LiFolder",
6362
"04-中间件/Elasticsearch/030-段合并.md": "LiFile",
6463
"06-场景设计": "LiFolder",
65-
"06-场景设计/01-cozeLoop专项": "LiFolder"
64+
"06-场景设计/01-cozeLoop专项": "LiFolder",
65+
"98-Private/97-运动健身": "LiFolder"
6666
}
Lines changed: 272 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,272 @@
1+
---
2+
date created: 2026-03-18 21:23:29
3+
date modified: 2026-03-18 21:31:09
4+
---
5+
# xv6 系统调用底层原理
6+
7+
> `user/sleep.c` 为例,深入剖析库函数调用与系统调用的底层机制
8+
9+
---
10+
11+
## 0. 起点:我们写的 user/sleep.c
12+
13+
在 MIT xv6 Lab 1 中,我们编写了如下用户程序:
14+
15+
```c
16+
#include "kernel/types.h"
17+
#include "kernel/stat.h"
18+
#include "user/user.h"
19+
20+
int main(int argc, char *argv[])
21+
{
22+
if (argc <= 1) {
23+
fprintf(2, "Usage: sleep ticks\n");
24+
exit(1);
25+
}
26+
int tick = atoi(argv[1]); // ← 库函数调用
27+
sleep(tick); // ← 系统调用
28+
exit(0);
29+
}
30+
```
31+
32+
这个文件只有 15 行,却包含了**两种截然不同的函数调用方式**:
33+
34+
- `atoi(argv[1])` —— 库函数调用
35+
- `sleep(tick)` —— 系统调用
36+
37+
它们在代码里写法一模一样,但底层执行路径完全不同。本文将以这两行为主线,逐一追踪它们各自的完整执行链路。
38+
39+
---
40+
41+
## 1. 核心区分:两种调用,两个世界
42+
43+
|维度|`atoi`(库函数调用)|`sleep`(系统调用)|
44+
|---|---|---|
45+
|定义位置|`user/ulib.c`|`kernel/sysproc.c`(真正实现)|
46+
|执行权限|用户态 U-Mode|内核态 S-Mode|
47+
|跳转方式|普通函数跳转(JAL 指令)|硬件陷阱(`ecall` 指令)|
48+
|是否跨越权限边界|否,全程用户态|是,强制切换到内核态|
49+
|执行内容|纯逻辑计算(字符串转数字)|操作底层时钟硬件、切换进程状态|
50+
51+
> [!TIP]
52+
> `sleep.c` 里的 `sleep(tick)` 和 `atoi(argv[1])` 调用语法相同,但一个永远不离开用户态,另一个必须跨越权限边界才能完成工作。
53+
54+
---
55+
56+
## 2. 轨迹 A:atoi 是如何被调用的?
57+
58+
### 2.1 它是怎么"找到"的——静态链接
59+
60+
`atoi` 的实现写在 `user/ulib.c` 中,它是一段纯粹的逻辑代码,**与操作系统内核毫无关联**。`sleep.c` 能调用它,靠的是**编译与链接**这两个阶段的配合。
61+
62+
**第一步:编译阶段打"空头支票"**
63+
64+
编译器在编译 `sleep.c` 时,看到 `user/user.h` 里有这样一行声明:
65+
66+
```c
67+
int atoi(const char*);
68+
```
69+
70+
编译器对自己说:"程序员保证有这个函数,我先放行,留个占位符,链接器最后来填地址。"于是生成了 `sleep.o`,其中 `atoi` 的地址暂时空着。
71+
72+
**第二步:链接阶段"兑现支票"**
73+
74+
当你运行 `make qemu` 时,`Makefile` 会调用链接器 `ld`,执行类似如下的命令:
75+
76+
```bash
77+
riscv64-unknown-elf-ld -o user/_sleep \
78+
user/sleep.o \
79+
user/ulib.o \ ← atoi 的真身在这里
80+
user/usys.o \
81+
user/printf.o \
82+
user/umalloc.o
83+
```
84+
85+
链接器在 `ulib.o` 的符号表里找到了 `atoi` 的真实地址,把 `sleep.o` 里的占位符替换掉。此后,`atoi` 的机器码就被**物理拼入**`_sleep` 可执行文件中。
86+
87+
**第三步:运行时,就像调用自己写的函数**
88+
89+
程序执行 `atoi(argv[1])` 时,CPU 只是发出一条普通的跳转指令(JAL),跳到内存中 `atoi` 代码所在的位置,计算完结果后跳回来。全程在用户态完成,没有任何权限切换。
90+
91+
### 2.2 小结
92+
93+
`atoi` 早在 `make qemu` 的那一刻,就被链接器缝合进了 `_sleep` 的二进制文件里。调用它和调用你自己写的一个普通函数没有区别。
94+
95+
---
96+
97+
## 3. 轨迹 B:sleep 是如何被调用的?
98+
99+
`sleep` 的情况复杂得多。`sleep.c` 绝对不可能在编译时把 `kernel/sysproc.c` 缝合进来——那是内核代码,运行在不同的权限层。
100+
101+
### 3.1 第一层:user/user.h 里的"欺骗性声明"
102+
103+
`user/user.h` 里有一行:
104+
105+
```c
106+
int sleep(int);
107+
```
108+
109+
你在 `sleep.c` 里写下 `sleep(tick)` 时,你以为自己在直接调用内核的 `sys_sleep`。实际上,链接器帮你绑定的是一个**汇编跳板(Stub)**,它在 `user/usys.S` 里。
110+
111+
### 3.2 第二层:user/usys.S —— 汇编跳板的真身
112+
113+
打开 `user/usys.S`,可以看到 `sleep` 的汇编实现:
114+
115+
```asm
116+
.global sleep
117+
sleep:
118+
li a7, SYS_sleep # 把 sleep 的系统调用编号写入 a7 寄存器
119+
ecall # 触发 CPU 硬件陷阱,瞬间跳入内核
120+
ret # 内核执行完后,回到这里,返回给 C 代码
121+
```
122+
123+
只有三行,但每一行都不可替代。
124+
125+
> [!TIP] **为什么必须用汇编写?不能用 C 吗?**
126+
>
127+
> C 语言是一门通用的抽象语言,它的目的是"在任何机器上都能跑",因此它的语法里根本没有"陷入内核"这个词。具体来说有三个原因:
128+
>
129+
> 1. **语法树里没这个词**:C 语言标准里没有 `traps_into_kernel()` 这样的语句,GCC 不知道该把它翻译成什么机器码。
130+
> 2. **寄存器必须被精确控制**:内核规定,执行 `ecall` 之前,系统调用编号必须放进 `a7` 寄存器。C 语言的变量可能被编译器放进任意寄存器,无法保证。汇编指令 `li a7, SYS_sleep` 是物理级别的强制锁定。
131+
> 3. **不能引入多余的栈帧开销**:C 函数调用会自动创建栈帧,这些多余的开销可能干扰内核对寄存器的保存逻辑。汇编代码"裸奔",只完成最关键的那一跳。
132+
133+
### 3.3 第三层:ecall —— 跨越权限边界的那一刻
134+
135+
`ecall` 是整个系统调用机制的核心。这条指令一旦执行:
136+
137+
- CPU 的特权级从 **U-Mode(用户态)** 瞬间提升到 **S-Mode(内核态)**
138+
- 程序的执行流被强制中断,跳入内核预设的陷阱入口(地址存储在 `stvec` 硬件寄存器中)
139+
- 内核开始接管 CPU 控制权
140+
141+
### 3.4 第四层:kernel/syscall.c —— 内核的路由分发器
142+
143+
内核被唤醒后,首先进入 `kernel/trap.c`,保存用户程序的寄存器现场,然后来到 `kernel/syscall.c` 进行路由:
144+
145+
```c
146+
// kernel/syscall.c 简化逻辑
147+
void syscall(void) {
148+
int num = p->trapframe->a7; // 读取 a7 寄存器里的系统调用编号
149+
p->trapframe->a0 = syscalls[num](); // 查表,调用对应的内核函数
150+
}
151+
```
152+
153+
它读取 `a7` 寄存器,发现值是 `SYS_sleep`(例如编号 13),于是通过函数指针表,把执行流分发给 `sys_sleep`。
154+
155+
### 3.5 第五层:kernel/sysproc.c —— 真正的内核实现
156+
157+
```c
158+
// kernel/sysproc.c
159+
uint64 sys_sleep(void) {
160+
int n;
161+
argint(0, &n); // 从寄存器读取参数 tick
162+
// 操作底层时钟,将当前进程标记为 SLEEPING 状态
163+
// 记录唤醒时间,让出 CPU 给其他进程
164+
// ...
165+
}
166+
```
167+
168+
`sys_sleep` 拥有最高权限,可以直接操作硬件时钟、修改进程控制块(PCB)、调度其他进程。这些都是 `sleep.c` 这个"平民程序"永远做不到的。
169+
170+
### 3.6 返回:原路遣返
171+
172+
时间到了,时钟中断唤醒该进程。内核执行 `sret` 指令(Supervisor Return):
173+
174+
- CPU 特权级从 S-Mode 降回 U-Mode
175+
- 执行流回到 `usys.S``ecall` 的下一行(`ret`
176+
- `ret` 将控制权交还给 `sleep.c``sleep(tick)` 的下一行
177+
178+
你的程序醒来,继续执行 `exit(0)`,仿佛什么都没发生过。
179+
180+
---
181+
182+
## 4. 为什么链接器能找到 ulib.o 和 usys.o?
183+
184+
链接器本身是一个"笨"工具——它不会自动扫描代码库,只在你明确传给它的文件里查找符号。它能同时找到 `atoi`(在 `ulib.o` 里)和 `sleep` 的汇编跳板(在 `usys.o` 里),原因完全相同:**`Makefile` 把它们都硬塞进去了**
185+
186+
打开 xv6 的 `Makefile`,可以找到:
187+
188+
```makefile
189+
ULIB = $U/ulib.o $U/usys.o $U/printf.o $U/umalloc.o
190+
191+
_%: %.o $(ULIB)
192+
$(LD) $(LDFLAGS) -o $@ $^
193+
```
194+
195+
`ULIB` 变量把这四个 `.o` 文件打包在一起,构建规则规定:**任何一个用户态程序在链接时,都必须把自身的 `.o` 加上整个 `$(ULIB)` 一起传给链接器**。因此,链接 `_sleep` 时实际执行的命令是:
196+
197+
```bash
198+
riscv64-unknown-elf-ld -o user/_sleep \
199+
user/sleep.o \
200+
user/ulib.o \ ← atoi 在这里,Makefile 塞进来的
201+
user/usys.o \ ← sleep 汇编跳板在这里,Makefile 塞进来的
202+
user/printf.o \
203+
user/umalloc.o
204+
```
205+
206+
`atoi``sleep` 跳板的查找机制是完全对称的,没有任何区别。两者都不是链接器"自动发现"的,而是 MIT 教授在编写构建脚本时就写死了:所有用户程序,无一例外,都要带上这四个"陪嫁"文件。
207+
208+
如果你分别把它们从 `ULIB` 里删掉,会得到对称的报错:
209+
210+
```
211+
# 删掉 ulib.o 后
212+
undefined reference to 'atoi'
213+
214+
# 删掉 usys.o 后
215+
undefined reference to 'sleep'
216+
```
217+
218+
两个错误的根源和解法完全一致。
219+
220+
---
221+
222+
## 5. 完整调用链:从 sleep.c 到 sys_sleep 的六棒接力
223+
224+
把以上所有内容串联起来,`sleep(tick)` 这一行代码的完整执行路径如下:
225+
226+
```
227+
user/sleep.c
228+
│ 执行到 sleep(tick),通过链接器缝合的地址跳转
229+
230+
user/usys.S [sleep 标签]
231+
│ li a7, SYS_sleep ← 写入系统调用编号
232+
│ ecall ← 触发硬件陷阱
233+
234+
RISC-V 硬件(CPU)
235+
│ 特权级从 U-Mode 提升到 S-Mode
236+
│ 跳入 stvec 寄存器指向的内核入口
237+
238+
kernel/trap.c [usertrap()]
239+
│ 保存用户寄存器现场(trapframe)
240+
│ 识别这是系统调用类型的陷阱
241+
242+
kernel/syscall.c [syscall()]
243+
│ 读取 a7 = SYS_sleep
244+
│ 查函数指针表,路由到 sys_sleep
245+
246+
kernel/sysproc.c [sys_sleep()]
247+
│ 读取参数 tick
248+
│ 操作底层时钟,将进程标记为 SLEEPING
249+
│ 让出 CPU,等待时钟中断唤醒
250+
251+
(时间到,时钟中断唤醒进程)
252+
│ sret 指令:S-Mode 降回 U-Mode
253+
254+
user/usys.S [ret]
255+
│ 返回给 C 调用者
256+
257+
user/sleep.c 继续执行 exit(0)
258+
```
259+
260+
---
261+
262+
## 附录:相关文件一览
263+
264+
| 文件 | 角色 | 运行权限 |
265+
| ------------------ | ---------------------- | ----------------- |
266+
| `user/sleep.c` | 用户程序入口,发起调用 | 用户态 |
267+
| `user/user.h` | 函数声明,告诉编译器"有这个函数" ||
268+
| `user/ulib.c` | `atoi` 等库函数的实现 | 用户态 |
269+
| `user/usys.S` | 所有系统调用的汇编跳板 | 用户态(触发 ecall 后切换) |
270+
| `kernel/trap.c` | 陷阱入口,保存现场 | 内核态 |
271+
| `kernel/syscall.c` | 系统调用路由分发器 | 内核态 |
272+
| `kernel/sysproc.c` | `sys_sleep` 等系统调用的真正实现 | 内核态 |
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
---
2+
date created: 2026-03-18 17:43:29
3+
date modified: 2026-03-18 17:43:40
4+
---
5+
# Lab1
6+
7+
## sleep (简单难度)
8+
9+
为 xv6 实现 UNIX 的 `sleep` 程序;你的 `sleep` 程序应该暂停用户指定的“滴答数”(ticks)。一个 tick(滴答)是 xv6 内核定义的时间概念,也就是定时器芯片两次硬件中断之间的时间间隔。你的代码必须写在 `user/sleep.c` 文件中。
10+
11+
**一些提示 (Hints):**
12+
13+
- [x] 在开始写代码之前,请先阅读 xv6 手册的第一章。
14+
- [ ] 看看 `user/` 目录下的其他程序(例如 `user/echo.c``user/grep.c``user/rm.c`),学习如何获取传递给程序的命令行参数(Command-line arguments)。
15+
- [x] 如果用户忘记传递参数,`sleep` 应该打印一条错误提示信息。
16+
- [x] 命令行参数是以字符串的形式传递进来的;你可以使用 `atoi` 函数将其转换为整数(具体实现请看 `user/ulib.c`)。
17+
- [ ] 使用 `sleep` 这个系统调用。
18+
- [ ] 查看 `kernel/sysproc.c` 中实现 `sleep` 系统调用的内核代码(寻找 `sys_sleep` 函数);查看 `user/user.h` 中提供给用户程序调用的 `sleep` 函数的 C 语言声明;还要查看 `user/usys.S` 中实现从用户代码跳转进内核执行 `sleep` 的底层汇编代码。
19+
- [ ] 确保你的 `main` 函数在最后调用了 `exit()` 来正常退出程序。
20+
- [ ] 把你的 `sleep` 程序加到 `Makefile` 文件的 `UPROGS` 列表中;搞定这一步后,运行 `make qemu` 就会自动编译你的程序,随后你就可以在 xv6 的 shell 里直接运行它了。
21+
- [x] 查阅 Kernighan 和 Ritchie 合著的《C程序设计语言(第二版)》(K&R)来学习 C 语言的语法。

docs/02-编程语言基础/python/基础语法.md

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
---
22
date created: 2026-01-19 23:20:11
3-
date modified: 2026-02-05 11:46:50
3+
date modified: 2026-03-18 14:41:33
44
---
55
# Python 基础语法
66

@@ -236,7 +236,6 @@ print(p1["name"]) # 输出: keith
236236
print(p1["job"]) # 输出: 属性不存在
237237
```
238238

239-
240239
---
241240

242241
### 5. 可调用对象:`__call__`
@@ -286,6 +285,7 @@ with DatabaseConnection() as db:
286285
```
287286

288287
---
288+
289289
### 总结对照表
290290

291291
| **魔法方法** | **对应的 Python 语法/操作** | **Java/Go 类比** |
@@ -345,7 +345,7 @@ let_it_quack(p) # 正常运行!虽然他是人,但他能“像鸭子一样
345345
346346
> [!TIP] `batman``__init__` 方法中的 `*args` 以及 `**kwargs`
347347
> 其实就是简单的参数透传,把传给`batman`的参数透传给父类。
348-
> 至于顺序,是因为Python 严格的 **函数调用参数顺序规则**:位置参数 (Positional) 必须在 关键字参数 (Keyword) 之前
348+
> 至于顺序,是因为Python 严格的 **函数调用参数顺序规则**:位置参数 (Positional) 必须在关键字参数 (Keyword) 之前
349349
>
350350
351351
### 3.1 详解:函数参数的“排队法则”
@@ -448,7 +448,8 @@ secure_delete("file.txt", force_delete=True) # ✅ 正确
448448

449449
## 4. 装饰器
450450

451-
装饰器本质上是一个“高阶函数”,它接受一个函数作为参数,并返回一个新的函数。
451+
> [!TIP]
452+
> 装饰器本质上是一个“高阶函数”,它接受一个函数作为参数,并返回一个新的函数。
452453
453454
```python
454455
# 装饰器(decorators)
@@ -478,6 +479,11 @@ print(say()) # Can you buy me a beer?
478479
print(say(say_please=True)) # Can you buy me a beer? Please! I am poor :(
479480
```
480481

482+
> [!TIP] @functools.wraps:保身份的“名片”
483+
> * 它的用途:当你自己写一个装饰器去包装别人的函数时用。
484+
> * 它解决的问题:函数被包装后,它的名字(__name__)和文档(__doc__)会变成装饰器内部那个“包装盒”的名字(通常叫wrapper)。这会导致调试和生成文档时很混乱。
485+
> * 它的作用:它像一张“名片”,把原始函数的名字、注释等信息,强行贴回到包装后的函数身上。
486+
481487
### 1. `@` 到底干了什么?
482488

483489
你代码中的 `@beg` 其实是一个 **“语法糖” (Syntactic Sugar)**
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
> https://github.com/Lysssyo/daily-smart-brief
2+

0 commit comments

Comments
 (0)