11.1 进程、线程与调度

一句话

进程是资源与隔离的单位(地址空间、fd、权限),线程是调度的单位(共享同一地址空间);上下文切换本身只要 1–3μs,真正贵的是 cache 与 TLB 被污染,所以调度器的目标不是 “切得勤”,而是 “切得值”。

进程与线程

维度 进程 线程
隔离 独立地址空间、fd 表、信号处理 共享地址空间与 fd
创建成本 高(fork + COW 页表) 低(clone 共享)
通信 IPC(管道、共享内存、socket) 直接读写共享内存
崩溃影响 互不影响 一个线程崩 = 整个进程崩
调度单位 线程(Linux 上进程/线程都是 task) 线程

Linux 不区分 “进程 / 线程” 的实现:都是 task_struct,差别只在 clone() 时共享哪些资源(CLONE_VM、CLONE_FILES、CLONE_THREAD)。

一次切换到底花了什么

成本 量级 说明
保存/恢复寄存器 + 切栈 1–3μs 直接成本,看起来很小
TLB 失效 数 μs – 数十 μs 换地址空间(跨进程)时更贵
Cache 污染 数十 μs 新任务的工作集把旧任务的缓存挤掉
调度器开销 亚 μs 选下一个任务

推论:切换频率比切换耗时更值得关注。每秒 10 万次切换,即使每次 2μs,也吃掉 20% 的 CPU——这类系统通常表现为 “CPU 高但吞吐不涨”。

CFS:按 “已经跑了多久” 排队

概念 含义 观测
vruntime 虚拟运行时间(按权重折算) /proc/<pid>/sched
权重 nice 值 → 权重(nice 每 +1 约减 10% CPU 份额) cpu.weight(cgroup v2)
调度延迟 一个周期内所有可运行任务至少各跑一次 默认约 6–24ms(随任务数调整)
最小粒度 单次运行下限 默认约 0.75ms

两个常见误解:

  • “nice 是绝对优先级”:不是,它只在竞争时决定 CPU 份额;CPU 空闲时大家都能跑满。
  • “线程越多越快”:CPU 密集任务超过核数只是增加切换;IO 密集任务才靠并发掩盖等待。

落到数字:切换成本与线程数

项 量级 说明
同进程线程切换 1–2μs 不换地址空间,TLB 基本保留
跨进程切换 2–5μs + TLB 失效 页表切换更贵
每秒 10 万次切换 吃掉 10%–20% CPU “CPU 高但吞吐不涨” 的典型
线程数与核数比 建议 ≤ 2–4×(IO 密集可更高) 再高只是增加切换
vmstat 1 5 | awk 'NR==1 || NR>2 {print $1, $13, $14}'   # cs 列 = 上下文切换/秒
pidstat -w -p <pid> 1 3                                  # cswch/nvcswch 分清自愿/非自愿

反模式

反模式 后果
用进程做高并发(每连接一进程) 内存与切换成本爆炸
线程数 = 请求数(无池化) 切换风暴,P99 抖动
依赖 nice 做 “实时保障” 只是相对份额,关键任务仍可能被饿
忽略 CPU 亲和 跨 NUMA 访问与 cache 失效(见 11.6 CPU Cache 与 NUMA)
在容器里按物理核数开线程 受 cpu.max 限制,线程多反而互相抢配额(见 6.3 namespace (隔离) 与 cgroups (限制))

三条铁律

🔑 进程管隔离、线程管并发;跨进程共享状态必须有明确协议。

🔑 切换的代价在缓存与 TLB,不在寄存器保存。

🔑 并发度按 “核数 × 阻塞率” 定,不按 “请求数” 定。

自检

Q1 一台机器 top 显示 CPU 使用率 90%、系统态占比很高、吞吐却不涨,最可能是什么问题?

▸ 答案:上下文切换或系统调用过多。常见来源:线程数远超核数、锁竞争导致频繁阻塞/唤醒、每次请求都做多次 syscall(日志、时间、序列化)。查 vmstat 1 的 cs(context switch)列与 pidstat -w,再对照业务的线程池配置。

Q2 为什么 “每连接一进程” 的模型撑不住高并发?

▸ 答案:因为每个进程都有独立地址空间与页表:内存开销(栈 + 页表 + 内核结构)、切换时 TLB 与 cache 全部失效、创建/销毁成本高。高并发需要 “少量线程 + 事件驱动”(见 11.4 epoll 与 IO 多路复用)或 “协程”(见 15.8 并发编程模型 (线程 / 协程 / Actor / CSP))。

Q3 nice 调到 -20 能让任务 “绝对优先” 吗?

▸ 答案:不能。CFS 里 nice 只影响权重(相对份额),CPU 空闲时低优先级任务照样能跑满;即使权重差距很大,也只是 “分到的比例不同”,不是 “独占”。要绝对保障需要实时调度类(SCHED_FIFO)或 cgroup 的 CPU 配额,代价是可能饿死其他任务。

Q4 容器里 nproc 显示 64,但 limits.cpu 是 2,开 64 个线程会怎样?

▸ 答案:会造成严重节流:64 个线程抢 2 核的配额(cpu.max),每个线程都被 CFS 周期切断,P99 抖动剧烈。正确做法是按 “配额 × 阻塞率” 设线程数(如 2–8),并监控 nr_throttled 比例(见 6.3 namespace (隔离) 与 cgroups (限制))。

Q5 什么情况下该用多进程而不是多线程?

▸ 答案:需要强隔离与故障不扩散时:不同租户的代码、会崩溃的第三方库、需要独立 GC/内存上限(如每个 worker 一个进程)、以及能利用 “多核 + 独立地址空间” 避免锁竞争的场景。代价是 IPC 与内存开销。典型例子:Nginx(多 worker 进程)、PostgreSQL(每连接一进程)。

Q6 nvcswch(非自愿切换)很高说明什么?

▸ 答案:说明任务被抢占:可运行但没抢到 CPU(通常是线程数远超核数,或存在高优先级任务)。它和 cswch(自愿,等 IO/锁)含义不同——前者指向 “CPU 不够”,后者指向 “在等待”。

关联