Linux 进程线程与调度观测:线程为什么没有变快
结论基线:Linux 语义依据 proc_pid_status(5)、proc_loadavg(5)、getrlimit(2) 与内核 cgroup v2 文档(查询于 2026-10-25),JVM 实现固定 OpenJDK
jdk-17.0.12+7;本机实测在 macOS 26.7.1 x86_64 + Oracle JDK 17.0.12+8 完成(行为差异逐处标注)。本页与 cgroup 配额正文分界:那边讲配额与容器记账,本页讲进程/线程/task 的观测、调度类与线程创建受限;TCP/HTTP 等协议观测归TCP 正文。没有 Docker 的机器无法实测 Linux cgroup/节流,对应段落全部标注推导。
问题:一次请求变慢后,OS 侧先回答什么
线程池耗尽、锁竞争、GC 停顿和调度受限在应用指标上都是"P99 高"。OS 侧观测要先分三类:有没有被运行(运行队列/时间片)、有没有在等内核(不可中断 I/O、页、锁)、有没有资格运行(配额、优先级、亲和性、限制)。cgroup 正文已给出"调度实体不等于业务请求"的口径;本页回答三个后续问题:标识怎样对上(TID/nid)、负载在数什么、线程创建失败是谁拒绝的。
task、线程与标识:TID、nid 与 namespace 映射
Linux 调度和记账单位是 task:一个进程是共享地址空间的 task 组,gettid() 返回的 TID 在线程自己的视角里也是"线程组长的 PID"。观测对不上的根源通常有三个:
| 标识 | 出处 | 常见误用 |
|---|---|---|
TGID(top/ps 的 PID) | /proc/PID/status 的 Pid | 拿它去匹配线程栈 |
TID(top -H/ps -eLf 的线程列) | /proc/PID/task/TID | 与 Java Thread.getId() 混为一谈 |
nid(jstack 线程行的 nid=0x…) | 平台 native 线程 id 的十六进制 | 当成 PID 或 Java id |
Linux 上 nid 是 OS TID 的十六进制,转换规则是"同 namespace、同主机、TID 转十六进制";proc_pid_status(5) 的 NStgid/NSpid 字段列出每层 namespace 里的编号,跨容器先核对再换算。本机实测(macOS 26.7.1,Oracle JDK 17.0.12+8):8 条驻留线程的探针 Thread.activeCount()=9,jstack 解析出 27 条 nid= 记录——JVM 自己的 GC/JIT/Signal Dispatcher 线程也在列表里,nid 计数不等于 Java 线程数(红7);且 macOS ps -M 只给线程的 CPU/优先级列、没有线程 id 列,nid↔OS id 转换在 macOS 不可用,Linux 上的等价转换按 jstack 文档与 proc(5) 推导标注。工程含义:先把"哪个线程"固定下来(TID/nid/名称三选二),再谈 CPU 与栈证据;JVM 诊断正文的 Thread.print 给 Java 侧视角,两边证据同窗口采集才能互相印证。
负载与运行队列:loadavg 在数什么
proc_loadavg(5):前三个字段是"运行队列中(R 状态)或等待磁盘 I/O(D 状态)"的作业数在 1/5/15 分钟的指数平均。由此两条直接结论:负载高不等于 CPU 忙(D 状态也算);指数平均意味着压测 30 秒只挪动 1 分钟线的尾部。本机 iostat -c 3 -w 1 实测(macOS):load average 三列与 KB/t、tps、MB/s 同屏输出,macOS 的负载口径与 Linux 不同(不含 Linux 语义的 D 状态作业),跨平台把 loadavg 当同一指标是错的。
| 信号 | 读法 | 不能推出的结论 |
|---|---|---|
| loadavg 持续 > 核数 | 队列有积压(R 或 D) | CPU 是瓶颈,也可能在等盘 |
vmstat 的 r 列高、us 高 | CPU 需求超供给 | 与 cgroup 节流的区别要靠 cpu.stat |
| b 列高(不可中断) | 等内核 I/O,kill -9 无效 | 不是应用死锁,kill 无法恢复 |
| cs(上下文切换)高 | 让出频繁 | 主动等待与被抢占要再分;切换多不是独立病因 |
判定"是不是调度受限"用差量证据:同窗口比较 pidstat -u -w -t 的线程 CPU 与切换、cgroup cpu.stat 节流计数(cgroup 正文)、应用侧请求耗时;Linux 的 PSI(/proc/pressure/{cpu,memory,io} 的 some/full avg10/avg60/avg300)给出"任务因等资源而停顿的时间占比",是负载与利用率之外的第三种读数(查询 2026-10-25;本机无 /proc/pressure,未实跑)。
优先级与调度类:nice、CFS 与 EEVDF 的版本边界
nice 值(-20..19)是竞争时的权重,不是带宽配额:nice -n 10 的进程在系统空闲时仍能用满 CPU,只在竞争时让出份额。这与 cgroup cpu.max 的硬上界、cpu.weight 的相对权重分属三层,混着说"调大 nice 限流"会得出错误结论。JVM 不会替你管理 nice:HotSpot 只在创建内部线程时设 Java 优先级到 OS 优先级的映射,业务线程默认继承启动时的 nice(可用 ps -o ni= -p PID 观察)。
调度类版本边界:Linux 6.6 起 CFS 被 EEVDF 取代(内核文档:"began transitioning to EEVDF in version 6.6",查询 2026-10-25),虚拟截止时间改变的是公平性与尾延迟形态,不改变 cgroup 带宽配额语义;部署内核 <6.6 与 ≥6.6 的延迟归因不能互相照搬。JVM 看到的核数从亲和性来:
原码连续摘录(仅统一公共缩进,截取函数局部):os_linux.cpp,jdk-17.0.12+7,L4781–L4795。
// pid 0 means the current thread - which we have to assume represents the process
if (sched_getaffinity(0, cpus_size, cpus_p) == 0) {
if (cpus_p != &cpus) { // can only be true when CPU_ALLOC used
cpu_count = CPU_COUNT_S(cpus_size, cpus_p);
}
else {
cpu_count = CPU_COUNT(cpus_p);
}
log_trace(os)("active_processor_count: sched_getaffinity processor count: %d", cpu_count);
}
else {
cpu_count = ::sysconf(_SC_NPROCESSORS_ONLN);
warning("sched_getaffinity failed (%s)- using online processor count (%d) "
"which may exceed available processors", os::strerror(errno), cpu_count);
}sched_getaffinity 统计的是当前进程有效亲和掩码里的核数;失败时回退在线核数,源码自己警告"可能大于可用核数"。容器里再叠加 cgroup 配额的最小值选择,链路见 cgroup 正文的 JVM 节。taskset/numactl 改亲和性后 JVM 的核数认知随之变化——线程池大小按这个数配,而不是按宿主机核数。
受限的线程创建:native thread 耗尽的三重来源
OutOfMemoryError: unable to create native thread 不是堆 OOM,是内核或 JVM 拒绝了新线程。Linux 上三个约束各自独立:
- 数量限制:cgroup
pids.max(cgroup v2 文档:"Hard limit of number of processes",命中记入pids.events的 max 计数、fork/clone 返回 -EAGAIN,查询 2026-10-25);或 getrlimit(2) 的RLIMIT_NPROC——注意它按真实 UID 统计进程(Linux 上含线程),fork 报 EAGAIN。/proc/PID/limits可查该进程实际生效值(proc_pid_limits(5))。 - 内存:每个线程栈是真实内存承诺(虚拟预留 + 触页后驻留),
-Xss与pids.max的组合决定账单;NMT 正文的 Thread 类别给出 JVM 视角。 - JVM 自身:
-XX:MaxRAMPercentage时代的 maxThread 边界与虚拟线程时代不同(虚拟线程正文)。
本机实测(macOS 26.7.1,task 级上限 kern.num_taskthreads=4096):探针目标 5000 线程,创建到 4069 后 pthread_create 返回 EAGAIN,JVM 报 unable to create native thread,~5.5 秒完成(红绿与阈值见实验)。它演示的是"任务级/用户级数量上限拒绝线程创建"这一形态,等价于 Linux pids.max 的拒绝路径;不等价于 RLIMIT_NPROC(按 UID 统计,macOS 线程不进该计数)与栈内存耗尽(本实验 -Xss512k,虚拟预留未触顶)。pids.max/RLIMIT_NPROC 行为按上述文档推导,未在 Linux 实跑。
实验:进程线程与调度观测
公开实验入口的 thread 模式复现上述受限场景;fd/mem/perm/disk/signal 模式分别对应 信号权限页与内存文件磁盘页的实验节。运行策略与清理协议见 README;所有数量断言只压方向阈值,不把单机观察值当契约。
架构师推演:线程数与核数预算(算例,非实测)
设服务 JVM:亲和性 8 核、cgroup 配额 4 核(cpu.max = 400000 100000)、容器 pids.max = 512、-Xss1m。三条账单互相独立:并行度上界 min(亲和 8, 配额 4) = 4(预算模型);线程数上界 ≈ 512 − JVM 内部线程(~25)− 余量,业务池配置 400 尚可、500 会把拒绝挪到线程创建;栈内存 400 × 1MiB ≈ 400MiB 驻留承诺,要并入容器内存预算(内存正文)。把线程数从 400 提到 800 的收益只有在瓶颈是"线程不够"时才存在——先用同窗口 TID 差量与队列证据确认,否则只是把节流和上下文切换放大。恢复责任:数量触顶是编排/运维层改配额,栈不足是 JVM 参数层,两者不是同一个变更窗口。
闭卷验收
本页 8 道闭卷题收进了下方自测面板:先回忆,再展开参考答案与评分点,最后给三档自评。
来源与版本
- proc_pid_status(5)、proc_loadavg(5)、proc_pid_limits(5)、getrlimit(2)(man-pages,查询 2026-10-25):TID/NSpid、loadavg R+D 语义、RLIMIT_NPROC 按真实 UID 计线程与 EAGAIN。
- 内核 cgroup v2 文档 pids 控制器与 PSI、EEVDF(查询 2026-10-25):pids.max 语义、/proc/pressure 字段、6.6 调度器边界。
- OpenJDK
jdk-17.0.12+7os_linux.cppL4781–L4795(本页摘录):亲和性计数与回退警告;本机运行为 Oracle JDK 17.0.12+8,构建号不同如实分记。 - 本机实验:Oracle JDK 17.0.12+8-LTS-286、macOS 26.7.1 x86_64,见实验 README 与证据记录;macOS 无 Linux /proc/PID/task、cgroup、/proc/pressure,对应结论全部标注推导未实跑。