仙石浩明の日記

2022年5月11日

WSL2 (Windows Subsystem for Linux 2) で物理ディスク上の独自 OS を動かしてみた

Windows で VMware Workstation Player を使って Linux ベースの独自 OS (以下 GCD OS と呼ぶ) を動かしていたのだけど、 WSL2 (Windows Subsystem for Linux 2) が物理ディスクをマウントできるようになったと聞き、 WSL2 が VMware の代りに使えるか試してみた。 ただし、物理ディスクをマウントするには Windows 11 ビルド 22000 以上が必要。

本当は Linux カーネルも自前でビルドしたものを使いたいが、 とりあえず WSL2 のカーネルをそのまま使い、 root ファイルシステムは物理ディスク上にインストールしてある GCD OS を使う方向で考えた。

以前、 OpenVZ な VPS サービス上で GCD OS を動かしたことがある (10年前!) ので、 その時と同様に chroot して物理ディスク上の GCD OS を起動するのが簡単そう。 つまり、 以下のシェルスクリプト gcd.sh を WSL2 上で実行するだけ:

#!/bin/sh
PATH="/mnt/c/WINDOWS/System32:/usr/bin:/bin:/usr/sbin:/sbin"
ROOT="/usr/local/GCD"

cd /mnt/c
wsl.exe --mount '\\.\PHYSICALDRIVE1' --bare

test -d $ROOT || mkdir $ROOT
mount LABEL=/ $ROOT

$ROOT/etc/init.d/chroot

このスクリプトは WSL2 の仮想ディスク (Virtual Hard Disk) 内に置いてもよいが、 私は Windows の C: ドライブに置いた。 WSL2 の内容を変更する必要がなくなるし、 WSL2 でどのディストリビューションを使っているかに依存しなくなる。 仮想ディスクが肥大化したら、インストールし直して初期状態に戻してもよい。

例えば C:\bin\gcd.sh に置いて、 次のように実行する:

C:\Windows\System32\wsl.exe -u root -- /mnt/c/bin/gcd.sh

タスクスケジューラーで Windows の起動時に自動的に実行すれば、 Windows にログインしなくても外部から ssh で GCD OS へログインできるので便利。 VMware より軽くて手軽で便利かも?

以下、このスクリプト gcd.sh を順に説明する:

まず、 「wsl.exe --mount '\\.\PHYSICALDRIVE1' --bare」 で WSL2 から物理ディスク 「PHYSICALDRIVE1」 が見えるようにする。 wsl.exe は Windows のコマンドだが、 WSL2 は Linux なのに /mnt/c にマウントされた C: ドライブ上の Windows のコマンドが実行できる (Linux カーネルの binfmt_misc を使っている)。

(Linux の) lsblk コマンドを使うと、 物理ディスクが見えることが確認できる:

Linux 5.10.16.3-microsoft-standard-WSL2.
kayano:~ $ lsblk
NAME   MAJ:MIN RM   SIZE RO TYPE MOUNTPOINTS
sda      8:0    0   256G  0 disk
sdb      8:16   0 339.7M  1 disk
sdc      8:32   0   256G  0 disk /mnt/virtual
sdd      8:48   0   7.3T  0 disk
├─sdd5   8:53   0   200G  0 part 
├─sdd6   8:54   0    16G  0 part [SWAP]
└─sdd7   8:55   0   6.8T  0 part /

sdd が WSL2 から見えるようになった物理ディスク (8TB HDD)。 パーティションが 3つあることが分かる。 うち sdd7 が GCD OS の root ファイルシステム。

一点注意すべきなのは、 Windows のシステムドライブ 「PHYSICALDRIVE0」 は指定できないので、 システムドライブとは異なる物理ディスクを使う必要があるという点。 8TB HDD など大容量のハードディスクを、 パーティションで区切って Windows と Linux の両方をインストールしている人は多いと思うのだけど、 残念ながら Windows と同じディスク上にある Linux パーティションは、 WSL2 から使うことはできない。 VMware ならできるのに...

仕方がないので私は (わざわざ) M.2 NVMe SSD を買って Windows のシステムドライブを NVMe へ移し、 SATA0 につないだ 8TB HDD を (Windows をインストールしていたパーティション 1〜4 を削除して) Linux 専用にした。

また、WSL2 上で wsl.exe を実行するとき、 カレントディレクトリが WSL2 の仮想ディスクだと、 どんな引数でも常に 「Invalid argument」 エラーになる:

root@kayano:~# /mnt/c/Windows/System32/wsl.exe --shutdown
/mnt/c/Windows/System32/wsl.exe: Invalid argument
                                                 root@kayano:~#

エラー出力後の改行がうまくいかないあたり、 いかにもバグっぽい?

カレントディレクトリが C: ドライブだと正常に実行できるようなので、 wsl.exe を実行する前に 「cd /mnt/c」 を行っている。

物理ディスクが WSL2 で見えるようになったら、 次にこの物理ディスクをマウントする: 「mount LABEL=/ $ROOT」。 ここでは LABEL が 「/」 のパーティションを 「$ROOT」 つまり /usr/local/GCD へマウントしている。

あとは GCD OS を起動するだけ: 「$ROOT/etc/init.d/chroot」。 /etc/init.d/chrootOpenVZ 上で GCD OS を起動するときも使った、 以下のシェルスクリプト:

#!/bin/sh
root=`echo $0 | sed -e 's@/etc/init.d/chroot$@@'`
if [ ! -d $root ]; then
   echo "Can't find root: $root"
   exit 1
fi
sed -n 's/^[a-z][_a-z]* \([^ ][^ ]*\) .*/\1/p' < /proc/mounts | while read d; do
    if [ -d "$d" ]; then
	test -d "$root$d" || mkdir "$root$d"
	mount -obind "$d" "$root$d"
    elif [ -f "$d" ]; then
	test -f "$root$d" || touch "$root$d"
	mount -obind "$d" "$root$d"
    fi
done
if [ -d /lib/modules/`uname -r` ]; then
    mount -obind /lib/modules $root/boot/lib/modules
fi
chroot $root /bin/sh <<EOF
swapon -a
mount -a -t ext4
/etc/init.d/svscanboot &
/etc/rc.d/rc.M
EOF

このシェルスクリプトは、 まず WSL2 の /proc/mounts を参照して、 WSL2 がマウントしているディレクトリとファイルを、 そのまま GCD OS のルート (/usr/local/GCD) へマウントする。 これで GCD OS でも /mnt/c にマウントされた Windows コマンドを実行できるようになる。

次に 「chroot /usr/local/GCD /bin/sh」 を実行して、 chroot 環境下で svscanboot/etc/rc.d/rc.M を実行する。 svscanboot は daemontools の起動スクリプト。 GCD OS のほとんどのデーモン類は daemontools の管理下で起動される。 一方 /etc/rc.d/rc.M は、 GCD OS のブートスクリプトで、 ネットワーク等の各種設定と、 一部のデーモン類の起動を行なう。

ssh サーバや WWW サーバも /etc/rc.d/rc.M が立ち上げるが、 いままで WSL2 のネットワークは Windows 内でしか見えないバーチャルネットワーク (内部ネットワーク) だったらしい。 「WSL2 外部から接続」 などとググっても、 外部から WSL2 の ssh サーバにログインするには (netsh.exe の) portproxy を使え、という話ばかり出てくる。

Hyper-V Virtual Switch

どのバージョンから可能になったのかは知らないが、 「仮想スイッチ マネージャー」 で設定すれば VMware のようなブリッジモードが WSL2 でも使えるようになる。

まず Windows 管理ツールの 「Hyper-V マネージャー」 を実行する。 仮想マシン (この例では KAYANO) を選択し、 「操作(A)」 メニューから 「仮想スイッチ マネージャー(C)...」 を選ぶと、 「仮想スイッチ マネージャー」のウィンドウが開く。

左ペイン 「仮想スイッチ」 の中から 「WSL」 を選び、 右ペインに表示される 「接続の種類」 として 「外部ネットワーク(E):」を選択し、 適切なネットワーク アダプターを選択する。

これで WSL2 が外部ネットワークと通信できるようになる。 (いまのところ) タグVLAN が使えるようにはできていないが、 VMware ではタグVLAN が使えるので、 なんとかしてタグVLAN が使えるようにしたいところ...

他のマシン (以下の例では senri) から ssh で GCD OS (kayano) へログインしてみる:

senri:/ # ssh kayano
Last login: Wed May 11 04:16:34 2022 from senri.gcd.org
Linux 5.10.16.3-microsoft-standard-WSL2.
kayano:~ # ip addr show dev eth0
6: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP qlen 1000
    link/ether 00:15:5d:ff:11:b1 brd ff:ff:ff:ff:ff:ff
    inet 192.168.18.40/24 brd 192.168.18.255 scope global eth0
       valid_lft forever preferred_lft forever
    inet6 fe80::215:5dff:feff:11b1/64 scope link
       valid_lft forever preferred_lft forever
kayano:~ # tcpspray senri
Transmitted 102400 bytes in 0.000315 seconds (317460.317 kbytes/s)
kayano:~ # free
             total       used       free     shared    buffers     cached
Mem:       8055704     573892    7481812          0      30704     178088
-/+ buffers/cache:     365100    7690604
Swap:     18874364          0   18874364
kayano:~ # chroot_escape /bin/bash
groups: cannot find name for group ID 11
groups: cannot find name for group ID 14
root@kayano:/# lsb_release -d
Description:    Ubuntu 20.04.4 LTS
root@kayano:/# exit
kayano:~ #

chroot 環境から脱出 (chroot_escape /bin/bash) すると WSL2 の Ubuntu 環境に戻る。 で、exit すると GCD OS に戻る。

この GCD OS で物理ディスクの読み書き速度を測ってみた:

kayano:/ # hdparm -t /dev/sdd7

/dev/sdd7:
 Timing buffered disk reads:  538 MB in  3.01 seconds = 178.93 MB/sec

kayano:/ # dd if=/dev/zero of=/tmp/test bs=1024k count=10240
10240+0 records in
10240+0 records out
10737418240 bytes (11 GB, 10 GiB) copied, 60.5833 s, 177 MB/s

読み書きともに 177MB/sec くらい。 Core i5-8500 マシンだとこんなもの? 同じ PC (同じ Windows) で VMware 上の同じ GCD OS でも測ってみると、 読込みが 180.62 MB/sec で書き込みが 132 MB/s だった。 簡易な測定なので、ほぼ同等と言っていいと思う。

ちなみに仮想化なしで直接このマシン上で測ると、 読込み 178.65 MB/sec 書込み 233 MB/s なので、 仮想マシンによるオーバーヘッドは多少あるようだ。 とはいえ AMD FX-4100 マシンとかだと読込み 174.82 MB/sec 書込み 95.3 MB/s なので、 実用上は 177MB/sec もあれば充分?

AMD FX とかの 10年前のマシンだと CPU がボトルネックになってる感じ。そろそろ 「Sandyで十分おじさん」 は卒業すべき?

ネットワークの速度も測ってみた:

kayano:/ # iperf3 -c esaka
Connecting to host esaka, port 5201
[  5] local 172.17.14.235 port 35504 connected to 192.168.18.20 port 5201
[ ID] Interval           Transfer     Bitrate         Retr  Cwnd
[  5]   0.00-1.00   sec   116 MBytes   970 Mbits/sec    0   3.01 MBytes       
[  5]   1.00-2.00   sec   111 MBytes   933 Mbits/sec    0   3.01 MBytes       
[  5]   2.00-3.00   sec   112 MBytes   944 Mbits/sec    0   3.01 MBytes       
[  5]   3.00-4.00   sec   112 MBytes   944 Mbits/sec    0   3.01 MBytes       
[  5]   4.00-5.00   sec   112 MBytes   944 Mbits/sec    0   3.01 MBytes       
[  5]   5.00-6.00   sec   111 MBytes   933 Mbits/sec    0   3.01 MBytes       
[  5]   6.00-7.00   sec   112 MBytes   944 Mbits/sec    0   3.01 MBytes       
[  5]   7.00-8.00   sec   112 MBytes   944 Mbits/sec    0   3.01 MBytes       
[  5]   8.00-9.00   sec   112 MBytes   944 Mbits/sec    0   3.01 MBytes       
[  5]   9.00-10.00  sec   111 MBytes   933 Mbits/sec    0   3.01 MBytes       
- - - - - - - - - - - - - - - - - - - - - - - - -
[ ID] Interval           Transfer     Bitrate         Retr
[  5]   0.00-10.00  sec  1.10 GBytes   943 Mbits/sec    0             sender
[  5]   0.00-10.05  sec  1.10 GBytes   938 Mbits/sec                  receiver

iperf Done.

1Gbits/sec の LAN なので、ほぼ上限の速度が出ている。 同条件で VMware でも測ってみると、 送信が 928 Mbits/sec で受信が 925 Mbits/sec だった。 仮想化なしでも、 送信が 943 Mbits/sec 受信が 941 Mbits/sec なのでほとんど同じ。

というわけで、 速度的にはディスクもネットワークも問題無さそう。

5/12 追記:

Hyper-V Virtual Switch Error

Windows を再起動すると、 仮想スイッチ 「WSL」 が 「外部ネットワーク」 に接続する際にエラーが発生し、 接続先が 「内部ネットワーク」 に戻ってしまう。 これは ネットワーク アダプター が既に使われているため。 起動直後は ネットワーク アダプター が 「Microsoft 仮想スイッチ プロトコル」 にバインドされているらしい。

したがって、 ネットワーク アダプター のプロパティを開いて、 全ての項目のチェックを外してから 「外部ネットワーク」 に切替えればよい。 と言っても、 毎回コントロールパネルを開いて、 ネットワークアダプタのプロパティを変更するのは現実的ではないので、 PowerShell スクリプトでネットワーク アダプター ($nic) のバインドを外すことにする:

$nic = "Realtek PCIe GbE Family Controller"
Disable-NetAdapterBinding -InterfaceDescription $nic
Set-VMSwitch -name WSL -NetAdapterInterfaceDescription $nic

Disable-NetAdapterBinding」 でネットワークアダプターの全てのバインドを外し、 次の 「Set-VMSwitch」 で仮想スイッチ 「WSL」 の接続を、 このネットワークアダプターへ切替える。

これで仮想スイッチ 「WSL」 がネットにつながった。

Hyper-V Virtual Switch

これで仮想スイッチにつながっている WSL2 は通信できるようになる。 WSL2 から外部へのアクセスはもちろん、 外部から WSL2 へのアクセスも自由。

vEthernet (WSL) Binding

ところが肝心の Windows 自体がネットから切り離されたまま。 WSL2 は通信できるのに、 Windows の WSL2 以外のプログラムは通信できない、 という (ちょっと) 不思議な状態に陥ってしまう。

再起動したとき、 一時的に仮想スイッチが無くなるので、 Windows は仮想スイッチ経由でネットに接続できていたことを忘れてしまうみたい。

仮想スイッチ 「vEthernet (WSL)」 のプロパティを開いて、 「Microsoft ネットワーク用クライアント」 や 「インターネット プロトコル バージョン 4 (TCP/IPv4)」 などに再度チェックを入れれば通信できるようになる。

もちろん手作業ではやってられないので、 次の PowerShell スクリプトで行う:

Get-NetAdapterBinding -name 'vEthernet*' |
 ? ComponentID -Match 'vmware_bridge|ms_tcpip|ms_tcpip6|ms_server|ms_msclient' |
 Enable-NetAdapterBinding -PassThru

Get-NetAdapterBinding -name 'vEthernet*'」 で仮想スイッチのバインドを列挙し、 「Microsoft ネットワーク用クライアント」 や 「インターネット プロトコル バージョン 4 (TCP/IPv4)」 など、 チェックを入れたい項目を抜き出して (ComponentID -Match)、 チェックを入れる (Enable-NetAdapterBinding)。

以上まとめると、 起動時に次の PowerShell スクリプト gcd.ps1 を実行すれば良い:

wsl --mount '\\.\PHYSICALDRIVE1' --bare
wsl -u root -- /mnt/c/bin/gcd.sh

$nic = "Realtek PCIe GbE Family Controller"
Disable-NetAdapterBinding -InterfaceDescription $nic
Set-VMSwitch -name WSL -NetAdapterInterfaceDescription $nic
Get-NetAdapterBinding -name 'vEthernet*' |
 ? ComponentID -Match 'vmware_bridge|ms_tcpip|ms_tcpip6|ms_server|ms_msclient' |
 Enable-NetAdapterBinding -PassThru

Windows の起動時に C:\bin\gcd.ps1 を実行するには、 タスクスケジューラの 「操作」 において、 「プログラム」 として 「%SystemRoot%\System32\WindowsPowerShell\v1.0\powershell.exe」 を設定し、 「引数」 として 「C:\bin\gcd.ps1」 を設定すればよい。

wsl --mount」 を PowerShell スクリプトへ移したことで、 C:\bin\gcd.sh は以下のようになる:

#!/bin/sh
ROOT="/usr/local/GCD"
test -d $ROOT || mkdir $ROOT
mount LABEL=/ $ROOT
$ROOT/etc/init.d/chroot
Filed under: システム構築・運用 — hiroaki_sengoku @ 09:37

No Comments »

No comments yet.

RSS feed for comments on this post. TrackBack URL

Leave a comment