make -j 竞态偶发挂速查:生成头文件依赖缺失与 order-only 依赖
更新时间:2026-10-24。FAQ 速查页,面向被「
make -j8偶发挂、重跑一次就好、串行永远没问题」折磨的同学。这类报错的本质是:Makefile 里有一条没写出来的依赖,串行执行时靠巧合的顺序掩盖了它,并行调度把真实顺序打乱后,某个任务在输入还没就位时就开跑了。病因收敛在五根轴上:代码生成器产出的头文件没有声明「谁先谁后」、多个编译任务并发mkdir同一目录互相踩踏、一条规则多个 target 被并行调度拆开执行、该用 stamp 接力的地方直接引用了中间产物、以及递归 make 之间抢同一份中间文件。顺一遍基本必中。更广义的「时好时坏」还可对照 make 偶发不确定报错速查。
本文要回答的问题
make -j报No such file or directory但文件明明会生成,为什么?- 为什么重跑一次就成功,能当成没问题吗?
- 生成头文件的正确依赖写法是什么?
order-only依赖(|)解决什么问题,什么时候必须用?- 一条规则产多个文件,并行下有什么坑?
一、报错原文与典型现象
报错形态五花八门,但模式高度一致:偶发、重跑即好、串行不现(路径以 /proj/demo 占位):
# 形态一:编译时找不到生成头文件
gcc -c /proj/demo/src/net.c -o build/net.o
/proj/demo/src/net.c:12:10: fatal error:
gen/config.h: No such file or directory# 形态二:目录还没建出来就往里写
gcc -c /proj/demo/src/util.c -o build/obj/util.o
Assembler messages:
Fatal error: can't create build/obj/util.o:
No such file or directory# 形态三:归档/链接时成员还没编完
ar rcs build/libcore.a build/a.o build/b.o
ar: build/b.o: No such file or directory读现象抓三个指纹:第一,报错里缺的文件是「本应由本次构建生成的」——gen/config.h、build/obj/util.o 都不是源码,是构建产物;第二,不带 -j 或 -j1 重跑必过;第三,失败后原地再跑一次 make 大概率成功(因为上一次已经把缺的文件补出来了)。三个指纹齐了,基本就是依赖声明缺了,不是代码问题。
二、病因处置速查表
| 报错现象 / 场景 | 可能原因 | 快速验证 | 处置 |
|---|---|---|---|
| fatal error: gen/config.h 找不到,偶发 | 生成头文件没进依赖列表 | make -j1 必过;make -j16 复现率高 | 把生成头声明为使用方的先决条件 |
| can't create build/obj/xx.o,偶发 | 输出目录无人负责创建,多任务并发 mkdir | 看目录是否在报错后存在 | 目录做成 order-only 依赖或 stamp |
| 规则产 a 和 b,并行下 b 偶发缺失 | 多 target 规则被调度拆成两次执行 | 日志里同一 recipe 跑了两次 | 改用分组目标(&:)或 stamp 接力 |
| ar 报成员 .o 不存在,偶发 | 归档规则依赖写漏了个别 .o | 对照 Makefile 的依赖列表 | 用变量统一对象列表,禁止手写散落 |
| 子目录 make 与父 make 抢同一文件 | 递归 make 各自调度同一中间产物 | 日志里同一文件被两进程写 | 中间产物收口到一层,或加 stamp 锁 |
| 加 -j 就挂、去掉就好,原因不明 | 综合竞态 | make --shuffle -j8 放大复现 | 用 shuffle 模式批量暴露缺失依赖 |
三、排查决策图
下面这张图是标准排查路线,颜色按角色区分:红是报错入口与命中的根因,蓝是排查动作,绿是通过分支,橙是修复动作,紫是防复发沉淀。
读这张图抓分流点:make -j1 必过先把「竞态」钉死,然后看报错里缺的文件类型分流——缺的是生成的头文件走依赖补声明,缺的是目录走 order-only,缺的是多目标规则的某个产物走分组目标或 stamp。三股最后都汇到「-j16 连跑十次全过」这个验收点,一次成功不算修好。
四、逐条排查展开
生成头文件缺依赖是 make -j 事故的头号主角。 典型场景:gen/config.h 由代码生成器(protoc、yacc、自研脚本)产出,net.c 里 #include "gen/config.h",但 Makefile 只写了 net.o: net.c,没写 net.o: gen/config.h。串行时生成器规则碰巧排在前面,天下太平;并行时 make 同时调起「编译 net.c」和「跑生成器」,编译先跑就报头文件不存在。修法一句话:谁 include 谁依赖——net.o: net.c gen/config.h。头文件被很多源文件用时,更省事的写法是让所有 .o 统一依赖一个 stamp:$(OBJS): gen/config.stamp,stamp 的 recipe 负责跑生成器再 touch 自己。CMake 工程同理,用 add_custom_command 产出头文件 + add_custom_target + add_dependencies 钉顺序,别指望 add_dependencies 对文件级生效——它只管 target 级。
目录创建竞态是二号主角,order-only 依赖就是为这个场景设计的。 老式写法在每条规则的 recipe 里 mkdir -p $(dir $@),并行下多个 gcc 同时 mkdir 同一目录,多数时候 mkdir -p 幂等无害,但碰上「一个进程刚 mkdir 完、另一个进程的父目录检查与创建交错」的窗口(尤其在某些文件系统上)就会偶发失败;更常见的失败是输出目录根本没人建,gcc 直接报 can't create。正确姿势是把目录声明为 order-only 先决条件:
build/obj/util.o: src/util.c | build/obj
build/obj:
mkdir -p $@| 右边是 order-only 依赖:目录存在且比目标旧也不触发重建(目录的 mtime 会因里面文件增删而变化,放左边会导致全量重编),只在目录不存在时先建。这一字之差是无数 Makefile 的隐伤。
多目标规则在并行下被拆开的坑专坑老 Makefile。 a.txt b.txt: gen.sh 这种写法,GNU make 传统语义是「这条规则跑一次同时产出两个文件」,但并行调度时如果 a 和 b 被两个不同目标分别需要,旧版 make 可能把 recipe 跑两次,两个进程同时写两个文件互相覆盖。GNU make 4.3 起用分组目标 a.txt b.txt &: gen.sh(注意是 &:)显式声明「一次执行产全部」。老版本只能用 stamp 接力:规则产 gen.stamp,a.txt b.txt: gen.stamp 里从 stamp 目录拷贝。CMake 里对应 add_custom_command(OUTPUT a b) 天然正确,问题多出在纯手写 Makefile。
递归 make 竞合是大型工程的老大难。 顶层 make 调子目录 make,父子两级各自调度,同一个中间产物(如公共头文件包)被两级规则同时生成。表现是偶发 truncated 或半旧半新的文件,与 目标文件损坏速查 的并行互踩病因同源。处置:中间产物的生成权收口到唯一一层,其他层只做依赖消费;跨目录共享用绝对路径加 stamp。
「重跑一次就好」绝不能当成没问题。 竞态的可怕之处不在偶发挂,而在偶发「挂得悄无声息」:某次并行调度下生成器输出被读了半截,编译却碰巧通过,产出的二进制带着陈旧配置进了仓库。所以只要确认过一次 -j 竞态,处置就不是「重跑」而是「补依赖」,否则每次 CI 都在掷骰子。
放大复现是排查竞态的标准动作。 竞态问题最大的敌人是「改完跑一次过了,以为修好了」。两个放大器:一是把并行度拉满 make -j$(nproc) 甚至超调 -j32;二是 GNU make 4.4 起的 --shuffle 选项,每轮随机打乱就绪任务的执行顺序,make --shuffle=reverse -j8 连跑十轮,缺失依赖几乎无所遁形。CI 里固定留一个 shuffle 任务,是新规则合入的最好试金石。
五、规范做法与预防
- 生成头文件统一走 stamp:所有代码生成产物挂一个
.stamp目标,全部编译任务 order-only 依赖它,新增生成器时模板化。 - 目录一律 order-only:输出目录只出现在
|右边,禁止 recipe 里散落mkdir -p当补丁。 - 多目标用
&:分组:make 4.3+ 工程把老式多目标规则全部改写为分组目标;老版本统一 stamp 接力。 - 对象列表单一来源:
.o列表用变量定义一次,链接、归档、清理共用,禁止多处手写。 - CI 加 shuffle 任务:
make --shuffle -j$(nproc)作为独立任务每晚跑,竞态缺陷在合入后一天内现形。 - 新规则评审问一句:这条规则的输入里有没有「本次构建才产生的」?有就必须进依赖列表。
这一篇在知识体系里的位置
本篇是 Make/CMake 排错速查簇的「并行竞态」入口篇:竞态导致两个进程写同一个 .o 产出坏文件的处置看 目标文件损坏 file truncated 速查;「时好时坏」但不止并行才出现的更大范围排查看 make 偶发不确定报错速查;竞态修好后还有头文件找不到的路径配置问题看 make 路径找不到速查。「-j1 定性 → 看缺什么分流 → shuffle 验收」这条主线对一切并行偶发类报错通用。
一句话总结
make -j 偶发挂先用 -j1 必过确认竞态,再看报错里缺什么分流:缺生成头文件就给它补显式依赖(谁 include 谁依赖,批量场景用 stamp),缺输出目录就改 order-only 依赖(| 右边),缺多目标规则的产物就改 &: 分组或 stamp 接力,递归 make 抢文件就把生成权收口一层。修完别信一次成功,make --shuffle -j 连跑十轮全过才算数,CI 里留一个 shuffle 常驻任务,竞态类报错基本绝迹。