执行模型:零 fork 三层
POSIX fork() 在 Win32 上没有等价物。MSYS2/Cygwin 的解法是在
syscall 层伪造它——昂贵、脆弱、并以怪癖闻名。niubash 的引擎
(rubash)走了另一条路:复刻 fork 的语义,不复刻它的机制。
结果是子壳与命令替换零进程创建成本,而脚本可观察的一切——退出码、fd 继承、
共享偏移、信号处置——与 GNU Bash 行为一致。
三层模型
| 层 | 机制 | 对应 fork 的哪一面 |
|---|---|---|
| 1. 进程内子壳 | ( list ) 与 $( ) 从不产生进程:以 ShellState::clone 复制变量、别名、函数、trap、历史,子壳结束即弃。 | fork 复制的「内存面」。 |
| 2. 真句柄 fd 表 | 槽位持有原生 Windows HANDLE;DuplicateHandle 复制的句柄共享同一内核文件对象——因此共享文件偏移,正是 POSIX「dup 共享 open file description」(落地前有 POC 实证)。fork_table() 逐句柄复制整表。 | fork 复制 fd 表但不复制文件对象。 |
| 3. 真进程只在边界 | 外部命令与管道成员经 CreateProcess + os_pipe 启动;后台作业与 coproc 各持真进程,作业控制基于 Job Objects,SIGCONT 经 ResumeThread。 | fork 真正无法避免的地方:进程边界。 |
换句话说:能不建进程的(子壳、命令替换)一律不建;必须建进程的(外部命令、 管道、后台作业)用 Win32 的原生路径直接建。没有一层「模拟的 POSIX」横在中间。
62 真内建 + 12 内联 coreutils + 管道内联臂
命令词按三层分发。62 个真内建进程内执行(GNU Bash 5.3 的
61 个内建全覆盖、无缺失,外加进程内 env——
coreutils 语义、排序输出、支持 -0)。另有隐藏 fast-path
(sleep/dirname/basename/uname/arch):
简单参数形态进程内执行,复杂形态回退外部命令,而 introspection
(type/command -v/enable)报告为
外部命令——与 GNU Bash 的报告一致。
| 进程内层 | 名单 | 说明 |
|---|---|---|
| 内联文件命令(12) | pwd printf mkdir touch chmod cp rm rmdir cat sed mkfifo tty | 高频文件类命令在引擎内进程内模拟执行(对齐 GNU coreutils 的 stdout/rc;带标志等复杂形态回退外部命令)。 |
| 管道内联臂 | true : false echo printf trap head cat sed grep wc tr | 按命令在管线中的位置匹配:管道成员命中即在进程内运行,遇到拒绝形态(如 grep 的正则元字符)才回退外部二进制。 |
组合起来,echo | cat 这样的管道一整个零进程。
名单不是文档口号:内建白名单与 fast-path 清单由 cargo test 的
文档同步测试守护——代码与文档任何一侧变动,另一侧不改则构建失败。
可观察行为:零差异是被证明的
「实现不同」不是「行为不同」的委婉说法。这套模型的可观察面——退出码传播、
fd 继承与共享偏移、信号处置——正是 GNU 上游 83 套测试语料覆盖的对象:
83/83 通过、82 套逐字节一致(见
兼容总览)。作为旁证,引擎还能端到端运行
bashdb 调试器的核心循环(list/step/next/where/continue/quit),以及 git
自家的测试框架 t/test-lib.sh——两者都对执行模型的细节极其敏感。
性能面上这层设计的收益(子壳、命令替换、后台作业)与持平的外部进程成本, 见性能特征。