tg-ch4 0.4.2-preview.1

Chapter 4 of rCore Tutorial: Address space management with SV39 virtual memory.
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
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
# 第四章:地址空间

本章在第三章"多道程序与分时多任务"的基础上,引入了 **RISC-V Sv39 虚拟内存机制**,为每个用户进程提供**独立的地址空间**(tg-ch4)。这是操作系统实现**进程隔离**和**内存保护**的关键一步。

通过本章的学习和实践,你将理解:

- 什么是虚拟内存,为什么需要地址空间隔离
- RISC-V Sv39 三级页表的结构和地址翻译过程
- 内核地址空间与用户地址空间的布局
- 异界传送门(MultislotPortal)如何解决跨地址空间切换问题
- ELF 文件如何被解析并加载到独立地址空间
- 系统调用中如何进行用户地址翻译和权限检查
- 堆内存管理(sbrk 系统调用)

> **前置知识**:建议先完成第一章至第三章的学习,理解裸机启动、Trap 处理、系统调用和多任务调度。

## 项目结构

```
ch4/
├── .cargo/
│   └── config.toml     # Cargo 配置:交叉编译目标和 QEMU runner
├── .gitignore           # Git 忽略规则
├── build.rs            # 构建脚本:下载编译用户程序,生成链接脚本和 APP_ASM
├── Cargo.toml          # 项目配置与依赖
├── LICENSE             # GPL v3 许可证
├── README.md           # 本文档
├── rust-toolchain.toml # Rust 工具链配置
├── test.sh             # 自动测试脚本
└── src/
    ├── main.rs         # 内核源码:初始化、调度、系统调用、页表管理器
    └── process.rs      # 进程结构:地址空间、ELF 加载、堆管理
```

<a id="source-nav"></a>

## 源码阅读导航索引

[返回根文档导航总表](../README.md#chapters-source-nav-map)

本章建议按“地址空间建立 -> 进程装载 -> 跨地址空间执行 -> 用户指针翻译”阅读。

| 阅读顺序 | 文件 | 重点问题 |
|---|---|---|
| 1 | `src/main.rs` 的 `kernel_space` | 内核恒等映射、堆映射、传送门映射分别解决什么问题? |
| 2 | `src/process.rs` 的 `new` | ELF 如何被映射到用户地址空间,用户栈与 `satp` 如何初始化? |
| 3 | `src/main.rs` 的 `schedule` | 异界传送门如何支撑跨地址空间执行与返回? |
| 4 | `src/main.rs` 的 `impls` | 系统调用里 `translate()` 如何做权限检查与地址翻译? |
| 5 | `src/process.rs` 的 `change_program_brk` | `sbrk` 如何驱动堆页映射的扩张与回收? |

配套建议:先读本章再回看 `tg-kernel-vm` 与 `tg-kernel-context/foreign`,会更容易理解抽象设计。

## DoD 验收标准(本章完成判据)

- [ ] 能说明本章为何必须引入 Sv39 与每进程独立地址空间
- [ ] 能从代码解释“内核恒等映射 + 用户地址空间映射 + 传送门映射”三者关系
- [ ] 能说明 `translate()` 在 syscall 中如何完成权限检查与地址翻译
- [ ] 能解释 `sbrk` 扩容/缩容时页映射范围如何变化
- [ ] 能执行 `./test.sh base`(练习时补充 `./test.sh exercise`)

## 概念-源码-测试三联表

| 核心概念 | 源码入口 | 自测方式(命令/现象) |
|---|---|---|
| 内核地址空间建立 | `ch4/src/main.rs` 的 `kernel_space` | 启动日志出现 `.text/.rodata/.data/(heap)` 映射信息 |
| ELF 装载到用户空间 | `ch4/src/process.rs` 的 `Process::new` | 用户程序入口为虚拟地址(如 `0x10000`)并可运行 |
| 跨地址空间执行 | `ch4/src/main.rs` 的 `schedule` + `MultislotPortal` | 用户态与内核态可正常往返,无地址空间切换崩溃 |
| 用户指针翻译与检查 | `ch4/src/main.rs` 的 `impls`(`translate`) | 非法用户地址会被拒绝而不是直接越界访问 |

遇到构建/运行异常可先查看根文档的“高频错误速查表”。

## 一、环境准备

### 1.1 安装 Rust 工具链

**Linux / macOS / WSL:**

```bash
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
source "$HOME/.cargo/env"
```

验证安装:

```bash
rustc --version    # 要求 >= 1.85.0(支持 edition 2024)
cargo --version
```

### 1.2 添加 RISC-V 64 编译目标

```bash
rustup target add riscv64gc-unknown-none-elf
```

### 1.3 安装 QEMU 模拟器

**Ubuntu / Debian:**

```bash
sudo apt update
sudo apt install qemu-system-misc
```

**macOS(Homebrew):**

```bash
brew install qemu
```

验证:

```bash
qemu-system-riscv64 --version    # 建议 >= 7.0
```

### 1.4 安装额外工具

```bash
cargo install cargo-clone
cargo install cargo-binutils
rustup component add llvm-tools
```

### 1.5 获取源代码

**方式一:只获取本实验**

```bash
cargo clone tg-ch4
cd tg-ch4
```

**方式二:获取所有实验**

```bash
git clone https://github.com/rcore-os/rCore-Tutorial-in-single-workspace.git
cd rCore-Tutorial-in-single-workspace/ch4
```

## 二、编译与运行

### 2.1 编译

```bash
cargo build
```

编译过程与第三章类似,`build.rs` 会自动下载 `tg-user`、编译用户程序并嵌入内核。

> 环境变量说明:
> - `TG_USER_DIR`:指定本地 tg-user 源码路径(跳过自动下载)
> - `TG_USER_VERSION`:指定 tg-user 版本(默认 `0.2.0-preview.1`)
> - `TG_SKIP_USER_APPS`:设置后跳过用户程序编译
> - `LOG`:设置日志级别(如 `LOG=INFO`、`LOG=TRACE`)

### 2.2 运行

**基础模式:**

```bash
cargo run
```

**练习模式:**

```bash
cargo run --features exercise
```

实际执行的 QEMU 命令等价于:

```bash
qemu-system-riscv64 \
    -machine virt \
    -nographic \
    -bios none \
    -kernel target/riscv64gc-unknown-none-elf/debug/tg-ch4
```

### 2.3 预期输出

```
[tg-ch4 ...] Hello, world!
[ INFO] .text    ---> 0x80200000..0x8020xxxx
[ INFO] .rodata  ---> 0x8020xxxx..0x8020xxxx
[ INFO] .data    ---> 0x8020xxxx..0x8020xxxx
[ INFO] (heap)   ---> 0x8020xxxx..0x81a00000

[ INFO] detect app[0]: 0x8020xxxx..0x8020xxxx
[ INFO] process entry = 0x10000, heap_bottom = 0xxxxx
[ INFO] detect app[1]: ...
...

Hello, world from user mode program!
Test power OK!
...
Test sbrk OK!
```

与前几章不同,你会看到:
- 内核地址空间的段映射信息(恒等映射)
- 每个进程的入口地址是 ELF 中的虚拟地址(如 `0x10000`),而非物理地址
- 用户程序在独立地址空间中运行,堆管理(sbrk)正常工作

### 2.4 运行测试

```bash
./test.sh           # 运行全部测试(基础 + 练习)
./test.sh base      # 仅运行基础测试
./test.sh exercise  # 仅运行练习测试
```

---

## 三、操作系统核心概念

### 3.1 为什么需要虚拟内存?

在前几章中,所有用户程序直接使用物理地址,存在严重问题:

| 问题 | 说明 |
|------|------|
| **安全性** | 用户程序可以读写任意物理地址,包括内核数据 |
| **隔离性** | 一个程序的 bug 可能破坏其他程序的内存 |
| **灵活性** | 程序必须加载到特定的物理地址,无法重定位 |

**虚拟内存**通过在 CPU 和物理内存之间加入一层**地址翻译**解决这些问题:

```
用户程序                MMU(地址翻译)              物理内存
┌──────────┐           ┌──────────┐           ┌──────────┐
│ 虚拟地址  │ ───────→ │  页表查找  │ ───────→ │ 物理地址  │
│ 0x10000  │           │ Sv39 三级 │           │ 0x80400000│
└──────────┘           └──────────┘           └──────────┘
```

每个进程拥有独立的页表,看到的虚拟地址空间完全相同(都从 `0x10000` 开始),但映射到不同的物理页面。

### 3.2 RISC-V Sv39 页表

**Sv39** 是 RISC-V 定义的 39 位虚拟地址的分页方案:

| 参数 | 值 |
|------|-----|
| 虚拟地址宽度 | 39 位(512 GiB 地址空间) |
| 物理地址宽度 | 56 位 |
| 页大小 | 4 KiB(12 位页内偏移) |
| 页表级数 | 3 级(每级 9 位,共 27 位 VPN) |
| 页表项大小 | 8 字节(64 位) |
| 每页页表项数 | 512 个 |

**虚拟地址结构:**

```
 38       30 29       21 20       12 11        0
┌──────────┬──────────┬──────────┬──────────┐
│  VPN[2]  │  VPN[1]  │  VPN[0]  │  Offset  │
│  9 bits  │  9 bits  │  9 bits  │  12 bits │
└──────────┴──────────┴──────────┴──────────┘
```

**三级页表查找过程:**

```
satp 寄存器 → 根页表物理地址
        │
        ▼
  ┌─ 根页表 ─┐
  │ VPN[2] 索引│ ──→ 得到二级页表地址
  └──────────┘
        │
        ▼
  ┌─ 二级页表 ─┐
  │ VPN[1] 索引│ ──→ 得到三级页表地址
  └──────────┘
        │
        ▼
  ┌─ 三级页表 ─┐
  │ VPN[0] 索引│ ──→ 得到物理页号 PPN
  └──────────┘
        │
        ▼
  物理地址 = PPN × 4096 + Offset
```

**页表项(PTE)标志位:**

| 标志 | 含义 |
|------|------|
| V (Valid) | 有效位,必须为 1 |
| R (Read) | 可读 |
| W (Write) | 可写 |
| X (Execute) | 可执行 |
| U (User) | 用户态可访问 |
| G (Global) | 全局映射(不随 TLB 刷新) |
| A (Accessed) | 已访问 |
| D (Dirty) | 已修改 |

**satp 寄存器:**

```
 63  60 59          44 43                    0
┌──────┬──────────────┬──────────────────────┐
│ MODE │    ASID      │      PPN             │
│ 4bit │   16 bit     │     44 bit           │
└──────┴──────────────┴──────────────────────┘
  MODE=8 表示 Sv39        根页表的物理页号
```

### 3.3 内核地址空间

tg-ch4 的内核地址空间使用**恒等映射**(Identity Mapping):虚拟地址 == 物理地址。

```
内核地址空间
┌────────────────────────────────────┐ 高地址
│ 传送门(PORTAL_TRANSIT = VPN::MAX)│ ← 虚拟地址空间最高页
├────────────────────────────────────┤
│ 调度栈(2 页)                      │
├────────────────────────────────────┤
│              ...                    │
├────────────────────────────────────┤
│ 堆区域(恒等映射)                   │ ← layout.end() ~ start+MEMORY
├────────────────────────────────────┤
│ .bss(恒等映射)                     │
│ .data(恒等映射)                    │
│ .rodata(恒等映射)                  │
│ .text(恒等映射)                    │
└────────────────────────────────────┘ 0x80200000
```

恒等映射的优势:内核可以直接通过虚拟地址访问任意物理内存,简化页表管理代码。

### 3.4 用户地址空间

每个用户进程拥有独立的地址空间,通过 ELF 解析创建:

```
用户地址空间
┌────────────────────────────────────┐ 高地址
│ 传送门(与内核共享同一物理页)        │ ← VPN::MAX
├────────────────────────────────────┤
│              ...                    │
├────────────────────────────────────┤
│ 用户栈(2 页 = 8 KiB)             │ ← VPN[(1<<26)-2, 1<<26)
├────────────────────────────────────┤
│              ...                    │
├────────────────────────────────────┤
│ 堆区域(通过 sbrk 动态扩展)        │ ← heap_bottom ~ program_brk
├────────────────────────────────────┤
│ .bss / .data / .rodata / .text     │ ← 从 ELF LOAD 段映射
└────────────────────────────────────┘ 低地址(如 0x10000)
```

**关键特性:**
- 所有映射都带有 **U(User)标志**,允许用户态访问
- 传送门页面在内核和用户地址空间**映射到相同虚拟地址**
- 每个进程有独立的堆(通过 `sbrk` 管理)

### 3.5 异界传送门(MultislotPortal)

**核心问题**:当内核和用户程序使用不同的页表时,切换 `satp` 后当前指令所在的虚拟地址可能变为无效,导致 CPU 无法继续执行。

**解决方案**:异界传送门——一个特殊的代码页面,同时映射到内核和所有用户地址空间的**相同虚拟地址**。

```
内核地址空间                     用户地址空间
┌──────────────┐               ┌──────────────┐
│              │               │              │
│   内核代码    │               │   用户代码    │
│              │               │              │
├──────────────┤               ├──────────────┤
│   传送门页面  │ ──── 同一 ──→ │   传送门页面  │
│ (VPN::MAX)   │   物理页面    │ (VPN::MAX)   │
└──────────────┘               └──────────────┘
```

**切换流程:**

```
内核态(内核地址空间)
    │
    ▼
跳转到传送门虚拟地址
    │
    ▼
在传送门内:切换 satp → 用户地址空间
    │  (传送门在两个地址空间的虚拟地址相同,所以不会崩溃)
    ▼
恢复用户寄存器,sret → U-mode
    │
    ▼
用户程序执行...
    │
    ▼
Trap → 传送门入口
    │
    ▼
在传送门内:切换 satp → 内核地址空间
    │
    ▼
跳转到内核 Trap 处理代码
```

### 3.6 ELF 加载

本章不再将用户程序作为原始二进制加载,而是解析 **ELF 格式**:

```
ELF 文件
├── ELF Header(入口地址、程序头表位置)
├── Program Header 1(LOAD: .text, 虚拟地址 0x10000, RX)
├── Program Header 2(LOAD: .data, 虚拟地址 0x20000, RW)
└── ...
```

加载过程:
1. 解析 ELF 头,验证是 RISC-V 64 位可执行文件
2. 遍历 LOAD 类型的程序头
3. 为每个段分配物理页面,设置对应的权限标志(R/W/X + U)
4. 将段数据从 ELF 复制到新分配的页面
5. 记录最高虚拟地址作为堆底

### 3.7 地址翻译与系统调用

引入地址空间后,系统调用的实现发生了根本变化:**用户传入的指针是虚拟地址,内核无法直接访问**。

以 `write` 系统调用为例:

```
第三章(无虚拟内存)              第四章(有虚拟内存)
─────────────────               ─────────────────
用户传入 buf = 0x80400000        用户传入 buf = 0x10200
    │                               │
    ▼                               ▼
内核直接读取 buf 地址           内核通过 translate() 查页表
    │                               │
    ▼                               ▼
输出数据                        得到物理地址 0x80500200
                                    │
                                    ▼
                                输出数据
```

`translate()` 方法会:
1. 通过进程的页表将虚拟地址翻译为物理地址
2. 检查页表项的权限标志(如可读、可写)
3. 权限不足时返回 `None`,系统调用返回 -1

### 3.8 堆内存管理(sbrk)

本章新增了 `sbrk` 系统调用,允许用户程序动态调整堆大小:

```
堆底(heap_bottom)            堆顶(program_brk)
     │                              │
     ▼                              ▼
     ┌──────────────────────────────┐
     │      已分配的堆空间           │
     └──────────────────────────────┘

sbrk(+4096)  →  扩展堆,映射新的物理页面
sbrk(-4096)  →  收缩堆,取消映射物理页面
sbrk(0)      →  返回当前堆顶地址
```

### 3.9 系统调用

| syscall ID | 名称 | 功能 |
|-----------|------|------|
| 64 | `write` | 写入数据(需地址翻译) |
| 93 | `exit` | 退出当前进程 |
| 124 | `sched_yield` | 让出 CPU |
| 113 | `clock_gettime` | 获取时间(需地址翻译) |
| 214 | `sbrk` | 调整堆大小 |
| 410 | `trace` | 追踪系统调用(**练习题**) |
| 222 | `mmap` | 映射内存(**练习题**) |
| 215 | `munmap` | 取消映射(**练习题**) |

---

## 四、代码解读

### 4.1 `src/main.rs` —— 内核主体

**启动流程 `rust_main`:**
1. 清零 BSS 段
2. 初始化控制台和日志
3. 初始化内核堆分配器(`tg_kernel_alloc`)
4. 分配并创建异界传送门
5. 建立内核地址空间(恒等映射 + 传送门映射),激活 Sv39 分页
6. 解析 ELF 加载用户进程,共享传送门页表项
7. 创建调度线程,进入 `schedule()` 函数

**调度函数 `schedule`:**
- 初始化传送门和系统调用
- 循环执行:通过传送门切换到用户进程 → Trap 返回 → 处理系统调用/异常
- 所有进程完成后关机

**页表管理器 `Sv39Manager`:**
- 实现 `PageManager<Sv39>` trait
- 提供物理页面的分配、映射和地址转换
- 使用 `OWNED` 标志位标记内核分配的页面

**地址翻译在系统调用中的应用(如 `write`、`clock_gettime`):**
- 构建权限标志(如 `READABLE`、`WRITABLE`)
- 调用 `address_space.translate()` 翻译用户地址并检查权限
- 翻译失败时返回 -1

### 4.2 `src/process.rs` —— 进程管理

**`Process::new(elf)`**:从 ELF 创建进程
- 验证 ELF 头 → 创建地址空间 → 映射 LOAD 段 → 分配用户栈 → 创建 ForeignContext

**`change_program_brk(size)`**:实现 sbrk
- size > 0:扩展堆,映射新页面
- size < 0:收缩堆,取消映射
- 返回旧的 break 地址

### 4.3 `Cargo.toml` —— 依赖说明

| 依赖 | 说明 |
|------|------|
| `xmas-elf` | ELF 文件格式解析库 |
| `riscv` | RISC-V CSR 寄存器访问(`satp`、`scause`) |
| `tg-sbi` | SBI 调用封装 |
| `tg-linker` | 链接脚本生成、内核布局定位 |
| `tg-console` | 控制台输出和日志 |
| `tg-kernel-context` | 用户上下文及异界传送门 `MultislotPortal`(`foreign` feature) |
| `tg-kernel-alloc` | 内核堆分配器 |
| `tg-kernel-vm` | 虚拟内存管理(地址空间、页表、页面管理) |
| `tg-syscall` | 系统调用定义与分发 |

---

## 五、编程练习

### 5.1 重写 trace 系统调用

引入虚存机制后,原来内核的 `trace` 函数实现就无效了。**请你重写这个系统调用的代码**,恢复其正常功能。

由于本章有了地址空间作为隔离机制,`trace` **需要考虑额外的情况**:

- 在读取(`trace_request` 为 0)时,如果对应地址用户不可见或不可读,则返回值应为 -1(`isize` 格式的 -1,而非 `u8`)。
- 在写入(`trace_request` 为 1)时,如果对应地址用户不可见或不可写,则返回值应为 -1。

### 5.2 实现 mmap 和 munmap 匿名映射

[mmap](https://man7.org/linux/man-pages/man2/mmap.2.html) 在 Linux 中主要用于在内存中映射文件,本次实验简化它的功能,仅用于申请内存。

**mmap 定义:**

```rust
fn mmap(&self, _caller: Caller, addr: usize, len: usize, prot: i32,
        _flags: i32, _fd: i32, _offset: usize) -> isize
```

- syscall ID:222
- 申请 `len` 字节物理内存,映射到 `addr` 开始的虚存,属性为 `prot`
- 参数:
  - `addr`:虚存起始地址(**必须按页对齐**)
  - `len`:字节长度(可为 0,按页向上取整)
  - `prot`:bit 0=可读,bit 1=可写,bit 2=可执行。其他位必须为 0
- 返回:成功 0,错误 -1
- 可能的错误:addr 未对齐、prot 无效、地址已映射、物理内存不足

**munmap 定义:**

```rust
fn munmap(&self, _caller: Caller, addr: usize, len: usize) -> isize
```

- syscall ID:215
- 取消 `[addr, addr + len)` 的映射
- 错误:存在未被映射的虚存

### 5.3 实现提示

- 页表项权限使用 `VmFlags::build_from_str()` 构建,如 `"U_WRV"` 表示用户态可写可读有效
- 注意 RISC-V 页表项格式与 `prot` 参数的区别
- **别忘了添加 `U`(用户态可访问)标志**
- 实现 `trace` 时,可参考 `main.rs` 中 `clock_gettime` 的实现,使用 `translate` 方法进行地址翻译和权限检查

### 5.4 实验要求

```
tg-ch4/
├── Cargo.toml          # 内核配置(需修改依赖配置)
├── src/                # 内核源代码(需修改)
│   ├── main.rs
│   └── process.rs
├── tg-kernel-vm/       # 虚拟内存模块(需拉取到本地并修改)
│   └── src/
│       ├── lib.rs
│       └── space/mod.rs
└── tg-user/            # 用户程序(自动拉取,无需修改)
```

> **注意**:`tg-kernel-vm` 需要拉取到本地才能修改:
> ```bash
> cd tg-ch4
> cargo clone tg-kernel-vm
> ```
> 然后修改 `Cargo.toml`:
> ```toml
> [dependencies]
> tg-kernel-vm = { path = "./tg-kernel-vm" }
> ```

**运行和测试:**

```bash
cargo run --features exercise    # 运行练习测例
./test.sh exercise               # 测试练习测例
```

---

## 六、本章小结

通过本章的学习和实践,你完成了操作系统中最重要的抽象之一——地址空间:

1. **虚拟内存**:通过 Sv39 三级页表将虚拟地址映射到物理地址,CPU 的每次内存访问都经过地址翻译
2. **进程隔离**:每个进程拥有独立的页表,无法访问其他进程或内核的内存
3. **异界传送门**:巧妙地解决了跨地址空间切换时代码无法执行的问题
4. **ELF 加载**:不再使用原始二进制,而是解析标准的 ELF 格式,按段设置权限
5. **地址翻译**:系统调用必须将用户虚拟地址翻译为物理地址才能访问
6. **堆管理**:通过 `sbrk` 实现动态堆扩展/收缩

在后续章节中,我们将在地址空间的基础上引入**进程**概念,实现 `fork`/`exec`/`waitpid` 等系统调用。

## 七、思考题

1. **恒等映射 vs 非恒等映射?** 内核使用恒等映射(VPN == PPN),用户使用非恒等映射。这样设计的好处是什么?如果内核也使用非恒等映射,会带来什么复杂性?

2. **为什么需要异界传送门?** 如果不使用传送门,直接在切换 `satp` 后执行下一条指令,会发生什么?能否用其他方式解决这个问题?

3. **页表项的 U 标志的作用?** 如果一个页面没有设置 U 标志,用户态程序访问它会发生什么?内核态呢?

4. **`translate()` 在系统调用中的必要性?** 为什么 `write` 系统调用不能直接用用户传入的指针?如果省略 translate 检查,可能导致什么安全问题?

5. **sbrk 与 mmap 的关系?** `sbrk` 只能线性扩展堆,而 `mmap` 可以在任意地址映射内存。现代操作系统中,`malloc` 通常同时使用两者。为什么?

## 参考资料

- [rCore-Tutorial-Guide 第四章](https://learningos.github.io/rCore-Tutorial-Guide/)
- [rCore-Tutorial-Book 第四章](https://rcore-os.cn/rCore-Tutorial-Book-v3/chapter4/index.html)
- [RISC-V Privileged Specification - Sv39](https://riscv.org/specifications/privileged-isa/)
- [RISC-V Reader 中文版](http://riscvbook.com/chinese/RISC-V-Reader-Chinese-v2p1.pdf)

---

## 附录:rCore-Tutorial 组件分析表

### 表 1:tg-ch1 ~ tg-ch8 操作系统内核总体情况描述表

| 操作系统内核 | 所涉及核心知识点 | 主要完成功能 | 所依赖的组件 |
|:-----|:------------|:---------|:---------------|
| **tg-ch1** | 应用程序执行环境<br>裸机编程(Bare-metal)<br>SBI(Supervisor Binary Interface)<br>RISC-V 特权级(M/S-mode)<br>链接脚本(Linker Script)<br>内存布局(Memory Layout)<br>Panic 处理 | 最小 S-mode 裸机程序<br>QEMU 直接启动(无 OpenSBI)<br>打印 "Hello, world!" 并关机<br>演示最基本的 OS 执行环境 | tg-sbi |
| **tg-ch2** | 批处理系统(Batch Processing)<br>特权级切换(U-mode ↔ S-mode)<br>Trap 处理(ecall / 异常)<br>上下文保存与恢复<br>系统调用(write / exit)<br>用户态 / 内核态<br>`sret` 返回指令 | 批处理操作系统<br>顺序加载运行多个用户程序<br>特权级切换和 Trap 处理框架<br>实现 write / exit 系统调用 | tg-sbi<br>tg-linker<br>tg-console<br>tg-kernel-context<br>tg-syscall |
| **tg-ch3** | 多道程序(Multiprogramming)<br>任务控制块(TCB)<br>协作式调度(yield)<br>抢占式调度(Preemptive)<br>时钟中断(Clock Interrupt)<br>时间片轮转(Time Slice)<br>任务切换(Task Switch)<br>任务状态(Ready/Running/Finished)<br>clock_gettime 系统调用 | 多道程序与分时多任务<br>多程序同时驻留内存<br>协作式 + 抢占式调度<br>时钟中断与时间管理 | tg-sbi<br>tg-linker<br>tg-console<br>tg-kernel-context<br>tg-syscall |
| **tg-ch4** | 虚拟内存(Virtual Memory)<br>Sv39 三级页表(Page Table)<br>地址空间隔离(Address Space)<br>页表项(PTE)与标志位<br>地址转换(VA → PA)<br>异界传送门(MultislotPortal)<br>ELF 加载与解析<br>堆管理(sbrk)<br>恒等映射(Identity Mapping)<br>内存保护(Memory Protection)<br>satp CSR | 引入 Sv39 虚拟内存<br>每个用户进程独立地址空间<br>跨地址空间上下文切换<br>进程隔离和内存保护 | tg-sbi<br>tg-linker<br>tg-console<br>tg-kernel-context<br>tg-kernel-alloc<br>tg-kernel-vm<br>tg-syscall |
| **tg-ch5** | 进程(Process)<br>进程控制块(PCB)<br>进程标识符(PID)<br>fork(地址空间深拷贝)<br>exec(程序替换)<br>waitpid(等待子进程)<br>进程树 / 父子关系<br>初始进程(initproc)<br>Shell 交互式命令行<br>进程生命周期(Ready/Running/Zombie)<br>步幅调度(Stride Scheduling) | 引入进程管理<br>fork / exec / waitpid 系统调用<br>动态创建、替换、等待进程<br>Shell 交互式命令行 | tg-sbi<br>tg-linker<br>tg-console<br>tg-kernel-context<br>tg-kernel-alloc<br>tg-kernel-vm<br>tg-syscall<br>tg-task-manage |
| **tg-ch6** | 文件系统(File System)<br>easy-fs 五层架构<br>SuperBlock / Inode / 位图<br>DiskInode(直接+间接索引)<br>目录项(DirEntry)<br>文件描述符表(fd_table)<br>文件句柄(FileHandle)<br>VirtIO 块设备驱动<br>MMIO(Memory-Mapped I/O)<br>块缓存(Block Cache)<br>硬链接(Hard Link)<br>open / close / read / write 系统调用 | 引入文件系统与 I/O<br>用户程序存储在磁盘镜像(fs.img)<br>VirtIO 块设备驱动<br>easy-fs 文件系统实现<br>文件打开 / 关闭 / 读写 | tg-sbi<br>tg-linker<br>tg-console<br>tg-kernel-context<br>tg-kernel-alloc<br>tg-kernel-vm<br>tg-syscall<br>tg-task-manage<br>tg-easy-fs |
| **tg-ch7** | 进程间通信(IPC)<br>管道(Pipe)<br>环形缓冲区(Ring Buffer)<br>统一文件描述符(Fd 枚举)<br>信号(Signal)<br>信号集(SignalSet)<br>信号屏蔽字(Signal Mask)<br>信号处理函数(Signal Handler)<br>kill / sigaction / sigprocmask / sigreturn<br>命令行参数(argc / argv)<br>I/O 重定向(dup) | 进程间通信-管道 <br>异步事件通知(信号)<br>统一文件描述符抽象<br>信号发送 / 注册 / 屏蔽 / 返回 | tg-sbi<br>tg-linker<br>tg-console<br>tg-kernel-context<br>tg-kernel-alloc<br>tg-kernel-vm<br>tg-syscall<br>tg-task-manage<br>tg-easy-fs<br>tg-signal<br>tg-signal-impl |
| **tg-ch8** | 同步互斥(Sync&Mutex)<br>线程(Thread)/ 线程标识符(TID)<br>进程-线程分离<br>竞态条件(Race Condition)<br>临界区(Critical Section)<br>互斥(Mutual Exclusion)<br>互斥锁(Mutex:自旋锁 vs 阻塞锁)<br>信号量(Semaphore:P/V 操作)<br>条件变量(Condvar)<br>管程(Monitor:Mesa 语义)<br>线程阻塞与唤醒(wait queue)<br>死锁(Deadlock)/ 死锁四条件<br>银行家算法(Banker's Algorithm)<br>双层管理器(PThreadManager) | 进程-线程分离<br>同一进程内多线程并发<br>互斥锁(MutexBlocking)<br>信号量(Semaphore)<br>条件变量(Condvar)<br>线程阻塞与唤醒机制<br>死锁检测(练习) | tg-sbi<br>tg-linker<br>tg-console<br>tg-kernel-context<br>tg-kernel-alloc<br>tg-kernel-vm<br>tg-syscall<br>tg-task-manage<br>tg-easy-fs<br>tg-signal<br>tg-signal-impl<br>tg-sync |

### 表 2:tg-ch1 ~ tg-ch8 操作系统内核所依赖组件总体情况描述表

| 功能组件 | 所涉及核心知识点 | 主要完成功能 | 所依赖的组件 |
|:-----|:------------|:---------|:----------------------|
| **tg-sbi** | SBI(Supervisor Binary Interface)<br>console_putchar / console_getchar<br>系统关机(shutdown)<br>RISC-V 特权级(M/S-mode)<br>ecall 指令 | S→M 模式的 SBI 调用封装<br>字符输出 / 字符读取<br>系统关机<br>支持 nobios 直接操作 UART | 无 |
| **tg-console** | 控制台 I/O<br>格式化输出(print! / println!)<br>日志系统(Log Level)<br>自旋锁保护的全局控制台 | 可定制 print! / println! 宏<br>log::Log 日志实现<br>Console trait 抽象底层输出 | 无 |
| **tg-kernel-context** | 上下文(Context)<br>Trap 帧(Trap Frame)<br>寄存器保存与恢复<br>特权级切换<br>stvec / sepc / scause CSR<br>LocalContext(本地上下文)<br>ForeignContext(跨地址空间上下文)<br>异界传送门(MultislotPortal) | 用户/内核态切换上下文管理<br>LocalContext 结构<br>ForeignContext(含 satp)<br>MultislotPortal 跨地址空间执行 | 无 |
| **tg-kernel-alloc** | 内核堆分配器<br>伙伴系统(Buddy Allocation)<br>动态内存管理<br>#[global_allocator] | 基于伙伴算法的 GlobalAlloc<br>堆初始化(init)<br>物理内存转移(transfer) | 无 |
| **tg-kernel-vm** | 虚拟内存管理<br>页表(Page Table)<br>Sv39 分页(三级页表)<br>虚拟地址(VAddr)/ 物理地址(PAddr)<br>虚拟页号(VPN)/ 物理页号(PPN)<br>页表项(PTE)/ 页表标志位(VmFlags)<br>地址空间(AddressSpace)<br>PageManager trait<br>地址翻译(translate) | Sv39 页表管理<br>AddressSpace 地址空间抽象<br>虚实地址转换<br>页面映射(map / map_extern)<br>页表项操作 | 无 |
| **tg-syscall** | 系统调用(System Call)<br>系统调用号(SyscallId)<br>系统调用分发(handle)<br>系统调用结果(Done / Unsupported)<br>Caller 抽象<br>IO / Process / Scheduling / Clock /<br>Signal / Thread / SyncMutex trait 接口 | 系统调用 ID 与参数定义<br>trait 接口供内核实现<br>init_io / init_process / init_scheduling /<br>init_clock / init_signal /<br>init_thread / init_sync_mutex<br>支持 kernel / user feature | tg-signal-defs |
| **tg-task-manage** | 任务管理(Task Management)<br>调度(Scheduling)<br>进程管理器(PManager, proc feature)<br>双层管理器(PThreadManager, thread feature)<br>ProcId / ThreadId<br>就绪队列(Ready Queue)<br>Manage trait / Schedule trait<br>进程等待(wait / waitpid)<br>线程等待(waittid)<br>阻塞与唤醒(blocked / re_enque) | Manage 和 Schedule trait 抽象<br>proc feature:单层进程管理器(PManager)<br>thread feature:双层管理器(PThreadManager)<br>进程树 / 父子关系<br>线程阻塞 / 唤醒 | 无 |
| **tg-easy-fs** | 文件系统(File System)<br>SuperBlock / Inode / 位图(Bitmap)<br>DiskInode(直接+间接索引)<br>块缓存(Block Cache)<br>BlockDevice trait<br>文件句柄(FileHandle)<br>打开标志(OpenFlags)<br>管道(Pipe)/ 环形缓冲区<br>用户缓冲区(UserBuffer)<br>FSManager trait | easy-fs 五层架构实现<br>文件创建 / 读写 / 目录操作<br>块缓存管理<br>管道环形缓冲区实现<br>FSManager trait 抽象 | 无 |
| **tg-signal-defs** | 信号编号(SignalNo)<br>SIGKILL / SIGINT / SIGUSR1 等<br>信号动作(SignalAction)<br>信号集(SignalSet)<br>最大信号数(MAX_SIG) | 信号编号枚举定义<br>信号动作结构定义<br>信号集类型定义<br>为 tg-signal 和 tg-syscall 提供共用类型 | 无 |
| **tg-signal** | 信号处理(Signal Handling)<br>Signal trait 接口<br>add_signal / handle_signals<br>get_action_ref / set_action<br>update_mask / sig_return / from_fork<br>SignalResult(Handled / ProcessKilled) | Signal trait 接口定义<br>信号添加 / 处理 / 动作设置<br>屏蔽字更新 / 信号返回<br>fork 继承 | tg-kernel-context<br>tg-signal-defs |
| **tg-signal-impl** | SignalImpl 结构<br>已接收信号位图(received)<br>信号屏蔽字(mask)<br>信号处理中状态(handling)<br>信号动作表(actions)<br>信号处理函数调用<br>上下文保存与恢复 | Signal trait 的参考实现<br>信号接收位图管理<br>屏蔽字逻辑<br>处理状态和动作表 | tg-kernel-context<br>tg-signal |
| **tg-sync** | 互斥锁(Mutex trait: lock / unlock)<br>阻塞互斥锁(MutexBlocking)<br>信号量(Semaphore: up / down)<br>条件变量(Condvar: signal / wait_with_mutex)<br>等待队列(VecDeque\<ThreadId\>)<br>UPIntrFreeCell | MutexBlocking 阻塞互斥锁<br>Semaphore 信号量<br>Condvar 条件变量<br>通过 ThreadId 与调度器交互 | tg-task-manage |
| **tg-user** | 用户态程序(User-space App)<br>用户库(User Library)<br>系统调用封装(syscall wrapper)<br>用户堆分配器<br>用户态 print! / println! | 用户测试程序运行时库<br>系统调用封装<br>用户堆分配器<br>各章节测试用例(ch2~ch8) | tg-console<br>tg-syscall |
| **tg-checker** | 测试验证<br>输出模式匹配<br>正则表达式(Regex)<br>测试用例判定 | rCore-Tutorial CLI 测试输出检查工具<br>验证内核输出匹配预期模式<br>支持 --ch N 和 --exercise 模式 | 无 |
| **tg-linker** | 链接脚本(Linker Script)<br>内核内存布局(KernelLayout)<br>.text / .rodata / .data / .bss / .boot 段<br>入口点(boot0! 宏)<br>BSS 段清零 | 形成内核空间布局的链接脚本模板<br>用于 build.rs 工具构建 linker.ld<br>内核布局定位(KernelLayout::locate)<br>入口宏(boot0!)<br>段信息迭代 | 无 |
## License

Licensed under GNU GENERAL PUBLIC LICENSE, Version 3.0.