用 distcc 把编译派发到 Windows 10 的 WSL
一台 4 核笔记本。局域网里另有一台 Windows 10,WSL2 里能看到 28 个核,gcc 也是 13.3.0。内核仓库不拷到 Windows,产物目录也不过去。笔记本负责 make、源码和链接,WSL 只接收 distcc 送过去的编译任务。
这套配置编完并启动了自定义内核。源码在 $HOME/src/linux,输出目录是 O=$HOME/kernel-build/linux。下文用主机别名 wsl-build 代替 Windows 的地址,写成自己的主机名或地址即可。SSH 用户用 builder 代替,换成 WSL 里的登录名即可。
分工
| 笔记本 | Windows 10 | WSL2 | |
|---|---|---|---|
| 系统 | Ubuntu 24.04 | 只做端口转发 | Ubuntu 24.04 |
| CPU | 4 核 | 不参与编译 | 28 核 |
| 编译器 | gcc 13.3.0 | — | gcc 13.3.0 |
| 职责 | make、预处理、链接、.o 落盘 |
网卡 :22 转到 WSL |
每次任务拉起一次 distccd,跑 cc1 |
两边 gcc 主版本必须一致,远程编出来的 .o 才能链进同一棵内核。distcc 3.4 从 Ubuntu 的 deb 解到用户目录,两边都不用为它提权安装:
1 | dpkg-deb -x distcc_3.4+really3.4-4build3_amd64.deb \ |
WSL 上同样解到 ~/distcc-local/root。打开 lzo 时,WSL 里要有 liblzo2.so.2。
笔记本怎么连进 WSL
ping Windows 地址时 TTL 是 128,包先到 Windows。WSL2 的 sshd 默认只听虚拟网卡,局域网访问不到。
一开始用管理员 PowerShell 把 Windows 的 22 转到 WSL 当时的地址:
1 | netsh interface portproxy add v4tov4 listenaddress=0.0.0.0 listenport=22 connectaddress=<wsl-address> connectport=22 |
WSL 重启后这个地址会变。转发还指向旧地址时,TCP 能连上,对端一个 SSH banner 都不发就断开。
后来改成固定入口:sshd 只听 WSL 自己的 127.0.0.1:22022,笔记本仍然只连 Windows 网卡上的 22。直接从笔记本连 22022 会超时。WSL 里的 /etc/ssh/sshd_config.d/99-wsl.conf:
1 | Port 22022 |
MaxStartups / MaxSessions 的默认值大约是 10。内核 make -j28 会同时打开几十条 SSH,不抬高的话,多出来的连接在握手阶段就被拒绝。认证用公钥,ssh-copy-id 一次即可。笔记本的 ~/.ssh/config:
1 | Host wsl-build |
HostName 换成 Windows 网卡上的地址或域名。User 换成 WSL 里的登录名。IdentityFile 换成实际使用的公钥。
主机串走 SSH
DISTCC_HOSTS 写成:
1 | export DISTCC_HOSTS='@wsl-build/28,lzo' |
@ 表示这条连接走 SSH。/28 是槽位数,对着 WSL 的核数。,lzo 压缩预处理后的大文件。distcc 在这条 SSH 里拉起一次 distccd,这个编译任务结束就退出。网上不放一个长期开着的 distccd,也就不用在 WSL 上为编译器白名单提权。
--localslots=0 不要写进这串主机列表。这份 distcc 3.4 会把它当成一台主机名。
DISTCC_SSH 指向自己的封装。每个任务单独一条 SSH,关掉 ControlMaster。单独压 28 路时,复用一条主连接可以通过;真正的 make 跑起来之后,控制套接字会消失,后面的任务全部退回笔记本。
1 |
|
远程命令里的 \$HOME 和 \$PATH 保持转义,让对端的 shell 再展开。PATH 以 /usr/bin:/bin 开头,远程用的才是系统里的 gcc,而不是 distcc 自己的包装程序。-i 后面的私钥路径换成自己的。
内核认的是名叫 gcc 的程序
把 CC 设成 distcc /usr/bin/gcc 时,这棵内核树上的子进程仍然是本机 gcc,任务没有分发出去。ccache 前缀也不行:CCACHE_PREFIX=distcc 会让 distcc 看到以 gcc gcc 开头的命令行,随后报找不到输入文件。
做法是做一个目录,放三份内容相同的脚本,名字分别是 gcc、cc、x86_64-linux-gnu-gcc,把这个目录放在 PATH 最前面。脚本用原来的程序名去执行 distcc。HOSTCC 保持 /usr/bin/gcc,主机工具留在笔记本上编。
依赖扫描(-E、-M、-MM)也留在本机。真正的 -c 先在 WSL 上试两次,两次都失败再允许落回本机。一次 SSH 中断会写下 ~/.distcc/lock/backoff_ssh_wsl-build_0。这个文件还在时,后面的任务会跳过 WSL。脚本每次调用都删掉它。锁文件名里的主机部分跟 DISTCC_HOSTS 里的主机一致。zsh 开着 nomatch 时,不加引号的 backoff_* 会让命令直接退出,所以删的是完整文件名。
1 |
|
DISTCC_FALLBACK=1 留到最后一次。关掉的话,一根断掉的 SSH 会让那个目标文件直接失败。
启动编译的入口把 ccache 和 CC 清掉,避免它们盖过上面的伪装目录:
1 |
|
同一输出目录只留一个 make。停的时候对进程组发 TERM。被杀掉的编译器可能留下长度为 0 的 .o,下次增量之前删掉这些半截文件,已经编完的留着。
大文件要压过 Windows 转发
AMD display 里的 dcn201_resource.c,预处理后大约 5.8 MB。不压缩时,24 路同时穿过 Windows 的端口转发,大约 6 路能回来,其余在传输中途断管。加上 ,lzo 之后,同样的 24 路和随后的 28 路都编完了,目标文件都是 69040 字节。
测远程时用已经生成的 .o.cmd 里那条完整命令。单独 make 一个 AMD display 文件时,目录里的 -I 可能没传进去,预处理会在本机停在 dm_services.h。那次失败发生在任务送出之前,不能当成 distcc 断线。
四核喂不饱 28 个编译器
默认情况下,预处理在笔记本上做。4 个核来不及给 28 个远程 cc1 准备 .i。槽位开到 28 时,WSL 上同时跑着的 cc1 也经常只有几个。小文件尤其如此,大文件仍会占满空闲槽位。
成功启动自定义内核的那次完整构建里,大约 14395 条 CC,其中大约 3034 条没分发出去,在本机编完。arch/x86/boot/bzImage 大约 15 MB。
要把预处理也放到 WSL,主机串改成 @wsl-build/28,cpp,lzo,并先启动 include server。distcc 向它要这次编译会用到的头文件,再把预处理和 cc1 一起送过去。
Ubuntu 24.04 自带的 distcc-pump 扩展在 Python 3.12 上起不来,报错和 PY_SSIZE_T_CLEAN 有关。include server 其余部分是 Python。压缩改走系统里的 liblzo2 之后,服务器能回答。用一个很小的探针文件做试验时,笔记本上没有对应的 gcc -E,目标文件在 wsl-build 上编出来。
include server 的套接字由 distcc-pump --startup 写进 INCLUDE_SERVER_PORT,再记到 pump.env。套接字还在,上面的 gcc 包装和 distcc-make.sh 就沿用带 ,cpp 的主机串;不在就退回只压缩、预处理仍在笔记本上的那一串。完整的 make -j28 仍然会丢会话。WSL 重启之后,日志是 Connection closed by wsl-build port 22 和 Broken pipe。回退打开时,这些文件在笔记本上编完,构建继续。
怎样确认任务在 WSL 上
编译日志里出现 compiled on wsl-build,这一份就是在 WSL 上编的。笔记本上堆着很多 cc1、WSL 上的 cc1 是 0 时,先看 backoff 锁还在不在,再看 22 端口的公钥登录还能不能通。pump 打开时,普通的 -c 不该再在笔记本上跑 gcc -E。
WSL 上看正在跑的编译器:
1 | ssh -o BatchMode=yes -o IdentitiesOnly=yes -i "$HOME/.ssh/id_ed25519" \ |
数字随文件大小起伏。预处理还在 4 核笔记本上时,这个数经常是个位数,不代表分发没生效。