[科普] 从单核到千核:Linux SMP 的“演化史”与工程细节

发布于:2025-08-07 ⋅ 阅读:(13) ⋅ 点赞:(0)

从单核到千核:Linux SMP 的“演化史”与工程细节

如果你只关心结论:
SMP = 对称多处理(Symmetric Multi-Processing);Linux 通过“三级缓存一致性→可抢占调度→Per-CPU 变量→RCU→NUMA 亲和→调度域”这一整套组合拳,把 1 核到 4096 核都当成“大号单核”来用。
但魔鬼藏在细节里,请坐稳,我们慢慢拆。



1 为什么需要 SMP?

1965 年,Gordon Moore 画了一条斜线;50 年后,主频撞墙,IPC(每周期指令数)也逼近极限。于是横向扩展——把多个 CPU 核心放在同一主板——成为必然。
操作系统必须回答两个问题:

  • 如何让所有 CPU 看到同样的内存?(缓存一致性)
  • 如何让所有 CPU 不互相踩脚?(同步、调度)

Linux 的回答是:SMP。对称意味着“任何 CPU 都能运行内核代码、都能处理中断”,而不是像早期主从结构那样“0 号核是管家,其余核只能跑用户态”。


2 硬件地基:从总线嗅探到目录式缓存一致性

2.1 MESI 及其变体

现代 CPU 的 L1/L2 缓存行有四种状态:

  • M(odified)
  • E(xclusive)
  • S(hared)
  • I(nvalid)

总线嗅探(snooping)让所有核监听总线,写操作会触发 Invalidate 消息。

例子:核 0 写入地址 0x1234,发现核 1 也有副本,于是广播“请把 0x1234 标成 I”。

2.2 NUMA & 目录式协议

当核数 > 64,总线广播风暴不可接受,于是出现 目录式缓存一致性(Intel QPI、AMD Infinity Fabric)。
每个 NUMA 节点维护一张“目录表”,记录“某缓存行被哪些节点持有”,从而把广播变成点对点。

Linux 用 CONFIG_NUMA 打开 NUMA 感知,启动时通过 ACPI SLIT/SRAT 表建立距离矩阵,调度器据此把进程“粘”在离内存最近的节点。


3 内核启动:从 1 个 CPU 到 n 个 CPU 的芭蕾

3.1 早期 boot:只有 BSP(Bootstrap Processor)

  • 通电后,硬件只唤醒 1 个核(BSP),其余核处于“Wait-for-SIPI”状态。
  • 内核解压、建立临时页表、切换到长模式(x86_64)。

3.2 唤醒 AP(Application Processor)

  • start_kernel()smp_init()native_smp_cpus_done()
  • 对每个 AP 调用 __cpu_up()
    1. 在 trampoline 页(低 4 MB)放置实模式 AP 入口;
    2. 发送 INIT-SIPI-SIPI 序列(x86)或 PSCI_CPU_ON(ARM);
    3. AP 跳转到 secondary_startup_64,建立 MMU,最终进入 start_secondary(),完成自己的 per_cpu 区域初始化。

细节:x86 的 trampoline 代码在 arch/x86/kernel/head_64.S,ARM 的启动入口在 arch/arm64/kernel/head.S


4 调度器:让 1000 个进程在 128 个核上跳舞

4.1 runqueue 的进化

  • Linux 2.4:全局 runqueue + 大内核锁(BKL),伸缩性差。
  • Linux 2.6:O(1) 调度器,Per-CPU runqueue。
  • Linux 3.x:CFS(完全公平调度器),红黑树管理 runnable 任务。
  • Linux 5.x:EEVDF(最新默认) + SCHED_EXT(BPF 可编程调度器)。

4.2 调度域(sched domain)

调度器把 CPU 组织成层次化拓扑:

Die -> Package -> Core -> SMT

每个层级都有 load balance 算法,通过 tick_balance()nohz_idle_balance() 定期迁移任务,避免“一核有难,七核围观”。

4.3 实时扩展

RT 补丁把 CONFIG_PREEMPT_RT 打开,把自旋锁变成可睡眠的 rt_mutex,让高优先级任务随时抢占。代价是吞吐量下降 5-10%。


5 同步原语:自旋锁、信号量、mutex、RCU,到底谁保护谁?

5.1 自旋锁(spinlock_t)

  • 在持锁期间禁止抢占(preempt_disable())。
  • 实现:arch_spin_lock() 使用 ticket lock(公平)或 qspinlock(MCS 队列锁,NUMA 友好)。

5.2 读写锁 & seqlock

  • rwlock_t:读者并发,写者独占。
  • seqlock_t:写者优先,读者重试(常用于 jiffies)。

5.3 mutex vs semaphore

  • mutex 只能睡眠,持有者明确;
  • semaphore 可计数,常用于 down_read()/up_write() 保护 struct 文件系统。

5.4 RCU(Read-Copy-Update)

  • 读者零开销(仅关抢占),写者复制后异步回收。
  • 实现:call_rcu() 把回调挂到每 CPU 的 rcu_data,下一次 grace period(GP)后回收。
  • 关键 API:
    • rcu_read_lock() / rcu_read_unlock()
    • synchronize_rcu()
  • 场景:路由表、链表遍历,DPDK 也偷师 RCU。

6 内存管理:Per-CPU 变量、slab、NUMA 节点与页着色

6.1 Per-CPU 变量

  • 编译器把 DEFINE_PER_CPU(int, foo) 展开为段 .data..percpu,每个 CPU 一份副本,避免 false sharing。
  • 访问:this_cpu_ptr(&foo) 在 x86 上用 %gs 段寄存器偏移。

6.2 slab 分配器

  • 原始 slab → slub(目前默认)。
  • Per-CPU cache:kmem_cache_cpu 直接分配,无锁路径。
  • NUMA 亲和:kmem_cache_node 维护 node-local partial 列表。

6.3 页迁移 & 页着色

  • migrate_pages() 可在 NUMA 节点间迁移匿名页,减少远程内存访问。
  • 页着色:把物理页地址哈希到不同 L3 slice,避免多核冲突(Intel CAT 扩展)。

7 中断、软中断与 NAPI:IPI 如何让 CPU 之间“打电话”

  • IPI(Inter-Processor Interrupt):x86 用 APIC, ARM 用 GIC
    • smp_send_reschedule(cpu):让某核重新调度。
    • flush_tlb_others():广播 TLB shootdown。
  • 软中断(softirq)
    • 10 个向量:HI_SOFTIRQ, TIMER_SOFTIRQ, NET_RX_SOFTIRQ…
    • ksoftirqd 每 CPU 一个守护线程,防止软中断饥饿。
  • NAPI:网卡硬中断触发后,关闭中断,改为轮询,减少跨核 IPI 风暴。

8 工具箱:/proc、perf、schedtrace、bpftrace 怎么看 SMP?

工具 说明 示例
lscpu 查看拓扑 lscpu -e
/proc/sched_debug 调度器内部状态 cat /proc/sched_debug | less
perf stat -a 全核 PMU 计数 perf stat -a -e cache-misses ./workload
bpftrace -e 'profile:hz=99 { @[cpu] = count(); }' 看核负载分布
taskset -c 0-3 ./a.out 绑核运行

9 未来:CXL、chiplet 与可扩展性的终局之战

  • CXL:把内存池化,NUMA 距离进一步拉大,Linux 需要新的“内存热插拔/故障隔离”子系统。
  • chiplet:一个 socket 内出现 16 个小芯片,缓存一致性走向“die-to-die”链路,Linux 需把调度域切得更细。
  • Rust in kernel:下一代同步原语可能用 lock_api + crossbeam 的思想,减少 data race。

10 小结:一张思维导图

SMP
├── 硬件
│   ├── 缓存一致性(MESI/目录)
│   └── NUMA
├── 启动
│   ├── BSP → AP
│   └── trampoline
├── 调度
│   ├── CFS/RT/EEVDF
│   └── sched domain
├── 同步
│   ├── spinlock/mutex
│   └── RCU
├── 内存
│   ├── Per-CPU 变量
│   └── slab/NUMA
├── 中断
│   ├── IPI
│   └── softirq/NAPI
└── 工具
    ├── perf
    └── bpftrace

Linux 的 SMP 不是一蹴而就,而是 30 年迭代的“活化石”。
今天,你在笔记本上跑 make -j16 或者在 4096 核服务器上跑 Spark,背后都是同一套代码。
下次再听到“多核优化”,不妨想想:从缓存行到调度器,从 RCU 到 NUMA,Linux 早已把能踩的坑都踩了。


研究学习不易,点赞易。
工作生活不易,收藏易,点收藏不迷茫 :)



网站公告

今日签到

点亮在社区的每一天
去签到