WindowsとUbuntuの相互運用の仕組み(後編)
WindowsとUbuntuの相互運用の仕組み(後編)です。bash.exeからWSLへのI/Oリダイレクション
初期リリースの「WSL」では、「bash.exe」はコンソールの入出力のみサポートしていた。ユーザーが「bash.exe」を起動し、「bash.exe」をSSHでリモートPCに接続するかのうような使い方ならばそれで十分だった。
しかしユーザーが「WSL」でWin32アプリケーションやWindowsのスクリプトと連携して作業を行うには、それでは不十分だった。
「WSL」のユーザーはすぐにそのことを指摘した。
実際この要望は2番目に課題として「#2 issue on our GitHub」に挙げられている。
現在のBuildでは、「WSLプロセス」から、もしくは「WSLプロセス」へ入出力をパイプする機能が利用できるようになっている。
WSLプロセス起動時のパラメーター
「LxssManager」のCOMインターフェースを経由して「WSLプロセス」を起動する時、 様々なパラメーターを指定できる。例えば実行するバイナリーへのパスや現在の作業ディレクトリーのようなパラメーターがある。
他にも「WSLプロセス」が使用する標準入力、標準出力、標準エラーのNTハンドルを指定するパラメーターもある。
これらのNTハンドルは、作成される「WSLプロセス」と共有するため「Lxss Manager Service」に渡される。
ハンドルのマーシャリングとアンマーシャリング
「NTプロセス」と「WSLプロセス」間でハンドルを共有することを「マーシャリング」と呼ぶ。「マーシャリング」は「LxBus MessagePort」オブジェクトの「ioctl」として実装されており、ハンドルからハンドルに対応した一意になる識別子を呼び出し側に返す。
この識別子は「WSLプロセス」生成時、他のパラメーターと共に「/init」デーモンに送られる。
(メッセージの送信)
このメッセージを受け取った「/init」はメッセージの内容を解析し、新しい「WSLプロセス」がこれらのハンドルを利用できるよう、ハンドルの「アンマーシャリング」を開始する。
「アンマーシャリング」も「LxBus MessagePort」オブジェクトの「ioctl」として実装されており、識別子からファイルディスクリプターを返す。
この「ioctl」を利用することで「/init」は3つのハンドルを「アンマーシャリング」し、ファイルディスクリプター0,1,2を置き換えるため「dup2」システムコールを呼び出す。
コンソールハンドル
もうひとつ「アンマーシャリング」されるハンドルがある。それは「コンソールハンドル」である。
「コンソールハンドル」は、「WSLインスタンス」内で「tty」デバイスを作成するために使用される。
1つの「bash.exe」ウィンドウから生成されたすべての「WSLプロセス」は、同じ「tty」デバイスを持つ。
子プロセスの生成へ
すべてのハンドルが「アンマーシャリング」されると、「/init」は「fork」システムコールと「exec」システムコールを使用し、子プロセスを生成する。コマンドの実行
もしあなたが「bash」に馴染みがあるなら、「-c」オプションの機能を知っているだろう。「-c」オプションは、「-c」オプションに指定された内容を実行する。
-c If the -c option is present, then commands are read from the
first non-option argument command_string. If there are argu‐
ments after the command_string, they are assigned to the
positional parameters, starting with $0.
first non-option argument command_string. If there are argu‐
ments after the command_string, they are assigned to the
positional parameters, starting with $0.
この時「bash」は、非インタラクティブモード(非対話モード)で動作する。
「Bash.exe」でも似たような動作を行い、「Bash.exe」に指定されたオプションの値を「/bin/bash」に送り、コマンドを実行する。
WSL内からWin32アプリケーションの起動
「WSL(Bash)」内からWin32アプリケーションを起動できるようになった。binfmt_miscという仕組み
「Linux」には、「binfmt_misc」と呼ばれる機能があり、ユーザーモードのアプリケーションはこの機能を利用してアプリケーションが扱う特定の実行形式(ファイルタイプ)をカーネルに登録することができる。例えばJavaやMono、Wineはこの機能を利用しており、ファイルタイプをLinux Kernelに登録している。
例えば「Linux」上で、Windowsアプリケーションである「exe」と「Wine」を結びつけ、「bash」から直接Windowsアプリケーションを起動することができる。
登録に使用する文字列
ファイルタイプを登録する時に使用する文字列はコロンで区切られた7つのフィールドで構成された文字列を使用する。いくつかのフィールドは任意であり、フィールド内の文字列を省略することができる。
ファイルタイプを登録するには、ユーザーモードアプリケーションが「binfmt_misc」登録ファイル(/proc/sys/fs/binfmt_misc/register)に文字列を書き込む。
例えば以下のように文字列を書き込む。
sudo echo ":WSLInterop:M::MZ::/init:" > /proc/sys/fs/binfmt_misc/register
上記フィールドの説明を紹介すると、以下のようになる。
フィールド | 内容 | 概要 |
---|---|---|
WSLInterop | 登録名 | 一意でなければならない |
M | 登録タイプ | マジックバイトの配列か拡張子を登録する |
MZ | マジックバイトの配列 | MZはWindowsのPEイメージのマジックバイトである |
/init | インタープリター | 実際に処理を行うプログラム |
インタープリターは、登録するファイルタイプを扱うことができるシステム上のスクリプトやバイナリーである。
execシステムコールは「binfmt_misc」の登録情報からインタープリターを列挙し、実行しようとしているファイルの条件に適合するインタープリターを探す。
もし条件に合うインタープリターが見つかった場合、execシステムコールはインタープリターへのパスをArgv[0]に設定し、残りのパラメーターを1つだけ右にずらして設定する。
シェバングのような仕組み
もしあなたがbashスクリプトやPerlスクリプトの頭に記述されるシェバング(#!)オペレーターに馴染みがあるなら、この仕組みに聞き馴染みがあるだろう。シェバングには実行するインタプリターの情報を記述し、シェルはシェバングを読み込みスクリプトの実行を行うインタプリターの呼び出しやパラメーターの引き渡しを行う。
#!/bin/bash
echo "this is bash script"
echo "this is bash script"
シェバングより柔軟な仕組み
「binfmt_misc」はもっと柔軟にシェバングと同じ目的を達成する仕組みである。ファイルの先頭にシェバングのような情報を記述する必要もないし、バイナリーの実行ファイルに対しても完璧に動作する仕組みである。
/initは複数の目的を実現する機能を備える
ファイルタイプを登録する時に使用する文字列でも紹介した通り、「/init」は「binfmt_misc」のインタプリターとして使用している。「WSL」において「/init」は、複数の目的を実現するためのソフトウェアである。
「/init」は「Microsoft」によって開発されており、「LxssManager.dll」に含まれるバイナリーリソースとして提供される。
「/init」には目的に応じて以下の動作モードがある。
- デーモンモード
- binfmt_miscインタープリターモード
1.デーモンモード
「/init」起動時、最初に自身のPID(プロセスID)を調べる。もしPIDが1なら「/init」はデーモンモードとして動作し、「WSL」内の「Lxss Manger service」の終端として動作する。
2.binfmt_miscインタープリターモード
PIDが1以外だったら、「/init」はbinfmt_miscインタープリターモードで動作する。このモードでは、NTバイナリーの起動を行う。
注意事項
NTバイナリーの起動に関する注意事項です。1.NTプロセスの権限
「NTプロセス」は、「bash.exe」を起動した環境の権限と同じ権限で起動する。例えば管理者権限で起動した「コマンドプロンプト」から「bash.exe」を起動した場合、「Bash」から「NTプロセス」を起動すると、「NTプロセス」は管理者権限で起動する。
2.DrvFSでのみ動作可能
「NTバイナリー」は、「DrvFS」上のパス内でのみ動作可能である。例えば「/mnt/c/Windows/System32」パスは、「DrvFS」上のパスである。
「VolFS」内のファイルの編集等の操作は出来ない。
例えば「~/」パスは、「VolFS」内のパスである。
3.作業ディレクトリーを引き継ぐ
「NTプロセス」の作業ディレクトリーは、現在の作業ディレクトリーが「DrvFS」内のパスであれば、現在の作業ディレクトリーがそのまま「NTプロセス」の作業ディレクトリーとして引き継がれる。もし現在の作業ディレクトリーが「VolFS」内のパスであれば、「NTプロセス」の作業ディレクトリーは、「bash.exe」を起動した時の現在の作業ディレクトリーになる。
4.将来のリリースではWindowsのPATH環境変数が共有される
現在の実装では、NTの環境変数とWSLの環境変数は別々に存在しており共有されていない。これはPATH環境変数も同様である。
将来のリリースでは、ユーザーのNTのPATH環境変数がWSLのプロセスと共有されるようになる。
5.ビルトインコマンドはcmd.exe経由で呼び出す
おそらくあなたが最初に打つであろうNTコマンドの一つは、Windowsの「dir」コマンドだろう。Linuxの「/bin/ls」はバイナリーで提供されているが、「dir」コマンドはそれと異なりビルトインコマンドである。
「dir」コマンドの実行はバイナリーの起動ではないため、相互運用の仕組みをそのまま利用して「dir」コマンドを実行することが出来ない。
Windowsの「dir」コマンドのようなバイナリーではないコマンドを実行するには、「cmd.exe」を経由して実行する。
例えば以下のような記述になる。
/mnt/c/Windows/System32/cmd.exe /c dir
頻繁に使用するコマンドであり、上記のように長い記述を行いたくない場合は、エイリアスを利用すると良い。
例えば以下のような記述になる。
alias dir='/mnt/c/Windows/System32/cmd.exe /c dir'
エイリアス設定後「dir」を入力するだけでWindowsの「dir」コマンドを利用できるようになる。
6.binfmt_miscのインタープリターが今後変更になるかも
現在「/init」がbinfmt_miscのインタープリターとして使用されているが、今後変更される可能性がある。WSLからWin32へのI/Oリダイレクション
「bash」内から「NTプロセス」を起動するためには、「WSL」が保持している標準入力、標準出力、標準エラーを表すファイルディスクリプターに「NTバイナリー」からアクセスする方法が必要になる。この方法はVSFファイルをマーシャリングするAPI群を導入することで実現している。
CreateProcessを想像してみよう
Windows APIのプロセスを生成する「CreateProcess」APIに馴染みがあるなら、おそらくあなたは理解が早いだろう。「CreateProcess」APIには、標準入力、標準出力、標準エラーを表すハンドルを渡すことができる。
ただし「WSL」が保持しているのはファイルディスクリプターであり、ハンドルではないという問題がある。
VFSファイルのマーシャリング
そこでVFSファイルのマーシャリングを行う。binfmt_miscのインタープリターである「/init」は、「NTプロセス」に向けて標準入力、標準出力、標準エラーのファイルディスクリプターのマーシャリングを必要としているか決定する。
ファイルディスクリプターのマーシャリング
「LxBus MessagePort」が提供する「Marshal VFS file ioctl」を使用してファイルディスクリプターのマーシャリングを行う。この時引数にファイルディスクリプターを指定する。
「LxCore.sys」ドライバーがこの「ioctl」を処理し、「LxCore.sys」ドライバーは「NTプロセス」が「VFSファイルオブジェクト」を参照できるように、指定されたファイルディスクリプターを使用してマーシャリングを行う。
その後「VFSファイルオブジェクト」は「LxBus MessagePort」ごとに管理されている「Marshalled VFSファイルオブジェクト」のリストに追加される。
この「ioctl」の呼び出しは、マーシャリングされた「VFSファイルオブジェクト」を識別する一意のトークンを返す。
bash.exeがトークンを受け取る
「WSLプロセス」は「VFSファイルオブジェクト」を識別する一意のトークンを「LxBus message」に乗せ、「LxBus MessagePort」を通じて「NTプロセス」へと送る。「NTプロセス(bash.exe)」がこのメッセージを受け取ると、トークンを指定して「Unmarshal VFS file ioctl」を呼び出す。
「LxCore.sys」は「Marshalled VFSファイルオブジェクト」リストを参照し、指定されたトークンに適合する「VFSファイルオブジェクト」を検索する。
「VFSファイルオブジェクト」が見つかれば「Marshalled VFSファイルオブジェクト」リストから「VFSファイルオブジェクト」を削除し、「VFSファイルオブジェクト」へのNTハンドルを呼び出し側に返す。
このNTハンドルは「CreateProcess」APIに指定する標準的なハンドルとして使用することができる。
このNTハンドルはWSLインスタンス内のファイルオブジェクトを参照するハンドルであるため、「Run-Down Protection」の対象になる。
ファイルオブジェクトに読み書きのアクセスを行うには、「NTプロセス」のアクセス中にWSLインスタンスの状態が変化しないことを保証するために、「Run-Down Protection」から許可を得る必要がある。