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 不够”,后者指向 “在等待”。
关联
- 前置:11.2 内存管理与虚拟内存
- 并列:6.3 namespace (隔离) 与 cgroups (限制)、15.8 并发编程模型 (线程 / 协程 / Actor / CSP)
- 下游:11.3 文件系统与 IO 模型 (阻塞 / 非阻塞 / 异步)、11.6 CPU Cache 与 NUMA