【NJU-OS-JYY笔记】操作系统:设计与实现
1. 绪论
1.1. 程序的执行与状态机
在计算机科学中,任何程序都可以被抽象为一个状态机,无论是我们熟知的日常工具(LibreOffice,Chrome)还是开发工具(IDE,GCC,GDB),它们本质没有任何区别。
- 程序状态 = 处理器状态(寄存器值)+ 内存状态 + 操作系统状态(文件描述符、信号处理等
- 每次 CPU 执行一条指令 = 状态机的一次迁移
程序的执行过程本质上是状态的不断变化。具体而言:
- 状态由以下两部分组成:
- 变量数值:包括全局变量和局部变量的值。
- 栈:程序执行过程中函数调用的上下文信息。
- 初始状态:当程序启动时,初始状态由
main
函数的第一条语句决定。此时,栈中仅包含一个StackFrame
,表示main
函数的初始调用,全局变量也被初始化为预设的初始值。
--- 程序通过操作系统的 execve
系统调用被加载到内存中,并初始化为一个独立的进程。
- 状态迁移:程序的执行可以看作是由一条条语句驱动的状态迁移。每一步执行都会导致状态的变化,例如变量值的更新或栈的修改。
--- 程序在内存中运行,经历一系列的状态迁移,完成计算任务,并通过系统调用与操作系统交互(如文件操作、进程管理、存储管理等)。程序通过调用 exit
或 exit_group
系统调用终止执行,操作系统回收其占用的资源。
--- model checking 可以在给定一个系统和一个我们希望它拥有的性质的前提下,model checking 算法会探索这个系统的每个状态,验证系统是否满足这个性质。举个例子:如果我们希望系统满足“无死锁”这个性质,那么 model checking 算法就会遍历系统的每个状态。如果它发现存在一个从初始状态可达的状态没有后继状态,那么它就找到了系统不满足“无死锁”的证据,然后给用户报错,否则它就可以告诉我们,这个系统确实是满足“无死锁”这个性质的。
一个直观的状态机迁移:
int main() {int sum = 0;for (int i = 0; i < 1000000; i++) { // 用户态循环sum += i; // 用户态加法}printf(「Sum: %d\n」, sum); // 库函数(内部含系统调用)return 0;
}
在这个程序中:
- 执行了约 200 万条 用户态指令(循环+加法)
- 只发生了 1-2 次 系统调用(通过
printf
的write
系统调用)
用户态执行
┌───────────────────────┐ 系统调用入口
│ 指令 1: mov eax, 0 │ │
│ 指令 2: add ebx, ecx │ ▼
│ 指令 3: cmp edx, 100 │ ┌───────────┐
│ ... (百万条指令) │ │ 内核态执行 │
│ │ │ sys_read │
│ 指令 N: call printf ├─────►│ sys_write │
└───────────────────────┘ └───────────┘▲ ││ ▼└───────┘系统调用返回
怎么获得一个 Linux 进程的系统调用序列?
# 启动新进程并跟踪
strace <命令> [参数]
# 示例:跟踪 `ls -l`
strace ls -l# 跟踪已运行的进程(按 PID)
strace -p <PID>
# 示例:跟踪 PID 为 1234 的进程
strace -p 1234# 常用选项:
strace -f # 跟踪子进程(适用于多进程程序)
strace -o log.txt # 将输出保存到文件(避免刷屏)
strace -T # 显示系统调用耗时
strace -s 1024 # 显示更长的参数内容(默认只显示 32 字节)
strace -e trace=<类别> # 只跟踪特定系统调用(如文件操作:`-e trace=file`)系统调用指令
进程管理: fork, execve, exit, waitpid, getpid, ...
操作系统对象和访问: open, close, read, write, pipe, mount, mkfifo, mknod, stat, socket, ...
地址空间管理: mmap, sbrk (mmap 的前身)
以及一些其他的机制: pivot_root, chmod, chown, ..
1.2. 如何证明程序的正确性
状态机是一个具有严格数学定义的概念。这意味着我们可以用形式化的方法来描述和分析程序的行为。这种形式化的方法为程序的正确性验证和优化提供了理论基础。
Curry-Howard Correspondence:
- 命题即类型:逻辑中的命题 (如 A→B→A→B) 对应函数类型 A→B→A→B。
- 证明即程序:命题的构造性证明过程对应该类型的一个具体程序(例如,一个实现 A→BA→B 的函数即为 “AA 蕴含 BB” 的证明)。
逻辑中的蕴含消除规则对应函数调用:若有一个类型为 A→BA→B 的函数 (证明),且输入类型 AA 的值 (前提),则输出类型 BB 的值 (结论)。而对于一阶谓词,例如 ∃x.P(x)∃x.P(x),就必须提供一个 xx 的实例,这构成了基于构造的 “直觉逻辑” 的基础
1.3. 操作系统的最大职责
操作系统的最大职责是为程序提供一个透明的运行环境。从程序的角度来看,它似乎“独占”了整个计算机的资源,并按顺序执行每一条指令。然而,实际情况并非如此:操作系统在后台持续运行,管理着硬件资源、进程调度、文件操作等复杂任务。
当程序执行系统调用(如 read
、write
、fork
等)时,程序的执行会被暂时挂起,而操作系统接管控制权,完成相应的任务(如与硬件交互或管理进程)。完成任务后,操作系统会将控制权交还给程序,使其继续执行。对于程序而言,这一过程是完全透明的,程序不会感知到时间的流逝或操作系统的干预。
一个最基本的 Unix 模型应该包含:
- 进程
- 系统调用
- 上下文切换
- 调度
1.4. 程序与操作系统的沟通桥梁
程序与操作系统之间的唯一通信桥梁是系统调用。系统调用是操作系统提供给程序的一组接口,允许程序请求操作系统完成特定任务(如文件读写、进程创建等)。例如,在 x86-64 架构中,程序可以通过 syscall
指令发起系统调用。
系统调用的重要性体现在以下几个方面:
- 桥梁作用:系统调用是程序与操作系统之间的接口,没有它,程序无法与外界进行交互。
- 安全性:系统调用提供了一种安全的机制,确保程序无法直接访问硬件资源,从而保护系统的稳定性和安全性。
操作系统提供了许多工具(如 strace
)来分析和调试程序的系统调用行为,帮助开发者理解程序的运行机制。
2. 虚拟化
2.1. 进程和程序
程序是状态机的静态描述,它描述了所有可能的程序状态,程序 (动态) 运行起来,就成了进程 (进行中的程序)。
操作系统作为进程(状态机)的管理者,一个直观的想法就如同 Windows api 一样,spawn 用来创建状态机,exit 用来销毁状态机,但 Unix 的答案是 fork 用来复制状态机,execve 用来复位状态机。
fork 的行为:
立即复制状态机,包括所有状态的完整拷贝,寄存器 & 每一个字节的内存,新创建进程返回 0,执行 fork 的进程返回子进程的进程号——“父子关系”。
Tips:
创建一棵层的进程树(A->B->C),并随机退出其中的一些进程——我们可以观察进程退出前后父子进程的关系。
----父进程(B
)终止后,子进程(C
)成为“孤儿”,操作系统内核(而非用户进程)负责回收终止进程的资源,并重置其子进程的父进程。这一机制确保所有进程最终都有有效的父进程,避免资源泄露。
int pid = fork();
if (pid == -1) { // 错误perror(「fork」); goto fail;
} else if (pid == 0) { // 子进程execve(...);perror(「execve」); exit(EXIT_FAILURE);
} else { // 父进程...int status;waitpid(pid, &status, 0); // testkit.C 中有
}
2.2. 进程地址空间
我们可以使用 pmap 查看某个进程的地址空间,pmap
命令通过读取 /proc/[pid]/maps
和 /proc/[pid]/smaps
文件来获取进程的内存映射信息。Linux 内核将这些信息以文本形式暴露在 /proc
文件系统中,pmap
通过解析这些文件实现功能,而无需直接调用特定的系统调用。
进程创建开始时,有一个初始状态,System V ABI 规定了部分寄存器和栈,Binary 中指定的 PT_LOAD 段,内存分为一段一段,每一段有访问权限。
区域地址范围用途内核空间 0xC0000000~0xFFFFFFFF 操作系统内核代码和数据(所有进程共享)栈(Stack)向下增长存储局部变量、函数调用信息(如返回地址、参数)堆(Heap)向上增长动态内存分配(malloc
/free
)共享库映射区固定存放动态链接库(如
区域 | 地址范围 | 用途 |
内核空间 | 0xC0000000~0xFFFFFFFF | 操作系统内核代码和数据(所有进程共享) |
栈(Stack) | 向下增长 | 存储局部变量、函数调用信息(如返回地址、参数) |
堆(Heap) | 向上增长 | 动态内存分配(malloc/free) |
共享库映射区 | 固定 | 存放动态链接库(如libc.so),多个进程可共享同一份代码。文件映射,匿名映射 |
数据段(Data) | 固定 | 存放全局变量、静态变量 |
代码段(Text) | 固定 | 存放可执行指令(只读) |
libc.so
),多个进程可共享同一份代码。
现代 OS 的主流方式,将地址空间划分为固定大小的页(Page,通常 4KB),物理内存划分为页帧(Page Frame)。
- 页表(Page Table) 负责虚拟地址→物理地址的映射:
- 多级页表(如 x86-64 采用 4 级页表:PML4→PDP→PD→PT)。
- TLB(快表):缓存常用页表项,加速地址转换。
- 缺页异常(Page Fault):
- 访问未映射的页时触发,OS 可能从磁盘(Swap 分区)加载数据。
进程的初始状态:
名称 | 状态 |
特殊寄存器 (浮点单元状态) | 所有错误状态标志都清零控制字设置 :RC=0: 默认的舍入方式:四舍五入PC=11: 默认使用最高的 双扩展精度M, UM, OM, ZM, DM, IM=1: 所有的浮点异常处于屏蔽 (Masked) 状态。这意味着如果发生这些错误,FPU 会自动处理 |
rFLAGS 寄存器 | DF=0: 方向标志向前(很多内存操作会向高地址增长)。CF=0: 没有进位/借位发生(就像刚重置了计算器)。PF, AF=0: 没有特定的奇偶校验位或半字节进位标志被设置(可以忽略)。ZF=0: 没有结果为0的比较(相当于还没开始比较)。SF=0: 结果是非负数(或者理解为结果的高位不是1)。OF=0: 没有发生有符号数的溢出(计算结果没有超出范围)。 |
堆栈状态 | High Addresses ... ...Auxiliary vector entries ... 0 Environment pointers ... 8+8*argc+%rsp 0 8+%rsp 参数数组 %rsp 参数个数 --- 它指向当前栈顶... ... Low Addresses |
线程状态 | 这个新线程会继承父线程当前的所有浮点单元状态在此之后,新线程的浮点状态就是其私有 (private) 的了。任何修改(例如更改舍入模式或屏蔽位)只影响该线程自身,不会影响父线程或其他线程。 |
- able data-draft-node="block" data-draft-type="table" data-size="normal" data-row-style="normal">
-
名称 状态 特殊寄存器 (浮点单元状态) 所有错误状态标志都清零控制字设置 :RC=0: 默认的舍入方式:四舍五入PC=11: 默认使用最高的 双扩展精度M, UM, OM, ZM, DM, IM=1: 所有的浮点异常处于屏蔽 (Masked) 状态。这意味着如果发生这些错误,FPU 会自动处理 rFLAGS 寄存器 DF=0: 方向标志向前(很多内存操作会向高地址增长)。CF=0: 没有进位/借位发生(就像刚重置了计算器)。PF, AF=0: 没有特定的奇偶校验位或半字节进位标志被设置(可以忽略)。ZF=0: 没有结果为0的比较(相当于还没开始比较)。SF=0: 结果是非负数(或者理解为结果的高位不是1)。OF=0: 没有发生有符号数的溢出(计算结果没有超出范围)。 堆栈状态 High Addresses ... ...Auxiliary vector entries ... 0 Environment pointers ... 8+8*argc+%rsp 0 8+%rsp 参数数组 %rsp 参数个数 --- 它指向当前栈顶... ... Low Addresses 线程状态 这个新线程会继承父线程当前的所有浮点单元状态在此之后,新线程的浮点状态就是其私有 (private) 的了。任何修改(例如更改舍入模式或屏蔽位)只影响该线程自身,不会影响父线程或其他线程。
able data-draft-node="block" data-draft-type="table" data-size="normal" data-row-style="normal">
able data-dra
ft-no
de="block" data-draft-type="table" data-size="normal" data-row-style="normal">
M
, UM
, OM
, ZM
, DM
, IM
=1: 所有的浮点异常处于屏蔽 (Masked) 状态。这意味着如果发生这些错误,FPU 会自动处理
名称 | 状态 |
特殊寄存器 (浮点单元状态) | 所有错误状态标志都清零控制字设置 :RC=0: 默认的舍入方式:四舍五入PC=11: 默认使用最高的 双扩展精度M, UM, OM, ZM, DM, IM=1: 所有的浮点异常处于屏蔽 (Masked) 状态。这意味着如果发生这些错误,FPU 会自动处理 |
rFLAGS 寄存器 | DF=0: 方向标志向前(很多内存操作会向高地址增长)。CF=0: 没有进位/借位发生(就像刚重置了计算器)。PF, AF=0: 没有特定的奇偶校验位或半字节进位标志被设置(可以忽略)。ZF=0: 没有结果为0的比较(相当于还没开始比较)。SF=0: 结果是非负数(或者理解为结果的高位不是1)。OF=0: 没有发生有符号数的溢出(计算结果没有超出范围)。 |
堆栈状态 | High Addresses ... ...Auxiliary vector entries ... 0 Environment pointers ... 8+8*argc+%rsp 0 8+%rsp 参数数组 %rsp 参数个数 --- 它指向当前栈顶... ... Low Addresses |
线程状态 | 这个新线程会继承父线程当前的所有浮点单元状态在此之后,新线程的浮点状态就是其私有 (private) 的了。任何修改(例如更改舍入模式或屏蔽位)只影响该线程自身,不会影响父线程或其他线程。 |
rFLAGS 寄存器
able data-draft-node="block" data-draft-type="table" data-size="normal" data-row-style="normal">
able data-draft-node="block" data-draft-type="table" data-size="normal" data-row-style="normal">
ab
le
data-draft-node="block" data-draft-type="table" data-size="normal" data-row-style="normal">ZF
=0: 没有结果为 0
的比较(相当于还没开始比较)。名称 状态 特殊寄存器 (浮点单元状态) 所有错误状态标志都清零控制字设置 :RC=0: 默认的舍入方式:四舍五入PC=11: 默认使用最高的 双扩展精度M, UM, OM, ZM, DM, IM=1: 所有的浮点异常处于屏蔽 (Masked) 状态。这意味着如果发生这些错误,FPU 会自动处理 rFLAGS 寄存器 DF=0: 方向标志向前(很多内存操作会向高地址增长)。CF=0: 没有进位/借位发生(就像刚重置了计算器)。PF, AF=0: 没有特定的奇偶校验位或半字节进位标志被设置(可以忽略)。ZF=0: 没有结果为0的比较(相当于还没开始比较)。SF=0: 结果是非负数(或者理解为结果的高位不是1)。OF=0: 没有发生有符号数的溢出(计算结果没有超出范围)。 堆栈状态 High Addresses ... ...Auxiliary vector entries ... 0 Environment pointers ... 8+8*argc+%rsp 0 8+%rsp 参数数组 %rsp 参数个数 --- 它指向当前栈顶... ... Low Addresses 线程状态 这个新线程会继承父线程当前的所有浮点单元状态在此之后,新线程的浮点状态就是其私有 (private) 的了。任何修改(例如更改舍入模式或屏蔽位)只影响该线程自身,不会影响父线程或其他线程。 SF
=0: 结果是非负数(或者理解为结果的高位不是
1)。名称 状态 特殊寄存器 (浮点单元状态) 所有错误状态标志都清零控制字设置 :RC=0: 默认的舍入方式:四舍五入PC=11: 默认使用最高的 双扩展精度M, UM, OM, ZM, DM, IM=1: 所有的浮点异常处于屏蔽 (Masked) 状态。这意味着如果发生这些错误,FPU 会自动处理 rFLAGS 寄存器 DF=0: 方向标志向前(很多内存操作会向高地址增长)。CF=0: 没有进位/借位发生(就像刚重置了计算器)。PF, AF=0: 没有特定的奇偶校验位或半字节进位标志被设置(可以忽略)。ZF=0: 没有结果为0的比较(相当于还没开始比较)。SF=0: 结果是非负数(或者理解为结果的高位不是1)。OF=0: 没有发生有符号数的溢出(计算结果没有超出范围)。 堆栈状态 High Addresses ... ...Auxiliary vector entries ... 0 Environment pointers ... 8+8*argc+%rsp 0 8+%rsp 参数数组 %rsp 参数个数 --- 它指向当前栈顶... ... Low Addresses 线程状态 这个新线程会继承父线程当前的所有浮点单元状态在此之后,新线程的浮点状态就是其私有 (private) 的了。任何修改(例如更改舍入模式或屏蔽位)只影响该线程自身,不会影响父线程或其他线程。 able data-draft-node="block" data-draft-type="table" data-size="normal" data-row-style="normal">
le data-draft-node="block" data-draft-type="table" data-size="normal" data-row-style="normal">
名称 | 状态 |
特殊寄存器 (浮点单元状态) | 所有错误状态标志都清零控制字设置 :RC=0: 默认的舍入方式:四舍五入PC=11: 默认使用最高的 双扩展精度M, UM, OM, ZM, DM, IM=1: 所有的浮点异常处于屏蔽 (Masked) 状态。这意味着如果发生这些错误,FPU 会自动处理 |
rFLAGS 寄存器 | DF=0: 方向标志向前(很多内存操作会向高地址增长)。CF=0: 没有进位/借位发生(就像刚重置了计算器)。PF, AF=0: 没有特定的奇偶校验位或半字节进位标志被设置(可以忽略)。ZF=0: 没有结果为0的比较(相当于还没开始比较)。SF=0: 结果是非负数(或者理解为结果的高位不是1)。OF=0: 没有发生有符号数的溢出(计算结果没有超出范围)。 |
堆栈状态 | High Addresses ... ...Auxiliary vector entries ... 0 Environment pointers ... 8+8*argc+%rsp 0 8+%rsp 参数数组 %rsp 参数个数 --- 它指向当前栈顶... ... Low Addresses |
线程状态 | 这个新线程会继承父线程当前的所有浮点单元状态在此之后,新线程的浮点状态就是其私有 (private) 的了。任何修改(例如更改舍入模式或屏蔽位)只影响该线程自身,不会影响父线程或其他线程。 |
名称 | 状态 |
特殊寄存器 (浮点单元状态) | 所有错误状态标志都清零控制字设置 :RC=0: 默认的舍入方式:四舍五入PC=11: 默认使用最高的 双扩展精度M, UM, OM, ZM, DM, IM=1: 所有的浮点异常处于屏蔽 (Masked) 状态。这意味着如果发生这些错误,FPU 会自动处理 |
rFLAGS 寄存器 | DF=0: 方向标志向前(很多内存操作会向高地址增长)。CF=0: 没有进位/借位发生(就像刚重置了计算器)。PF, AF=0: 没有特定的奇偶校验位或半字节进位标志被设置(可以忽略)。ZF=0: 没有结果为0的比较(相当于还没开始比较)。SF=0: 结果是非负数(或者理解为结果的高位不是1)。OF=0: 没有发生有符号数的溢出(计算结果没有超出范围)。 |
堆栈状态 | High Addresses ... ...Auxiliary vector entries ... 0 Environment pointers ... 8+8*argc+%rsp 0 8+%rsp 参数数组 %rsp 参数个数 --- 它指向当前栈顶... ... Low Addresses |
线程状态 | 这个新线程会继承父线程当前的所有浮点单元状态在此之后,新线程的浮点状态就是其私有 (private) 的了。任何修改(例如更改舍入模式或屏蔽位)只影响该线程自身,不会影响父线程或其他线程。 |
名称 | 状态 |
特殊寄存器 (浮点单元状态) | 所有错误状态标志都清零控制字设置 :RC=0: 默认的舍入方式:四舍五入PC=11: 默认使用最高的 双扩展精度M, UM, OM, ZM, DM, IM=1: 所有的浮点异常处于屏蔽 (Masked) 状态。这意味着如果发生这些错误,FPU 会自动处理 |
rFLAGS 寄存器 | DF=0: 方向标志向前(很多内存操作会向高地址增长)。CF=0: 没有进位/借位发生(就像刚重置了计算器)。PF, AF=0: 没有特定的奇偶校验位或半字节进位标志被设置(可以忽略)。ZF=0: 没有结果为0的比较(相当于还没开始比较)。SF=0: 结果是非负数(或者理解为结果的高位不是1)。OF=0: 没有发生有符号数的溢出(计算结果没有超出范围)。 |
堆栈状态 | High Addresses ... ...Auxiliary vector entries ... 0 Environment pointers ... 8+8*argc+%rsp 0 8+%rsp 参数数组 %rsp 参数个数 --- 它指向当前栈顶... ... Low Addresses |
线程状态 | 这个新线程会继承父线程当前的所有浮点单元状态在此之后,新线程的浮点状态就是其私有 (private) 的了。任何修改(例如更改舍入模式或屏蔽位)只影响该线程自身,不会影响父线程或其他线程。 |
名称 | 状态 |
特殊寄存器 (浮点单元状态) | 所有错误状态标志都清零控制字设置 :RC=0: 默认的舍入方式:四舍五入PC=11: 默认使用最高的 双扩展精度M, UM, OM, ZM, DM, IM=1: 所有的浮点异常处于屏蔽 (Masked) 状态。这意味着如果发生这些错误,FPU 会自动处理 |
rFLAGS 寄存器 | DF=0: 方向标志向前(很多内存操作会向高地址增长)。CF=0: 没有进位/借位发生(就像刚重置了计算器)。PF, AF=0: 没有特定的奇偶校验位或半字节进位标志被设置(可以忽略)。ZF=0: 没有结果为0的比较(相当于还没开始比较)。SF=0: 结果是非负数(或者理解为结果的高位不是1)。OF=0: 没有发生有符号数的溢出(计算结果没有超出范围)。 |
堆栈状态 | High Addresses ... ...Auxiliary vector entries ... 0 Environment pointers ... 8+8*argc+%rsp 0 8+%rsp 参数数组 %rsp 参数个数 --- 它指向当前栈顶... ... Low Addresses |
线程状态 | 这个新线程会继承父线程当前的所有浮点单元状态在此之后,新线程的浮点状态就是其私有 (private) 的了。任何修改(例如更改舍入模式或屏蔽位)只影响该线程自身,不会影响父线程或其他线程。 |
名称 | 状态 |
特殊寄存器 (浮点单元状态) | 所有错误状态标志都清零控制字设置 :RC=0: 默认的舍入方式:四舍五入PC=11: 默认使用最高的 双扩展精度M, UM, OM, ZM, DM, IM=1: 所有的浮点异常处于屏蔽 (Masked) 状态。这意味着如果发生这些错误,FPU 会自动处理 |
rFLAGS 寄存器 | DF=0: 方向标志向前(很多内存操作会向高地址增长)。CF=0: 没有进位/借位发生(就像刚重置了计算器)。PF, AF=0: 没有特定的奇偶校验位或半字节进位标志被设置(可以忽略)。ZF=0: 没有结果为0的比较(相当于还没开始比较)。SF=0: 结果是非负数(或者理解为结果的高位不是1)。OF=0: 没有发生有符号数的溢出(计算结果没有超出范围)。 |
堆栈状态 | High Addresses ... ...Auxiliary vector entries ... 0 Environment pointers ... 8+8*argc+%rsp 0 8+%rsp 参数数组 %rsp 参数个数 --- 它指向当前栈顶... ... Low Addresses |
线程状态 | 这个新线程会继承父线程当前的所有浮点单元状态在此之后,新线程的浮点状态就是其私有 (private) 的了。任何修改(例如更改舍入模式或屏蔽位)只影响该线程自身,不会影响父线程或其他线程。 |
名称 | 状态 |
特殊寄存器 (浮点单元状态) | 所有错误状态标志都清零控制字设置 :RC=0: 默认的舍入方式:四舍五入PC=11: 默认使用最高的 双扩展精度M, UM, OM, ZM, DM, IM=1: 所有的浮点异常处于屏蔽 (Masked) 状态。这意味着如果发生这些错误,FPU 会自动处理 |
rFLAGS 寄存器 | DF=0: 方向标志向前(很多内存操作会向高地址增长)。CF=0: 没有进位/借位发生(就像刚重置了计算器)。PF, AF=0: 没有特定的奇偶校验位或半字节进位标志被设置(可以忽略)。ZF=0: 没有结果为0的比较(相当于还没开始比较)。SF=0: 结果是非负数(或者理解为结果的高位不是1)。OF=0: 没有发生有符号数的溢出(计算结果没有超出范围)。 |
堆栈状态 | High Addresses ... ...Auxiliary vector entries ... 0 Environment pointers ... 8+8*argc+%rsp 0 8+%rsp 参数数组 %rsp 参数个数 --- 它指向当前栈顶... ... Low Addresses |
线程状态 | 这个新线程会继承父线程当前的所有浮点单元状态在此之后,新线程的浮点状态就是其私有 (private) 的了。任何修改(例如更改舍入模式或屏蔽位)只影响该线程自身,不会影响父线程或其他线程。 |
名称 | 状态 |
特殊寄存器 (浮点单元状态) | 所有错误状态标志都清零控制字设置 :RC=0: 默认的舍入方式:四舍五入PC=11: 默认使用最高的 双扩展精度M, UM, OM, ZM, DM, IM=1: 所有的浮点异常处于屏蔽 (Masked) 状态。这意味着如果发生这些错误,FPU 会自动处理 |
rFLAGS 寄存器 | DF=0: 方向标志向前(很多内存操作会向高地址增长)。CF=0: 没有进位/借位发生(就像刚重置了计算器)。PF, AF=0: 没有特定的奇偶校验位或半字节进位标志被设置(可以忽略)。ZF=0: 没有结果为0的比较(相当于还没开始比较)。SF=0: 结果是非负数(或者理解为结果的高位不是1)。OF=0: 没有发生有符号数的溢出(计算结果没有超出范围)。 |
堆栈状态 | High Addresses ... ...Auxiliary vector entries ... 0 Environment pointers ... 8+8*argc+%rsp 0 8+%rsp 参数数组 %rsp 参数个数 --- 它指向当前栈顶... ... Low Addresses |
线程状态 | 这个新线程会继承父线程当前的所有浮点单元状态在此之后,新线程的浮点状态就是其私有 (private) 的了。任何修改(例如更改舍入模式或屏蔽位)只影响该线程自身,不会影响父线程或其他线程。 |
名称 | 状态 |
特殊寄存器 (浮点单元状态) | 所有错误状态标志都清零控制字设置 :RC=0: 默认的舍入方式:四舍五入PC=11: 默认使用最高的 双扩展精度M, UM, OM, ZM, DM, IM=1: 所有的浮点异常处于屏蔽 (Masked) 状态。这意味着如果发生这些错误,FPU 会自动处理 |
rFLAGS 寄存器 | DF=0: 方向标志向前(很多内存操作会向高地址增长)。CF=0: 没有进位/借位发生(就像刚重置了计算器)。PF, AF=0: 没有特定的奇偶校验位或半字节进位标志被设置(可以忽略)。ZF=0: 没有结果为0的比较(相当于还没开始比较)。SF=0: 结果是非负数(或者理解为结果的高位不是1)。OF=0: 没有发生有符号数的溢出(计算结果没有超出范围)。 |
堆栈状态 | High Addresses ... ...Auxiliary vector entries ... 0 Environment pointers ... 8+8*argc+%rsp 0 8+%rsp 参数数组 %rsp 参数个数 --- 它指向当前栈顶... ... Low Addresses |
线程状态 | 这个新线程会继承父线程当前的所有浮点单元状态在此之后,新线程的浮点状态就是其私有 (private) 的了。任何修改(例如更改舍入模式或屏蔽位)只影响该线程自身,不会影响父线程或其他线程。 |
辅助向量 (Auxiliary Vector
)
操作系统通过这个表向新进程传递一些关键的系统信息或运行时参数。这些信息对于程序正常启动、动态链接器 (ld-Linux
) 工作等都至关重要。
类型名称 (a_type) | 值 (Value) | 信息内容 (a_un) | 含义解释 |
AT_NULL | 0 | ignored | 辅助向量的结束标记! 遇到这个类型,表示后面没有有效条目了,辅助向量结束。 |
AT_IGNORE | 1 | ignored | 忽略此条目。 这个条目没有意义,里面的值(a_un)也不用管。 |
AT_EXECFD | 2 | a_val (整数值) | 解释器程序可用的文件描述符。 如果程序需要一个解释器(如 ld-linux,负责动态链接),系统可能会将这个文件描述符传给解释器,解释器可以用它来直接读取真正要运行的程序文件(比如你的 ./myprogram)。 |
AT_PHDR | 3 | a_ptr (指针) | 程序在内存中的程序头表 (Program Header Table) 位置。 如果系统已经把你的程序加载到内存里了,这个指针就指向 ELF 文件结构中程序头表的起始地址。告诉解释器(如果需要的话)去哪里找内存映射信息。 |
AT_PHENT | 4 | a_val (整数值) | 每个程序头表条目的大小 (字节)。 告诉解释器上面提到的程序头表(AT_PHDR)中,每个条目的长度是多少字节(通常是 sizeof(Elf64_Phdr) = 56 字节)。 |
AT_PHNUM | 5 | a_val (整数值) | 程序头表条目的个数。 告诉解释器上面提到的程序头表(AT_PHDR)里总共有多少个条目。 |
AT_PAGESZ | 6 | a_val (整数值) | 系统内存分页大小 (字节)。 告诉程序操作系统管理内存的基本单位有多大(比如 4KB = 4096 字节)。很多内存操作需要对齐到这个大小。 |
AT_BASE | 7 | a_ptr (指针) | 解释器程序自身在内存中的加载基地址。 如果程序是通过解释器运行(如 ld-linux),这个指针指向解释器自己代码在内存里的起始地址。 |
AT_FLAGS | 8 | a_val (位掩码) | 解释器相关的标志位 (位掩码)。 给解释器(如 ld-linux)使用的特定标志位组合。未定义的位是0。应用程序一般不需要关心。 |
AT_ENTRY | 9 | a_ptr (函数指针) | 程序真正的入口地址 (Entry Point)。 指向程序可执行文件的入口点地址(在 ELF 文件中标记为 e_entry)。这是程序的代码开始执行的地方(比如 _start 符号位置)。解释器最终要把控制权交给这里。这是最重要的指针之一。 |
AT_NOTELF | 10 | a_val (整数值) | 非 ELF 文件标志。 如果值 非0,就表示当前运行的程序 不是 标准的 ELF 格式文件(可能是其他格式如 a.out, PE等)。如果是 0,就是标准的 ELF 格式。 |
AT_UID / AT_EUID / AT_GID / AT_EGID | 11 / 12 / 13 / 14 | a_val (整数值) | 用户和组标识符。 AT_UID: 进程的实际用户ID (启动进程的用户)。 AT_EUID: 进程的有效用户ID (通常与 AT_UID 相同,或受 setuid 程序影响)。 AT_GID: 进程的实际组ID。 AT_EGID: 进程的有效组ID。 |
AT_PLATFORM | 15 | a_ptr (字符串指针) | 硬件平台标识字符串。 指向一个描述底层硬件平台架构的字符串(例如 "x86_64")。 |
AT_HWCAP | 16 | a_val (位掩码) | CPU 硬件能力位掩码。 一个整数值,其中的位代表 CPU 支持的特定扩展指令集或功能(例如 MMX, SSE, SSE2, AVX 等)。这个值对应于 CPUID 指令(功能号1)返回的 EDX 寄存器中的一部分特征位。 |
AT_CLKTCK | 17 | a_val (整数值) | 系统时钟节拍频率 (clock tick)。 每秒有多少次 times() 函数会增加时钟计数(通常是 100)。 |
AT_SECURE | 23 | a_val (整数值) | 安全模式标志。 如果值是 1,表示程序是以提升权限模式启动的(例如用户执行了一个设置了 suid 或 sgid 位的程序)。值是 0 则表示没有特殊权限。安全敏感的程序需要注意这个标志。 |
AT_BASE_PLATFORM | 24 | a_ptr (字符串指针) | 基础平台标识字符串。 与 AT_PLATFORM 类似,但指向的字符串描述的是更基础、更通用的平台架构,可能与硬件平台字符串不同(例如 AT_PLATFORM 是 "i686", AT_BASE_PLATFORM 可能是 "i386")。 |
AT_RANDOM | 25 | a_ptr (指针) | 16字节安全随机数 (Securely Generated Random Bytes)。 指向操作系统提供的、专门用于增强安全性的 16 字节随机数据(通常由内核的安全随机数生成器产生)。程序可以使用这个随机数作为密钥种子或 ASLR 的加强。 |
AT_HWCAP2 | 26 | a_val (位掩码) | 扩展 CPU 硬件能力位掩码。 未来可用的额外 CPU 特性位掩码(当前 AMD64 ABI 定义中该值为 0,保留给未来扩展)。可能对应 CPUID 后续指令或不同寄存器的特征位。 |
AT_EXECFN | 31 | a_ptr (字符串指针) | 被执行的程序的文件名。 指向一个字符串,该字符串保存了用户(或 shell)传递给 exec() 系列函数的原始程序路径名(例如 "/usr/bin/ls" 或 "./hello")。与 argv[0] 不同,argv[0] 可能是修改过的(例如通过符号链接启动),而 AT_EXECFN 通常指向实际被执行的文件路径。 |
2.3. 地址空间管理
mmap 系统调用,可以在状态机状态上增加/删除/修改一段可访问的内存:
- MAP_ANONYMOUS: 匿名 (申请) 内存
- fd: 把文件 “搬到” 进程地址空间中 (例子:加载器)
ptrace/process_vm_writev
,可以修改另一个进程的地址空间
ptrace
系统调用
通过PTRACE_PEEKDATA
和PTRACE_POKEDATA
请求读写目标进程内存。调试工具(如 GDB)即依赖此机制,但需附加到目标进程(可能暂停其执行)。process_vm_writev
系统调用
允许直接向目标进程写入数据,效率高于ptrace
,且无需暂停目标进程。需要CAP_SYS_PTRACE
能力或权限配置。
适用场景:
- 高效进程间通信:需要跨进程批量传输数据时(如共享内存的替代方案)
- 动态热补丁:运行时修改目标进程的代码/配置
- 调试/监控工具:注入代码或修改内存状态进行调试
- 性能敏感操作:相比
ptrace
无需频繁暂停目标进程,延迟更低
2.4. 一切皆文件
文件描述符是指向操作系统对象的 “指针”——系统调用通过这个指针 (fd) 确定进程希望访问操作系统中的哪个对象。我们有 open, close, read/write, lseek, dup 管理文件描述符。
//openp = malloc(sizeof(FileDescriptor));
//closedelete(p);
//read/write*(p.data++);
//lseekp.data += offset;
//dupq = p;
任何可以读写的东西都可以是文件,比如:
真实的设备
- /dev/sda
- /dev/tty
虚拟的设备 (文件)
- /dev/urandom (随机数), /dev/null (黑洞), ...
- 它们并没有实际的 “文件”
- 操作系统为它们实现了特别的 read 和 write 操作
- /drivers/char/mem.C
- 甚至可以通过 /sys/class/backlight 控制屏幕亮度
- procfs 也是用类似的方式实现的
优点
- 统一的编程模型
- 开发人员只需掌握
open()/read()/write()/close()
等少量 API - 例如:
echo 「hello」 > /dev/pts/0
直接写入终端
- 强大的组合能力
- 管道操作:
ls | grep txt | sort | uniq
- 流重定向:
program 2>&1 > log.txt
- 抽象层次清晰
- VFS 虚拟文件系统(Linux 核心组件)
- FUSE 框架允许用户态文件系统实现
- 透明的资源访问
/proc/pid/maps
查看进程内存映射/sys/class/net
管理网络设备
缺点
- 文件操作需要用户/内核态切换(昂贵)
- 网络通信用文件 API 效率低下
- 额外的延迟和内存拷贝
- 单线程 I/O
解决方案
分层 API 架构,操作系统通过「内核基础 API + 用户态封装」来解决这些矛盾:
Windows NT
┌───────────────────────┐
│ Win32 API │ ← 图形/多媒体/COM 等高级接口
├───────────────────────┤
│ POSIX 子系统层 │ ← 翻译 Unix API 调用,兼容层适配传统抽象
├───────────────────────┤
│ NT 内核基础 API │
│ (Objects, Handles) │ ← 微内核设计:进程/线程/虚拟内存等基本对象
└───────────────────────┘
WSL (Windows Subsystem for Linux)
┌───────────────────────┐
│ Linux 二进制 (ELF) │
├───────────────────────┤
│ lxss.sys (翻译层) │ ← 转换 Linux 系统调用→NT API
├───────────────────────┤
│ Windows NT 内核 │
└───────────────────────┘
甚至连我们与电脑交互的终端也都是文件,pty:
伪终端(pseudo terminal,有时也被称为 pty)是指伪终端 master 和伪终端 slave 这一对字符设备。其中的 slave 对应 /dev/pts/ 目录下的一个文件,而 master 则在内存中标识为一个文件描述符(fd)。伪终端由终端模拟器提供,终端模拟器是一个运行在用户态的应用程序。
Master 端是更接近用户显示器、键盘的一端,slave 端是在虚拟终端上运行的 CLI(Command Line Interface,命令行接口)程序。Linux 的伪终端驱动程序,会把 master 端(如键盘)写入的数据转发给 slave 端供程序输入,把程序写入 slave 端的数据转发给 master 端供(显示器驱动等)读取。请参考下面的示意图(此图来自互联网):
我们打开的终端桌面程序,比如 GNOME Terminal,其实是一种终端模拟软件。当终端模拟软件运行时,它通过打开 /dev/ptmx 文件创建了一个伪终端的 master 和 slave 对,并让 shell 运行在 slave 端。当用户在终端模拟软件中按下键盘按键时,它产生字节流并写入 master 中,shell 进程便可从 slave 中读取输入;shell 和它的子程序,将输出内容写入 slave 中,由终端模拟软件负责将字符打印到窗口中。
/dev/ptmx 是一个字符设备文件,当进程打开 /dev/ptmx 文件时,进程会同时获得一个指向 pseudoterminal master(ptm)的文件描述符和一个在 /dev/pts 目录中创建的 pseudoterminal slave(pts) 设备。通过打开 /dev/ptmx 文件获得的每个文件描述符都是一个独立的 ptm,它有自己关联的 pts,ptmx(可以认为内存中有一个 ptmx 对象)在内部会维护该文件描述符和 pts 的对应关系,对这个文件描述符的读写都会被 ptmx 转发到对应的 pts。我们可以通过 lsof 命令查看 ptmx 打开的文件描述符。
可以看到进程的 0u(标准输入)、1u(标准输出)、2u(标准错误输出)都绑定到了伪终端 /dev/pts/8 上,这样一来对这个文件描述符的读写都会被 ptmx 转发到对应的 pts。
查看具体的关联
# 查看伪终端主从设备
$ ps -ef | grep pts
user 5678 1234 0 10:00 pts/1 00:00:00 bash# 查看文件描述符指向
$ ls -l /proc/1234/fd/12
lrwx------ 1 user user 64 Jan 01 12:34 12 -> /dev/ptmx$ ls -l /dev/pts/1
crw--w---- 1 user tty 136, 1 Jan 01 10:00 /dev/pts/1
完整数据流转示例(以执行 ls -l
为例)
当我们对当前终端进行操作,或者运行程序时:
- 在同一个会话里,相关的进程会被放进同一个或不同的进程组。比如,你在 shell 里敲
ls | grep txt &
,这个管道命令里的ls
和grep
通常就在同一个新的后台进程组里。而 shell 自己在另一个进程组。 - 为什么重要? 信号(如
Ctrl-C
产生的SIGINT
)通常是发给整个前台进程组的!这就是为什么Ctrl-C
能杀掉一个正在运行的复杂命令(比如包含管道的命令)里的所有相关进程。Ctrl-Z
(发送SIGTSTP
暂停信号)也是发给整个前台进程组。
为什么我们需要 sigaction 替代 Unix 的信号机制?
- 跨平台兼容性
- 易导致信号丢失
- 防止嵌套信号干扰
// 传统 signal 的脆弱实现
void handler(int sig) {signal(SIGINT, handler); // 必须手动重新注册// 处理逻辑
}
signal(SIGINT, handler);// 使用 sigaction 的可靠实现
struct sigaction sa;
sa.sa_handler = handler;
sigemptyset(&sa.sa_mask);
sa.sa_flags = SA_RESTART; // 自动重启系统调用
sigaction(SIGINT, &sa, NULL);
2.5. 加载器和链接器
为了这一章,我直接看了一本书,写的很好,相见恨晚:
加载器:
- 在程序运行时激活。
- 核心任务:
- 内存映射:解析可执行文件格式(如 ELF),将代码/数据段加载到虚拟内存的指定位置(如
.text
段只读,.data
段可读写)。 - 动态链接:若程序依赖动态库(如
glibc
),加载器调用动态链接器(ld-Linux.so
)加载共享库并解析符号地址(运行时完成)。 - 环境初始化:设置栈/堆空间,传递命令行参数,跳转到程序入口点(如
_start
)。
- 内存映射:解析可执行文件格式(如 ELF),将代码/数据段加载到虚拟内存的指定位置(如
连接器:
- 程序构建阶段(编译后)的工具,由编译器(如 GCC)调用。
- 核心任务:
- 符号解析:将不同目标文件(
.o
/.obj
)中的函数/变量引用与其定义匹配(例如,main.C
调用printf()
时,链接器在libc.a
中查找其实现)。 - 重定位:为所有代码段(
.text
)、数据段(.data
)分配内存地址,并修正代码中的相对地址引用。 - 库整合:将静态库(如
.a
)代码直接复制到可执行文件中,或记录动态库(如.so
)的引用信息。
- 符号解析:将不同目标文件(
- 输出:生成可执行文件(如 ELF 格式),包含完整的程序逻辑和元数据(符号表、重定位表)。
动态链接核心:
- 延迟绑定(Lazy Binding):
- 仅在函数首次调用时解析地址,减少程序启动时的开销。
- 未调用的函数不解析,节省内存和 CPU 资源。
- 位置无关代码(PIC):
- GOT 存储绝对地址,PLT 通过相对跳转访问 GOT,使共享库可加载到任意内存地址。
- 读写分离:
- PLT(代码段)只读,GOT(数据段)可写,符合现代操作系统的内存保护机制。
GOT(全局偏移表)
- 定位:位于数据段(
.got
或.got.plt
节区),内容在运行时动态修改。 - 作用:
- 存储外部符号(如共享库函数、全局变量)的绝对地址。
- 首次调用函数时,由动态链接器(
ld-Linux.so
)解析符号地址并填入 GOT。 - 后续调用直接通过 GOT 跳转,避免重复解析。
- 结构:
GOT[0]
:.dynamic
段的地址(动态链接元数据)。GOT[1]
:link_map
结构指针(描述已加载共享库的链表)。GOT[2]
:_dl_runtime_resolve
函数地址(符号解析入口)。GOT[3..n]
:存储实际函数地址(如printf
、scanf
)。
PLT(过程链接表)
- 定位:位于代码段(
.plt
节区),内容在编译后固定不变。 - 作用:
- 作为跳转到外部函数的代理代码,首次调用时触发地址解析。
- 通过间接跳转(
jmp *GOT[n]
)实现延迟绑定。
- 结构:
PLT[0]
:公共桩代码,调用GOT[2]
(_dl_runtime_resolve
)。PLT[1..n]
:每个外部函数对应一项,包含:jmp *GOT[n]
(首次指向 PLT 内部的push
指令)。push 序号
(符号在重定位表中的索引)。jmp PLT[0]
(触发解析流程)。
步骤 | 行为 |
① | 程序调用 call printf@plt(跳转到 PLT[n])。 |
② | PLT[n] 执行 jmp *GOT[n],此时 GOT[n] 指向 PLT[n] 的第二条指令(push 序号)。 |
③ | 执行 push 序号(压入符号索引),再 jmp PLT[0]。 |
④ | PLT[0] 将 GOT[1](link_map)压栈,跳转至 GOT[2](_dl_runtime_resolve)。 |
⑤ | _dl_runtime_resolve 解析符号地址,写入 GOT[n],跳转到目标函数。 |
结果:符号地址被缓存至GOT,后续调用直接跳转。 | call printf@plt → jmp *GOT[n] → 直接跳转至目标函数地址(无需解析)。 |
3. 并发
我们可以很容易地把状态机模型扩展为共享内存上的多线程模型——只是每次选择一个状态机执行一步,通过提供 spawn 和 join 两个 API 来利用现有多处理器系统的共享内存能力。
然而,由于编译优化的 “无处不在” (处理器也是编译器),共享内存并发的行为十分复杂。与此同时,人类又恰好是物理世界 (宏观时间) 中的 “sequential creature”,编程语言的直觉 (顺序/选择/循环结构) 也是围绕顺序程序设计,因此共享内存上的并发编程是非常具有挑战性的 “底层技术”。
互斥:别的我没学会,一把大刀保平安,我用的可熟了
我们希望控制事件发生的先后顺序,比如 A->B->C,互斥锁只是确保分开 A,B,C,但做不到顺序控制,这里边得需要同步。
// 万能消费者生产者公式
// 想清楚生产/消费的条件是什么?void fish_before(char ch) {mutex_lock(&lk);while (!can_print(ch)) {cond_wait(&cv, &lk);}quota--;mutex_unlock(&lk);
}void fish_after(char ch) {mutex_lock(&lk);quota++;current = next(ch);assert(current);cond_broadcast(&cv);mutex_unlock(&lk);
}
如何避免死锁?
Lock ordering1. 任意时刻系统中的锁都是有限的2. 给所有锁编号 (Lock Ordering)3. 严格按照从小到大的顺序获得锁
任意时刻,总有一个线程获得 “编号最大” 的锁,这个线程总是可以继续运行,因为他已经有了所有需要的小锁# 例子
假设 5 个哲学家(线程)和 5 把筷子(锁),编号为 1~5。规则要求:哲学家必须先拿编号小的筷子,再拿编号大的筷子(如必须先拿 3 号筷才能拿 4 号筷)。运行过程:哲学家 A 拿起筷子 1 和 2(锁 1、2),哲学家 B 拿起筷子 1 和 3(锁 1、3)。
此时系统中最大编号为 3(被哲学家 B 持有)。
哲学家 B 无需等待更大编号的锁(已持有所有小于 3 的锁),可吃完后释放锁 1 和 3。
释放后,其他哲学家(如等待锁 3 的哲学家 C)可继续获取锁 3 并推进任务。
Do not communicate by sharing memory; instead, share memory by communicating.
——Effective Go
4. 持久化
4.1. 设备和驱动程序
我们拿优盘插到电脑中,然后就看了这个盘符,我们可以进去查看文件和拷贝文件,这里面发生了什么?
其实,这里面用到了 udev
技术,监听内核发出的设备事件(如插入、拔出、状态变化),在 Linux 系统中用户空间(Userspace)的动态设备管理 /dev
目录下的设备文件。
关键流程如下:
- 当设备插入、移除或状态变化时,比如优盘,内核识别设备然后加载对应的 USB 驱动(如
usb-storage
),udev 依赖驱动已成功加载,否则无法获取设备信息,驱动程序工作在内核空间(Kernel Space),直接与硬件交互,比如声卡驱动将音频数据转换为电信号驱动扬声器发声 - 内核通过
netlink
套接字发送uevent
事件(包含设备路径、子系统类型、属性等元数据) udevd
守护进程(用户空间)监听uevent
,解析事件内容并匹配预定义的规则udevd
读取/etc/udev/rules.d/
和/lib/udev/rules.d/
下的规则文件,按文件名数字优先级升序执行匹配
# /etc/udev/rules.d/99-usb.rules
SUBSYSTEM==「block」, KERNEL==「sd*」, ATTRS{idVendor}==「0781」, ATTRS{idSerial}==「A1B2C3D4」, SYMLINK+=「backup_disk」, MODE=「0660」, GROUP=「storage」匹配键(条件):
SUBSYSTEM(设备类型)、KERNEL(内核设备名)、ATTRS{...}(/sys 中的设备属性,如厂商 ID)操作键(动作):
SYMLINK(创建软链接)、MODE(权限)、GROUP(归属组)、RUN(执行脚本)
- 根据规则匹配结果执行操作:
- 命名控制:自定义设备名(如将 USB 磁盘固定为
/dev/backup_disk
)。 - 权限设置:通过
MODE=「0666」
、GROUP=「users」
修改设备文件权限。 - 符号链接:创建软链接(如
SYMLINK+=「cdrom」
指向/dev/sr0
)。 - 脚本执行:设备插入时自动挂载(
RUN+=「/bin/mount /dev/sdb1 /mnt」
)
- 最终在
/dev
下生成设备文件,完成设备可用性配置
这样我们就可以以文件的形式使用设备了。
好滴,那么为啥我们就能以文件的形式去使用这些设备的,关键就在于驱动程序:
Everything Is a File, 通过执行操作系统对象的指针可以访问一切,open/close,read/write,lseek。
驱动程序实现了当前设备的 struct file_operations 的实现,它把系统调用翻译成与设备能听懂的数据,甚至包含如/dev/null 和 proc/stat 这种虚拟文件。
- 驱动通过
ioremap()
将 BAR 映射的物理地址转为内核虚拟地址,直接读写设备寄存器 - 设备直接写内存地址(MSI Data)触发中断,支持多向量和定向投递,驱动注册中断处理函数
- 应用程序,通过
read()
/write()
或ioctl()
发送控制命令
没想到吧,连虚拟机都是文件,KVM 的所有关键操作(虚拟机生命周期管理、CPU 调度、内存映射)均通过 ioctl
实现用户态与内核态的交互:
kvm_fd = open(「/dev/kvm」, O_RDWR); // 打开 KVM 设备
vm_fd = ioctl(kvm_fd, KVM_CREATE_VM, 0); // 创建虚拟机
vcpu_fd = ioctl(vm_fd, KVM_CREATE_VCPU, 0); // 创建 vCPU
ioctl(vcpu_fd, KVM_RUN, 0); // 运行 vCPU
当调用 KVM_RUN
后,虚拟 CPU 开始执行客户机指令。若客户机触发 I/O 操作或特权指令(如访问设备寄存器),CPU 会通过硬件虚拟化技术(Intel VT/AMD-V)退出到宿主机内核模式(VM-Exit)。KVM 内核模块捕获退出原因(如 KVM_EXIT_IO
),并通过 ioctl
的返回值通知用户态 QEMU 处理 I/O 模拟
4.2. 存储设备原理
存储的本质在于通过改变物理介质的状态来记录信息,该介质需具备至少两种可稳定区分的状态。
存储经历了百年的发展历史,从磁带,磁鼓,到磁盘,软盘,到 CD 光盘,Flash memory 闪存和 U 盘,flash memory 活到了今天,配合 FTL 技术(软件定义磁盘)成为了存储界的版本答案,让我们来一一回顾,存储的前世今生。
4.2.1. 磁带
磁带存储的本质是磁场与磁性材料的可控相互作用:电流脉冲定向磁化介质,剩磁状态固化信息,电磁感应逆向解码。其高密度、低成本的特性,使其在云备份(如 AWS Glacier)、科研归档等场景仍不可替代。随着 HAMR(热辅助磁记录)等技术的发展,磁带存储密度正迈向 100Gb/in²以上,持续拓展数字历史的承载边界。
写入原理:电信号→磁化状态
- 输入数据(如数字信号)转换为电流脉冲序列。当电流通过磁头线圈时,根据安培定律产生磁场
- 磁带表面涂覆硬磁性材料(如γ-Fe₂O₃或 CrO₂),磁头缝隙处的磁场作用于匀速运动的磁带,使磁性颗粒按特定方向磁化。
读取原理:磁化状态→电信号
- 磁化区域经过读取磁头时,磁力线穿过磁头铁芯,在线圈中感应电动势
- 磁通翻转(代表 1)→ 产生正向/负向脉冲电压,无翻转(代表 0)→ 无显著电压变化
- 信号经放大器鉴别后还原为原始数据
- 读取过程不改变磁化状态,数据可重复读取,磁带寿命约 30 年
擦除原理:退磁还原
- 磁带通过消磁磁头,施加高频交变磁场,使磁性颗粒磁畴方向随机化,整体剩磁归零
工作方式
- 顺序存取:数据按物理顺序排列,读取需遍历前方数据(平均延迟秒级),适合备份等低频访问场景
- 流式传输:现代数据流磁带机(如 LTO-9)通过单主动轮驱动,磁带匀速运动(4-10m/s),磁头静止接触
优劣势:
- 容量大:LTO-9 单卷 45TB,成本$0.01/GB;
能耗低:脱机保存无需供电;
寿命长:抗电磁干扰,适合冷数据归档 - 顺序访问:随机读写效率极低;
4.2.2. 磁鼓
磁带(1928 年由 Fritz Pfleumer 发明)本质是线性一维存储介质:数据按顺序记录在磁性涂层上,读写需顺序扫描。其优势是成本低、容量大,但随机访问效率极低(如读取末端数据需遍历整盘磁带)
1932 年奥地利工程师 Gustav Tauschek 发明的磁鼓采用圆柱形旋转结构:数据分布在鼓面磁道上,通过固定磁头与高速旋转(>3000 RPM)实现毫秒级延迟访问。以 IBM 701 计算机(1953 年)为例,磁鼓作为内存使用,容量 62.5KB,远超同期磁带。
线性移动 -> 旋转扫描,带来革命: 录音 -> 实时计算
- 并行磁头:多磁头同时读写不同磁道,提升吞吐量
- 旋转寻址:数据随鼓面旋转周期性经过磁头,平均延迟仅 5ms
4.2.3. 磁盘
磁鼓有两个核心局限:
- 磁鼓仅能利用圆柱体单表面存储数据(磁性涂层覆盖外表面),内部空间完全浪费。而磁盘采用双面盘片+多盘片堆叠设计(如 IBM RAMAC 350 含 50 张 24 英寸盘片),存储密度提升数十倍
- 磁鼓依赖固定磁头+鼓面旋转实现数据定位,虽比磁带顺序读写快(平均延迟 5ms),但无法实现真正随机寻址,磁盘通过可移动悬臂磁头(如 RAMAC 的液压驱动臂)直接定位任意磁道,寻址时间缩短至毫秒级
磁盘有一些工程优化的突破:
- 磁盘引入空气轴承技术(IBM 1301,1962 年),磁头悬浮于盘面之上,间隙缩小至亚微米级,提升密度且降低磨损
- 1973 年 IBM 推出温彻斯特架构(Winchester),盘片与磁头密封在无尘腔体内,避免粉尘污染,磁头停泊区设计,断电时自动归位,防止盘面刮伤
- 磁鼓表面电镀磁介质易剥落,磁盘采用溅镀工艺生成纳米级磁性合金层(如钴铬钽合金),耐磨性提升 10 倍,支持更高转速
4.2.4. 软盘
磁盘到软盘的演进,主要是便携性需求驱动带来的。
磁盘的固有限制
- 体积与成本:1956 年 IBM 推出的首款硬盘 RAMAC 305 重达 1 吨,容量仅 5MB,价格高昂且需专用机房。
- 不可移动性:早期硬盘为封闭式设计,数据无法跨设备传输,限制了个人计算机(PC)的普及。
软盘的技术突破
- 便携性设计:1967 年 IBM 开发出直径 32 英寸软盘,目标是为大型机提供成本低于 5 美元的可更换微代码载体。
- 小型化迭代:
- 1971 年:8 英寸软盘(81KB);
- 1976 年:5.25 英寸软盘(360KB),成为 IBM PC 标配;
- 1983 年:3.5 英寸软盘(1.44MB),索尼设计保护壳提升耐用性。
- 核心优势:轻便、可拆卸、低成本,满足个人数据交换需求(如软件分发、文件传输)。
市场催化
- PC 普及浪潮:1980 年代 IBM PC 和 Apple II 兴起,软盘成为唯一通用移动存储介质,1996 年全球用量达 50 亿片
4.2.5. CD 光盘
软盘到 CD 光盘:容量危机与多媒体需求升级
- 容量限制:1.44MB 无法满足软件/媒体文件增长, Windows 95 安装需 13 张软盘
- 速度低下:读写速率仅 100KB/s,大型文件传输耗时过长
- 易受磁场干扰、物理损坏,数据丢失风险高
随着激光技术的发展,我们可以利用光学反射进行相位差解码:
- 激光发射:光头发射波长 780nm 的近红外激光束,聚焦为直径约 1μm 的光点。
- 反射差异:
- 平坦区域(Land):激光垂直反射回光电探测器,光强高(代表二进制「0」序列)。
- 凹坑区域(Pit):凹坑深度设计为激光波长的 1/4(约 0.12μm),使反射光与入射光相位差 180°,产生相消干涉,反射光强显著减弱。
- 信号转换:光电探测器将反射光强差异转换为电信号(高电平=「0」,低电平=「1」),再经解码电路还原为数字信息。
这就带来了两个技术飞跃:
- 容量跃迁:CD-ROM(700MB)为软盘的 486 倍,单盘可存储百科全书或多媒体软件,DVD(4.7GB)进一步支持高清视频
- 大规模量产:聚碳酸酯注塑成型实现大规模量产,坑点刻蚀精度达纳米级,有了母盘,可以 3s 中快速注塑成型一个 CD 光盘,如果是车间流水线,可想而知数据拷贝的速率有多快。
CD 盘片从下至上分层构成
层级材料与功能厚度/特性聚碳酸酯基板透明塑料,承载凹坑结构 1.2mm,折射率 1.55 反射层铝或金膜,反射激光(铝=「银盘」,金=「金盘」)0.05μm,反射率>70%
层级 | 材料与功能 | 厚度/特性 |
聚碳酸酯基板 | 透明塑料,承载凹坑结构 | 1.2mm,折射率1.55 |
反射层 | 铝或金膜,反射激光(铝="银盘",金="金盘") | 0.05μm,反射率>70% |
保护层 | 丙烯酸树脂,防氧化与物理损伤 | 提升盘面硬度和耐刮性 |
封面层 | 印刷标签层 | 不影响光学读取 |
保护层丙烯酸树脂,防氧化与物理损伤提升盘面硬度和耐刮性封面层印刷标签层不影响光学读取
4.2.6. flash memory
开始聊聊当前的版本答案---闪存,从光学机械式向固态电子式跃迁,“你还能比电快啊?”。
CD 光盘的固有缺陷
- 机械脆弱性:CD 依赖精密光学头与旋转盘片,易受划痕、灰尘影响,误码率高达 10⁻¹²;
- 读写性能低下:恒定线速度(CLV)设计导致内圈读取仅 150KB/s,且不支持随机访问;
- 不可动态更新:CD-RW 擦写需全盘擦除,耗时长达 10 分钟,擦写寿命仅约 1000 次;
- 容量天花板:红光激光波长 780nm 限制最小凹坑尺寸,单碟最大 700MB,无法匹配数据爆炸需求。
闪存技术的颠覆性创新
- 非机械结构:浮栅晶体管(Floating Gate MOSFET)通过电子隧穿存储数据,无移动部件,抗振动性提升 100 倍;
- 高速读写:NAND 闪存随机访问延迟<100μs,连续读写速度>500MB/s(CD 的 3000 倍以上);
- 动态擦写能力:支持页级写入(4KB)和块级擦除(256KB),擦写寿命达 10 万次(QLC 闪存);
- 三维堆叠扩容:3D NAND 技术实现 200+层堆叠,单芯片容量突破 1Tb,成本降至$0.01/GB(2025 年)
闪存的基本存储单元是浮栅晶体管,由控制栅(Control Gate)、浮栅(Floating Gate)、源极(Source)和漏极(Drain)构成
。浮栅被二氧化硅(SiO₂)绝缘层包裹,形成电荷“陷阱”,其电荷状态决定数据值(0 或 1):
- 浮栅:悬浮于氧化层中,用于捕获电子,电荷可长期保留(断电后不丢失)。
- 控制栅:施加电压以操控电子运动。
- 氧化层:隧穿氧化层(Tunnel Oxide)厚度仅 5-10nm,是电子隧穿的关键通道
写入(Program)
- 电子注入:向控制栅施加高电压(15-20V),电子通过 Fowler-Nordheim 隧穿效应穿越氧化层,被浮栅捕获。
- 电荷效应:浮栅带负电后,晶体管的阈值电压(Threshold Voltage)升高,读取时被识别为“0”。
擦除(Erase)
- 电子释放:在源极施加反向高压(约 20V),浮栅电子被拉出,电荷消散,阈值电压降低,状态变为“1”。
- 操作单位:擦除以块(Block)为单位(通常 256KB-4MB),而非单个字节。
读取(Read)
- 电压检测:向控制栅施加中等电压,若浮栅有电荷(阈值电压高),晶体管不导通(输出“0”);无电荷则导通(输出“1”)。
- NOR/NAND 差异:
- NOR:支持随机访问,可直接读取任意字节。
- NAND:需按页(Page,通常 4-16KB)顺序读取。
4.2.7. FTL 技术
闪存是有擦写限制的,SLC 闪存约 10 万次,QLC 闪存仅 100 次。若频繁写入同一物理块,会导致局部快速损坏。
传统文件系统(如 EXT4、NTFS)和块设备驱动(如 Linux 的 block_device_operations
)基于扇区(512B)或块(通常 4KB) 的覆盖写入设计。操作系统通过 BIO(Block I/O)接口提交读写请求,默认支持原地更新(in-place update)。
FTL 的本质是在闪存物理限制与传统系统之间构建的“翻译层”,(操作系统->BIO->闪存)通过三大创新解决结构性矛盾:
- 虚拟化覆盖写 → 适配操作系统;
FTL 的映射机制
逻辑地址(LBA)→物理地址(PBA)转换:FTL 维护动态映射表,将操作系统的扇区请求转换为闪存的页操作。当数据更新时,FTL 将新数据写入空白页,并更新映射表指向新位置,原位置标记为“无效”(Invalid),通过“异地更新”(Out-of-Place Update)模拟磁盘的覆盖写行为,避免物理擦除延迟
- 动态磨损均衡 → 对抗寿命短板;
新数据优先写入擦写次数少的“年轻块”(Low EC)
- 写入聚合(Write Coalescing):
缓存多个小 BIO,合并为整页写入
- 自动垃圾回收 → 提升空间利用率。
删除操作是对一个块的,而块包含多个页面,操作系统如果只删除某个页面时,就会出现一个块部分页面为有效,部分为无效,所以将无法删除,因为擦除操作需作用于整个块,且仅当块内所有页均为“无效”状态时才能执行,这样那部分无效空间就完全没法利用了。所以回收机制就是,先迁移有效页到新块,再擦除原块。
- 可靠性增强
闪存出厂即含坏块,使用中还会新增坏块。FTL 通过映射表屏蔽坏块,将数据重定向到备用块
FTL 利用 RAM 缓存映射表,并通过多通道并发访问闪存芯片提升吞吐量
ECC 纠错:集成 BCH/LDPC 算法修复位翻转(Bit Flip)
没有 FTL,闪存将无法被现有操作系统直接使用,其寿命和性能会因物理限制而大幅缩水。
4.3. 目录树管理
4.3.1. 隐藏文件
Linux 系统中以.
开头的隐藏文件机制起源于 Unix 系统的设计哲学。在早期 Unix 系统中,ls
命令存在一个显示逻辑缺陷:默认不显示名称以.
开头的文件。这个设计最初是为了隐藏系统关键文件(如.
和..
),后来逐渐演变为隐藏用户配置文件的通用方案。
# 显示所有隐藏文件(含系统级)
ls -a # 输出示例:.bashrc .cache .git/# 创建隐藏文件/目录
touch .secret_config
mkdir .config# 修改现有文件为隐藏
mv config .config
隐藏文件有如下应用场景:
# 典型隐藏配置文件
.bashrc # 用户 Shell 配置
.gitconfig # Git 全局配置# 热加载机制:修改后立即生效
source ~/.bashrc# 隐藏 SSH 密钥
chmod 600 ~/.ssh/id_rsa# 防止配置泄露
echo 「export SECRET_KEY=...」 >> .env
内核实现逻辑如下:
// fs/ext4/dir.C 中的关键函数
int ext4_readdir(struct file *file, struct dir_context *ctx) {struct ext4_dir_entry_2 *de;while ((de = ext4_next_dir_entry(file, ctx->pos)) != NULL) {if (de->name[0] == 『.』) continue; // 跳过隐藏文件ctx->pos = de->inode;if (!dir_emit(ctx, de->name, de->name_len, de->inode, DT_UNKNOWN))return 0;}return 0;
}
4.3.2. 约定俗成的目录结构
Linux 目录结构遵循文件系统层次标准(FHS)
,其核心设计理念源于 Unix 的「一切皆文件」哲学。这种树状结构通过严格的层级划分实现:
- 单一根目录:所有路径以
/
为起点 - 功能隔离:不同类别文件严格分区存放
- 标准化扩展:通过
/usr
和/opt
实现软件生态扩展
目录功能定位典型内容示例访问权限特殊属性/bin
基础用户命令存放区 ls
, cp
, mv
, rm
, chmod
755 静态链接二进制文件,单用户模式可用/sbin
系统管理命令存放区 fdisk
, ifconfig
, reboot
, service
750 仅 root 可执行,包含硬件管理工具/boot
系统启动关键文件存储 vmlinuz
, initrd.img
, grub.cfg
, System.map
755 内核镜像存放区,需保留足够空间(建议≥2GB)/dev
设备文件集散地/dev/sda1
, /dev/tty
, /dev/null
, /dev/zero
755 虚拟设备文件,按需动态创建/etc
系统配置中枢 passwd
, fstab
, hosts
, ssh/sshd_config
644 关键配置文件集群,修改需谨慎/home
用户主目录容器/home/user1
, /home/developer
755 每个用户独立目录,支持配额管理/lib
32 位系统库文件存储 libc.so.6
, libstdc++.so.6
755 静态库与内核模块存放区/lib64
64 位系统库文件存储 libc-2.31.so
, libcrypto.so.3
755 动态链接库主要存放位置/media
可移动设备自动挂载点/media/usb
, /media/cdrom
755udev 自动挂载,支持热插拔/mnt
临时挂载点/mnt/backup
, /mnt/data
755 手动挂载专用,推荐使用 systemd-mount/opt
第三方软件安装区/opt/Docker
, /opt/postgresql
755 商业软件标准安装路径,建议保持独立/proc
内核状态镜像/proc/cpuinfo
, /proc/meminfo
, /proc/net/dev
动态权限虚拟文件系统,反映实时系统状态/root
超级用户主目录/root/.bashrc
, /root/scripts
700 仅 root 可访问,存放敏感配置/run
运行时数据暂存区/run/user/1000
, /run/Docker.sock
1777tmpfs 文件系统,重启数据清空/srv
服务数据存储/srv/www
, /srv/nfs
755FHS 标准服务数据存储区/sys
内核对象树/sys/devices
, /sys/bus
, /sys/class/net
动态权限 sysfs 文件系统,展示硬件拓扑/tmp
临时文件回收站/tmp/cache
, /tmp/uploads
1777 自动清理机制,建议配合 tmpreaper
目录 | 功能定位 | 典型内容示例 | 访问权限 | 特殊属性 |
/bin | 基础用户命令存放区 | ls, cp, mv, rm, chmod | 755 | 静态链接二进制文件,单用户模式可用 |
/sbin | 系统管理命令存放区 | fdisk, ifconfig, reboot, service | 750 | 仅root可执行,包含硬件管理工具 |
/boot | 系统启动关键文件存储 | vmlinuz, initrd.img, grub.cfg, System.map | 755 | 内核镜像存放区,需保留足够空间(建议≥2GB) |
/dev | 设备文件集散地 | /dev/sda1, /dev/tty, /dev/null, /dev/zero | 755 | 虚拟设备文件,按需动态创建 |
/etc | 系统配置中枢 | passwd, fstab, hosts, ssh/sshd_config | 644 | 关键配置文件集群,修改需谨慎 |
/home | 用户主目录容器 | /home/user1, /home/developer | 755 | 每个用户独立目录,支持配额管理 |
/lib | 32位系统库文件存储 | libc.so.6, libstdc++.so.6 | 755 | 静态库与内核模块存放区 |
/lib64 | 64位系统库文件存储 | libc-2.31.so, libcrypto.so.3 | 755 | 动态链接库主要存放位置 |
/media | 可移动设备自动挂载点 | /media/usb, /media/cdrom | 755 | udev自动挂载,支持热插拔 |
/mnt | 临时挂载点 | /mnt/backup, /mnt/data | 755 | 手动挂载专用,推荐使用systemd-mount |
/opt | 第三方软件安装区 | /opt/docker, /opt/postgresql | 755 | 商业软件标准安装路径,建议保持独立 |
/proc | 内核状态镜像 | /proc/cpuinfo, /proc/meminfo, /proc/net/dev | 动态权限 | 虚拟文件系统,反映实时系统状态 |
/root | 超级用户主目录 | /root/.bashrc, /root/scripts | 700 | 仅root可访问,存放敏感配置 |
/run | 运行时数据暂存区 | /run/user/1000, /run/docker.sock | 1777 | tmpfs文件系统,重启数据清空 |
/srv | 服务数据存储 | /srv/www, /srv/nfs | 755 | FHS标准服务数据存储区 |
/sys | 内核对象树 | /sys/devices, /sys/bus, /sys/class/net | 动态权限 | sysfs文件系统,展示硬件拓扑 |
/tmp | 临时文件回收站 | /tmp/cache, /tmp/uploads | 1777 | 自动清理机制,建议配合tmpreaper使用 |
使用
4.3.3. 目录树实现
目录树本质上是多叉树(N-ary Tree)的变种,每个节点包含:
- 数据域:存储名称(目录名/文件名)、元数据(权限、时间戳等)
- 子节点指针:指向直接子目录或文件
- 父节点引用:维护层级关系(非必需,常见于内存结构)
// Linux 内核目录项结构体示例
struct dentry {/* RCU lookup touched fields */unsigned int d_flags; /* protected by d_lock */seqcount_t d_seq; /* per dentry seqlock */struct hlist_bl_node d_hash; /* lookup hash list */struct dentry *d_parent; /* parent directory 父目录*/struct qstr d_name;struct inode *d_inode; /* Where the name belongs to - NULL is* negative 与该目录项关联的 inode */unsigned char d_iname[DNAME_INLINE_LEN]; /* small names 短文件名*//* Ref lookup also touches following */struct lockref d_lockref; /* per-dentry lock and refcount */const struct dentry_operations *d_op; /* 目录项操作 */struct super_block *d_sb; /* The root of the dentry tree 这个目录项所属的文件系统的超级块(目录项树的根)*/unsigned long d_time; /* used by d_revalidate 重新生效时间*/void *d_fsdata; /* fs-specific data 具体文件系统的数据 */struct list_head d_lru; /* LRU list 未使用目录以 LRU 算法链接的链表 *//** d_child and d_rcu can share memory*/union {struct list_head d_child; /* child of parent list 目录项通过这个加入到父目录的 d_subdirs 中*/struct rcu_head d_rcu;} d_u;struct list_head d_subdirs; /* our children 本目录的所有孩子目录链表头 */struct hlist_node d_alias; /* inode alias list 索引节点别名链表*/
};
一个有效的 dentry 结构必定有一个 inode 结构,这是因为一个目录项要么代表着一个文件,要么代表着一个目录,而目录实际上也是文件。所以,只要 dentry 结构是有效的,则其指针 d_inode 必定指向一个 inode 结构。但是 inode 却可以对应多个(硬链接)。
整个结构其实就是一棵树,如果看过我的设备模型 kobject 就能知道,目录其实就是文件(kobject、inode)再加上一层封装,这里所谓的封装主要就是增加两个指针,一个是指向父目录,一个是指向该目录所包含的所有文件(普通文件和目录)的链表头。
这样才能有我们的目录操作(比如回到上次目录,只需要一个指针步骤【..】,而进入子目录需要链表索引需要多个步骤)
功能特性:
- 缓存机制:LRU 算法管理活跃目录项(命中率>95%)
- 路径解析:通过父指针快速构建完整路径
- 符号链接:支持跨文件系统链接解析
4.3.4. 硬链接和软链接
传统文件系统的目录树本质上是带标签的多叉树,但随着现代文件系统通过引入共享机制,使目录树演变为有向无环图(DAG):
- 多父节点支持:文件/目录可被多个父目录引用
- 共享计数器:记录每个节点的引用数量
- 循环防范:通过拓扑排序确保无环路
多叉树适用场景
- 传统文件系统:如 ext4、XFS 的默认目录结构
- 简单存储需求:不需要跨用户共享文件的场景
- 快速遍历需求:通过父节点快速定位子节点
有向无环图适用场景
- 企业级存储系统:需要多部门共享文档的场景
- 容器化环境:Docker 镜像层间的目录共享
- 分布式系统:GlusterFS 等分布式文件系统
节点与边的数据结构定义
typedef struct dag_inode {uint64_t cid; // 内容寻址标识(基于 SHA-256 哈希值)vector_t parent_dirs; // 父目录指针数组(核心:支持多父节点)inode_meta_t meta; // 元数据(权限、时间戳等)block_ref_t data_ref; // 数据块指针(指向实际文件内容)
} dag_inode_t;
- 节点本质:每个
dag_inode_t
结构体代表一个文件或目录节点。 - 关键字段:
cid
:文件内容的唯一标识(通过哈希计算),相同内容的文件共享同一个 cid,实现数据去重。parent_dirs
:存储所有父目录的指针(传统文件系统仅支持单父目录,此设计实现多目录硬链接)。data_ref
:指向文件数据块的指针(若为目录则可能为空)。
边(隐式存在于 dir_entry_t
)
typedef struct {dag_inode_t* inode; // 指向目标节点的指针(定义边的方向)char name[256]; // 目录项名称(边的标签)
} dir_entry_t;
- 边的本质:
- 目录到文件的边:通过
dir_entry_t
表示。inode
字段指向目标节点(文件或子目录),形成有向边(目录 → 文件)。name
是边在父目录中的名称(例如libc.so
)。
- 文件到数据块的边:通过
block_ref_t
实现(图中未展开)。 - 目录间的边:通过
parent_dirs
中的指针连接(例如/home/user
指向其父目录/home
)。
- 目录到文件的边:通过
通过内容寻址建立节点间的有向关系:
- 硬链接边:共享相同数据块(
data
字段相同),多个目录项指向同一 inode(引用计数管理) - 软链接边:创建特殊节点存储目标路径解析信息
- 目录边:建立父子目录关系
1. 硬链接的 DAG 特性
- 自动去重机制:相同内容的文件在 DAG 中仅存储一次,所有硬链接指向同一 CID 节点。
- 引用计数管理:删除操作仅减少引用计数,数据节点在所有硬链接删除后才释放。
- 无路径依赖性:移动或重命名源文件不影响硬链接访问(因依赖 CID 而非路径)。
2. 软连接的 DAG 特性
- 路径解析开销:访问时需递归解析路径字符串,可能跨越多个 DAG 子图。
- 失效风险:若目标路径被删除或移动,软连接变为“死链”(Dangling Link)。
- 元数据独立:拥有独立的创建时间、权限等属性(与目标文件无关)
组件传统树结构 DAG 结构优化收益元数据存储线性 inode 表 B+树索引查询速度提升 5x 数据块引用直接/间接指针内容寻址(CID)冗余数据减少 70%
组件 | 传统树结构 | DAG结构 | 优化收益 |
元数据存储 | 线性inode表 | B+树索引 | 查询速度提升5x |
数据块引用 | 直接/间接指针 | 内容寻址(CID) | 冗余数据减少70% |
链接机制 | 仅符号链接 | 硬链接+软链接+跨卷链接 | 跨设备共享实现 |
链接机制仅符号链接硬链接+软链接+跨卷链接跨设备共享实现
4.3.5. 文件的元数据
文件元数据(Metadata)是数字世界的「身份档案」,它以结构化形式记录着文件的全部非内容属性。在 Linux 系统中,每个文件至少包含 46 项元数据字段,构成其数字身份的核心要素
// ext4 文件系统 inode 结构体(简化版)
struct ext4_inode {__le16 i_mode; // 文件类型与权限(0644)__le16 i_uid; // 所有者 ID(0=root)__le32 i_size; // 文件大小(字节)__le32 i_atime; // 访问时间戳__le32 i_mtime; // 修改时间戳__le32 i_ctime; // 状态变更时间戳__le16 i_gid; // 所属组 ID__le16 i_links_count; // 硬链接数__le32 i_blocks; // 占用数据块数__le32 i_flags; // 文件特性标志__le64 i_block[15]; // 数据块指针数组__le32 i_extra_isize; // 扩展元数据长度// ...(其他扩展字段)
};
以上为传统的元数据,现在又出现了一种很牛的设计叫 xattr。
扩展属性(xattr)是突破传统文件元数据限制的革命性设计,允许用户以键值对形式附加任意元数据到文件/目录上,突破 POSIX 标准定义的 12 个基础属性限制。其核心特性包括:
- 命名空间隔离
user
:普通用户可读写(如user.version
)security
:安全框架专用(如 SELinux 标签)system
:系统级元数据(如文件编码)trusted
:需 root 权限访问
- 跨对象支持:文件、目录、符号链接均可附加属性
- 独立演化:属性值与文件内容解耦,支持独立修改
典型场景:在 Linux ext4 中单文件 xattr 容量达 4KB,支持数万属性条目;而 macOS HFS+通过 B*树内联存储,限制单属性大小但支持无限数量
4.3.6. mount
Linux 中的 mount 操作是连接外部存储设备到文件系统的核心机制。其过程涉及用户空间命令到内核的系统调用、数据结构构建和资源管理。以下是详细解析:
一、用户空间:mount 命令的解析
当执行 mount /dev/sdb1 /mnt 时:
参数解析
-t 指定文件系统类型(如 ext4、nfs)
-o 定义挂载选项(如 ro 只读、rw 读写、noexec 禁用执行)
设备路径(如 /dev/sdb1)和挂载点(如 /mnt)
系统调用触发
命令通过 glibc 库调用内核的 sys_mount() 系统调用,传递参数到内核空间
二、内核空间:挂载的核心流程
1. 系统调用入口:sys_mount()
将用户空间参数(设备路径、挂载点、文件系统类型)复制到内核空间。
2. 文件系统类型识别
调用 get_fs_type(),根据 type 参数(如 ext4)查找已注册的 file_system_type 结构体。该结构体包含文件系统操作函数指针(如 mount 回调)。
3. 创建源文件系统:vfs_kern_mount()
分配 vfsmount**:内核创建 vfsmount 结构,记录挂载元数据(如设备名、挂载标志)。
填充超级块:调用文件系统特定的 get_sb() 函数(如 ext4_fill_super()),从设备读取超级块(super_block),初始化根目录的 dentry(目录项)和 inode(索引节点)。
4. 挂载点查找与锁定:lock_mount()
检查挂载点目录(如 /mnt)是否有效,并防止并发挂载冲突。
若挂载点已被其他文件系统覆盖(如嵌套挂载),需找到最顶层的挂载点。
5. 关联源文件系统:graft_tree()
将新创建的 vfsmount 链接到挂载点,更新全局挂载哈希表 mount_hashtable,建立父子关系。
6. 命名空间集成
将新挂载的文件系统添加到当前进程的挂载命名空间(namespace),使挂载对所有共享该命名空间的进程可见。
挂载操作的核心是 将存储设备关联到文件系统树:
- 用户命令 → 系统调用 → 内核数据结构构建(
vfsmount
、super_block
)→ 命名空间集成。 - 关键创新点:
- 统一抽象:通过 VFS 层兼容不同文件系统(ext4/NFS/tmpfs)。
- 动态扩展:支持物理设备、虚拟文件(ISO)、网络存储的透明接入。
通过 mount
,Linux 将异构存储转化为统一的文件树,这正是 “一切皆文件” 哲学的基石。
pivot_root
是 Linux 系统调用,用于将进程的根文件系统切换到新路径,同时将旧根移动到新根下的子目录。其核心目标是实现强隔离的根文件系统切换,避免传统 chroot
的挂载泄漏问题。,单纯的 chroot
仅重定向路径解析,进程仍共享主机的挂载命名空间(如 /proc
)。而 pivot_root
结合 Mount Namespace 可创建完全独立的文件系统视图--------Docker 的感觉是不是出来了
4.3.7. overlayfs
OverlayFS 是一种 联合挂载文件系统,通过叠加多个目录(称为“层”)形成单一视图,实现文件系统的动态合并与隔离。其核心结构包含三个目录:
- Lower Directory(下层)
- 只读层:存储基础文件(如容器镜像或系统固件),不可修改。
- 支持多个层级(如
lowerdir=/dir1:/dir2
),优先级从左到右递减。
- Upper Directory(上层)
- 可读写层:存储用户新增或修改的文件,所有变更在此层生效。
- Work Directory(工作目录)
- 临时目录:用于文件系统内部操作(如文件重命名时的原子操作),用户无需直接访问。
- Merged Directory(合并视图)
- 最终呈现的统一目录,合并所有层内容,用户通过此目录访问文件。
关键机制
- 写时复制(Copy-on-Write, CoW):
当修改lower
层文件时,OverlayFS 自动将其复制到upper
层再修改,保持底层只读性。 - 文件删除与隐藏:
- Whiteout 文件:删除
lower
层文件时,在upper
层创建特殊标记(字符设备文件),使文件在合并视图中“消失”。 - Opaque 目录:标记目录为不透明,隐藏下层同名目录内容。
- Whiteout 文件:删除
- 同名文件冲突处理:
优先显示upper
层文件,其次是lower
层中靠左的目录(后写入者优先)。
OverlayFS 一大应用就是 Docker 镜像层,Docker 镜像由多层只读文件(镜像层)组成,容器运行时需将这些层叠加为统一视图。联合文件系统(如 OverlayFS)依赖底层块设备接口实现分层管理:
- 分层叠加:块设备(如
/dev/loop0
)将镜像层文件虚拟化为独立的“磁盘”,供 OverlayFS 挂载为lowerdir
(只读层)和upperdir
(可写层)。 - 直接使用文件的缺陷:普通文件无法被联合文件系统直接识别为可挂载的存储单元,需转换为块设备才能被挂载。
启用写时复制(CoW)机制
- 容器内修改文件时,CoW 机制需将原始文件从只读层复制到可写层再修改。块设备提供原子化的数据块操作接口,使复制过程高效且不破坏底层镜像。
- 若直接操作文件:频繁复制大文件(如数据库)将导致 I/O 性能骤降,而块设备通过按需复制数据块(而非整个文件)显著优化性能。
4.3.8. lvm
LVM(Logical Volume Manager,逻辑卷管理器)是 Linux 环境下对物理磁盘进行抽象化管理的核心机制,通过逻辑层动态分配存储资源,解决传统分区僵化问题。
LVM 在物理磁盘和文件系统之间构建逻辑层,将存储资源池化并动态分配,其核心组件如下:
- 物理卷(PV, Physical Volume)
- 本质:物理磁盘(如
/dev/sda1
)、RAID 设备或整个硬盘。 - 管理单元:PV 被划分为 PE(Physical Extent),默认大小 4MB,是 LVM 的最小存储单元。
- 初始化命令:
pvcreate /dev/sdb1
(将分区初始化为 PV)。
- 卷组(VG, Volume Group)
- 本质:多个 PV 的存储池,整合物理资源形成统一空间(如将 50GB + 60GB 磁盘合并为 110GB 的 VG)。
- 管理机制:VG 中的 PE 可跨物理磁盘分配,打破物理边界。
- 逻辑卷(LV, Logical Volume)
- 本质:从 VG 中划分的虚拟分区(如
/dev/vg_data/lv_home
)。 - 组成单元:LV 由 LE(Logical Extent) 构成,LE 与 PE 一一对应且大小相同。
- 创建命令:
lvcreate -L 20G -n lv_data vg_data
。
- 文件系统(FS)
- 建立在 LV 之上(如 ext4/XFS),通过
mount
挂载使用。
有了 lvm,可以很轻松进行磁盘的动态管理:
// 扩容
lvextend -L +5G /dev/vg_data/lv_data # 增加 5GB 空间
resize2fs /dev/vg_data/lv_data # 调整文件系统大小(在线生效)// 缩容
umount /data # 卸载文件系统
resize2fs /dev/vg_data/lv_data 15G # 先缩小文件系统
lvreduce -L 15G /dev/vg_data/lv_data # 再缩小 LV// 快照(Snapshot)
创建 LV 的只读时间点副本,仅记录变更数据(CoW 机制)
备份数据库时创建快照,避免服务中断。
lvcreate -s -n lv_snap -L 2G /dev/vg_data/lv_data
典型应用场景
- 数据库存储:动态扩展数据库空间(如 MySQL 数据目录)。
- 云服务器:按需调整云硬盘容量,适配业务增长。
- 虚拟化平台:为虚拟机提供弹性磁盘,支持快照备份。
- 混合磁盘管理:整合 SSD(高速缓存)与 HDD(冷数据存储)
4.4. 数据库系统
4.4.1. 文件目录
文件目录其实也可以当数据库使用:
# 目录结构
db/
├── users/
│ ├── .schema # id:int, name:string
│ ├── 1.txt # {「id」:1, 「name」:「Alice」}
│ └── by_role/ # 索引目录
│ └── 3/ → ../1.txt # 按角色 ID 分类
└── orders/├── 100.txt # {「id」:100, 「user_id」:1}└── 100_user.txt → ../users/1.txt # 关联用户
表结构映射
- 目录 = 表:每个目录对应一张表(如
users/
目录即users
表)。 - 文件 = 记录:目录内的文件对应表中的一行记录(如
users/1.txt
表示id=1
的用户)。 - 文件内容 = 字段值:文件内容以键值对或 JSON 格式存储字段数据,例如:
// users/1.txt
{「id」:1, 「name」:「Alice」, 「role_id」:3}
软链接实现表关联:
一对一双向软链接互指 profile/1.txt → users/1.txt
(反之亦然)一对多在“多”方目录创建指向“一”方的软链接 orders/100.txt → users/1.txt
多对多建立中间目录存储关联对
一对一 | 双向软链接互指 | profile/1.txt → users/1.txt(反之亦然) |
一对多 | 在“多”方目录创建指向“一”方的软链接 | orders/100.txt → users/1.txt |
多对多 | 建立中间目录存储关联对 | user_role/1_3.txt(软链接至 users/1 和 roles/3) |
user_role/1_3.txt
(软链接至 users/1
和 roles/3
)
查询操作的实现
- 条件查询
grep
模拟 WHERE:
grep -l 『「role_id」:3』 users/*.txt # 查找角色 ID 为 3 的用户
find
模拟 JOIN:
find orders/ -lname 『*users/1.txt』 # 查找用户 1 的所有订单
- 聚合计算,使用
awk
统计:
awk -F: 『{sum += $2} END {print sum}』 orders/*.txt # 计算订单总金额
4.4.2. SQL
关系型数据库通过表结构和指针机制重构目录树逻辑:
- Everything is Table
- 对象表化:每个实体(用户、订单)对应一张表,每行代表一个对象实例
- ID 即指针:外键(如
user_id
)是跨表索引的内存地址映射
SELECT orders.* FROM users
JOIN orders ON users.id = orders.user_id -- ID 的指针跳转
WHERE users.name = 『Alice』;
SQL 本质:JOIN 操作即指针配对(users.id
→ orders.user_id
)
事务与 ACID:指针操作的原子保障
ACID 确保指针跳转的可靠性:
ACID 属性指针操作意义实现机制原子性指针批量更新要么全成功要么全失败 Undo Log 回滚一致性指针始终指向有效内存地址外键约束+锁机制隔离性并发指针互不干扰 MVCC 多版本控制持久性指针变更永久有效 Redo Log
ACID属性 | 指针操作意义 | 实现机制 |
原子性 | 指针批量更新要么全成功要么全失败 | Undo Log回滚 |
一致性 | 指针始终指向有效内存地址 | 外键约束+锁机制 |
隔离性 | 并发指针互不干扰 | MVCC多版本控制 |
持久性 | 指针变更永久有效 | Redo Log持久化 |
持久化
START TRANSACTION;
UPDATE accounts SET balance = balance - 100 WHERE id = 1; -- 指针 A
UPDATE accounts SET balance = balance + 100 WHERE id = 2; -- 指针 B
COMMIT; -- 原子提交两个指针更新
海量优化:当关系模型遇到性能瓶颈
随着数据量增长,传统关系模型需针对性优化:
- 查询优化策略
- 索引即快捷指针:B+树索引将全表扫描 O(n)降至 O(log n)
- 覆盖索引:在索引中内联数据,避免回表查询(如
SELECT id,name FROM users
)
游标:精细化指针控制器
游标(Cursor)实现逐行精准控制,避免内存溢出
DECLARE user_cursor CURSOR FOR
SELECT id FROM users WHERE status = 『active』; -- 定义结果集指针
OPEN user_cursor;
FETCH NEXT FROM user_cursor; -- 指针向前移动
WHILE @@FETCH_STATUS = 0 DO-- 处理当前行数据FETCH NEXT FROM user_cursor;
END WHILE;
CLOSE user_cursor;
- 分布式改造方案
优化方向实现方式典型案例水平分片按 ID 哈希分布到不同物理节点 MySQL 分库分表读写分离写主库→读从库(异步指针同步)AWS RDS Proxy 列式存储按列组织数据,提升聚合查询速度
优化方向 | 实现方式 | 典型案例 |
水平分片 | 按ID哈希分布到不同物理节点 | MySQL分库分表 |
读写分离 | 写主库→读从库(异步指针同步) | AWS RDS Proxy |
列式存储 | 按列组织数据,提升聚合查询速度 | Google BigQuery |
Google BigQuery
4.4.3. NoSQL
关系型数据库的瓶颈无法满足 Web 2.0 需求
- 海量数据处理能力不足
互联网应用(如社交网络、电商平台)产生的数据量呈指数级增长,关系型数据库在单机或集群模式下难以高效存储和查询 PB 级数据。例如,频繁的多表关联查询或复杂事务操作在高并发场景下性能急剧下降 - 扩展性受限
关系型数据库依赖纵向扩展(升级硬件),但单台服务器的性能存在物理上限。横向扩展(增加节点)则因事务一致性(ACID)和复杂关联查询的约束难以实现,导致成本高昂且运维复杂 - 高并发读写延迟
如电商秒杀或社交平台热点事件,关系型数据库的锁机制和事务管理易引发死锁,导致响应延迟,无法支撑百万级并发请求
数据模型的革新需求
- 非结构化/半结构化数据的爆发
用户生成内容(图片、日志、地理位置)、物联网传感器数据等多为动态、无固定结构。关系型数据库的严格模式(Schema)需预先定义表结构,无法灵活适应数据类型的变化 - 灵活性与开发效率
NoSQL 的文档型(如 MongoDB)、键值对(如 Redis)等数据模型允许动态增减字段,无需停机修改表结构,加速了迭代开发周期
当数据模型突破二维结构,NoSQL 提供新思路:
- 数据模型对比
类型数据结构指针逻辑适用场景键值数据库 Key-ValueKey 即数据地址指针缓存(Redis)文档数据库 JSON
类型 | 数据结构 | 指针逻辑 | 适用场景 |
键值数据库 | Key-Value | Key即数据地址指针 | 缓存(Redis) |
文档数据库 | JSON文档 | 文档内嵌指针(_id) | 内容管理(MongoDB) |
列族数据库 | 列簇+行键 | 行键定位列簇指针 | 日志分析(HBase) |
图数据库 | 节点+边 | 边即关系指针 | 社交网络(Neo4j) |
文档文档内嵌指针(_id)内容管理(MongoDB)列族数据库列簇+行键行键定位列簇指针日志分析(HBase)图数据库节点+边边即关系指针社交网络(Neo4j)
- ACID 与 BASE 的权衡
- 关系型数据库:强一致性保障指针精确(ACID)
- NoSQL:最终一致性换取吞吐量(BASE 原则)
BASE: Basically Available(基本可用)Soft-state(软状态)Eventually consistent(最终一致)
例:微信朋友圈使用 HBase 实现写扩散,异步同步到好友时间线
4.5. 计算机系统安全
4.5.1. 访问控制
UID(用户标识符)
- UID 是内核中用户的唯一整数标识(0~65535),用于判定进程对文件、设备等资源的访问权限。
- 根用户(UID 0):拥有系统最高权限,可绕过所有权限检查。
- 系统用户(UID 1–499):供守护进程使用,权限受限。
- 普通用户(UID ≥500):常规用户权限范围。
- 生成方式:
- 本地系统:通过
/etc/passwd
静态分配,或 LDAP 动态映射。 - 分布式系统:采用 Snowflake、号段模式、Redis 自增等算法生成全局唯一 UID
- 本地系统:通过
GID(组标识符)
- 定义与作用:
将多个 UID 归为一组,统一分配权限(如共享目录的读写权限)。 - 附属组:
用户可属于多个组(通过/etc/group
配置),进程的权限取所有所属组的并集
Linux 进程实际运行时涉及四类身份标识:
标识类型缩写作用实际用户 IDruid 进程启动者的原始 UID 有效用户 IDeuid 权限检查的依据,决定文件访问能力保存用户 IDsuid 临时存储 euid,用于权限恢复文件系统用户 IDfuid
标识类型 | 缩写 | 作用 |
实际用户ID | ruid | 进程启动者的原始 UID |
有效用户ID | euid | 权限检查的依据,决定文件访问能力 |
保存用户ID | suid | 临时存储 euid,用于权限恢复 |
文件系统用户ID | fuid | 文件操作时的最终权限标识(通常等于 euid) |
文件操作时的最终权限标识(通常等于 euid)
当进程尝试访问文件时,内核比较 euid 和文件属主的 UID,并检查文件模式(mode)中的权限位
文件模式(mode):权限规则的载体
文件模式是一个 12 位整数(如 0o40755
),分为两部分:
- 基础权限(9 位):
rwxr-xr-x
:分别定义属主、属组、其他用户的读/写/执行权限。
- 特殊权限位(3 位):
- SetUID(4):以文件属主身份运行进程(euid = 文件属主 UID)。
- SetGID(2):对文件以属组身份运行;对目录则新建文件继承目录属组。
- Sticky Bit(1):目录内文件仅属主可删除(如
/tmp
)
chmod u+s /usr/bin/passwd 此时普通用户执行 passwd 时,进程 euid 临时变为 0(root),从而修改 /etc/shadow
程序权限需求 SetUID 作用 passwd
修改 /etc/shadow 临时赋予 root 写权限 ping
发送 ICMP 包获取 RAW Socket 权限 sudo
程序 | 权限需求 | SetUID 作用 |
passwd | 修改 /etc/shadow | 临时赋予 root 写权限 |
ping | 发送 ICMP 包 | 获取 RAW Socket 权限 |
sudo | 切换用户 | 以目标用户身份执行命令 |
切换用户以目标用户身份执行命令
4.5.2. 攻破一个进程
缓冲区溢出
高地址
┌─────────────────┐
│ 参数 │ ← 调用者压入的参数(若参数过多)
├─────────────────┤
│ 返回地址(EIP)│ ← 函数执行完后跳转的位置
├─────────────────┤
│ 旧 EBP │ ← 保存调用者的栈帧基址
├─────────────────┤
│ 局部变量 │ ← 当前函数的变量(如 buffer[64])
└─────────────────┘
低地址(栈顶) → ESPESP(栈指针):始终指向当前栈顶(最低地址)
EBP(基址指针):指向当前栈帧的基地址,用于定位参数和局部变量(如[EBP-4]表示第一个局部变量)
EIP(指令指针):存储下一条待执行指令的地址,被覆盖后程序将跳转到攻击者指定的地址#include <stdio.h>
#include <string.h>void vulnerable() {char buffer[64]; // 局部变量,位于栈帧底部gets(buffer); // 无边界检查的输入函数
}int main() {vulnerable();return 0;
}
溢出过程分析
- 栈帧初始化:
- 调用
vulnerable()
时,栈中压入main
的返回地址(EIP)和旧 EBP。 - 局部变量
buffer
分配在旧 EBP 下方(如地址0xbffff2c0
)。
- 输入超量数据:
- 若用户输入 70 字节(如
「A」*68 + 「\xef\xbe\xad\xde」
):- 前 64 字节填满
buffer
。 - 后续 4 字节覆盖旧 EBP(破坏栈帧链)。
- 最后 4 字节覆盖返回地址(EIP)(如
0xdeadbeef
)。
- 前 64 字节填满
- 函数返回时的劫持:
vulnerable()
执行RET
指令时,从栈顶弹出覆盖后的 EIP 到指令寄存器。- CPU 跳转到
0xdeadbeef
执行(可能是攻击代码入口)。
攻击者如何利用此漏洞
攻击载荷构造
plaintext复制| 填充数据(64 字节) | 覆盖的 EBP(4 字节) | 恶意代码地址(4 字节) | Shellcode(恶意指令) |
- Shellcode:一段机器码(如启动
/bin/sh
),通常注入到buffer
起始位置。 - 覆盖的 EIP:指向
buffer
起始地址(需预测地址,或通过 NOP 雪橙增加命中率)。
实际攻击步骤
- 定位偏移量:
- 通过调试器(如 GDB)确定
buffer
到 EIP 的偏移(如 72 字节)。
- 绕过保护机制:
- 若系统启用 ASLR,需结合内存泄露漏洞获取地址。
- 若启用栈不可执行(NX),需改用 ROP 链(复用已有代码片段)
防御机制与最佳实践
编程层防护
- 替换危险函数:
- 用
fgets(buffer, sizeof(buffer), stdin)
替代gets()
,限制输入长度。
- 用
- 编译器增强:
- GCC 的
-fstack-protector
:插入金丝雀值(Canary)于 EBP 和局部变量之间,函数返回前校验其完整性。
- GCC 的
操作系统级防护
机制原理效果 ASLR 随机化栈/堆/库的基址,增加预测 EIP 的难度迫使攻击者需地址泄露漏洞 DEP/NX 标记栈内存为“不可执行”,阻止 Shellcode 运行需结合 ROP 绕过 KPTI 隔离用户/内核页表,阻止 Meltdown
机制 | 原理 | 效果 |
ASLR | 随机化栈/堆/库的基址,增加预测EIP的难度 | 迫使攻击者需地址泄露漏洞 |
DEP/NX | 标记栈内存为“不可执行”,阻止Shellcode运行 | 需结合ROP绕过 |
KPTI | 隔离用户/内核页表,阻止Meltdown类攻击窃取内核数据 | 缓解旁路攻击 |
类攻击窃取内核数据缓解旁路攻击
硬件漏洞 meltdown
利用 CPU 乱序执行和推测执行的优化缺陷:
- 权限绕过:用户态程序访问内核内存(如
0xffffffffc0000000
)触发异常,但乱序执行已读取数据。 - 旁路泄露:通过缓存时序攻击(Cache Timing)推断内核数据值。例如,根据数组
array[data*4096]
的加载时间差异泄露字节内容
KPTI(内核页表隔离)防御机制
- 地址空间分割:
- 旧模式:用户态与内核态共享页表,用户可映射内核地址。
- KPTI 模式:用户进程维护两份页表:
- 用户页表:仅含用户空间地址,无内核映射。
- 内核页表:完整映射内核,仅在陷入内核时切换。
- 性能代价:每次系统调用/中断需切换页表(TLB 刷新),导致性能下降 10–20%。
4.6. 虚拟机,容器,微服务
之前 nemu 运行程序只有原来本机性能的 1/10,vmware 做到了什么?使虚拟机成为了商用的产品,即 1 台 pc 可以虚拟化出 100 台卖给你,ISP 厂商狂喜。
guest ring3 -> host ring3
- 指令捕获
- Guest OS 的内核代码(原需 Ring0)被置于 Host Ring3 执行。
- 当 Guest 执行敏感指令时,CPU 触发异常(如
#GP
),控制权移交 VMware Hypervisor。
- 指令翻译与模拟
- Hypervisor 截获异常,解析触发异常的指令。
- 通过二进制翻译引擎动态重组代码块:
- 将敏感指令(如
IN/OUT
端口操作)替换为调用 Hypervisor 模拟函数的跳转指令。 - 生成安全的“重组指令块”(Translated Code Block),在 Host Ring3 执行模拟逻辑。
- 将敏感指令(如
- 执行环境隔离
- Guest Ring3 应用:直接运行于 Host Ring3(无需翻译),性能接近原生。
- Guest Ring0 代码:翻译后在 Host Ring3 模拟执行,实现“Ring0 行为”的软模拟
x86 硬件虚拟化,8 级页表
影子页表(Shadow Page Table)的复杂化处理
- 传统挑战:
在 4 级页表时代,VMware 的影子页表技术已需维护两套页表(Guest 页表 + 影子页表),并通过缺页异常同步映射关系。8 级页表使页表项数量指数级增长,同步开销剧增。 - VMware 的优化:
- 选择性同步机制:仅监控 Guest 修改页表的行为(如
mov cr3
指令),而非全量同步,减少 VM Exit 次数。 - 跳转缓存(Jump Cache):缓存已翻译的页表项,避免重复处理相同路径的页表遍历。
- 选择性同步机制:仅监控 Guest 修改页表的行为(如
硬件辅助内存虚拟化(EPT/NPT)的深度整合
- 技术原理:
Intel EPT(Extended Page Tables)和 AMD NPT(Nested Page Tables)允许 CPU 硬件直接处理“Guest 虚拟地址 → Host 物理地址”的两级转换,无需 Hypervisor 介入。 - VMware 的创新应用:
- 动态切换机制:默认启用 EPT/NPT,但在嵌套虚拟化或特殊指令(如
invvpid
)场景下动态切换回影子页表,兼顾性能与兼容性。 - 大页(Huge Page)支持:通过 EPT/NPT 直接映射 2MB/1GB 大页,减少 8 级页表的遍历层级,降低 TLB 未命中率
- 动态切换机制:默认启用 EPT/NPT,但在嵌套虚拟化或特殊指令(如
操作系统虚拟化---namespace+cggroups - Docker
云时代-微服务-- k8s
serverless--容器的概念也不要了-faas
oversubscribed -》 流量计费 qps
cicd
5. Reference
Model checking 简述
操作系统原理 (2025 春季学期)
HTTPS://jyywiki.cn/OS/manuals/sysv-abi.pdf
HTTPS://www.cnblogs.com/yubo-guan/p/18804432
HTTPS://www.cnblogs.com/sparkdev/p/11605804.HTML
深入了解之链接器与加载器_加载器_邱学喆_InfoQ 写作社区
HTTPS://book.douban.com/subject/3652388/、
HTTPS://www.cnblogs.com/yikoulinux/p/14470713.HTML