114 / 163 · C11 · 约 8 分钟
多处理器调度(进阶)
随着多核芯片普及,操作系统必须在多个CPU间分配线程。本章用原创视角讲解缓存层次带来的一致性难题、共享数据仍需锁保护的原因,以及调度器如何利用缓存亲和性来减少迁移开销。
多核普及带来的新调度任务
芯片设计师无法再靠提高单核频率来获得性能,因为功耗会急剧上升,于是把多个较慢的核心放进同一颗芯片。普通顺序程序只能占用其中一个核心,因此程序员必须把工作拆成线程才能真正用上额外核心。操作系统此时不再只决定“下一个跑谁”,还要决定“跑在哪颗核心上”,这带来了负载均衡与数据局部性之间的新权衡。
每核缓存如何破坏简单内存视图
每个核心都拥有自己的小而快的缓存,用来保存最近访问过的数据和指令。当一个核心改写了某个地址后只更新自己的缓存、尚未写回主存时,另一个核心如果立刻去读同一地址,就会从主存拿到过期值。硬件通过总线侦听等协议让各缓存互相通知失效或更新,从而维持表面上的共享内存。即便如此,软件仍不能假设一次普通读写是原子的。
锁与亲和性:调度器必须同时考虑的两件事
即使硬件保证了最终一致性,多个核心同时修改同一链表或队列仍会导致丢失更新或重复释放。因此共享结构必须用互斥锁保护。锁本身会成为可扩展瓶颈。与此同时,线程一旦在某核上运行过,该核缓存里就装满了它的热数据;把它迁到另一核会引发大量缓存缺失。优秀的多处理器调度器因此尽量让线程留在原来的核心(缓存亲和性),只在负载严重不均时才迁移。
常见误区
- 以为多加几个核就能让任意单线程程序自动变快
- 认为硬件缓存一致性已经足够,共享队列不必再加锁
- 为了绝对公平而频繁迁移线程,导致缓存不断被冲刷
运行一个例子
最低标准 C11 · 完整程序 · 下载 .c
#include <stdio.h>
int main(void) {
printf("Multiprocessor Scheduling Simulation\n");
printf("====================================\n");
printf("Assigning jobs considering cache affinity:\n");
printf("CPU 0: JobA (initial placement)\n");
printf("CPU 1: JobB (initial placement)\n");
printf("CPU 0: JobC (affinity to previous cache)\n");
printf("CPU 1: JobD (load balance)\n");
printf("Note: Keeping jobs on same CPU preserves cache contents.\n");
return 0;
}
在本地编译
gcc -std=c11 -Wall -Wextra -Wpedantic -Werror ostep-10-multiprocessor.c -o example && ./example预期结果
Multiprocessor Scheduling Simulation
====================================
Assigning jobs considering cache affinity:
CPU 0: JobA (initial placement)
CPU 1: JobB (initial placement)
CPU 0: JobC (affinity to previous cache)
CPU 1: JobD (load balance)
Note: Keeping jobs on same CPU preserves cache contents.
CHECK YOUR UNDERSTANDING
合上答案,试着解释。
某个线程在CPU 0上修改了变量x后被调度器立刻迁到CPU 1,CPU 1上的第一次读可能看到什么?为什么?
查看参考答案
可能读到修改前的旧值。因为写可能还停留在CPU 0的缓存里,主存尚未更新,而CPU 1的缓存是空的,只能从主存取数;若硬件侦听尚未完成失效,就会得到过期数据。
继续查证
标准草案与官方章节会更新;版本标记只说明示例最低要求。