南京大学蒋炎岩操作系统笔记(p8-p14)
p8:并发bug和应对(死锁/数据竞争/原子性违反/防御性违反和动态分析)
防御性编程:对输入、状态和返回值进行检查,提前处理异常情况,提高程序的健壮性和可靠性。
死锁(Deadlock):
定义:多个线程(或进程)互相等待对方释放资源,导致所有线程都无法继续执行。
产生死锁的四个必要条件:
① 互斥 ② 占有并等待 ③ 不可剥夺 ④ 循环等待
Lock Ordering(锁顺序)---一种避免死锁的方法:
核心思想:所有线程必须按照统一的顺序获取锁
并发bug:数据竞争Data Race(不上锁会带来的一种问题)
定义:多个线程同时访问同一个共享变量,且至少有一个线程进行写操作,没有使用锁或其他同步机制进行保护。
Lockdep
定义:是 Linux 内核的运行时死锁检测工具。它记录程序执行过程中观察到的锁获取顺序,并构建锁依赖有向图。若发现锁依赖图中存在环,例如 x → y 和 y → x,则说明不同线程可能以相反顺序获取锁,从而产生循环等待,Lockdep 会报告潜在死锁。
ThreadSanitizer
定义:是运行时数据竞争检测工具。它根据同一线程中的程序顺序以及线程间的 release-acquire 同步关系,建立 happens-before 关系图。对于不同线程中访问同一内存位置且至少有一次写操作的两个事件 x 和 y,如果无法证明 x happens-before y 或 y happens-before x,则认为两次访问并发发生,并报告数据竞争。
Sanitizers(运行时动态分析工具)
| 工具 | 主要检测内容 |
|---|---|
| AddressSanitizer(ASan) | 越界、释放后使用、重复释放等非法内存访问 |
| ThreadSanitizer(TSan) | 多线程数据竞争 |
| MemorySanitizer(MSan) | 读取未初始化的数据 |
| UBSanitizer(UBSan) | 整数溢出、错误对齐、除零等未定义行为 |
注:上述工具通常是在 GCC 或 Clang 编译时手动加入对应选项,编译器不会默认开启。
Canary检测栈空间越界代码如下:
核心思想:在栈空间的上下边界放置一段固定的特殊值,程序运行一段时间后,再检查这些位置:
- 如果值没有变化,说明没有越界写;
- 如果值被修改,说明程序写出了合法栈空间。

p9:操作系统的状态机模型 (操作系统的加载; thread-os 代码讲解)
在裸机系统中,CPU 复位后会进入硬件规定的初始状态,程序计数器 PC 指向固定的 ROM 地址。ROM 中保存厂商提供的固件,固件负责初始化基本硬件,并从 Flash、SD 卡等存储介质中将二级加载器、操作系统或用户程序加载到 RAM,最后修改 PC 并跳转执行。整个启动过程依赖硬件与软件之间预先规定的地址、文件格式和调用约定。
Firmware:BIOS和UEFI
定义:都属于计算机主板中的Firmware(固件),复杂在操作系统启动前初始化硬件并加载启动程序。
1.BIOS(基本输入输出系统)是传统PC使用的启动固件。
传统 BIOS 通常读取磁盘第一个扇区(512字节),也就是 MBR,然后把其中的启动代码加载到物理内存(7c00)并执行。
电脑上电后,BIOS主要完成:
CPU 复位
↓
执行 BIOS
↓
检测并初始化内存、键盘、磁盘等硬件
↓
按照启动顺序寻找启动设备
↓
读取磁盘启动扇区
↓
跳转执行 Bootloader
↓
Bootloader 加载操作系统
BIOS特点:
- 启动方式较老;
- 通常配合 MBR 分区表;
- 启动代码空间有限;
- 主要工作在早期 x86 实模式环境;
- 界面通常是键盘操作的文字界面;
- 对大容量磁盘和现代安全机制支持较弱
2.UEFI(统一可扩展固件接口)
它不再只是从磁盘第一个扇区读取一小段代码,而是可以直接识别磁盘中的 EFI System Partition,ESP 分区,然后从其中加载 .efi 启动程序。
它在 CPU 复位后完成硬件初始化,并通过统一的 EFI API 和设备驱动框架向启动程序提供磁盘、文件系统、图形和网络等服务。
现代UEFI PC的典型流程是:
CPU 上电或复位
↓
执行 UEFI Firmware
↓
初始化 CPU、内存和基本设备
↓
加载 UEFI 驱动
↓
UEFI Boot Manager 查看启动项
↓
读取 GPT 磁盘中的 ESP 分区
↓
加载 .efi 启动程序
↓
EFI 启动程序加载操作系统内核
↓
操作系统接管硬件
操作系统启动的一般流程:
上电/复位 Reset
↓
执行固件 Firmware
├─ 传统机器:BIOS
└─ 现代机器:UEFI
↓
固件完成硬件初始化并选择启动设备
↓
加载 Bootloader
↓
Bootloader 加载操作系统内核
↓
内核初始化并启动用户空间程序
Makefile的一些基本概念:
1、定义:用来描述“项目如何编译”的配置文件,是make工具所读取的配置文件。
最基本的格式为:
目标文件: 依赖文件
执行命令
2、Makefile中常用的自动变量:
| 自动变量 | 含义 |
|---|---|
$@ |
当前目标文件 |
$< |
第一个依赖文件 |
$^ |
所有依赖文件 |
示例代码:
main: main.o utils.o
$(CC) -o $@ $^
P10:状态机模型的应用
Profiler(性能分析器)是一种用来查找程序性能瓶颈的工具
Profiler通常分析以下指标:
- CPU时间:各函数占用了多少处理器时间。
- 调用次数:函数执行了多少次。
- 调用关系:哪个函数调用了哪个函数。
- 内存使用:在哪里申请内存,是否存在泄漏或频繁分配。
- 缓存未命中:CPU是否经常需要访问较慢的内存。
- 线程状态:线程是否长时间阻塞或等待锁。
常见工具包括:
- Linux
perf gprof- Valgrind的
Callgrind - Visual Studio Profiler
- Java VisualVM
- Python
cProfile
P11:操作系统上的进程 (最小 Linux; fork, execve 和 exit)
最小Linux:
整个操作系统世界都是由系统调用(进程、内存、文件、网络...)创建出来的。
这个最小Linux实际上就是“Linux内核 + 内存中的BusyBox文件系统 + 第一个用户程序 /init”,不需要真实硬盘,也能在QEMU中启动并进入Shell。

注:强调“静态链接”是因为静态程序不依赖相应动态链接器和共享库。
initramfs:启动时使用的内存文件系统,它只包含两个文件如下:
.
├── bin
│ └── busybox
└── init
其中busybox 是一个集成了许多常用Linux命令的小型程序,例如:
ls
cd
cat
echo
mkdir
sh

fork()
| 所在进程 | 返回值 |
|---|---|
| 父进程 | 子进程的PID(大于0) |
| 子进程 | 0 |
| 创建失败 | -1,仅父进程继续运行 |
根据返回值的不同,在两个进程中分别执行不同的代码。
execve()
重置一个状态机,重置成某一个程序的初始状态。(也就是替换当前进程中的程序)
先通过 fork() 创建一个新进程,再让子进程通过 execve() 执行另一个程序;execve() 成功后 PID 不变,但原程序的代码和内存被新程序完全替换。
exit()
exit()------也就是清理后退出
高层、负责清理:
执行 atexit,刷新 stdio,结束整个进程。
_exit()------也就是整个进程立即退出
低层、快速退出:
不执行 atexit,不刷新 stdio,通常结束整个进程。
syscall(SYS_exit)---------也就是当前线程立即退出
原始 Linux 系统调用:
不清理用户态资源,只结束当前线程。
P12:进程的地址空间 (pmap; vdso; mmap; 游戏修改器/外挂)
进程的地址空间由若干连续的"段“组成。
地址空间: 一个进程能够使用的全部虚拟地址范围。
vdso:非陷入系统调用
作用: Linux 内核将一小段可执行代码映射到用户进程地址空间, 让部分系统服务在用户态直接完成,避免陷入内核。
优点: 减少用户态/内核态切换; 避免 syscall 指令开销; 适合高频时间查询。
进程地址空间:mmap
mmap 就是在进程的虚拟地址空间中划出一段区域,并规定这段虚拟地址应该对应文件、匿名内存或共享内存。
它主要有三种用途:
- 将文件映射到内存,之后可以像操作数组一样读写文件;
- 申请匿名内存;
- 创建进程间共享内存。
突破进程隔离:
-
进程隔离
Linux 中每个进程都有独立的虚拟地址空间,正常情况下一个进程不能直接访问另一个进程的内存,这是操作系统保护进程安全的机制。 -
获取目标进程 PID
可以使用ps、top等命令找到目标进程的 PID,后续通过 PID 定位目标进程。 -
查看进程内存布局
/proc/[PID]/maps或pmap PID可以查看目标进程的内存区域,例如代码段、数据段、堆(heap)和栈(stack),从而确定可能存放目标数据的区域。 -
访问目标进程内存
/proc/[PID]/mem可以映射目标进程的虚拟内存;也可以使用 ptrace 机制,在具备相应权限的情况下读取或修改目标进程的内存。gdb等调试工具底层也依赖ptrace。 -
内存扫描定位数据
如果不知道变量地址,可以先搜索已知数值,例如先搜索2000,改变程序中的数值为1700后再次搜索,通过不断缩小候选范围找到目标地址。 -
核心流程
获取 PID → 查看内存布局 → 获得访问权限 → 扫描/定位目标数据 → 读取或修改内存 → 验证结果。
P13:C标准库的实现(系统调用的封装,内存空间管理)
系统调用虽然是操作系统提供的底层接口,但直接使用它比较麻烦,所以通常会提供更高层的 API 对系统调用进行封装。
高级 API
↓
库函数封装
↓
系统调用
↓
操作系统内核
↓
硬件
P15:fork的应用(文件描述符的复制、写时复制、创建平行宇宙的魔法)
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐

所有评论(0)