在 WSL2 中完美使用 systemd 的方法

WSL2 和一般 Linux 有個很大的差別在於他有微軟自己的 init,而非一般 Linux distro 常見的 systemd。這會造成的一個問題是有些仰賴 systemd 的 distro 的 package manager 在使用上會有點問題,例如 Arch Linux。

這篇簡單紀錄了在 WSL2 中的 ArchWSL 完美設定出 systemd 環境的方法。其他的 Distro 應該也能使用這篇的方法。

參考用的 dotfiles: maple3142/dotfiles

2022/11/17 更新: 因為今天 WSL 正式於 Microsoft Store 上推出 1.0.0 之後我終於能在 Windows 10 上安裝新版的 WSL 了,並且能成功使用內建的 systemd 支援,這部分可以參考最後的段落。

systemd bottle

要在 WSL2 中跑 systemd 的方法用的是 namespace 的功能,會需要使用別人寫好的工具幫你把 systemd 包裝在那個環境之中,讓它變成 PID 1 之後就能正常運作了。可以從下面兩個擇一安裝:

兩個我都有用過,用法也相當接近,後來因為效能上的考量,最後使用的是 subsystemctl。本文後面也都是以 subsystemctl 來介紹。

shell 方面都假設為使用的是 zsh

基本操作

安裝完成後要先用 sudo subsystemctl start 把 systemd 啟用,每次重啟 WSL 之後也都要重新做一次這件事,這部分可以利用在你的 .zshrc 中寫入:

1
2
3
4
5
6
7
if [[ -v WSL_DISTRO_NAME ]] then
if (( $+commands[subsystemctl] )); then
if ! subsystemctl is-running; then
sudo subsystemctl start
fi
fi
fi

這樣它就會自動幫你看情況啟用,所以基本上只需要輸入一次 sudo 的密碼而已。

之後可以用 subsystemctl shell 進入可以執行 systemd 的環境,或是用 subsystemctl exec 直接執行想要的指令,例如 subsystemctl exec -- systemctl status docker

使用 subsystemctl exec 最好要加上 --,不然後面指令的 flags 也會被 subsystemctl 當成是它自己的 flags

例如要更新 Arch Linux 就記得要用 subsystemctl exec -- pacman -Syu,這樣如果有觸發到需要 systemd 的 package 也能正常使用。

這樣其實就基本上能使用 systemd,如果到這邊就滿意的話那就可以不用讀下去了。後面的文章是在說明怎麼把 subsystemctl shell 弄成可以自動啟用的方法。

subsystemctl shell 的一些問題

subsystemctl shell 看起來很方便,自然會想直接利用 .zshrc 讓它進入 systemd 的環境之中,不過實際上使用會發現它還有很多麻煩的小地方要處裡。

例如當你輸入 exit 之後會發現它只退出了 subsystemctl shell,而沒把整個 shell 給退出之類的小問題。

另一個問題是輸入 cat,然後輸入 abc 之後按兩下 Backspace 之後可能會發現它變成了 abc\cb: arkane-systems/genie#145

一些 WSL 該有的環境變數如 WSL_DISTRO_NAME 不見了,這會讓許多 WSL 的功能產生問題,例如執行 .exe 檔案的功能之類的。這個是最大的問題,也是最難修正的一個問題。

修復 exit

這個其實很好處理,把單純的 subsystemctl shell 改成 exec subsystemctl shell 即可。前者是 shell 會去 fork 一個新的 process 出來,然後在裡面執行指令,而後者是把當前的 process 直接替換掉。在後者的情況下它 exit 的時候就是把整個 process 給退出了,所以就能解決這個問題。

1
2
3
if ! subsystemctl is-inside; then
exec subsystemctl shell --quiet
fi

修復 Backspace

這個問題的解決方法就直接寫在了 issue 底下,執行 stty -echoprt 或是 stty sane 之後就能正常使用了。

雖然他說是 Ubuntu-specific 的問題,但是我在 Arch Linux 下也有這個問題,幸好解法一樣

修復環境變數

這個是最大的問題,執行 subsystemctl shell 之後許多的環境變數都會消失不見,包括 PWD PATHWSL_DISTRO_NAME 等等,所以修復好這個是很重要的。

可以在 genie 的 readme 中看到它有支援複製環境變數的功能,讀一下 source code 之後可以知道它是把環境變數等相關資訊先寫入到一個檔案之中,然後再用另外的腳本把它載入回來而已。

這個功能雖然 subsystemctl 不支援,但是其實可以自己實作。我是直接在 .zshrc 中實作,這樣自己就不用去改 Rust 的程式 (因為我也不太會改...)。

整個完整的 implementation 如下,把它放到 .zshrc 的開頭即可:

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
# Start genie in WSL if exists
if [[ -f ~/.subsystemctl_env ]] then
source ~/.subsystemctl_env
rm ~/.subsystemctl_env
stty -echoprt # fix backspace
fi
if [[ -v WSL_DISTRO_NAME ]] then
if (( $+commands[subsystemctl] )); then
if ! subsystemctl is-running; then
sudo subsystemctl start
fi
if ! subsystemctl is-inside; then
cat > ~/.subsystemctl_env << EOF
export PATH="$PATH"
export WSL_DISTRO_NAME="$WSL_DISTRO_NAME"
export WSL_INTEROP="$WSL_INTEROP"
export WSLENV="$WSLENV"
export DISPLAY="$DISPLAY"
export WAYLAND_DISPLAY="$WAYLAND_DISPLAY"
export PULSE_SERVER="$PULSE_SERVER"
cd "$PWD"
EOF
exec subsystemctl shell --quiet
rm ~/.subsystemctl_env # should never reach here, but it is convenient for testing...
fi
fi
fi

這個作法目前唯一的缺點是當你用 ssh 連到 wsl 之中的時候不會有 WSL 相關的環境變數而已,其他一切正常。

WSLg

如果想要使用 WSLg 而非一般的 X server 的話還需要點其他的調整才行。

要使用 WSLg 的話 DISPLAY 要設定為 :0,不過直接在 subsystemctl shell 的 namespace 中還是無法正常使用。Google 可以搜尋到這個解決方案,簡單來說就是把 /tmp/.X11-unix/X0 link 到 /mnt/wslg/.X11-unix/X0 去即可。

所以就加上這部分的 code 即可:

1
2
3
4
5
6
7
8
9
10
11
if [[ -v WSL_DISTRO_NAME ]] then
if [[ -S /mnt/wslg/.X11-unix/X0 ]] then
WSLG_EXIST=1 # prefer wslg if it exists
if [[ ! -S /tmp/.X11-unix/X0 ]] then
# fix wslg not working in subsystemctl namespace
# https://github.com/arkane-systems/genie/issues/175#issuecomment-922526126
ln -s /mnt/wslg/.X11-unix/X0 /tmp/.X11-unix/X0
fi
fi
if (( $+commands[subsystemctl] )); then
# omitted...

那個 WSLG_EXIST 是因為我在下面會根據這個決定要不要改動 DISPLAY 到 X server 去:

1
2
3
if [[ "1" != "$WSLG_EXIST" ]] then
export DISPLAY=$(ip route show default | awk '{print $3}'):0
fi

[更新] 使用新版 WSL 內建的 systemd

編輯 /etc/wsl.conf 新增這幾行:

1
2
[boot]
systemd=true

然後如果前面有使用 subsystemctl 的話請把檢查條件改為:

1
if [[ "$(ps --no-headers -o comm 1)" != "systemd" ]] && (( $+commands[subsystemctl] )); then

然後 wsl --shutdown 後重啟利用 ps --no-headers -o comm 1 檢查看看是不是 systemd,之後可以測試如 systemctl status 的指令確定有沒有正常運作。