一台 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
2
dpkg-deb -x distcc_3.4+really3.4-4build3_amd64.deb \
"$HOME/kernel-build/distcc-local/root"

WSL 上同样解到 ~/distcc-local/root。打开 lzo 时,WSL 里要有 liblzo2.so.2。

笔记本怎么连进 WSL

ping Windows 地址时 TTL 是 128,包先到 Windows。WSL2 的 sshd 默认只听虚拟网卡,局域网访问不到。

一开始用管理员 PowerShell 把 Windows 的 22 转到 WSL 当时的地址:

1
2
netsh interface portproxy add v4tov4 listenaddress=0.0.0.0 listenport=22 connectaddress=<wsl-address> connectport=22
New-NetFirewallRule -DisplayName "WSL SSH" -Direction Inbound -Protocol TCP -LocalPort 22 -Action Allow

WSL 重启后这个地址会变。转发还指向旧地址时,TCP 能连上,对端一个 SSH banner 都不发就断开。

后来改成固定入口:sshd 只听 WSL 自己的 127.0.0.1:22022,笔记本仍然只连 Windows 网卡上的 22。直接从笔记本连 22022 会超时。WSL 里的 /etc/ssh/sshd_config.d/99-wsl.conf:

1
2
3
4
5
6
7
Port 22022
ListenAddress 127.0.0.1
PubkeyAuthentication yes
PermitRootLogin no

MaxStartups 4096
MaxSessions 4096

MaxStartups / MaxSessions 的默认值大约是 10。内核 make -j28 会同时打开几十条 SSH,不抬高的话,多出来的连接在握手阶段就被拒绝。认证用公钥,ssh-copy-id 一次即可。笔记本的 ~/.ssh/config:

1
2
3
4
5
6
7
Host wsl-build
HostName wsl-build
User builder
Port 22
IdentityFile ~/.ssh/id_ed25519
IdentitiesOnly yes
BatchMode yes

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
2
3
4
5
6
7
8
9
10
#!/bin/bash
# ~/kernel-build/distcc-ssh
host=$1
shift
remote=$(printf '%q ' "$@")
exec /usr/bin/ssh -o BatchMode=yes -o IdentitiesOnly=yes -i "$HOME/.ssh/id_ed25519" \
-o ControlMaster=no -o ControlPath=none \
-o ServerAliveInterval=30 -o ServerAliveCountMax=4 \
-o ConnectTimeout=15 \
"$host" "export PATH=/usr/bin:/bin:\$HOME/distcc-local/root/usr/bin:\$PATH; exec $remote"

远程命令里的 \$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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#!/bin/bash
# ~/kernel-build/distcc-masq/gcc ,cc 与 x86_64-linux-gnu-gcc 是同一份
rm -f "$HOME/.distcc/lock/backoff_ssh_wsl-build_0"
export PATH="/usr/bin:/bin"
name=$(basename "$0")
distcc="$HOME/kernel-build/distcc-local/root/usr/bin/distcc"

pump_env="$HOME/kernel-build/pump.env"
if [ -f "$pump_env" ]; then
. "$pump_env"
fi
if [ -z "${INCLUDE_SERVER_PORT:-}" ] || [ ! -S "$INCLUDE_SERVER_PORT" ]; then
unset INCLUDE_SERVER_PORT
export DISTCC_HOSTS='@wsl-build/28,lzo'
fi

for arg in "$@"; do
case "$arg" in
-E|-M|-MM) exec "/usr/bin/$name" "$@" ;;
-c) break ;;
esac
done

export DISTCC_FALLBACK=0
for try in 1 2; do
rm -f "$HOME/.distcc/lock/backoff_ssh_wsl-build_0"
if ( exec -a "$name" "$distcc" "$@" ); then
exit 0
fi
done

export DISTCC_FALLBACK=1
rm -f "$HOME/.distcc/lock/backoff_ssh_wsl-build_0"
exec -a "$name" "$distcc" "$@"

DISTCC_FALLBACK=1 留到最后一次。关掉的话,一根断掉的 SSH 会让那个目标文件直接失败。

启动编译的入口把 ccache 和 CC 清掉,避免它们盖过上面的伪装目录:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#!/bin/bash
# ~/kernel-build/distcc-make.sh
cd "$HOME/src/linux" || exit 1
export PATH="$HOME/kernel-build/distcc-masq:/usr/bin:/bin"
unset CCACHE_PREFIX USE_CCACHE CCACHE_RECACHE DISTCC_VERBOSE CC
export HOSTCC=/usr/bin/gcc
export DISTCC_SSH="$HOME/kernel-build/distcc-ssh"
export DISTCC_FALLBACK=1
if [ -f "$HOME/kernel-build/pump.env" ]; then
. "$HOME/kernel-build/pump.env"
fi
if [ -z "${INCLUDE_SERVER_PORT:-}" ] || [ ! -S "$INCLUDE_SERVER_PORT" ]; then
unset INCLUDE_SERVER_PORT
export DISTCC_HOSTS='@wsl-build/28,lzo'
fi
exec make O="$HOME/kernel-build/linux" -j28

同一输出目录只留一个 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
2
ssh -o BatchMode=yes -o IdentitiesOnly=yes -i "$HOME/.ssh/id_ed25519" \
builder@wsl-build 'ps -eo comm | grep -c cc1'

数字随文件大小起伏。预处理还在 4 核笔记本上时,这个数经常是个位数,不代表分发没生效。