计算机系统纵深笔记:从半导体物理到 AI Infra

9 阅读40分钟

一、体系结构演变与性能瓶颈

现代计算系统的性能瓶颈已从计算能力转向数据搬运。理解这一范式转移,需要从体系结构的演变出发,分析性能的本质约束。

1.1 核心瓶颈:内存墙

能耗真相:以 Horowitz 2014 广为引用的 45nm 数据为参考,一次 FP32 加法运算能耗约 0.9 pJ,一次 L1 缓存命中约 0.5-1 pJ,而一次 DRAM 读取能耗约 100-500 pJ。具体数值随工艺节点变化,但数量级差异保持稳定——DRAM 读取能耗比浮点运算高两个数量级以上

缓存命中的核心价值:数据访问完全发生在片上 SRAM 中,直接省掉了 DRAM 内部的行激活和预充电操作。单次读操作时,一整行(例如 8KB)的所有单元电荷全部被泄放到位线上,感测放大器需要为这数千条位线的寄生电容(每条约 0.1-0.2 pF)进行充放电,将微弱的模拟信号(几十毫伏)放大到数字电平(约 1V)。这个过程消耗的能量,远比某个微小单元电容自身的充放电大得多。

本质:内存绝大多数能量,都花在了"为了读出一个小小的信号,而不得不驱动一整行位线和复杂的外围电路"这件事上。这也正是 PIM(存内计算)试图在数据所在位置直接完成计算、从而避免这种巨大搬运开销的物理根源。

延迟鸿沟:DRAM 核心时序(tRCD + CL + tRP)约 40-50ns,在 4GHz CPU 上约合 165 个时钟周期。但实际端到端负载延迟(从 CPU 发出地址到数据返回,含内存控制器排队、PHY 延迟、总线传输)通常在数十到上百纳秒量级。而纯 ALU 加法仅需 1 个周期。

带宽的"虚假宣传" :理论带宽由总线位宽、传输频率和通道数决定;实际有效带宽在随机访问模式下可能跌至理论值的 10-30%,根本原因在于行激活和预充电的物理开销。

内存资源分配失衡的量化推导:处理器核心数量大约每 2 年翻倍,DRAM 容量大约每 3 年翻倍,二者增速严重不匹配。

设核心数 C(t)=C02t/2C(t) = C_0 \cdot 2^{t/2},内存容量 M(t)=M02t/3M(t) = M_0 \cdot 2^{t/3},

则单核心可均分内存 S(t)=S02t/6S(t) = S_0 \cdot 2^{-t/6},

两年后 S(2)0.7937S0S(2) \approx 0.7937 S_0,只剩原来的 79.37% ,下降约 20%。这种增速剪刀差与上层软件日益增长的内存消耗需求形成尖锐矛盾。


1.2 DRAM 标准运行流程

DRAM 一次完整读操作的五个步骤及物理过程:

  1. 预充电:将灵敏放大器两端电压统一调整至基准参考电压 VDD/2(注:预充电调节的是位线电压,而非存储单元电容电压)
  2. 行激活:导通字线,存储单元电荷发生共享,扰动位线电压(从 VDD/2 偏移几十毫伏)
  3. 信号放大:灵敏放大器正反馈锁存差分信号,自动通过位线恢复存储电容内电荷
  4. 列读取:从行缓冲区选取指定列数据,向外输出至 DRAM 芯片外部
  5. 预充电复位:关闭已激活行,复位位线电压至 VDD/2,等待下次访问

上述每个步骤之间都有严格的最小时间间隔(tRP、tRCD、CL、tRAS),这些时序参数是理解 DRAM 延迟构成的基础。

1.3 优化的层次

性能优化是分层的系统工程:

  • 算法层:选择更高效的算法和数据结构(如 O(n log n) vs O(n²))
  • 物理层:理解底层硬件特性(缓存行为、流水线、SIMD)来指导代码编写
  • 系统层:操作系统调度、I/O 模型、内存管理策略的协同

1.4 总线与互联

总线是连接处理器、内存和 I/O 设备的关键通道。DDR5 单通道位宽 64bit,传输速率可达 5600MT/s。现代系统采用多通道架构提升总带宽,GPU 更是通过 HBM 的 5120bit 超宽总线实现 TB/s 级带宽。

1.5 多 Bank 并行调度

DRAM 内部由多个独立的 Bank(存储体)组成,每个 Bank 拥有独立的行列译码器、位线和灵敏放大器。内存控制器可同时对不同 Bank 进行并发调度,极大提升吞吐量。Bank 并行调度的三层分工:

  • 用户态(应用程序) :只操作虚拟地址,没有任何指令或 API 可以指定数据存到几号 Bank、手动命令某个 Bank 做激活/预充电。
  • 内核态 + MMU:操作系统内核负责虚拟地址→物理地址映射,自动把不同进程、不同大内存块打散分配到不同 Bank。
  • 硬件层(内存控制器) :最终并行执行者,自动给不同 Bank 派发命令、错开访存流程、流水线调度,最大化利用 Bank 并行带宽。Bank 并行是底层软硬件全自动优化,用户层只能顺势利用(通过连续访问、按通道对齐分配等模式),不能主动操控调度。

1.6 延迟掩盖技术

从 CPU 到 GPU,硬件发展出一系列延迟掩盖技术:

  • 缓存层次:L1 → L2 → L3 → DRAM 的多级缓存
  • 乱序执行:CPU 在等待访存时执行其他无关指令
  • 多线程切换:GPU 通过零开销线程切换隐藏访存延迟(单线程阻塞时立即切换)
  • 预取:硬件和软件预取提前将数据调入缓存

1.7 DRAM 刷新机制与优化研究

DRAM 存储单元电荷按 RC 指数规律衰减。在 JEDEC 规范要求的 85°C 高温下,少数工艺弱势单元的保持时间急剧缩短。64ms 是基于最弱单元在最恶劣条件下的安全阈值,但绝大多数单元的实际保持时间远超此值,部分可达数秒。一刀切 64ms 全局刷新造成极大资源冗余。

学界已提出多种差异化刷新方案:

  • AVATAR 方案:所有存储行初始采用低频刷新,每 15 分钟检测留存错误,出现错误则提升该行刷新频率。长期运行一年可降低 60%  刷新频次;每年执行一次留存测试重置规则可将刷新缩减率提升至 70% 。运维要求:每年需重置刷新分级规则,适配 VRT(可变留存时间)长期随机变化。
  • REACH Profiler 高速检测方案:基于 368 款移动端 LPDDR4 内存芯片实测。核心思路:借助高温环境加速留存错误产生,大幅缩短硬件留存时长检测时间。实测效果:实现 99% 错误覆盖率50% 误判率前提下,检测速度提升 2.5 倍
  • 片上 ECC 内存的检测难题:片上 ECC 会自动修正基础留存错误,向内存控制器屏蔽真实硬件故障信息,导致留存时长检测无法获取原始精准错误数据。

二、汇编与栈帧

2.1 x86-64 调用约定

寄存器传参:前六个整数或指针参数通过 %rdi, %rsi, %rdx, %rcx, %r8, %r9 传递。返回值放在 %rax。超过六个的参数通过栈传递。

寄存器保存约定

  • 调用者保存:%rax, %rdi, %rsi, %rdx, %rcx, %r8, %r9, %r10, %r11——函数内可自由修改,调用者如需保留应自行保存
  • 被调用者保存:%rbx, %rbp, %r12-%r15——若函数要使用,必须在修改前压栈,返回前恢复

栈帧%rsp 始终指向栈顶,栈向低地址增长。分配空间时 %rsp 减小(subq),释放时增大(addq)。现代 GCC 通常不将 %rbp 作为帧指针,而是当作普通被调用者保存寄存器使用。

2.2 参数传递的三种方式

  1. 寄存器传参:少量参数优先,速度最快
  2. 栈传参:当参数数量超出寄存器传参上限时,ABI 规定多余的参数通过栈传递,编译器在栈上为这些参数分配连续空间并传递对应地址
  3. 参数表传参:将参数组织为内存中的列表,通过寄存器或栈传递指针。当参数较多或逻辑上属于同一对象时,程序员或 ABI 可能采用结构体/参数表方式传递

2.3 对齐要求

字、双字、四字的自然边界分别为 2、4、8 字节的整数倍地址。未对齐访问可能导致额外访存开销或流水线处理开销——如果数据跨越 cache line 边界或页边界,延迟显著增加;如果不跨边界,现代 x86 处理器通常可以在单次 cache line 访问中完成,不严格表现为"两次内存访问"。部分操作双四字(128 位)的指令要求自然边界对齐(16 字节),否则生成通用保护异常(#GP)。


三、浮点数与数值表示

3.1 IEEE 754 双精度

符号位(1) + 阶码(11) + 尾数(52)。规格化数含隐含前导 1,等效 53 位二进制有效位,约 15-17 位十进制精度。真值:v=(1)S×(1.M)2×2E1023v = (-1)^S \times (1.M)_2 \times 2^{E-1023}

3.2 精度丢失的根源

  1. 进制转换:大量十进制小数(如 0.1)是二进制无限循环小数
  2. 对阶丢失:大数+小数时小数尾数右移超出位宽被吞没
  3. 灾难性抵消:两个相近大数相减放大舍入误差
  4. 误差累积:多步运算中舍入误差逐步叠加

3.3 高精度方案

  • 算法优化(首选,零成本):重构公式规避相近数相减
  • 十进制类型(Java BigDecimal、Python decimal):适配金融场景,规避进制转换误差
  • 任意精度库(GMP/MPFR):科学超高精度计算
  • 80 位扩展精度(long double):仅延后误差出现,无法根治

四、流水线与指令执行

4.1 流水线基本原理

处理器执行一条指令分为多个阶段(取指、译码、执行、访存、写回),不同指令的不同阶段可在同一时钟周期内重叠执行。现代 CPU 流水线深度通常为 14-19 级。

4.2 流水线冒险

  • 结构冒险:多个指令争用同一硬件资源
  • 数据冒险:后续指令依赖前一条指令的结果。通过转发(旁路)  解决大多数 RAW(读后写)冒险
  • 控制冒险:分支指令导致流水线中已取指令作废。Load-Use 冒险是一种特殊的数据冒险——在经典五级流水线模型中,Load 指令的结果在下一周期才能使用,若下一条指令立即使用该结果,即使转发也无法消除这一周期的停顿。在现代乱序执行 CPU 中,Load 指令的结果往往比普通 ALU 指令更晚就绪,因此紧随其后的依赖指令可能需要等待

4.3 分支预测与投机执行

现代 CPU 在遇到条件跳转时不会停止流水线,而是预测分支去向并继续投机执行。预测正确则性能无损;预测失败则丢弃所有投机结果并回滚,惩罚通常为 15-20 个时钟周期。若错误路径触发了额外的缓存未命中或占用了 MSHR(Miss Status Holding Register)资源,可能间接影响正确路径的访存延迟,扩大恢复成本。

条件移动指令(CMOV)  消除了控制流分叉,将条件选择转化为数据依赖——编译器会确保两个方向的值在执行 CMOV 前均已就绪。本质上是用冗余计算换取确定性,避免了分支预测失败的惩罚。

4.4 微指令与微架构

x86 的复杂指令(CISC)在 CPU 内部被译码为更简单的微指令(μops)。微指令直接控制执行单元,是理解现代 CPU 微架构的关键抽象。超标量处理器每周期可发射多条微指令到不同的功能单元。


五、缓存系统

5.1 缓存组织结构

所有现代 CPU 缓存遵循统一的数学模型:

  • 参数 S、E、B:S 个集合,每集合 E 行(相联度),每行 B 字节数据块
  • 总容量:C = S × E × B(仅数据,不含标记位和有效位开销)
  • 地址划分:标记位(高位)| 集合索引(中间位)| 块内偏移(低位)
  • 三种类型:直接映射(E=1)、E 路组相联(E>1)、全相联(S=1)

5.2 缓存未命中的三种类型

  • 冷不命中(Compulsory Miss) :首次访问该数据块
  • 容量不命中(Capacity Miss) :工作集超过缓存容量
  • 冲突不命中(Conflict Miss) :不同数据映射到同一缓存集合,即使缓存整体仍有空间也会发生。增加相联度可有效缓解

5.3 写策略

  • 写命中:写直达(同时写缓存和内存,简单但慢)和写回(仅写缓存,标记脏位,替换时写回内存,主流方案)
  • 写未命中:写分配(先加载块再写,配合写回)和非写分配(直接写内存,配合写直达)

5.4 真实多级缓存系统(以 Intel Core i7 Haswell 为例)

  • 每核私有 L1:32KB,8 路组相联,分离 i-cache 和 d-cache,延迟约 4 个时钟周期
  • 每核私有 L2:256KB,8 路组相联,统一缓存,延迟约 10 个时钟周期
  • 所有核共享 L3:8MB,16 路组相联,延迟约 30-50 个时钟周期

5.5 缓存一致性协议(MESI)

多核处理器中,各核心持有私有缓存,必须通过缓存一致性协议保证所有核心看到的数据一致。现代 x86-64 使用 MESI 协议,每个缓存行标记为四种状态之一:

  • Modified(已修改) :数据仅在本核心缓存中有效,且已被修改(脏),与内存不一致。其他核心持有该行的副本必须为 Invalid。
  • Exclusive(独占) :数据仅在本核心缓存中有效,且与内存一致(干净)。
  • Shared(共享) :数据可能存在于多个核心的缓存中,且与内存一致。
  • Invalid(无效) :该缓存行不包含有效数据。

状态转换逻辑

  • 读操作:若某核心持有 Modified 副本,需提供最新数据(通常通过缓存间直接传输,即 cache-to-cache transfer),双方转入 Shared 状态,不一定先写回内存。仅当该行被替换出缓存时才写回内存。
  • 写操作:核心必须首先获得 Exclusive 权(使其他核心副本全部 Invalid),然后执行写入并进入 Modified 状态。

实现方式:小规模多核系统(如单 socket 处理器)常采用总线窥探(Bus Snooping) ——每个核心监控总线上的地址请求,根据自身缓存状态做出响应。大规模系统(多 socket、几十核以上)则常使用目录协议(Directory-based Coherence) ,通过分布式目录记录每个缓存行的共享者信息,避免广播带来的带宽压力。

5.6 缓存的数学基础

缓存效率分析的严谨性依赖于对局部性原理的数学刻画:

  • 布尔代数:地址解码与标记比较的硬件逻辑基础
  • 信息论:缓存容量与缺失率的理论界限
  • 组合数学:替换策略(LRU、PLRU)的建模与分析

5.7 存储器山(Memory Mountain)

通过扫描不同大小和步长的数组并测量吞吐量,绘制出三维曲面图——存储器山。

  • 空间局部性:步长越小,吞吐量越高。步长超过一个缓存块大小时,空间局部性收益为零
  • 时间局部性:数组越小,越能放入上层高速缓存,性能越高
  • 核心启示:同一程序在同一台机器上,仅因访存模式不同,性能差异可达一个数量级(10 倍以上)

矩阵乘法的定量优化:通过分析和重排矩阵乘法的三重循环,可以直接量化局部性带来的提升。改善空间局部性——调整循环顺序(如 i,j,k → k,i,j)让内层循环步长为 1,可将每周期迭代所需的未命中数从 1.25 次降到 0.5 次。改善时间局部性——对矩阵分块,使块内数据在被替换前充分复用,未命中次数从约 (9/8)n3 (9/8)n^3 降至 (1/(4B))n3(1/(4B))n^3(B 为块大小)。


六、虚拟内存

6.1 核心抽象

虚拟内存本质是对物理内存的虚拟化:通过 MMU 硬件拦截每次内存访问,将虚拟地址翻译为物理地址。三大设计目标:充当缓存、简化内存管理、提供访问保护。

6.2 页表结构

  • 页大小:典型 4KB,x86-64 也支持 2MB/1GB 巨页
  • PTE 结构:包含有效位、物理页号、权限位(R/W、U/S、XD)、脏位、访问位
  • 多级页表:传统 x86-64 常使用四级页表,新处理器已支持五级页表(LA57,57 位虚拟地址)。48 位 VA 中 VPN 分为 4 个 9 位字段,每字段索引 512 个条目。前三级 PTE 指向下一级页表基址,第四级指向最终物理页框

页表遍历的硬件机制:MMU 遍历多级页表的过程由硬件状态机自动完成,对操作系统内核和执行的指令完全透明。只有当发生 TLB 不命中且页表项无效(触发缺页故障)或权限检查失败时,才会以异常形式陷入内核。

6.3 VMA 与页表的耦合关系

VMA(虚拟内存区域)  是内核为每个进程维护的元数据结构(vm_area_struct),描述一段连续的虚拟地址空间的权限、映射类型等属性。它与页表的本质区别在于:

  • VMA:是一种"懒"数据结构,定义"合法访问区间与规则"。负责权限和映射域管理,不直接记录物理页框映射。
  • 页表(PTE) :完成"虚拟页 → 物理页"的精确映射。缺页发生时,操作系统正是通过查找 VMA 来确定这次访问是否合法、应该按什么方式调入页面。

这种分离设计的优势是:VMA 维护连续的抽象区间(如代码段、堆、栈),而页表按需分配物理页,两者协同实现了高效的稀疏地址空间管理。

6.4 地址翻译与 TLB

  • TLB 加速:MMU 内部的硬件缓存,缓存近期 PTE。L1 TLB 命中通常只需 1-2 个时钟周期,L2 TLB 命中约 7-20 个周期,相比页表遍历(需多次访问主存)开销极小。
  • 并行访问优化:Intel 利用 VPO 和 PPO 位完全一致的性质,使 L1 d-cache 的组选择和行读取与地址翻译并行进行。但这也制约了 L1 大小:总容量不能超过页大小 × 相联度,因此在 4KB 页下 L1 仅为 32KB
  • L1 Cache 受限的物理根源:组索引与块内偏移位必须全部落在页内偏移(VPO)范围内,因此(总大小 / 相联度 / 块大小)必须在硬约束内

6.5 缺页异常处理

当 PTE 指示页面不在内存时,触发缺页异常,由 OS 的页故障处理程序负责:

  1. 检查 VMA 确认访问合法性,非法则发 SIGSEGV
  2. 合法则选定牺牲页(若脏则写回磁盘),从磁盘读入新页
  3. 更新 PTE,返回并重新执行故障指令

在磁盘 I/O 等待期间,操作系统会调度另一个就绪进程运行,而非让 CPU 空转。部分系统会同时采用预调页策略,利用空间局部性提前调入相邻页面以降低后续缺页率。

TLB 未命中的定量代价:以 DDR4-3200(tCK=0.625nst_{CK}=0.625\text{ns}CL=22CL=22, tRCD=22tRCD=22, tRP=22tRP=22, 4GHz CPU)为例:

访问场景内存访问次数最坏延迟
TLB 命中 + 行命中1 次~55 周期
TLB 命中 + 行未命中1 次~110 周期
TLB 未命中 + 行未命中5 次(4 次页表 + 1 次数据)~550 周期
纯 ALU 加法01 周期

2MB 大页让 PT 层直接省略(只剩 3 级页表),每减少一级页表就少一次内存访问(省约 110 周期)。AI 推理服务必须禁用 Swap——如果缺页涉及磁盘 IO(毫秒级),延迟从 550 个周期暴涨到 5-10ms(数千万个周期),尾部延迟直接崩溃。

6.6 写时复制(COW)

fork 创建子进程时,内核仅复制页表和 VMA 元数据,将所有可写页标记为只读。任一进程写入时触发保护异常,内核识别 COW 页面后才分配新物理页并复制。这是 fork 高效的根本原因——仅在真正修改时才付出拷贝代价。

fork 后紧跟 execve 的模式中,COW 机制实际未发生作用:因为 execve 会立即废弃子进程的整个地址空间,使 fork 时创建的只读页面从未被真正写入。

6.7 内存映射

  • mmap 允许用户态建立文件到地址空间的映射
  • 两类文件对象:磁盘普通文件(代码段、数据段)和匿名文件(bss、堆、栈,首次访问时分配零页)
  • 核心应用:mmap 可避免用户态与内核态之间的显式数据拷贝(用户态直接读内核 page cache),但从 page cache 到 socket buffer 的复制仍然存在。真正的零拷贝通常由 sendfilesplice 或 io_uring 的 zerocopy 机制实现

七、异常控制流

7.1 异常的分类

类别产生原因行为举例
中断外部 I/O 设备信号异步,返回下条指令磁盘 DMA 完成、定时器中断
陷阱有意的异常同步,返回下条指令系统调用
故障无意且可能可恢复同步,可能重试或终止缺页异常
终止不可恢复的致命错误同步,直接终止内存校验错

7.2 异常的深层设计哲学

除了"权限分离"和"安全兜底"外,异常机制还有两个更根本的设计目标:

效率和吞吐量——处理异步事件:中断机制使 CPU 在 I/O 设备工作时可切换到其他进程,解决了 CPU 与龟速 I/O 设备的矛盾。这是多任务操作系统和事件驱动编程的基石。

统一管理——构建公共基础机制:异常表统一了所有入口(系统调用、中断、故障),硬件只负责检测和分发(查表跳转),具体处理逻辑完全由操作系统定义。这是一种底层的"订阅-发布机制"。

7.3 进程

进程为程序提供两个关键假象:独立的逻辑控制流和私有的地址空间。

fork 与 execve 的组合

  • fork:一次调用,两次返回。子进程获得父进程地址空间的独立副本(通过 COW 机制延迟拷贝)
  • execve:一次调用,永不返回(除非出错)。完全替换当前地址空间,但不改变 PID
  • 标准模式:fork 创建子进程,子进程调用 execve 运行目标程序,父进程可继续工作

7.4 "用户态优先"原则

核心原则:尽可能把事在用户态解决,把内核态当成最后手段。

用户态 → 内核态的切换代价

  • 模式切换:CPU 从 Ring 3(用户态)切换到 Ring 0(内核态)
  • 上下文保存:需要保存用户态程序的寄存器、栈指针等大量现场
  • TLB 与缓存污染:内核代码和数据会冲刷掉一部分 TLB 和 CPU 缓存,切回用户程序时有"缓存未命中"惩罚

两种用户态优先的具体实现

A. malloc 的内存池malloc 不是每次都找内核的。它在用户态维护一块大的"内存池",调用 malloc 时只是从用户态内存池切下一块,极快。只有内存池枯竭时,才调用 sbrk 或 mmap 进入内核申请新内存。

B. LD_PRELOAD 库打桩:每次 malloc 调用的拦截、日志记录、安全检查都发生在用户态,没有任何模式切换的开销。只有在自定义逻辑执行完毕后,才往下调用真正的库函数,把"进入内核"这个昂贵操作留给真正的库函数。

边界说明:对于真正的硬件异常(如缺页、除零),无法在用户态拦截并阻止进入内核。但程序可通过 signal 或 sigaction 在用户态注册自定义函数来处理信号,避免被内核默认动作粗暴终止。

7.5 信号

信号是操作系统提供的进程间异步通知机制。关键特性:

  • 发送 ≠ 接收:两步分开的过程
  • 信号不排队:同类型信号最多一个未决(内核用位向量记录)
  • 阻塞信号:可选择阻塞某些信号的接收,被阻塞信号保持未决状态

信号处理程序是并发的逻辑流,编写时必须遵守严格约束:

  • 只调用异步信号安全函数。printfmalloc 不安全的根本原因:它们内部使用了锁(printf 获取终端锁,malloc 获取堆锁)。如果信号处理程序中断了主程序正在持有同一把锁的临界区,再尝试获取该锁就会死锁——主程序持有锁等待信号处理程序返回,信号处理程序等待主程序释放锁。write_exit 是系统调用的薄封装,不持有任何用户态锁,因此安全
  • 用 volatile sig_atomic_t 声明共享标志
  • 访问共享全局数据时在主程序和处理程序中都要暂时阻塞信号

信号的竞态条件while (!pid) pause() 模式存在死锁风险(信号在 while 检查之后、pause 之前到达)。sigsuspend 能原子地完成"解除屏蔽并挂起等待信号"这一组合操作,是安全的等待方式。

信号处理与钩子函数:信号处理在思想上是"系统级的钩子函数",但触发源是异步硬件/内核事件,与主程序强制并发,且只能调用异步信号安全的小部分函数。这与应用层框架的同步回调有着本质区别。

Java 异常与内核信号的关系:Java 异常的抛出和捕获逻辑全部在用户态完成。但某些异常(如 NullPointerException)的触发源头可能是内核信号——在 Oracle HotSpot 等常见 JVM 实现中,JVM 利用 SIGSEGV 来辅助实现空指针检测(将零页映射为不可访问,访问时触发 SIGSEGV,JVM 在用户态信号处理函数中捕获后转化为 NullPointerException)。这不是 Java 语言规范要求,而是特定 JVM 实现的优化策略。

7.6 非本地跳转(setjmp/longjmp

setjmp/longjmp 是 C 语言提供的用户级异常控制流机制。

  • 功能:允许程序从一个深层嵌套的函数调用中,直接跳转到某个预先保存好的位置,打破常规的调用/返回栈规则
  • setjmp(env) :在当前位置保存处理器上下文(寄存器、栈指针、PC 等),返回 0
  • longjmp(env, val) :从 setjmp 保存的位置恢复上下文,导致 setjmp 再次"返回",但这次返回值为 val
  • 应用场景:深层嵌套调用中的错误恢复(类似异常处理),或实现简单的用户级线程协同调度
  • 与信号处理的结合:在信号处理程序中,可用 sigsetjmp/siglongjmp 安全地跳回主程序。这为底层"异常处理"思想提供了 C 语言层面的原语实现

八、Intel CET 与影子栈

CET(Control-Flow Enforcement Technology)  是 Intel 硬件级防控制流劫持技术,影子栈(Shadow Stack) 是其核心组件。

核心分工

  • 普通数据栈:保存局部变量、函数参数、运行时返回地址
  • 影子栈:只读专用栈,仅存合法返回地址,硬件维护,软件无法显式写入

工作流程

  • CALL 指令:CPU 同时将同一返回地址分别压入普通栈和影子栈
  • RET 指令:硬件自动对比两栈返回地址,不匹配则触发#CP(Control Protection)异常

防护目标:专门对抗 ROP/JOP 等控制流劫持攻击。

近调用 vs 远调用

  • 近调用(Near CALL):同代码段内跳转,仅更新 RIP,现代平坦内存模型下绝大多数调用属于此类
  • 远调用(Far CALL):跨代码段跳转,同时修改 CS:RIP。现代 64 位平坦模型几乎不存在传统意义的远调用,syscall 是快速特权级跳转而非远调用

九、内存管理

9.1 动态内存分配器

两类分配器

  • 显式分配器malloc/free):应用显式申请和释放
  • 隐式分配器(垃圾收集):应用显式申请,系统自动回收

性能指标

  • 吞吐量:单位时间内完成的分配/释放请求数
  • 峰值内存利用率:U(k)=maxikPi/HkU(k) = \max_{i \le k} P_i / H_kPiP_i 为有效载荷聚合值,Hk H_k 为堆大小)。取历史高水位线作为分子,因为聚合有效载荷随分配/释放波动,而堆大小只增不减,不加 max 会导致释放后利用率失真

碎片

  • 内部碎片:发生在块内部,成因包括对齐填充、元数据开销、分配策略不分割大块
  • 外部碎片:堆中总空闲空间足够但无单一连续块能满足请求

9.2 分配策略

  • 首次适配:第一个满足条件的空闲块
  • 最佳适配:最接近请求大小的空闲块(利用率更高,但搜索代价也更高)
  • 隔离空闲链表:按大小类分组的空闲链表数组。在基础分配策略中,是唯一能同时显著提升吞吐量和利用率的方案(工业级分配器如 slab、jemalloc、tcmalloc 等也能做到,它们均可视为隔离链表思想在不同复杂度层级上的扩展)

边界标记与合并:在每个块的末尾复制一份头部(脚部),实现常数时间判断相邻块状态。已分配块可省去脚部——只有空闲块被合并时才需要脚部,已分配块的脚部对合并无用。

9.3 垃圾收集

标记-清除算法

  • 标记阶段:从根节点(寄存器、栈、全局变量中的指针)出发,递归标记所有可达堆块
  • 清除阶段:线性扫描堆,回收所有已分配但未标记的块

C 语言的保守式垃圾收集:无法确知某值是指针还是整数,保守地假设任何落在已分配块范围内的整数值都是指针,宁可错误保留垃圾(内存泄漏)也不能错误回收在用数据。


十、链接

10.1 编译流程

C 预处理器 → 编译器(生成汇编)→ 汇编器(生成可重定位目标文件 .o)→ 链接器(生成可执行文件)。

分开编译支持模块化和增量编译:修改一个文件只需重新编译该文件后重新链接。

10.2 链接器两大任务

符号解析:将每个符号引用恰好与一个符号定义关联。三类符号:全局符号(非 static)、外部符号(其他模块定义)、局部符号(static 修饰,作用域限于本模块)。

强弱符号规则

  • 不允许多个同名强符号
  • 一个强符号和多个弱符号,选择强符号
  • 多个弱符号,链接器任选其一

常见陷阱:多个模块中定义同名但不同类型的弱符号,导致不可预测的内存覆盖错误。

重定位:将多个 .o 文件合并,为每个符号确定最终绝对地址,更新所有指令中的符号引用。

10.3 ELF 格式

现代 Linux/Unix 使用统一的可执行可链接格式(ELF),主要节区:

  • .text:机器指令代码
  • .rodata:只读数据
  • .data:已初始化全局和静态变量
  • .bss:未初始化全局和静态变量(文件中不占空间,加载时分配并清零)
  • .symtab:符号表
  • .rel.text / .rel.data:重定位信息

10.4 库打桩

库打桩允许在不修改程序源码的情况下截获并替换对库函数的调用:

  • 链接时打桩--wrap 参数
  • 运行时打桩LD_PRELOAD 环境变量,动态链接器优先加载自定义 .so 中的符号

十一、I/O 与网络编程

11.1 Unix I/O 模型

一切 I/O 设备均被建模为无结构的字节序列——文件。操作系统内核不区分文件类型,不解释数据语义。

不足值read/write 请求 n 字节,实际处理字节数 m 可能满足 1 \le m < n。成因包括:遇到 EOF(read 返回 0)、从终端读取文本行(一次一行)、网络套接字受 MTU 限制(通常约 1500 字节)、写入时内核缓冲区暂时用尽。

11.2 系统调用的真实代价

一次简单的系统调用(如 getpid)约 100-300 个时钟周期;涉及复杂操作的(如读写磁盘文件)内核处理逻辑会显著增大开销,具体数值取决于 I/O 设备和路径。若以一次一字节方式拷贝大文件,大量 CPU 时间空耗在内核出入开销上,吞吐量骤降至灾难级。

11.3 带缓冲 I/O(RIO)

核心思想:在用户空间维护内部缓冲区,摊还系统调用开销

  • 读缓冲:先检查内部未消费数据,耗尽后一次性 read 大块填充
  • 写缓冲:多次小量写入先聚合于缓冲区,待满或显式刷新时一并 write
  • 效果:将多次系统调用合并为一次,极大减少上下文切换

11.4 内核文件共享数据结构

数据结构作用域内容
描述符表每进程以 fd 为索引,指向打开文件表条目
打开文件表全局文件位置、引用计数、指向 v-node 的指针
v-node 表全局文件元数据(大小、权限、类型)

三种共享情景

  • 单进程多次 open 同一文件:不同 fd → 不同打开文件表条目 → 各自独立的文件位置
  • fork:子进程继承描述符表副本 → 共享同一打开文件表条目 → 共享文件位置
  • dup2(oldfd, newfd):使 newfd 指向 oldfd 的打开文件表条目,实现 I/O 重定向

11.5 网络编程基础

  • IP 协议:尽力而为交付,不保证数据包到达。丢失时静默丢弃
  • TCP 协议:在 IP 之上提供可靠传输、面向连接、字节流抽象。承载约 99% 的互联网流量
  • 套接字:连接的端点,抽象为文件描述符。套接字地址 = IP 地址(32 位 IPv4 / 128 位 IPv6)+ 端口号(16 位)
  • 字节序:网络字节序为大端,x86 主机为小端,必须使用 htonl/ntohl 等转换函数

十二、并发编程

12.1 并发的三种模型

模型共享数据难度意外共享风险实现复杂度调度控制多核利用
基于进程困难低(隔离性好)简单无(内核控制)可以
基于事件容易无(单线程)高(状态机复杂)完全控制
基于线程容易高(共享一切)中等无(内核控制)可以

12.2 线程同步

竞态条件:程序的正确结果依赖于特定的执行交错顺序。cnt++ 在汇编层面由三条指令组成(加载、更新、存储),构成临界区。若两条线程的临界区交错执行,将导致更新丢失。

进度图是分析临界区问题的可视化工具:以线程1的执行进度为横轴、线程2为纵轴,轨迹表示一条并发执行路径。每个线程的临界区在坐标轴上形成一段不安全区间,两线程临界区的笛卡尔积交集构成了轨迹的不安全区域。若执行轨迹穿过此区域,意味着临界区被交错执行,结果错误。

信号量

  • P(s):若 s > 0 原子减 1 返回;若 s == 0 挂起线程
  • V(s):原子加 1,若有阻塞线程则唤醒一个
  • 不变式:s >= 0 始终成立
  • Linux 常见实现(NPTL)中,无竞争时走用户态原子操作快速路径(futex 的 FUTEX_WAIT/FUTEX_WAKE 仅在竞争时才通过系统调用进入内核)

互斥锁:初始化为 1 的信号量。P(mutex) 加锁,V(mutex) 解锁。在进度图中,信号量在状态空间形成禁止区域(信号量值绝不可能为负),恰好包围不安全区域,保证对临界区的互斥访问。

死锁:一组线程各自持有部分资源并等待对方释放,形成循环等待。统一加锁顺序可从根本上规避死锁。

12.3 经典同步问题

生产者-消费者:使用三个信号量——mutex(互斥访问缓冲区)、slots(计数空槽,初始 n)、items(计数物品,初始 0)。

读者-写者:偏向读者方案允许同时多个读者,写入者独占。首位读者获取 w 锁阻止写入者,末位读者释放 w锁允许写入者进入。中间读者只更新引用计数,无需等待。

12.4 线程安全

四类线程不安全函数

  1. 不保护共享变量的函数——用互斥锁修复
  2. 在多次调用间保持状态的函数(如 rand)——改用可重入版本(如 rand_r
  3. 返回指向静态变量指针的函数(如 ctime)——用锁-复制技术修复
  4. 调用线程不安全函数的函数——改用线程安全版本或重构

可重入函数:不依赖可变共享状态,也不返回指向内部静态数据的引用。只读共享数据(如 strlen 读取的字符串)不破坏可重入性。可重入函数必然是线程安全的,且无需任何同步操作。

12.5 阿姆达尔定律

若程序可加速部分占时间 P,该部分加速 k 倍,则加速比 S(k)=1(1P)+P/kS(k) = \frac{1}{(1-P) + P/k}。即使 kk \to \infty,加速比上限也仅为 1/(1P)1/(1-P)。程序中不可并行的串行部分将成为最终瓶颈。

12.6 多路并行累加器与硬件上限

多路并行累加器的"路数"由目标 CPU 的硬件执行能力决定。约束条件包括:

  • 执行单元数量:能并行算多少
  • 物理寄存器堆深度:能放下多少个中间变量
  • 数据加载带宽:加载单元每周期能读取的数据量,避免因数据供不上而空转

从软件指令的并行性追溯到硬件执行单元的物理限制,是程序优化的核心思想。


十三、性能优化方法论

13.1 优化阻挡器

三类编译器无法自动跨越的障碍:

函数调用的隐藏代价:在循环条件中调用 strlen(s) 导致每次迭代执行 O(n) 扫描,总复杂度变为 O(n²)。编译器不敢自动优化——无法确定该函数是否有副作用。

内存别名:编译器保守地认为两个指针可能指向同一内存,阻止将冗余访存优化为寄存器操作。引入局部累加器可消除此障碍。

跨语言对比:Java 中 String.length() 是 O(1)(读取对象内部数组的 length 属性),Python 中 len(s) 也是 O(1)(读取结构体头部字段),两者都消除了 C 语言 strlen 的陷阱。但"不把 O(n) 操作放进循环条件"的通用教训依然适用于其他数据结构:

# 列表中删除是 O(n),放在 while 条件里就是 O(n²)
while target in my_list:
    my_list.remove(target)
// 在循环里反复调用 Collections.max() 是 O(n²)
for (int i = 0; i < list.size(); i++) {
    int max = Collections.max(list);
}

通用优秀习惯:当你在循环条件里写任何表达式时,快速问自己一句:"它每次迭代都会被重新计算一次,这样真的没问题吗?"

13.2 指令级并行

循环展开:每次迭代处理多个元素,减少循环控制开销,为进一步优化创造空间。

重新结合变换:移动括号改变计算结合顺序,将长顺序依赖链拆分为多条短链。能将性能从受限于延迟提升至受限于吞吐量。警告:浮点运算不满足结合律,编译器默认不做此优化。

多路并行累加:用多个独立累加器并行累积不同下标的元素,进一步突破功能单元数量造成的吞吐量限制。

SIMD 向量化:一条指令同时处理打包的多个数据(AVX 指令一次处理 8 个 float 或 4 个 double),在理想情况下可显著突破标量执行性能上限(经典实验平台上 CPE 可低至 0.06,具体数值取决于硬件和数据类型)。

13.3 能耗与性能的权衡

能耗已成为比延迟和带宽更致命的瓶颈。从片外内存搬运一个 64 位数据的能耗大约是执行一次双精度浮点运算能耗的 100-200 倍。解决方案包括:高带宽内存(HBM)、增加片上 SRAM、近存/存内计算(PIM)、算法与架构协同优化(稀疏化、量化、知识蒸馏)。


十四、数据库的系统级透视

数据库本质上是在"内存墙"之上搭建的高度智能的存储与检索系统。

缓冲池与连接私有内存

1. 共享仓库:Buffer Pool(缓冲池)
在数据库实例启动时就分配好,是一块巨大的、所有连接共享的内存。无论哪个 Session 来读数据页,都直接访问这个公共仓库。数据页在内存中全局只存一份。注意:如果数据库使用 O_DIRECT 绕过 OS Page Cache(如 MySQL InnoDB),则只有一份副本;如果依赖 OS Page Cache(如 PostgreSQL),则可能同时存在 DB Buffer Pool 和 OS Page Cache 两份。

2. 个人工位:Session 的私有内存
当创建连接时,数据库分配:

  • 排序区(Sort Area) :执行 ORDER BY 或 GROUP BY 时,如果数据量超过 sort_buffer_size,数据库必须把数据拿到私有工位排序
  • 连接缓冲区(Join Buffer) :表连接时暂存驱动表数据
  • 线程栈与临时表空间:存放局部变量、游标、以及复杂 SQL 产生的内部临时表

3. 数据流动路径

  1. 数据库先去共享的 Buffer Pool 里找数据
  2. 如果不在内存,从磁盘读入 Buffer Pool
  3. 把查询目标行从共享 Buffer Pool 复制一份到 Session 私有的发送缓冲区,再通过网络发给客户端

这里的设计权衡:数据页在内存中全局只存一份(避免浪费),但为了准备发送给客户端的结果集,需要在 Session 私有内存里做一次内存级拷贝。这份拷贝开销就是"逻辑读"的代价之一。

4. 连接池的用武之地

  • 创建 Session 的成本:不仅是 TCP 三次握手,更关键的是在内存中创建整套私有工作区,并进行用户认证和权限校验
  • Session 的隐性成本:一个空闲连接即使不执行 SQL,也占用内存和 CPU 调度资源(一个线程)。数万个连接同时存在时,上下文切换开销可能使数据库大部分 CPU 耗费在"管理连接"上而非"处理数据"

写协议(WAL) :执行 UPDATE 时,数据库先顺序追加写日志(快速),在内存缓冲池中完成修改,之后异步批量写回磁盘。在简单快速的顺序写和复杂缓慢的随机写之间充当调度者和保护者。


附录:未来扩展方向

主体完成了从半导体物理到系统软件的「知识地基」构建。本附录基于这一地基,规划两条纵向深钻路线:一条向下沉入硬件与物理实现,一条向上延伸至系统与工程极限。二者最终在 AI Infra 汇合,并统一由数学/物理理论层支撑。


一、向下:物理与硬件纵深(系统工程师 → 架构师)

1.1 半导体与工艺

  • CMOS → FinFET → GAAFET
  • EUV 光刻与工艺节点演进
  • 制程缩尺的物理极限(量子隧穿、漏电流)

1.2 芯片封装与互联

  • 先进封装:CoWoS / Foveros / EMIB
  • Die-to-Die 互联:UCIe / BoW
  • 内存与扩展总线:HBM / CXL / CCIX

1.3 存储系统前沿

  • PIM(存内计算)/ PNM(近存计算)
  • NVRAM / SCM(Storage Class Memory)
  • 内存计算的编程模型与系统集成

1.4 功耗与热设计

  • Power Wall / Thermal Wall 的物理约束
  • DVFS / 动态功耗管理
  • 散热与封装协同设计

1.5 新型计算范式

  • 光计算 / 硅光子互联
  • 神经形态计算
  • 量子计算基础(作为体系结构的对比参照)

二、向上:系统与软件纵深(性能工程师 → AI Infra)

2.1 GPU 与异构计算

  • CUDA 编程模型(Grid / Block / Warp)
  • Tensor Core 工作原理与指令映射
  • Occupancy 与延迟隐藏
  • CUTLASS 模板库的抽象层次

2.2 编译器体系

  • LLVM 基础设施
  • MLIR Dialects 与 Progressive Lowering
  • TVM / XLA 的计算图与调度分离
  • 算子自动调优(AutoTVM / Ansor)

2.3 AI 推理引擎

  • vLLM / SGLang 的 PagedAttention 与 Continuous Batching
  • TensorRT-LLM 的 Graph Optimization
  • ONNX Runtime 的执行提供者机制

2.4 分布式系统

  • RDMA(RoCE v2 / InfiniBand)
  • NCCL 集合通信原语
  • Kubernetes + GPU Operator 的调度链路
  • Service Mesh 与推理流量的灰度发布

2.5 性能工程

  • 硬件性能计数器与采样(perf / eBPF)
  • Nsight Systems / Nsight Compute 的分析闭环
  • Flame Graph 与自定义 Profiler 设计
  • 性能回归检测的自动化流水线

三、理论工具层(支撑上下两条路线)

3.1 数学

  • 凸优化(编译器调度、资源分配)
  • 数值分析(混合精度误差建模)
  • 信息论(模型量化、熵编码)
  • 随机过程(排队论、尾部延迟建模)

3.2 物理

  • 电磁学(信号完整性、SerDes)
  • 热力学(散热极限、功耗密度)
  • 固体物理(半导体器件原理)
  • 量子力学基础(隧穿效应、新型器件)

四、研究与产业跟踪

4.1 学术会议

  • 体系结构:ISCA / MICRO / HPCA / ASPLOS
  • 系统:OSDI / SOSP / ATC / EuroSys
  • AI 系统:MLSys / NeurIPS(系统方向)

4.2 工业界趋势

  • GPU 架构路线图(NVIDIA / AMD / 国产替代)
  • 大模型推理系统的工程实践
  • 内存与互联技术的标准化进展(JEDEC / CXL Consortium)
  • AI 芯片生态的碎片化与整合

五、长期能力目标

阶段一:会分析系统

看懂性能瓶颈 → 解释瓶颈的根因 → 给出可落地的优化方案

阶段二:会设计系统

设计推理服务架构 → 设计资源调度策略 → 建立系统性能模型

阶段三:会定义系统

从业务需求反推硬件规格 → 定义编译与算子策略 → 主导系统架构决策


参考