make 编译内存超限 cc1plus 被 Killed 速查
更新时间:2026-10-24。FAQ 速查页,面向用 GNU make 驱动 C/C++ 构建的同学。make 编译内存超限指编译进行到一半,编译器进程(
cc1plus/cc1/ld)被系统强行杀掉,只留下一句Killed signal terminated program cc1plus和 make 的Error 4汇总行。这不是代码错误,是内存账没算平:要么-j并行度开得超出物理内存承受力,要么单个翻译单元(重度模板、LTO)本身就是内存怪兽,要么容器 cgroup 限额比物理内存更早勒死进程。本篇以报错原文为锚逐条定位;如果报错是internal compiler error: Segmentation fault而非Killed,请转 make 编译器内部崩溃速查;如果是链接期符号缺失,请转 undefined reference 链接错误速查。
本文要回答的问题
Killed signal terminated program cc1plus是谁下的手?make -j开多大才和内存匹配,有没有可算的公式?- 单个
.cpp文件也能把 16GB 内存吃光吗? - Docker / CI 容器里内存明明够,为什么还是被 Killed?
- swap、zram 该不该加,加多少?
一、报错原文与典型现象
最典型的现场(工程路径以 /proj/demo 占位),-j 开大后编译到一半暴毙:
$ make -j16
g++ -O2 -c src/render.cpp -o build/render.o
g++ -O2 -c src/scene.cpp -o build/scene.o
g++: fatal error: Killed signal terminated program cc1plus
compilation terminated.
make: *** [Makefile:31: build/render.o] Error 4链接阶段的同款死法,死者换成了 ld:
collect2: fatal error: ld terminated with signal 9 [Killed]
compilation terminated.
make: *** [Makefile:58: bin/app] Error 1读懂现场抓住三件东西:谁死了——cc1plus 是 C++ 编译前端、cc1 是 C、ld/collect2 是链接器,被点名的那个进程才是内存大户;信号 9 即 SIGKILL——用户态程序不会给自己发 9,几乎百分百是内核 OOM killer 或 cgroup 限额越界后的强杀;是否可复现——固定编到某文件必死,是该文件本身吃内存;死的位置随机漂移,是并行总账超支。实锤证据在 dmesg:内核会把每次 OOM 选 victim 的过程完整记账。
二、病因处置速查表
| 报错现象 / 场景 | 可能原因 | 快速验证 | 处置 |
|---|---|---|---|
Killed signal terminated program cc1plus,仅 -j 大时出现 | 并行总内存超物理内存,OOM killer 出手 | dmesg -T | grep -i "killed process" 见 cc1plus | -j 降到 可用内存GB÷2;或 make -j16 -l8 限负载 |
固定某个 .cpp 必被 Killed,-j1 也死 | 单文件模板/constexpr 实例化内存爆 | /usr/bin/time -v g++ -c x.cpp 看 Maximum resident set size | 拆文件、显式实例化外移、该文件单独降 -O1 |
| 物理内存够但容器内编译被杀 | cgroup memory.max 限额先于 OOM 触发 | cat /sys/fs/cgroup/memory.max 与 dmesg 无记录 | 调大容器限额或按限额反推 -j |
ld terminated with signal 9,开了 -flto | LTO 链接把全程序 IR 载入内存 | 关 -flto 重链成功即实锤 | -flto=4 限并行、换 mold/lld、减 -g 为 -g1 |
| 无 swap 的小内存机,大文件偶发被杀 | 无缓冲垫,内存毛刺直接触发 OOM | free -h 的 Swap 行为 0 | 加 swap 文件或启用 zram |
32 位容器/交叉环境报 virtual memory exhausted | 地址空间耗尽而非物理内存 | 报错原文是 virtual memory exhausted | 换 64 位工具链;-fno-fat-lto-objects 减占用 |
报错为 Resource temporarily unavailable 伴编译失败 | 进程数/内存 ulimit 限制 | ulimit -a 看 max processes 与 virtual memory | 调整 ulimit 或 systemd 单元的 LimitAS/TasksMax |
三、排查决策图
下面这张图按「死的是谁、死在哪种内存边界上」分岔:红是报错入口与命中的根因,蓝是排查动作,绿是验证通过,橙是修复动作,紫是防复发沉淀。
读这张图抓一条主线:先拿 dmesg 实锤,再按「死点是否固定」分流。死点漂移(这次 render.cpp 下次 scene.cpp)是并行总账问题,药方是降 -j;死点固定是单文件问题,药方是拆文件降优化;dmesg 干干净净却照死,多半是 cgroup 在用户态静默限额,去查 /sys/fs/cgroup/memory.max。三类修复后都要连续复编几次确认峰值稳定——内存问题最怕「碰巧过了」。
四、逐条排查展开
先让 OOM killer 现形。 内核内存耗尽时按 oom_score 挑最肥的进程杀掉,编译期被杀的几乎总是 cc1plus:
dmesg -T | grep -iE "out of memory|killed process"
# 典型输出:Out of memory: Killed process 48213 (cc1plus) total-vm:6.8G anon-rss:5.9G
journalctl -k --since "1 hour ago" | grep -i oom # systemd 系统total-vm 与 anon-rss 两列就是它死前吃下多少内存的实账。只要 cc1plus 出现在名单里,根因锁死,不用再怀疑代码。
-j 与内存的配比公式。 一个 cc1plus 编普通 C++ 文件峰值 0.5-1.5GB,重度模板(boost、Eigen、大型 constexpr 表)单进程能吃 3-6GB。经验公式:-j 上限 ≈ 可用内存 GB ÷ 2。8GB 内存开 -j16 必崩不是编译器不行,是数学不行。比死 -j 温和的做法是负载阈值:make -j16 -l8(负载超 8 不再派新作业),机器忙时自动收;最稳的是构建脚本按 nproc 与 内存GB/2 取小者:
JOBS=$(nproc); MEMJ=$(($(free -g | awk '/Mem:/{print $7}')/2)); [ $MEMJ -lt $JOBS ] && JOBS=$MEMJ; make -j$JOBS单文件内存怪兽的治理。 判据是 -j1 单独编它也死。先用 /usr/bin/time -v g++ -O2 -c huge.cpp -o /dev/null 量出真实峰值(看 Maximum resident set size),再按峰值选招:模板显式实例化声明挪到头文件、定义集中到一个 .cpp(其它 TU 只链接不展开);巨型 constexpr 表改运行期查表;该文件在 Makefile 里单独一条规则降 -O1(很多内存爆发生在特定优化 pass);头文件依赖太胖的用 Pimpl 割断。
容器 cgroup 限额这条暗线。 Docker --memory=4g 或 K8s limits 生效时,进程组总内存越界会被 cgroup 直接杀,而且较新内核的记账未必进 dmesg——典型假象是「宿主机内存很空、容器里编译照死」。核验:
cat /sys/fs/cgroup/memory.max # cgroup v2;v1 在 memory.limit_in_bytes
cat /sys/fs/cgroup/memory.current处置两选一:调大限额,或按限额而不是物理内存反推 -j。CI 镜像里把这两个值打进构建日志,排错不用猜。
链接阶段的内存账另算。 死者是 ld 时,凶手通常是 LTO:-flto 把全程序 IR 载入内存做跨 TU 优化,大工程链接峰值可达十几 GB。药方按命中率排序:-flto=4 限 LTO 并行分区数;换内存友好的链接器 mold 或 lld(同工程峰值常降一半以上);调试信息 -g 降为 -g1;静态库合并链接时确认没有重复归档。
地址空间耗尽是另一种长相。 报错不是 Killed 而是 virtual memory exhausted: Cannot allocate memory 时,先怀疑 ulimit 与 32 位地址空间:ulimit -v(virtual memory 上限)在很多 CI 镜像里被刻意压低;32 位容器里 cc1plus 单进程地址空间上限只有 3GB 量级,大模板文件很容易顶穿。核验 ulimit -a 与 getconf LONG_BIT,处置是放开 ulimit 或换 64 位工具链。systemd 管的构建服务还要留意单元文件里的 LimitAS= 与 TasksMax=,它们也会在 dmesg 不留痕的情况下让编译失败。
swap 与 zram 是缓冲垫不是地基。 无 swap 的机器上内存毛刺直接触发 OOM,加一个 swap 文件(fallocate -l 4G /swapfile && mkswap && swapon)或启用 zram(压缩内存盘当 swap,比磁盘 swap 快一个量级)能吸收瞬时尖峰。但长期峰值远超物理内存时 swap 只会把「被杀」换成「卡死」——治本仍是降 -j 或拆文件。
五、规范做法与预防
- 并行度写进构建脚本并动态算:禁裸
make -j$(nproc),按min(nproc, 可用内存GB/2)取;容器内先读 cgroup 限额再算。 - CI 留环境指纹:构建日志开头打
free -h、df -h、cat /sys/fs/cgroup/memory.max、g++ --version四行快照。 - 巨型翻译单元设门禁:单文件编译峰值超 2GB 的进台账,拆文件或单独降优化,不让它悄悄越长越胖。
- swap/zram 常备:构建机至少配 内存×0.5 的 swap 或 zram,专吃瞬时毛刺。
- 监控 oom 计数:
grep -c oom /var/log/messages或 node_exporter 的node_vmstat_oom_kill指标进告警,被杀一次就发现一次,而不是等用户报障。
这一篇在知识体系里的位置
本篇是 Makefile 排错速查簇的内存资源篇,定位「编译器被系统因内存杀掉」:编译器自尽式段错误的看 make 编译器内部崩溃速查;构建卡死不动的看 make 构建卡死无响应速查;类型报错的看 error: cannot convert 类型不匹配速查;链接符号问题的看 undefined reference 链接错误速查;重复定义冲突的看 multiple definition 重复定义速查;CMake 依赖查找失败的看 CMake 找不到依赖包速查;语法解析、头文件路径、权限、规则缺失、乱码、版本与编译器探测等其它岔路见 Makefile missing separator 速查、fatal error 头文件找不到速查、构建 Permission denied 速查、make: No rule to make target 速查、make 构建输出乱码速查、CMake 版本不兼容速查、CMake 找不到编译器速查、CMake FetchContent 拉取失败速查。「先 dmesg 查 OOM 名单再谈其它」这一招对所有进程莫名被杀类故障通用。
一句话总结
make 编译内存超限先拿 dmesg 实锤 OOM 名单:死点漂移就是并行总账超支,-j 降到 可用内存GB÷2(或 make -jN -lM 限负载);死点固定就是单文件模板怪兽,量出峰值后拆文件、显式实例化外移、单独降 -O;dmesg 干净却照死必查 cgroup memory.max;链接期 ld 被杀则 -flto=4、换 mold、-g 降 -g1。小内存机常备 swap/zram 当缓冲垫,CI 日志留内存与版本快照,这类「灵异被杀」就降格成了例行算术题。