编程语言研究书库

第 7 章拆解 — 线程、进程与信号:zcron 的组装之路

线程、进程与信号:zcron 的组装之路

这一章讲什么: 单个进程之外的世界。信号怎么被「故意」用起来(不杀进程,而是查它的状态); fork 一次调用两次返回;exec 把子进程的内存整个换成另一个程序;线程是进程内共享内存的轻量执行路径。 收尾项目 zcron——一个迷你 cron——把四块积木全部用上,组装成一个能脱离终端、能热重载、不攒僵尸的常驻服务。

主线例子:zcron 的一次 tick。书上给了一组带真实时间戳的推演:now = 1741812000 时, 三个作业各走哪条路、谁被执行、谁被跳过,数字全程可对账。

1. 顶层全景:zcron 的一张架构图

  信号层   SIGTERM ─→ g_running=false(优雅停机)
           SIGHUP  ─→ g_reload=true(热重载)      ← handler 只做原子写,别的一律不干
           SIGCHLD ─→ while waitpid(WNOHANG)(收尸循环)

  循环层   poll(stdin, 1000ms)                    ← 单线程心跳:stdin 可读 → REPL;超时 → tick()
           tick(): 扫一遍作业,isDue(now)? → spawnJob()
           spawnJob(): fork() → 父进程立即返回,子进程 execve("/bin/sh -c 命令")
           exec 失败 → _exit(127)(不冲父进程继承来的缓冲区)

  结构层   schedule.zig(纯数据,isDue 逻辑,6 个单测)  config.zig(配置文件→Schedule)
           repl.zig(六条命令)  scheduler.zig(引擎)  signals.zig(三个 handler)  main.zig(纯装配)

  图说:每个文件只拥有一件事;schedule.zig 无 I/O 无系统调用,所有单测完全隔离地跑。
  书的设计宣言:zcron 的架构偏向简单和清晰,选择「演示健壮系统编程」的模式,而不是性能。[^1]

2. 核心原理

2.1 信号当作查询接口:原子标志模式

信号最常见的出场是被杀掉(SIGINT/SIGTERM),这一章先演示「故意用信号」:给进程注册 SIGUSR1/SIGUSR2 两个用户自定义信号的 handler——收到 SIGUSR1 就刷新一个随机数,收到 SIGUSR2 就报告运行时长,不重启、不重编译、不看日志,外部随时问进程内部状态1。

关键约束在 handler 里能干什么:信号 handler 可以在任意两条指令之间插入执行,POSIX 严格限制 handler 内可调用的函数——禁止 malloc、printf、绝大多数锁。所以 handler 只做一个原子写(把布尔标志置真),真正的活留给主循环:主循环每 2 秒醒一次,用 load(.seq_cst) 查标志,置真就干活、清零。共享变量一律 std.atomic.Value,handler 里 .monotonic 写、主循环里 .seq_cst 读——并发信号递送绝不产生撕裂读写2。实测:跑起来打印 PID 7206,另一个终端 kill -USR1 7206,这边输出 [SIGUSR1] Current random value: -18200547013。

2.2 fork:一次调用,两次返回

进程是独立的执行环境(自己的内存空间);线程是进程内共享内存的轻量执行单元。fork(2) 造子进程的方式是复制自己:调用一次、返回两次——父进程里返回子进程的 PID,子进程里返回 0,负数是失败4。

线程与进程的取舍,书给的经验法则值得背:要紧密共享数据、性能优先——用线程;要隔离、容错、跑不同可执行文件——用进程。进程隔离的代价是创建贵(fork 要复制页表和文件描述符表),回报是一个进程崩了不会拖死另一个;线程创建便宜(只要新栈和线程控制块),但任何一个线程空指针解引用会带崩整个进程。通信同构:线程直接共享变量(配互斥锁/原子),进程必须走 IPC——管道、套接字、信号5。

fork 之后的义务:waitpid(2) 不是可选的。不收割,内核就在进程表里留着子进程的壳——僵尸进程,ps 里显示 <defunct>;反复 fork 不收尸的长跑服务最终会耗尽进程表6。答案是 SIGCHLD handler(第 2.6 节)。

2.3 线程:spawn 与 join 的生命周期

std.Thread.spawn(.{}, func, .{args}) 三参数:配置(默认)、函数、参数元组;创建可能因系统资源限制失败,所以带 try。join() 是同步点:主线程等它等到完——没有 join,main 可能先走到头、整个进程退出,后台线程连打印的机会都没有7。

非确定性的现场教学:跑两次 nThreads -- 4,输出一次是 0 1 3 2,一次是 0 3 1 2——代码里 spawn 的顺序是确定的,但 OS 调度器把哪个线程放上 CPU 的顺序不归你管;唯一保证的是最后那行(所有 join 都返回之后才打印)永远在最后8。

std.process.run() 是第四种「造执行流」:它 spawn 子进程、等它结束、把 stdout/stderr 抓成堆上字符串,一次调用全包;返回的 term 是个联合体(.exited/.signal/.stopped/.unknown),UNIX 惯例 0 是成功、非零是错误——书上用 ls ./doesNotExist(必失败)和 uptime(必成功)对照演示两条路径9。

2.4 并发 TCP:服务器一连接一线程,客户端压测

上一章的服务器是同步的——一个连接没伺候完,新的只能排队。并发版的两处改动:accept 循环里每接受一个连接,Thread.spawn(.{ .allocator = allocator }, handleConnection, .{io, stream, thread_num}),然后 detach()——「发射后不管」:主循环不等任何工作线程,保持对新连接的响应;连接计数用全局原子计数器 fetchAdd,比互斥锁便宜。spawn 失败也有兜底:手动 close 那个客户端流再 continue——一个客户端的局部故障不许拖垮整个服务器10。

配套的并发客户端是一次现成的压测:concClient 127.0.0.1 1234 10 起 10 个线程各连一次、各发 ping from client #N、各收回声。实测输出 10 行,顺序乱(3、0、4、7、9、8、5、1、2、6),但每条消息都回到了正确的发送者手里——服务器端每连接一个独立线程 + 原子计数分号,会话互不串味11。

2.5 单线程事件循环:为什么 zcron 不用线程

设计决策一:留在前台,用 poll() 当心跳。一个朴素的双线程替代方案是「一线程睡觉发作业,一线程阻塞在 stdin.readLine()」——但两个线程立刻引入共享可变状态,每次 tick、每个列表操作、每次重载都要互斥锁保护。poll(stdin_fd, 1000ms) 把两件事合成一件:进程完全休眠,直到「stdin 来了字符」或「一秒到了」二选一,然后单线程顺序处理——同步问题整个消失。书点明这是系统编程的基石模式,Redis 和 Nginx 的事件循环就是这个形状12。

2.6 信号处理三件套:原子标志、SIGCHLD 循环、SA_RESTART

zcron 的信号层管三个信号,每个都对应一个真实事故:

  • SIGTERM → 优雅停机;SIGHUP → 热重载——把 SIGHUP 分给重载是深根的 UNIX 惯例(当年表示调制解调器掉线,如今是「重读配置」的标准信号,kill -HUP 是运维的肌肉记忆)13。
  • SIGCHLD → 防僵尸的收尸循环。子进程结束时内核把退出状态留在进程表里等父进程认领;handler 用 while + WNOHANG(不阻塞)把所有已结束的子进程一次收干净。循环不是装饰:POSIX 不排队信号——两个孩子同时退出,可能只递送一个 SIGCHLD,只收一次就漏一个僵尸14。
  • 一个 0.16 的坑值得单独记:循环里直接调 std.c.waitpid() 而不是高级封装 std.posix.waitpid()——0.16 的封装把 ECHILD(没有更多子进程,正是循环的终止条件)标成 unreachable,在它看来「没有子进程还 wait」是逻辑错误;原始 C 函数返回 -1,可以干净地查15。
  • 装 handler 时带 SA_RESTART:让被信号打断的系统调用(比如那个 poll)自动重启,保持一秒心跳不断16。

self-pipe 技巧也在这里交代了为什么不用:事件循环同时等信号和 I/O 的经典解法是在 handler 里往管道写一个字节、让 poll 因管道可读而醒来——把异步信号变成同步 I/O 事件。原子标志在 zcron 的场景已经够用,省掉一根管道的管理开销17。

2.7 fork+exec:任务的完整发射序列

调度器执行作业的两段式:

spawnJob(job):
    pid = fork()
    pid < 0  → 记录错误,返回          ← 父路径失败
    pid != 0 → 父进程立即返回          ← 调度不等待,继续 tick
    pid == 0 → execCommand(command); _exit(127)
                                         ↑
        execCommand: argv = {"/bin/sh", "-c", command}
                     execve(SHELL, argv, CHILD_ENV)   ← 成功就永不返回

图说:execve 不是「启动新程序」,而是把当前子进程的整个内存映像换成新可执行文件——所以成功绝不返回;返回到 Zig 代码只可能是失败(比如 /bin/sh 不存在),这时 _exit(127) 兜底。必须用裸的 _exit 而不是高级函数:它不等标准 I/O 缓冲区 flush——那些缓冲区是从父进程 fork 来的副本,一冲就把父进程已经输出过的内容再打一遍,日志里出现重复行18。

调度不等待是关键的非阻塞架构:一个跑 10 分钟的备份脚本不能让调度器失去时间的跟踪——父进程派完作业立刻回到自己的计时循环19。走 /bin/sh -c 是为了让用户在配置里直接写重定向、管道、shell 内建命令,zcron 自己永远不用懂引号和转义20。

2.8 脱离终端:双 fork 的守护进程序列

detach() 八步,前四步是「为什么要两次 fork」的全部答案21:

第 1 次 fork:父进程立刻 exit → shell 拿回了提示符,子进程成孤儿
setsid():孤儿成为新会话、新进程组的组长 —— 与原来的控制终端永久脱钩
第 2 次 fork:会话组长再 exit → 存活的孙进程「绝不可能是会话组长」
           (System V 系系统上,会话组长只要打开终端设备就可能重新获得控制终端;
            不是组长,永远没这个风险 —— 这是生产级守护进程与幼稚实现的分水岭)
chdir("/"):别占着启动时所在的那个文件系统,管理员才能卸载它
dup2 ×3:stdin/stdout/stderr 全部重定向到 /dev/null
         (终端已经不存在了,残留的 print 不许往一个死终端上写)

图说:完成之后,进程没有终端、对 SIGHUP 这类会话信号免疫、生命周期与启动它的用户完全无关22。书也坦白省了两步硬化:umask(0) 和把 PID 写进 /var/run——真实部署前该补23。实测:ps ax 里那个进程的状态是 Ss(会话组长、睡眠中),kill -SIGTERM <pid> 是对付它的正规方式24。

2.9 主线例子:tick 的一次对账

书上给了一段 live 推演,数字全部可用25:zcron 醒来,unixNow() = 1741812000。扫描开始:

作业 0 backup:  interval=60s, last_run=1741811940
              1741812000 − 1741811940 = 60 → 满足阈值
              → 打日志、spawnJob(后台拉起)、last_run 更新为 1741812000

作业 1 清临时文件: interval=300s, last_run=1741811800
              1741812000 − 1741811800 = 200 < 300 → 不够
              → continue,一个系统调用都不发,看下一个

图说:isDue() 是全程序唯一的调度逻辑(now >= last_run + interval 的布尔比较),因为它住在无 I/O 的 schedule.zig,配 6 个完全隔离的单测。整个 zcron 的调度判断就这行减法26。

实测的 REPL 交互也值得看一眼:list 里三个作业恰好都显示 in 3s(5/10/20 秒的最小公倍数对齐的刹那);run 1 强制立即执行作业 1,last_run 当场更新,下次自然触发从那一刻起算 10 秒27。日志前 10 行里 job 0 fired 每 5 秒一行、date 每 10 秒、uptime 每 20 秒穿插——节拍全对28。

3. 加厚:zwhich 找零的 REPL 与配置链

两个容易略过的完整度细节。其一,REPL 的 quit 走 signals.requestStop()——和真实 SIGTERM 同一条原子路径置 g_running = false,不管用户敲 quit 还是外面 kill,停机语义完全一致29。其二,配置文件的查找链:命令行参数优先,其次 ~/.config/zcron/zcron.conf(XDG 规范——freedesktop 定的「用户配置属于 ~/.config/<应用名>/」),最后退回当前目录的 ./zcron.conf;main.zig 里唯一非平凡的逻辑就是用 $HOME 拼这个路径,其余全是委托30。

还有一条运转纪律值得记下:跑 zcron 时把 stderr 重定向到日志文件(zig build run 2>/tmp/zcron.log)。std.log.scoped(.scheduler) 的 info(scheduler): 前缀写往 stderr,不重定向就会和 REPL 提示符在终端上互相搅行——UNIX 服务把 stderr 接进日志而不是留在终端上,这就是具体原因之一31。

4. 作者的判断与证据

  • 判断(全章落点): zcron 不是章末的新想法,是同一批想法的组合——「系统编程不是一堆不相干的原语,而是一套懂了每件就能预测地组合的工具」;调度器里每个设计决策都能在前面某个小程序里找到出处32。证据:从 sigInt.zig(一个 handler 置标志)到 zcron(几百行、六个文件)的整条链,每步都有可跑的代码。
  • 判断: 单线程事件循环优于双线程分工,因为共享可变状态与互斥锁的代价高于 poll 的复杂度12。证据:Redis/Nginx 的同构先例(引证,非实测)。
  • 判断: 固定栈数组(MAX_JOBS = 128)胜过动态 ArrayList——执行路径零堆分配、无碎片、关键时刻不可能分配失败;代价是 128 个作业的上限,书明说这是为个人服务设计的取舍33。
  • 坦白的缺口: 生产硬化缺 umask(0) 和 PID 文件;stderr 该重定向到日志文件(不然 info(scheduler): 前缀会跟 REPL 提示符在终端上搅在一起)2328。

4. 边界与局限

  • fork/setsid/exec 全套是 POSIX 专属,Windows 没有 fork;builtin.os.tag 的守卫在不适用的平台上会直接拒建。
  • zcron 的配置上限 128 个作业、命令存 [MAX_CMD + 1]u8 定长缓冲——超长命令处理是隐式截断33。
  • 作业按「单进程隔离」设计,但没有超时杀任务的机制;一个挂死的作业不会阻塞调度器,但会在系统里留一个孤儿进程。
  • 并发 TCP 服务器一线程一连接,没有线程池——成百上千连接时的内存与调度开销没有讨论(第 8、9 章才处理)。
  • detach 示例没有处理 fork 之后、dup2 之前的失败路径清理。

5. 可带走的

  1. 信号 handler 只许做原子写——malloc/printf/锁全禁;真正的处理放主循环。
  2. kill -USR1/-USR2 是把信号当查询接口的现成模式:不重启就问出进程内部状态。
  3. fork 一次调用两次返回:父得 PID、子得 0;fork 之后父进程的 wait 义务逃不掉。
  4. 不 wait 的后果有名字:僵尸进程;反复 fork 的服务必须配 SIGCHLD + WNOHANG 循环收尸。
  5. 那个循环必须是 while:POSIX 不排队信号,两个孩子同时退出可能只响一声。
  6. 0.16 坑:std.posix.waitpid() 把 ECHILD 当 unreachable;收尸循环用 std.c.waitpid() 原始版。
  7. exec 成功永不返回;exec 失败用 _exit(127) 兜底,且必须用裸 _exit 防继承的缓冲区二次 flush。
  8. 双 fork + setsid:第一脱离终端、第二永不当会话组长(杜绝重获控制终端),然后 chdir("/")、dup2 三连。
  9. poll(fd, 1000ms) 单线程心跳:stdin 与定时器两个事件源一次等,同步问题整个消失。
  10. detach() + SA_RESTART + 原子标志 + fork/exec + 收尸循环——这五件套就是一切「常驻小服务」的骨架。

6. 原文地图

主题原书章原文位置
信号作查询接口Using signals intentionallytext/80-fm-using-signals-intentionally.txt:5(搜「uptime」)
handler 的原子限制Using signals intentionallytext/80-fm-using-signals-intentionally.txt:72(搜「any point between instructions」)
线程 vs 进程取舍Using signals intentionallytext/80-fm-using-signals-intentionally.txt:300(搜「consequential decisions」)
fork 两次返回Forking processestext/81-fm-forking-processes.txt:37(搜「returns twice」)
僵尸与进程表耗尽Forking processestext/81-fm-forking-processes.txt:121(搜「zombie」)
join 是同步点Forking processestext/81-fm-forking-processes.txt:266(搜「synchronization point」)
非确定性双跑Running a variable number of threadstext/82-fm-running-a-variable-number-of-threads.txt:143(搜「0 1 3 2」)
run() 与 term 联合体Running a variable number of threadstext/82-fm-running-a-variable-number-of-threads.txt:185(搜「spawning, waiting」)
一连接一线程+detachRunning a variable number of threadstext/82-fm-running-a-variable-number-of-threads.txt:567(搜「fire and forget」)
10 并发客户端实录Writing a concurrent TCP clienttext/83-fm-writing-a-concurrent-tcp-client.txt:257(搜「Client 3 received」)
六文件各自的所有权Writing a concurrent TCP clienttext/83-fm-writing-a-concurrent-tcp-client.txt:328(搜「schedule.zig」)
poll 单线程事件循环Explaining the design decisionstext/84-fm-explaining-the-design-decisions.txt:41(搜「poll(stdin_fd」)
self-pipe 技巧Explaining the design decisionstext/84-fm-explaining-the-design-decisions.txt:135(搜「asynchronous signal」)
SIGHUP 重载惯例Explaining the design decisionstext/84-fm-explaining-the-design-decisions.txt:173(搜「modem line」)
双 fork 的意义Detaching from the terminal (fork, setsid)text/85-fm-detaching-from-the-terminal-fork-setsid.txt:144(搜「O_NOCTTY」)
dup2 三连到 /dev/nullDetaching from the terminal (fork, setsid)text/85-fm-detaching-from-the-terminal-fork-setsid.txt:91(搜「dev_null」)
省掉的硬化步骤Detaching from the terminal (fork, setsid)text/85-fm-detaching-from-the-terminal-fork-setsid.txt:178(搜「umask」)
exec 不返回与 _exit(127)Using fork and exec to run periodic taskstext/86-fm-using-fork-and-exec-to-run-periodic-tasks.txt:135(搜「never returns」)
POSIX 不排队信号Handling SIGTERM and SIGCHLDtext/87-fm-handling-sigterm-and-sigchld.txt:69(搜「does not queue」)
SA_RESTART 保心跳Handling SIGTERM and SIGCHLDtext/87-fm-handling-sigterm-and-sigchld.txt:77(搜「SA_RESTART」)
ECHILD 当 unreachableThe zcron implementation detailstext/88-fm-the-zcron-implementation-details.txt:175(搜「ECHILD」)
tick 时间戳推演The zcron implementation detailstext/88-fm-the-zcron-implementation-details.txt:419(搜「1741812000」)
全章落点:组合The zcron implementation detailstext/88-fm-the-zcron-implementation-details.txt:860(搜「composable tools」)
REPL list/run 实录The zcron implementation detailstext/88-fm-the-zcron-implementation-details.txt:696(搜「in 3s」)
日志节拍验证The zcron implementation detailstext/88-fm-the-zcron-implementation-details.txt:799(搜「job 0 fired」)

本章拆解依据原书第 7 章(含各小节)。每条出处可用 node scripts/book-verify.mjs systems-programming-with-zig 回核。

Footnotes

  1. 出处:「Using signals intentionally」第 5 段(text/80-fm-using-signals-intentionally.txt:5,搜「uptime」)与第 9 段(text/80-fm-using-signals-intentionally.txt:9,搜「without being restarted」)。 ↩

  2. 出处:「Using signals intentionally」第 72 段(text/80-fm-using-signals-intentionally.txt:72,搜「any point between instructions」)。 ↩

  3. 出处:「Using signals intentionally」第 239 段(text/80-fm-using-signals-intentionally.txt:239,搜「1820054701」)。 ↩

  4. 出处:「Forking processes」第 37 段(text/81-fm-forking-processes.txt:37,搜「returns twice」)。 ↩

  5. 出处:「Using signals intentionally」第 300 段(text/80-fm-using-signals-intentionally.txt:300,搜「consequential decisions」)与第 306 段(text/80-fm-using-signals-intentionally.txt:306,搜「rule of thumb」)。 ↩

  6. 出处:「Forking processes」第 121 段(text/81-fm-forking-processes.txt:121,搜「zombie」)。 ↩

  7. 出处:「Forking processes」第 266 段(text/81-fm-forking-processes.txt:266,搜「synchronization point」)。 ↩

  8. 出处:「Running a variable number of threads」第 143 段(text/82-fm-running-a-variable-number-of-threads.txt:143,搜「0 1 3 2」)。 ↩

  9. 出处:「Running a variable number of threads」第 185 段(text/82-fm-running-a-variable-number-of-threads.txt:185,搜「spawning, waiting」)。 ↩

  10. 出处:「Running a variable number of threads」第 567 段(text/82-fm-running-a-variable-number-of-threads.txt:567,搜「fire and forget」)。 ↩

  11. 出处:「Writing a concurrent TCP client」第 257 段(text/83-fm-writing-a-concurrent-tcp-client.txt:257,搜「Client 3 received」)。 ↩

  12. 出处:「Explaining the design decisions」第 41 段(text/84-fm-explaining-the-design-decisions.txt:41,搜「poll(stdin_fd」)与第 49 段(text/84-fm-explaining-the-design-decisions.txt:49,搜「Redis」)。 ↩ ↩2

  13. 出处:「Explaining the design decisions」第 173 段(text/84-fm-explaining-the-design-decisions.txt:173,搜「modem line」)。 ↩

  14. 出处:「Handling SIGTERM and SIGCHLD」第 69 段(text/87-fm-handling-sigterm-and-sigchld.txt:69,搜「does not queue」)。 ↩

  15. 出处:「The zcron implementation details」第 175 段(text/88-fm-the-zcron-implementation-details.txt:175,搜「ECHILD」)。 ↩

  16. 出处:「Handling SIGTERM and SIGCHLD」第 77 段(text/87-fm-handling-sigterm-and-sigchld.txt:77,搜「SA_RESTART」)。 ↩

  17. 出处:「Explaining the design decisions」第 135 段(text/84-fm-explaining-the-design-decisions.txt:135,搜「asynchronous signal」)。 ↩

  18. 出处:「Using fork and exec to run periodic tasks」第 135 段(text/86-fm-using-fork-and-exec-to-run-periodic-tasks.txt:135,搜「never returns」)与第 149 段(text/86-fm-using-fork-and-exec-to-run-periodic-tasks.txt:149,搜「without flushing」)。 ↩

  19. 出处:「Using fork and exec to run periodic tasks」第 35 段(text/86-fm-using-fork-and-exec-to-run-periodic-tasks.txt:35,搜「non-blocking」)。 ↩

  20. 出处:「Explaining the design decisions」第 55 段(text/84-fm-explaining-the-design-decisions.txt:55,搜「/bin/sh」)。 ↩

  21. 出处:「Detaching from the terminal (fork, setsid)」第 144 段(text/85-fm-detaching-from-the-terminal-fork-setsid.txt:144,搜「O_NOCTTY」)。 ↩

  22. 出处:「Detaching from the terminal (fork, setsid)」第 170 段(text/85-fm-detaching-from-the-terminal-fork-setsid.txt:170,搜「no terminal」)。 ↩

  23. 出处:「Detaching from the terminal (fork, setsid)」第 178 段(text/85-fm-detaching-from-the-terminal-fork-setsid.txt:178,搜「umask」)。 ↩ ↩2

  24. 出处:「Detaching from the terminal (fork, setsid)」第 196 段(text/85-fm-detaching-from-the-terminal-fork-setsid.txt:196,搜「Ss」)。 ↩

  25. 出处:「The zcron implementation details」第 419 段(text/88-fm-the-zcron-implementation-details.txt:419,搜「1741812000」)。 ↩

  26. 出处:「The zcron implementation details」第 39 段(text/88-fm-the-zcron-implementation-details.txt:39,搜「isDue」)。 ↩

  27. 出处:「The zcron implementation details」第 696 段(text/88-fm-the-zcron-implementation-details.txt:696,搜「in 3s」)。 ↩

  28. 出处:「The zcron implementation details」第 799 段(text/88-fm-the-zcron-implementation-details.txt:799,搜「job 0 fired」)。 ↩ ↩2

  29. 出处:「The zcron implementation details」第 295 段(text/88-fm-the-zcron-implementation-details.txt:295,搜「same atomic path」)。 ↩

  30. 出处:「The zcron implementation details」第 571 段(text/88-fm-the-zcron-implementation-details.txt:571,搜「command-line argument」)与第 609 段(text/88-fm-the-zcron-implementation-details.txt:609,搜「XDG」)。 ↩

  31. 出处:「The zcron implementation details」第 667 段(text/88-fm-the-zcron-implementation-details.txt:667,搜「interleave」)。 ↩

  32. 出处:「The zcron implementation details」第 860 段(text/88-fm-the-zcron-implementation-details.txt:860,搜「composable tools」)。 ↩

  33. 出处:「Explaining the design decisions」第 75 段(text/84-fm-explaining-the-design-decisions.txt:75,搜「MAX_JOBS」)。 ↩ ↩2

On this page