进程、线程与 CPU 调度:从运行模型到 Linux 排障
应用明明没有死循环,CPU 使用率却居高不下;线程数量不多,延迟却突然抖动;机器还有空闲核心,任务为什么仍然排队?这些现象都绕不开三个基础问题:进程如何隔离资源,线程如何共享执行上下文,内核又如何把可运行任务分配给有限的 CPU。
本文从操作系统的运行模型出发,串起进程、线程、上下文切换、调度策略和 Linux 排障方法。文中的“任务”泛指内核能够调度的执行实体,在 Linux 中通常对应一个线程。
1. 进程与线程分别解决什么问题
进程首先是一个资源容器。每个进程拥有独立的虚拟地址空间,并持有文件描述符、信号处理方式、工作目录和安全凭据等资源。两个普通进程即使使用了相同的虚拟地址,也会通过各自的页表映射到不同的物理页,因此一个进程通常不能直接读写另一个进程的内存。
线程则是进程内部的执行单元。同一进程中的线程共享代码段、堆、全局变量和打开的文件,但各自拥有:
-
程序计数器,用于记录下一条将要执行的指令;
-
寄存器现场;
-
独立栈,用于保存函数调用帧和局部变量;
-
调度状态、优先级和线程局部存储。
两者的核心差异如下:
| 维度 | 进程 | 同一进程内的线程 |
|---|---|---|
| 地址空间 | 默认相互隔离 | 共享 |
| 数据交换 | 需要管道、套接字、共享内存等 IPC | 可直接访问共享变量 |
| 故障影响 | 通常局限在当前进程 | 非法内存访问可能终止整个进程 |
| 创建与切换成本 | 通常较高 | 通常较低,但并非没有成本 |
| 资源所有权 | 持有地址空间和系统资源 | 主要持有执行现场 |
“线程一定比进程快”并不严谨。实际成本取决于是否切换地址空间、缓存是否失效、是否跨 CPU 迁移以及内核实现。工程上更重要的取舍是:需要强隔离时选择多进程,需要低成本共享大量状态时选择多线程。
2. 一次程序启动后发生了什么
在类 Unix 系统中,创建新进程常见的组合是 fork 加 exec:
-
fork创建调用进程的子进程。 -
父子进程起初拥有内容相同的虚拟地址空间。
-
内核通过写时复制避免立即拷贝全部物理内存。
-
exec用新的程序映像替换当前进程的地址空间。 -
调度器在合适的时机让新任务获得 CPU。
创建线程不需要建立全新的资源容器。Linux 的 clone 系列系统调用可以控制父子任务共享哪些资源,用户态线程库通常在此基础上实现线程 API。
程序并不是创建后就持续占有 CPU。任务会在几类状态之间转换:
时间片用完 / 被抢占 +--------------------------------+ | v 新建 -> 就绪 --------调度--------> 运行 --------> 终止 ^ | | | 等待 I/O、锁或事件 +-------- 事件完成 <--------+ 阻塞
-
就绪:具备运行条件,正在运行队列中等待 CPU;
-
运行:当前正在某个 CPU 上执行;
-
阻塞:等待 I/O、锁、定时器或其他事件,不参与 CPU 竞争;
-
终止:执行结束,父进程尚未回收退出信息时可能短暂成为僵尸进程。
Linux 工具中的状态字符更加细分。例如 R 表示正在运行或可运行,S 表示可中断睡眠,D 通常表示不可中断睡眠,Z 表示僵尸状态。D 状态任务常在等待块设备或网络文件系统,不能简单归因于“CPU 不够”。
3. 用户态与内核态不是进程状态
用户态和内核态描述的是 CPU 当前的特权级,而就绪、运行和阻塞描述的是任务的调度状态,两组概念不能混为一谈。
应用调用 read、write 或 futex 时会通过系统调用进入内核态。中断和异常也会让 CPU 转入内核执行相应处理程序。处理完成后,CPU 可以返回原线程,也可能因调度决策转去执行另一个线程。
一次系统调用不一定引发上下文切换。例如读取的数据已经在页缓存中,read 可能很快返回;若数据尚未就绪,线程才可能进入阻塞状态并让出 CPU。反过来,上下文切换也不一定由系统调用直接触发,时间片到期、高优先级任务唤醒和 CPU 负载均衡都可能导致切换。
4. 上下文切换到底保存了什么
当 CPU 从任务 A 切换到任务 B 时,内核需要保存 A 的寄存器、程序计数器和栈指针等执行现场,再恢复 B 的现场。若两个任务属于不同进程,还可能切换页表相关状态。
真正昂贵的不只有保存和恢复寄存器,还包括间接成本:
-
CPU 流水线中的工作被打断;
-
新任务的指令和数据可能不在当前 CPU 缓存中;
-
地址空间变化可能降低 TLB 命中率;
-
任务迁移到另一个 CPU 后,缓存局部性进一步变差;
-
大量线程竞争同一把锁,会形成唤醒、争抢、再睡眠的循环。
因此,线程数超过 CPU 核数并不必然有问题。I/O 密集型线程在等待期间会让出 CPU,适度超配能够提高利用率;CPU 密集型任务若远多于核心数,通常只会增加排队和切换成本。
5. 调度器要优化哪些目标
调度没有对所有工作负载都最优的单一算法。常见目标包括:
| 指标 | 含义 | 典型关注场景 |
|---|---|---|
| CPU 利用率 | CPU 处于有效工作的比例 | 批处理、计算任务 |
| 吞吐量 | 单位时间完成的任务数 | 离线作业、服务端请求 |
| 周转时间 | 从任务到达到完成的总时间 | 批处理 |
| 等待时间 | 在就绪队列中等待的时间 | 高负载系统 |
| 响应时间 | 到达后首次得到响应的时间 | 交互式应用 |
| 公平性 | 各任务是否合理获得 CPU | 多租户、桌面系统 |
经典算法适合帮助理解权衡:
先来先服务 FCFS
按到达顺序执行,实现简单,但一个长任务可能让许多短任务等待,产生“护航效应”。
最短作业优先 SJF
优先运行预计执行时间最短的任务,在执行时间可准确预知时能降低平均等待时间,但现实中通常只能估计,还可能让长任务长期饥饿。
时间片轮转 RR
每个任务获得一个时间片,时间片用完后回到队尾。时间片太大时接近 FCFS,太小时则会产生大量上下文切换。它适合解释分时系统如何改善交互响应。
优先级调度
高优先级任务先运行,但低优先级任务可能饥饿。逐步提高长期等待任务的优先级,即“老化”,可以缓解这个问题。
这些算法是模型,不应直接套作对现代 Linux 调度器的完整描述。现代调度器还要处理多核负载均衡、CPU 亲和性、NUMA、实时任务、能耗和控制组配额等问题。
6. Linux 调度中的几个实用概念
普通任务和实时任务使用不同调度类别。普通业务进程通常采用公平调度策略;实时策略如 SCHED_FIFO 和 SCHED_RR 拥有更强的抢占能力,配置不当可能让普通任务长期得不到 CPU,不应仅为“降低延迟”就随意启用。
nice 值
普通任务可以通过 nice 值表达相对权重。nice 值越低,通常能获得越多 CPU 时间。它影响的是竞争时的相对份额,并不等价于预留固定百分比的 CPU:
nice -n 10 ./batch-job renice 5 -p 12345
CPU 亲和性
CPU 亲和性限制任务可以在哪些 CPU 上运行。固定 CPU 有时能改善缓存局部性,但也可能阻碍负载均衡:
taskset -cp 0-3 12345
除非已经通过性能数据确认迁移是瓶颈,否则不要把绑核当作通用优化。
容器 CPU 限额
容器内看到多个 CPU,不代表它能持续使用全部算力。cgroup 的 CPU 配额可能让任务运行一段时间后被节流。此时节点整体仍有空闲 CPU,容器内部请求却会排队。排查容器性能时,必须同时查看配额、节流次数和节流时长。
7. 并发正确性:共享内存带来的代价
线程共享地址空间让通信变得直接,也引入了数据竞争。count++ 通常包含读取、计算和写回多个步骤,两个线程交错执行可能丢失更新。
互斥锁保护临界区时,应遵守三个原则:临界区尽量短,不在持锁期间执行不可控的阻塞 I/O,所有访问共享不变量的路径使用一致的同步协议。
死锁需要四个条件同时成立:
-
互斥:资源同一时刻只能被一个任务占用;
-
占有并等待:任务持有资源的同时等待其他资源;
-
不可剥夺:资源不能被系统强制收回;
-
循环等待:多个任务形成首尾相接的等待环。
工程上常通过固定加锁顺序、一次性申请资源、超时与回退、缩小共享状态等方式破坏其中至少一个条件。增加线程数不能解决锁竞争,反而可能让问题更严重。
8. 用 Linux 工具定位 CPU 与调度问题
排障时应先区分“正在消耗 CPU”和“正在等待 CPU”。下面是一条从系统到线程的基本路径。
第一步:判断整机压力
uptime vmstat 1 mpstat -P ALL 1
重点观察:
-
uptime的负载平均值不是 CPU 使用率,它统计可运行任务以及部分不可中断睡眠任务; -
vmstat中r长期明显高于可用 CPU 数量,说明运行队列存在竞争; -
us高通常表示用户态计算多,sy高表示内核态工作多,wa高表示 CPU 存在 I/O 等待; -
单个 CPU 满载而其他 CPU 空闲时,应检查单线程瓶颈和 CPU 亲和性。
第二步:定位进程和线程
top pidstat -u -w -p 12345 1 top -H -p 12345 ps -L -p 12345 -o pid,tid,psr,stat,pcpu,comm
pidstat -w 能观察自愿与非自愿上下文切换。自愿切换通常发生在线程主动等待 I/O、锁或条件变量时;非自愿切换通常意味着被调度器抢占。切换数量必须结合吞吐量和延迟看,单独一个“很大”的数字不能证明异常。
第三步:获取调用路径
perf top -p 12345 perf record -g -p 12345 -- sleep 30 perf report
性能剖析要覆盖问题发生的时间窗口,并注意采样开销和符号信息。若热点集中在锁、系统调用或内存分配器,还需结合应用运行时工具继续分析。例如 Java 应用可以将高 CPU 的原生线程 ID 转成十六进制,再与线程转储中的 nid 对照。
9. 三类常见现象如何判断
CPU 使用率高,吞吐量也高
这可能只是系统接近设计容量。先检查延迟和错误率是否仍满足目标,再决定是否优化。仅仅追求更低的 CPU 使用率没有业务价值。
CPU 使用率不高,负载平均值却很高
任务可能大量处于 D 状态,或容器正在被 CPU 配额节流。检查进程状态、磁盘延迟、网络文件系统和 cgroup 指标,不要只扩容线程池。
上下文切换暴增,吞吐量下降
常见原因包括线程过多、锁竞争、频繁短时休眠和不合理的生产者/消费者唤醒。应结合 pidstat、perf、运行时线程分析和业务流量确认根因。
10. 常见误区
-
进程切换一定发生,系统调用就一定切线程。 系统调用只是进入内核的一种方式,能立即完成时可以返回原线程。
-
负载平均值为 8,就表示 CPU 使用率是 800%。 负载平均值反映任务队列压力,不是利用率百分比。
-
线程越多,并发能力越强。 并发上限还受 CPU、锁、内存、连接池和下游容量约束。
-
提高优先级可以让程序整体更快。 优先级主要改变竞争者之间的 CPU 分配,无法消除程序自身的计算量。
-
看到上下文切换多就应该绑核。 切换和迁移是不同问题,绑核还可能制造新的热点。
11. 总结
进程提供资源隔离,线程提供进程内的并发执行,调度器负责在有限 CPU 上分配运行机会。理解三者的关键不是背诵定义,而是建立因果链:任务为什么从运行转为阻塞,为什么重新进入就绪队列,为什么得到或得不到 CPU,以及切换对缓存和延迟造成了什么影响。
遇到性能问题时,先用运行队列、任务状态、用户态与内核态占比判断瓶颈类型,再下钻到具体进程、线程和调用栈。指标只有放在同一时间窗口并结合吞吐量、延迟与资源限制,才有可靠的解释力。
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐

所有评论(0)