从硅片到协程

自底向上理解计算机执行层次 —— CPU晶体管 → 单核/多核 → 超线程 → 指令执行 → 进程 → OS线程 → 协程

EXECUTION HIERARCHY PANORAMA

L1

CPU 硬件基础

晶体管 → 逻辑门 → ALU/寄存器 → 控制单元 → 时钟
L2

单核 → 多核

频率提升瓶颈 → 多核并行 → 缓存一致性 → NUMA
L3

超线程 (SMT)

一个核心两套上下文 → 资源复用 → 30%吞吐提升
L4

指令执行

取指 → 译码 → 执行 → 访存 → 写回 → 流水线
L5

进程

虚拟地址空间 → 页表 → PCB → 上下文切换
L6

OS 线程

轻量执行单元 → 线程调度 → 内核态切换 → 锁竞争
L7

协程 (Coroutine)

用户态调度 → 极低切换开销 → 百万并发
L1

CPU 硬件基础 HARDWARE

从晶体管到运算核心 —— CPU的物理构成

晶体管 (Transistor)

CPU的基本构建单元,本质是微型电子开关。现代CPU含数十亿晶体管(Apple M2约200亿,AMD EPYC约500亿)。NAND门需4个晶体管,SRAM一个存储单元需6个。通过3nm/5nm/7nm工艺不断提升集成密度。

逻辑门 → 组合逻辑

晶体管组合成AND/OR/NOT/XOR逻辑门,再构建加法器、多路选择器、译码器等。ALU(算术逻辑单元)由大量加法器和逻辑门构成,负责整数运算与逻辑判断。

寄存器 (Register)

CPU内部最快的存储单元,延迟约0.1-0.3ns。x86-64有16个通用寄存器(RAX,RBX...),ARM64有31个(X0-X30)。寄存器是程序能直接访问的最快存储。

控制单元 (CU)

CPU的"指挥中心",取指令、译码生成控制信号、协调ALU/寄存器/缓存协同工作。通过微操作(μop)将一条指令拆解为多个硬件步骤。现代CU支持乱序执行和分支预测。

~0.1ns
寄存器延迟
~1ns
L1 Cache
~4ns
L2 Cache
~10ns
L3 Cache
~100ns
主内存
L2

单核 → 多核 MULTI-CORE

频率墙、功耗墙之后,横向扩展成为性能增长主旋律

⚡ 单核时代 (2005前)

靠提升主频获取性能:Pentium 4冲到3.8GHz。但功耗∝频率²(P=αCV²f),3.8GHz功耗约130W,继续提升面临功耗墙散热墙。Intel NetBurst架构的失败宣告频率路线走到尽头。

P_dynamic = α · C · V² · f // α: 活跃因子 C: 电容 V: 电压 f: 频率 // 频率↑ → 电压必须↑ → 功耗立方级增长!

🔋 多核时代 (2005后)

同一芯片集成多个核心:Intel Core 2 Duo开启双核。同等功耗下,双核2GHz > 单核4GHz。当今桌面8-16核,服务器64-128核(AMD EPYC 9754达128核/256线程)。

// Amdahl定律限制 Speedup = 1 / [(1-p) + p/n] // p=并行比例 n=核心数 // 串行部分5% → 64核最多20x加速

缓存一致性 (MESI)

多核各有私有L1/L2缓存,MESI协议定义4种缓存行状态:Modified、Exclusive、Shared、Invalid。通过总线嗅探(Bus Snooping)实现核心间同步。

NUMA架构

多路服务器每个CPU有本地内存控制器,访问本地内存~100ns,远端~150-200ns。Linux numactl可绑定进程到特定NUMA节点,避免跨节点访问惩罚。

big.LITTLE

ARM首创,Apple M系列采用P-core+E-core混合架构。性能核处理重负载,效率核处理后台任务。Intel 12代+跟进P/E Core。调度器需感知核心类型。

L3

超线程 SMT HYPERTHREADING

一个物理核心,两套架构状态 —— 用闲置资源换吞吐

核心思想

单条指令执行中,ALU/访存单元常处于空闲(如cache miss等待)。SMT在同一个物理核上维护两套架构状态(寄存器组、PC),让两个硬件线程交替使用闲置执行单元,提高资源利用率。

共享 vs 独占资源

独占:寄存器组、PC指针、栈指针、指令TLB → 每线程一份
共享:ALU、FPU、L1数据缓存、L2/L3缓存 → 两线程争用
因此超线程并非"双核",而是更高效的"单核复用"。

性能收益与限制

典型吞吐提升20%~40%,绝非2倍。整数运算密集型收益高,浮点密集型收益低。单线程性能可能因缓存争用而轻微下降。数据库、Web服务器等IO密集场景收益显著。

场景超线程提升原因
数据库查询25~40%大量cache miss,另一线程可填充空闲
Web服务器20~35%IO等待多,SMT填补空窗
视频编码10~20%SIMD单元争用,收益有限
科学计算-5~10%FPU满载,争用反而降低性能
L4

指令执行 INSTRUCTION

取指 → 译码 → 执行 → 访存 → 写回 —— CPU的五级流水线

① 取指 (IF - Instruction Fetch)

PC寄存器指向的内存地址,通过L1指令缓存读取指令字节。x86变长1~15B,ARM64固定4B。分支预测器在此阶段猜测下一条指令地址。

② 译码 (ID - Instruction Decode)

将机器码翻译为μop(微操作)。x86复杂指令被拆解为多个RISC-like μop。如 ADD [mem],EAX 拆为 load→add→store。寄存器重命名也在此阶段完成。

③ 执行 (EX - Execute)

ALU执行算术逻辑,FPU执行浮点,AGU计算内存地址。现代CPU支持乱序执行:操作数就绪即执行。保留站(Reservation Station)负责调度。

④ 访存 (MEM - Memory Access)

Load/Store通过L1D→L2→L3→主存层次读写数据。Cache miss时流水线可能阻塞数十到数百周期。Store Buffer允许写操作异步完成。

⑤ 写回 (WB - Write Back)

结果写入目标寄存器或标志寄存器。按序完成(Retire)保证精确中断和程序语义正确。ROB确保指令虽乱序执行但按序提交。

流水线冒险 (Hazard)

数据冒险:后续指令依赖前序结果 → Forwarding缓解
控制冒险:分支跳转打乱流水线 → 分支预测+推测执行
结构冒险:争用同一硬件 → 增加资源或插气泡

分支预测

if/else等条件跳转占指令20%+。现代CPU使用两级自适应预测器,准确率>97%。预测错误需冲刷流水线(15~20周期惩罚)。Spectre漏洞正是利用推测执行副作用。

ISA: x86 vs ARM

x86-64(CISC,变长1-15B,微码翻译μop) vs ARM64(RISC,定长4B,直接执行)。RISC-V同为RISC。ISA是软硬件契约:OS和编译器面向ISA编程,CPU微架构自由实现。

L5

进程 PROCESS

操作系统最核心的抽象 —— 资源分配与隔离的基本单位

进程的本质

进程 = 虚拟地址空间 + PCB(进程控制块) + 打开的文件/资源。每个进程拥有独立页表层次(x86-64四级),将虚拟地址翻译为物理地址。进程间内存天然隔离,通信需IPC。

上下文切换代价

进程切换需保存/恢复全部寄存器、切换页表基址(CR3)→刷新TLB、切换内核栈。TLB重建是最大开销,可达数千周期。典型切换耗时1~10μs

虚拟地址空间布局

x86-64下48位虚拟地址:高地址内核空间(共享)、栈区(向下增长)、堆区(向上增长)、BSS/数据/代码段。ASLR将各区域随机偏移,防止缓冲区溢出攻击。

// Linux fork()的写时复制(COW) pid_t pid = fork(); // 子进程复制父进程的页表 // 物理页标记为只读共享,直到任一方写入 // 写入时触发Page Fault → 复制该页 → 各自独立修改 // 进程切换内核路径 schedule() → context_switch() → switch_mm() // 切换页表CR3,刷TLB → switch_to() // 保存/恢复寄存器,切换内核栈
L6

OS 线程 THREAD

CPU调度的基本单位 —— 共享地址空间的轻量级执行流

🔄 进程 vs 线程

维度进程线程
地址空间独立(隔离)共享(同进程内)
创建开销~1ms~10μs
切换开销1~10μs0.5~5μs
页表切换需要(CR3→刷TLB)不需要
通信IPC(管道/Socket等)直接读写共享变量
安全性高(内存隔离)低(一线程崩溃全完)

⚙️ 线程调度机制

Linux线程=进程(都是task_struct),1:1模型——每个用户线程对应一个内核线程。

CFS调度器:红黑树管理可运行线程,按虚拟运行时间排序。时间片通常1~10ms。

切换触发:时间片耗尽、IO阻塞、高优先级抢占、主动yield、信号中断。

内核态切换:保存用户态寄存器→切换内核栈→调度→恢复目标线程。

线程模型演进

1:1(Linux):用户线程=内核线程,简单但有内核开销。
N:1(Green Thread):多用户→一内核,无法利用多核。
M:N(Go goroutine):多用户→多内核,兼顾轻量与并行。

锁与同步

pthread_mutex(futex→内核切换)、spinlock(自旋等)、RW Lock(读共享写独占)、atomic(CAS硬件原子指令,无锁)。错误使用导致死锁/活锁/优先级反转。

L7

协程 Coroutine COROUTINE

用户态的轻量级线程 —— 百万并发不是梦

协程的本质

用户态调度的执行单元,切换不需要进入内核态。由运行时管理调度。一个OS线程可运行数万协程,切换仅需保存/恢复少量寄存器(约0.2~0.5μs),比线程切换快10~50倍。

协作式 vs 抢占式

协作式:协程主动yield(Python generator、Lua)。简单但一个不yield就阻塞全部。
抢占式:运行时强制挂起(Go 1.14+基于信号抢占)。更安全。

stackful vs stackless

stackful(有栈):每协程有独立调用栈(Go goroutine 2KB~1GB动态增长),可在任意层级yield。
stackless(无栈):编译器转为状态机(Rust async/await、C++20 co_await),零开销但无法深层yield。

Go Goroutine (stackful, M:N)

go func() { // 创建: ~0.3μs result := doWork() // 初始栈仅2KB ch <- result // channel通信 }() // GMP调度模型: // G(Goroutine) → M(Machine/OS线程) // → P(Processor/逻辑处理器) // P的数量 = GOMAXPROCS (默认=CPU核心数) // 某G阻塞IO时, M释放P给其他M使用

Rust async/await (stackless, M:N)

async fn fetch_data() -> Result<Data> { let resp = client.get(url).await?; // yield点 let body = resp.text().await?; // 状态机 Ok(parse(body)) } // Future::poll() 驱动状态机推进 // tokio: 多线程调度, work-stealing // 零成本抽象: 编译期转换, // 运行时无额外栈开销
维度进程OS线程协程
创建开销~1ms~10μs~0.3μs
切换开销1~10μs0.5~5μs0.2~0.5μs
内存占用~10MB(栈+页表)~8MB(栈)~2KB(goroutine)
最大并发数~数千~数万~数百万
调度者内核内核用户态运行时
切换进内核
多核利用✅ 原生✅ 原生⚠️ 需M:N映射