跳转至

在 WSL 中运行命令

wsl.exe 的两个特性决定了每一次来宾调用应该如何编写。写错时两者都是静默的:命令仍然会运行,并且仍然以 0 退出,只是返回了错误的字节。它们就是第 1 节和第 2 节。结尾一节回答在发行版内运行 Orca 的写入后紧接着引出的问题:发行版的磁盘镜像会发生什么。

1. 始终使用 --exec,绝不使用 --

wsl.exe -d <distro> -- <argv> 会在来宾系统运行之前,针对来宾环境展开每个参数中的 $name。这是 wsl.exe 本身的行为,而不是来宾 shell——即使命令中完全没有 shell,它也会发生:

$ wsl.exe -d Ubuntu-24.04 --      /usr/bin/printf %s '$HOME'
/home/you
$ wsl.exe -d Ubuntu-24.04 --exec  /usr/bin/printf %s '$HOME'
$HOME

因此在 -- 下,脚本的含义与它字面表达的不同。awk '{print $2}' 到达来宾系统时变成 awk '{print }',并打印整行;位置参数 "$1"、shell 局部变量以及 "\$literal" 都会被以同样方式清空或改写。(不含 $ 的扩展不受影响——像 s/(a)(b)/\2\1/ 这样的 sed 反向引用在两种方式下都能保留。)在 Windows 侧转义 $ 无法可靠地修复这一点——早先的一次尝试会跳过所有前面带反斜杠的 $,而这恰恰是 POSIX 脚本用来表示字面美元符号的情况。

使用 src/shared/wsl-login-shell-command.ts 中的 buildWslExecArgs() 构建 argv。一个测试会遍历代码树,如果 -- 形式再次出现就失败。

sh -s -- <path> 中的 -- 是 shell 参数分隔符,与此无关;保持原样。

2. 机器可读输出必须加围栏

Orca 通过发行版用户的交互式登录 shell(bash/zsh 使用 -ilc)运行来宾命令,因为这是唯一会读取 ~/.bashrc 的 shell,而 nvm、mise 和 asdf 正是在那里安装它们的 PATH 条目。去掉 -i 会破坏这些用户的工具检测。

代价是交互式 shell 还会运行发行版的 rc/motd,而这些输出会进入 stdout——也就是答案到达的同一数据流。标准 Ubuntu 24.04 无需任何自定义即可复现:

$ wsl.exe -d Ubuntu-24.04 --exec bash -ilc 'git --version'
To run a command as administrator (user "root"), use "sudo <command>".
See "man sudo_root" for details.

git version 2.43.0

任何解析 stdout 的调用方都必须使用 buildWslCapturedLoginShellCommand(),它会为负载加围栏,并返回与之匹配的 readStdout。.trim() 没有用:横幅是前缀,而不是周围空白,因此与 "directory" 比较的 stat 探测根本不会匹配。

围栏携带每次调用唯一的 nonce,因此用 cat 读取一个内容恰好引用了标记的文件时不会被截断;它还会保留负载的退出状态,使 exit 2 → ENOENT 的映射继续有效。

不要为通过 exec 进入长时间运行程序的命令加围栏(codex app-server、交互式终端)。它永远到不了结束围栏,而在那种情况下,shell 自身的输出要么属于该程序,要么正是用户想要看到的内容。

尽量完全不使用 shell

当调用方只需要一个已知二进制文件和一个已知环境时,完全跳过登录 shell,直接运行该二进制文件:

wsl.exe -d <distro> --exec /usr/bin/env PATH=… HOME=… /usr/bin/git -C <dir> status

direct-git 读取路径就是这样做的。它在结构上免疫上述两个问题,并且避免每次调用都支付登录 shell 的启动开销,同时也绕开了会阻塞或打印输出的配置文件。通过一次带围栏的探测解析 PATH/HOME,按发行版缓存,然后使用这种形式。

磁盘:发行版 VHDX 只会增长

以上所有内容都会把 Orca 的写入放到发行版内部,这会引出另一个问题。WSL2 将整个来宾文件系统保存在单个动态扩展的 ext4.vhdx 中。在发行版内删除文件确实会释放块——供 ext4 复用——但主机可见的 .vhdx 不会自行缩小。

找到文件

路径取决于发行版的安装方式,因此不要假设唯一路径:

安装方式 ext4.vhdx 位于
较新的 wsl --install %LOCALAPPDATA%\wsl\{guid}\
Microsoft Store %LOCALAPPDATA%\Packages\<PackageFamilyName>\LocalState\
wsl --import 操作员指定的位置

与安装方式无关的答案是注册表,它把每个发行版的目录记录为 BasePath:

Get-ChildItem HKCU:\Software\Microsoft\Windows\CurrentVersion\Lxss |
  ForEach-Object { Get-ItemProperty $_.PSPath } |
  Select-Object DistributionName, BasePath

实测行为

WSL 2.7.11.0 / Ubuntu-24.04,一台机器。大小均为磁盘占用大小——通过 GetCompressedFileSize 获得的已分配字节数,而不是逻辑文件长度。这一区别在下面很重要:在这台机器上,vhdx 不是稀疏的,因此这两个数值相同;但在稀疏 vhdx 上,逻辑大小会固定在历史峰值,只有磁盘占用大小在空间被回收时才会下降。如果测量错误的指标,回收看起来就像没有效果。

步骤 ext4.vhdx 磁盘占用大小(字节)
基线 21,673,017,344
写入 1 GiB 22,746,759,168
删除它 22,746,759,168
写入新的不可压缩 1 GiB 22,746,759,168
同时保持 3 GiB 存活 24,894,242,816

第四行是关键:第二个 GiB 没有带来任何增长,因为 ext4 把第一个 GiB 释放的块交给了它。只有超过之前的峰值时,文件才会增长。

回收空间

稀疏模式允许来宾系统将已释放的块交还给主机,因此文件可以缩小,而不只是增长。两个前提条件都容易忽略:发行版必须已停止(vhdx 在挂载期间无法转换),并且 wsl --manage 只存在于 WSL 2.5 及更高版本——用 wsl --version 检查。

wsl --terminate <distro>
wsl --manage <distro> --set-sparse true

对于尚未创建的发行版,等效做法是在 %UserProfile%\.wslconfig 的 [experimental] 下设置 sparseVhd = true —— 该文件位于 Windows 用户配置文件中,不在发行版内部,也不在 ~/.wslconfig,并且在你创建它之前并不存在。

两者都不会触及已经存在的空闲空间。若要处理这种情况,请关闭 WSL,并从提升权限的提示符中手动压缩该文件。单独执行 compact vdisk 会失败,因为没有选择虚拟磁盘,因此 select 和只读 attach 是必需的,而不是可选的:

wsl --shutdown
diskpart
DISKPART> select vdisk file="C:\path\to\ext4.vhdx"
DISKPART> attach vdisk readonly
DISKPART> compact vdisk
DISKPART> detach vdisk
DISKPART> exit

有两个注意事项,均未在此处验证:现场反馈称,对于已经是 sparse 的 vhdx,compact vdisk 是无效操作(需先用 --set-sparse false 转换回去),并且未测量 sparse 模式的运行时开销。Microsoft 的 磁盘空间指南 包含当前的定位/扩展/压缩流程以及 --manage 的版本下限;.wslconfig 包含 sparseVhd。启用 sparse 模式和执行压缩都是逐机器决策;Orca 不会替你做其中任何一项。

在被测机器上,该 vhdx 不是 sparse:fsutil sparse queryflag 报告 "NOT set as sparse",并且不存在 %UserProfile%\.wslconfig 用于启用。Microsoft 文档将 sparseVhd 的默认值记录为 false,因此这是预期状态,而不是本地特殊情况 —— 但该标志是按 vhdx 设置的,在磁盘创建时或通过显式转换设置,因此请检查你自己的发行版,而不是假定任何一侧。

这对 Orca 意味着什么

当推测性 worktree 准备和镜像 worktree 向发行版写入时,vhdx 增长是预期行为。其大小单调不减,并大致跟踪峰值并发使用量 —— 但它可能漂移到峰值以上,且上述测量是复用的最佳情况:第二个 GB 在第一个被释放后立即从同一块组中分配。在持续高周转下 —— 数周内创建和删除许多 worktree,没有 fstrim/discard,sparse 关闭 —— 分配会变得分散,文件会稳定在高于实时峰值的水平。

因此,仅增长本身并不是泄漏的证据。远高于实时峰值使用量的增长值得调查,并且正是上述回收步骤所处理的情况。

本页原文 Markdown:在 AtomGit 查看·内容源自开源项目 orca48/orca