子进程、并行调度与 Dyndep

构建执行流程CommandRunner 把”启动/回收一条命令”抽象掉了。本文钻进这层抽象底下:命令到底怎么被起成子进程、怎么并行、怎么和外层 make 协调,以及 dyndep 这种”边构建边发现依赖”的机制。

1. Subprocess:一个异步子进程

struct Subprocesssubprocess.h:38)封装单个异步子进程,它完全被动——靠调用方在 fd 就绪时通知它。公有接口只有四个:Finish()Done()GetOutput()、析构(subprocess.h:43)。

POSIX 关键私有成员(subprocess.h:70):

std::string buf_;        // 累积子进程的合并输出
int fd_;                 // 非 console: 该子进程专属 pipe 读端;console: -1
pid_t pid_;              // 子进程 PID,回收后置 -1
bool use_console_;

1.1 Start:posix_spawn + /bin/sh

尽管头文件注释提到 fork/exec,POSIX 下实际用 posix_spawnsubprocess-posix.cc:56):

  • 建 pipe:非 console 模式 pipe(output_pipe),读端给 fd_,写端作为合并的 stdout+stderr 交给子进程(subprocess-posix.cc:58)。
  • 重定向:通过 posix_spawn_file_actions,把 stdin 接 /dev/nulldup2 写端到 fd 1 和 2(subprocess-posix.cc:113)。
  • 独立进程组:非 console 时加 POSIX_SPAWN_SETPGROUPsubprocess-posix.cc:103),让子进程进自己的进程组,使 Ctrl-C 不会直接打到它——而是由 Ninja 统一处理。
  • 执行:固定 /bin/sh -c <command>subprocess-posix.cc:132),所以命令字符串就是一段 shell。

console 模式不建 pipe、不进独立进程组——它直接共享 Ninja 的终端(典型如交互式命令、需要彩色输出的命令),靠 SIGCHLD + waitpid(WNOHANG) 检测结束。

1.2 输出收集与完成判定

OnPipeReadysubprocess-posix.cc:150)用 4KB 栈缓冲 read 追加到 buf_,读到 EOF 就关 fd 并置 fd_ = -1Finishsubprocess-posix.cc:178)阻塞 waitpid 拿退出码;ParseExitStatussubprocess-posix.cc:186)把被 SIGINT/SIGTERM/SIGHUP 杀死映射成 ExitInterruptedDone()subprocess-posix.cc:214):console 进程退出即完成,普通进程要消费完输出且 pipe 关闭才算完成。

2. SubprocessSet:一组子进程 + 信号

SubprocessSetsubprocess.h:101)围着一组子进程跑 ppoll/pselect 循环。running_ 是在跑的、finished_ 是已完成待回收的队列。WorkResult 枚举按优先级递增排序:NoWork < JobserverTokenAvailable < SubprocFinished < Interruptedsubprocess.h:111),同时发生时取最高。

2.1 信号处理

构造函数(subprocess-posix.cc:252)阻塞 INT/TERM/HUP/CHLD,并装上只设标志位的处理器(interrupted_s_sigchld_received)。这些处理器只在 ppoll/pselect 等待期间生效——关键在于 ppoll 的最后一个参数传 old_mask_subprocess-posix.cc:353),在等待期间原子地解除信号阻塞。这是经典的”自管道/信号安全等待”模式,避免信号在检查和等待之间丢失。

2.2 DoWork

DoWork(ppoll 路径 subprocess-posix.cc:322):为每个有效 fd 建 pollfd,可选追加 jobserver fd,ppoll 等待。返回后先查 console 子进程是否结束,处理待决中断(中断优先于 fd 事件以尽快退出),再遍历有 IO 事件的子进程调 OnPipeReady,完成的移入 finished_NextFinishedsubprocess-posix.cc:476)出队,Clearsubprocess-posix.cc:484)用 kill(-pid_, ...)(负 pid = 杀整个进程组)终止所有子进程。

Windows 实现在 subprocess-win32.cc:用 CreateProcess(不预置 cmd /c,以支持超长命令行,subprocess-win32.cc:111)+ 命名管道 + IO completion port(GetQueuedCompletionStatussubprocess-win32.cc:260),用 SetConsoleCtrlHandler 处理 Ctrl-C。

3. 并行模型:-j、-l 与 console pool

RealCommandRunner::CanRunMorereal_command_runner.cc:60)把三重约束揉在一起:

size_t subproc_number = subprocs_.running_.size() + subprocs_.finished_.size();
int64_t capacity = config_.parallelism - subproc_number;   // -j 上限
if (jobserver_) capacity = INT_MAX;                         // jobserver 模式:交给 token 限流
if (config_.max_load_average > 0.0f) {                      // -l 负载上限
  int load_capacity = config_.max_load_average - GetLoadAverage();
  if (load_capacity < capacity) capacity = load_capacity;
}
if (capacity < 0) capacity = 0;
if (capacity == 0 && subprocs_.running_.empty()) capacity = 1;  // 保证有进度
  • -j N:默认 = 核数 + 2(GuessParallelismninja.cc:256)。
  • -l NGetLoadAverageutil.cc:981 POSIX 用 getloadavg)超过阈值就不再起新命令。
  • console poolkConsolePool 深度为 1(state.cc:65),保证同时只有一条 console 命令在跑——它要独占终端。pool 的延迟/放出机制见 构建图模型 §5

4. Jobserver:和外层 make 协调并发

当 Ninja 被一个更大的 make 构建调用(例如 make 里某个目标是 ninja ...),如果 make 和 ninja 各自按 -j N 起进程,总并发会爆炸。GNU make 的 jobserver 协议用一个跨进程共享的 token 池解决:谁要起一个 job 就先从池里取一个 token,完成后归还。

Jobserverjobserver.h:23)实现了这个协议的客户端。一个硬约束(jobserver.h:185):所有取到的 token 都必须归还,即使 ninja 提前退出

  • 解析 MAKEFLAGSParseMakeFlagsValuejobserver.cc:69)从环境变量识别 --jobserver-auth=,支持 POSIX FIFO(fifo:PATH)和 Windows 信号量两种模式。
  • 取/还 token:POSIX 实现 PosixJobserverClientjobserver-posix.cc)——TryAcquire 首次返回一个 implicit slot(不读 pipe),之后 read 一字节算一个 token(jobserver-posix.cc:47);Release收到的那个确切字节写回 pipe(jobserver-posix.cc:63,符合规范)。
  • 集成进 Plan:每条边带一个 Jobserver::Slot job_slot_graph.h:225)。Plan::FindWorkbuild.cc:162)取到就绪边后先 TryAcquire,拿不到 token 就返回 nullptr(不开工,build.cc:170);Plan::EdgeFinishedbuild.cc:212)归还 token。
  • 等待 token:jobserver 模式下 CanRunMore 返回 INT_MAX,并发完全由 token 限。WaitForCommandOrJobserverTokenreal_command_runner.cc:103)把 FIFO 读端加入 ppoll 监控集,token 可用时返回 jobserver_token_available,主循环据此 continue 回去重试 FindWorkbuild.cc:806)。

5. Dyndep:构建期才知道的依赖

绝大多数依赖在解析 manifest 时就确定了。但有一类场景,连”有哪些输入/输出”都要等前一个构建步骤跑完才知道——经典例子是 Fortran 模块:编译某个 .f90 之前,无法静态知道它 use 了哪些模块、又会生成哪些 .moddyndep 机制处理这种动态依赖。

用法上,一条边声明 dyndep = some_filesome_file 本身是个构建产物,内容是一小段 Ninja 语法描述该边的额外隐式输入/输出。等 some_file 被构建出来后,Ninja 再解析它、把发现的依赖并入构建图。

5.1 数据结构与解析

Dyndepsdyndep.h:30)保存单条边的动态信息:restat_implicit_inputs_implicit_outputs_DyndepFiledyndep.h:42)是 map<Edge*, Dyndeps>

DyndepParserdyndep_parser.cc:33)复用 Manifest 解析Parser 基类,但语法严格受限:必须以 ninja_dyndep_version = 1 开头(dyndep_parser.cc:77,只接受 1.0),build 行只允许隐式输出/输入(|)和 restat 绑定,禁止显式输入、order-only 和额外显式输出dyndep_parser.cc:128)。

5.2 加载与回填

DyndepLoader::LoadDyndepsdyndep.cc:30)解析 dyndep 文件,对每条引用它的边调 UpdateEdgedyndep.cc:81):

  • restat_ 为真则给边加 restat=1 绑定;
  • 追加 implicit outputs 到 outputs_、增 implicit_outs_,并设各新输出的 in_edge(重复生产仍报 “multiple rules generate”);
  • 追加 implicit inputs——插入位置同样在 inputs_.end() - order_only_deps_dyndep.cc:106),维持 构建图模型 §2.1 的三段不变量。

5.3 两个触发时机

Node::dyndep_pending_graph.h:151)标记”期待 dyndep 但未加载”。加载在两处触发:

  1. 图构建期graph.cc:138):脏检查 RecomputeNodeDirty 中,若边的 dyndep 节点已就绪(无 in_edge 或其已 outputs_ready),立即 LoadDyndeps
  2. 构建运行期build.cc:234):Plan::NodeFinished 中,若刚完成的节点 dyndep_pending(),调 builder_->LoadDyndeps 加载这个”刚生成好的” dyndep 文件。

后者随后调 Plan::DyndepsLoadedbuild.cc:327),它会重算受影响边的脏状态、把新可达的子图通过 AddSubTarget 加进计划、并对相关边调 EdgeMaybeReady 检查是否就绪——于是动态发现的依赖被无缝并入正在进行的构建。

6. 小结:行号速查

主题 位置
Subprocess(POSIX 成员) subprocess.h:70
Start(posix_spawn / 进程组) subprocess-posix.cc:56
SubprocessSet::DoWork(ppoll) subprocess-posix.cc:322
信号安全等待(old_mask) subprocess-posix.cc:353
CanRunMore(-j/-l/jobserver) real_command_runner.cc:60
console pool 深度 1 state.cc:65
Jobserver 协议 jobserver.h:23
token 取/还(FindWork/EdgeFinished) build.cc:170 / build.cc:212
Dyndeps / DyndepFile dyndep.h:30 / dyndep.h:42
UpdateEdge(回填) dyndep.cc:81
两个触发点 graph.cc:138 / build.cc:234
DyndepsLoaded build.cc:327

至此,从 build.ninja 文本到并行执行命令的完整链路就走完了。回到 架构概述 可重温全景,或从 目录 跳转任意章节。


本站访问量

This site uses Just the Docs, a documentation theme for Jekyll.