OSを作っていて一番面倒なのはデバイスドライバを揃えることなんじゃないかという気がしている。スケジューラとかメモリ管理はまぁまぁ面白いが、デバイスドライバはできれば避けて通りたい。というわけで、Linuxが流用できないか検討することにした。
tl;dr
転倒で顔面直撃というアホみたいな怪我で仕事できずLKL(Linuxカーネルをユーザランドライブラリにしたもの)にUSB/IPを付けてLinuxのUSBスタックをアプリから使う実験をしている https://t.co/DWAW1nvCSo 。USBメモリは見えた。次はホスト側のlibusb化かな。。dmesg: https://t.co/VWKZEypHRf pic.twitter.com/hQtDaxSzU6
— okuoku (@okuoku) June 23, 2019
Kconfigとdmesgはgistに上げておいた: https://gist.github.com/okuoku/ba845c80065df71ee6a5d435572eff66
- USB/IPのVHCIを使用して、socket経由でUSBトラフィックを転送し、LKL側のUSBスタックにmass storageを認識させることができた
- LKLでUNIXドメインソケットやUSBスタックを活用している例は他に見当らなかったが、configで適当に有効にしたら動いた
-
socketpair
syscallは自前でブリッジを書いた
最終的にはWindowsアプリにLinuxカーネルを内蔵し、そのカーネルに内蔵したUSBデバイスドライバを使用してネットワークアクセスやファイルシステムの操作をしたいと考えている。今のところはUSB/IPの都合でLinuxホストでしか動かない。
USB/IPはシンプルな構造で今回のような実験にも便利だし、例えば仮想USBデバイスを作るといった目的にも面白いかもしれない。メンテナンス状況はあまり良くないがWindows実装も存在する( https://github.com/cezuni/usbip-win など )。
LKL (Linux Kernel Library)
Linuxは基本的にカーネルとして使用するソフトウェアだが、それをユーザランド、つまり通常のアプリケーションとして実行しようというプロジェクトがいくつかある。
LKL https://github.com/lkl/linux は、NOMMUとVirtioのコードを再利用し、可能な限り少い手間で単なるユーザランドライブラリとしてLinuxカーネルをビルドできるようにしている。
- https://www.iij.ad.jp/dev/report/iir/034/02_03.html
- IIJ の解説
- https://www.slideshare.net/hajimetazaki/iijlab-seminar-linux-kernel-library-reusable-monolithic-kernel-in-japanese
- IIJの解説(スライド)
- https://retrage01.hateblo.jp/entry/2018/07/21/153000
- LKL.js: Linux kernelを直接JavaScript上で動かす -- Emscriptenを使用してWebブラウザ上で動作させる計画
LKLとそれをホストするアプリケーションとの間のインターフェースは、通常のsyscallとvirtioが想定されている。残念ながらVirtioでは直接USBを通すことはできないため、USBトラフィックをソケットを通してやりとりすることができる USB/IP を今回は使用することにした。
USB/IP (USB Over IP)
USB/IPは、元々はNAISTで開発された( http://usbip.sourceforge.net/old/index.html )USBトラフィックをsocketに載せるプロトコルで、LinuxやWindowsのUSBドライバの論理構造を大きく変えずにネットワーク化することを特徴としている。
このUSB/IP機能は、Linux 2.6.15 あたりでstagingに含まれ、Linux 3以降は通常のドライバとしてUSBスタックの一部になっている。
ドライバの内部構造をほぼそのまま使用するため、ドライバの使用方法も非常にシンプルで、
- 通信の相手先とsocketを確立する
- stubドライバ(デバイスの提供側) または VHCI(仮想ホストコントローラドライバ、デバイスを受け取る側)にsocketのfdを渡す
の2点さえ行えばあとはドライバが面倒を見てくれる。
実際のUSB/IPでは usbipd
のようなユーザランドdaemonが有るが、これは実際のデバイス通信は扱わず、サーバ上でのデバイスの公開処理といった管理面だけを担当する。一度デバイスの接続が確立されると、このdaemonが居なくてもデバイスを使用しつづけることができる。
USB/IP ホストの準備
今回はUSB/IPホストとクライアントは同じマシンとし、
- ホスト(デバイス提供側) : 本物のLinuxカーネル
- クライアント : LKL
という組み合せとした。
ホストへのドライバ組込み
USB/IPはLinuxのmainlineに既に組込まれているものの、基本的にデフォルトでは使用できるようになっていない。Ubuntuなら、
apt install hwdata linux-tools-generic
としてツールを導入し、
modprobe usbip_host
modprobe vhci-hcd
としてドライバを組み込んでから、
/usr/lib/linux-tools-4.15.0-52/usbip list -l
としてデバイスの一覧が取得できることを確認する。基本的に全ての操作はrootで行う必要がある。
デバイス番号の調査
LKLで試したいUSBデバイスを接続し、デバイス番号を調べておく。 dmesg
コマンドには、
[109315.277997] usb 1-1: Product: DataTraveler 3.0
[109315.277998] usb 1-1: Manufacturer: Kingston
[109315.277999] usb 1-1: SerialNumber: 60A44C413B99F1A1B9980092
のように表われるので、 1-1
がデバイス番号であることがわかる。
LKLの準備
LKLは今のところネットワークの研究に使用することを想定しているようで、それ以外のサポートはあまり充実していない。
一応、"hijack"ライブラリとして、既存のユーザランドプログラムのsyscallをリダイレクトするためのツールは用意されているが、今回はこれは使用せず、カーネルインターフェースを直接叩く方向とした。
デバイスドライバ
LKLのリポジトリのデフォルト設定を置き換え、USBスタックやUNIXドメインソケット等が含まれるようにする。
とくに、 USB
、 SCSI
、 UNIX
(UNIXドメインソケット) のような通常のカーネルでは含まれているものがLKLのデフォルトには存在しないので手動で含めている。
あとはリポジトリの README.md
に従って、 make -C tools/lkl
でLKLライブラリをビルドできる。
socketpair
APIの実装
LKLカーネルにデータを流し込むためには何かread
/ write
できるプリミティブが必要になる。LKLではTCP/IPに実績があるのでそれを使うのも良いが、IPアドレスを振ったり、ブリッジを用意するのが面倒だったので今回はUNIXドメインソケットを使用することにした。
UNIXドメインソケットでは、socketpair
APIを使うことで、 双方向に通信できる fdのペアを入手することができる。(Linuxのpipe(2)は単方向なのでこの目的に使えない)
LKLのライブラリは read
とか write
のような、いくつかのsyscallアダプタを提供しているが、 socketpair
は存在しないので自前で用意する必要がある。
int /* -errno */
lkl_socketpair_unix(int out[2]){
long params[6];
long r;
params[0] = AF_UNIX; /* family(int) */
params[1] = SOCK_STREAM; /* type(int) */
params[2] = 0; /* protocol(int) */
params[3] = (long)(&out[0]); /* usockvec(int*) */
params[4] = 0;
params[5] = 0;
r = lkl_syscall(199 /* Socketpair*/ , params);
return r;
}
汎用のsyscallインターフェースとして lkl_syscall
がLKLから提供されているので、これを使用してsyscall番号 119
(asm-genericに定義がある)を直接呼び出す。syscall番号はamd64等他のアーキテクチャとは一致しないのに注意する。
エラーがあった場合は負値の errno
番号を直接返してくる。
ホストfd → LKL fd 転送スレッドの実装
ホストfdはread
やwrite
のようなAPIで読み書きできるが、LKLのfdは専用のAPIである、lkl_sys_read
などを使用する必要がある。このため、 socketpair
でできたfdの片方をUSB/IPドライバに渡し、もう片方は転送スレッドを用意してそこでデータを転送する。
これは単に write
できたら lkl_sys_read
で書き出す、またはその逆を行えば良いだけになる。
static void*
thr_lkl_to_host(void* arg){
int r;
void* buf;
int from;
int to;
fdbridge_param_t* param = (fdbridge_param_t*)arg;
buf = malloc(FDBRIDGE_BUFSIZE);
from = param->from;
to = param->to;
for(;;){
r = lkl_sys_read(from, buf, FDBRIDGE_BUFSIZE);
if(r<0){
printf("ERR\n");
return 0;
}else{
r = write(to, buf, r);
if(r<0){
printf("HOST WRITE ERR: %d\n",errno);
}
}
}
}
LKLは基本的にマルチスレッドセーフになっており、未知のスレッドからsyscallが呼ばれた場合は自動的にLKLのコンテキストが確保される。
if (lkl_ops->tls_get) {
task = lkl_ops->tls_get(task_key); // ★ スレッドにtaskが関連付けられていなかったら?
if (!task) {
ret = new_host_task(&task); // ★ 新しいtaskを確保する
if (ret)
goto out;
lkl_ops->tls_set(task_key, task);
}
}
... リークするんじゃないかなコレ。まぁ良いや。
実際のデバイスの接続
というわけで、LKL上のUSBスタックに実際のUSBデバイスを接続してみる。今回は、本物のLinuxカーネルで練習したあとにLKLでやるという2段構えにしている。
本物のLinuxカーネルでの試行
まずは、usbip
コマンドとusbipd
daemonがやっている操作を手動でやってみることにした。そもそもUNIXドメインソケットで良いのかどうかもよくわからなかったので。
やることは:
- 事前にデフォルトのドライバを切断し、USB/IPの提供するstubドライバに置き換えておく。これは
usbip bind -b 1-1
コマンドで行える。また、 置き換え後にbusnum/devnumを調べる (後述) -
socketpair
syscallでfdを2つ用意する - 片方を
echo -n "4" > /sys/bus/usb/drivers/usbip-host/1-1/usbip_sockfd
のようにしてstubドライバに渡す - もう片方を
echo -n "0 3 65538 3" > /sys/devices/platform/vhci_hcd.0/attach
のようにして仮想ホストコントローラに渡す
の4ステップで完了となる。一度渡されたfdはデバイスドライバ側に保持されるのでcloseしてしまったりしても構わない。
usbip_sockfd
にはfd番号を直接渡すだけで良いので簡単だが、 vhci_hcd.0/attach
の方はちょっと複雑で、計4つのパラメータが必要になる。この4つの構成はUSB/IPのソースコードに書かれていて:
* @rhport: port number of vhci_hcd
* @sockfd: socket descriptor of an established TCP connection
* @devid: unique device identifier in a remote host
* @speed: usb device speed in a remote host
rhport
は0でOK、sockfd
はfd番号、devid
はstubドライバ側のID、speed
はUSB的なSpeedでHigh-Speedなら 3 となる。
stubドライバのIDは、stubドライバをアタッチした後に確定するので、 /sys/bus/usb/drivers/usbip-host/1-1/busnum
や devnum
を事前にcat
して調べておく必要がある。
LKLでやってみる
LKLでは、仮想ホストコントローラとしてLKL側のものを使用する以外は、特に違いはない。
で、上のコードを実行すると、
[ 1.492667] vhci_hcd vhci_hcd.0: pdev(0) rhport(0) sockfd(1)
[ 1.492683] vhci_hcd vhci_hcd.0: devid(65539) speed(3) speed_str(high-speed)
Done.
[ 2.490873] usb 1-1: new high-speed USB device number 2 using vhci_hcd
[ 2.671030] usb 1-1: SetAddress Request (2) to port 0
[ 2.735418] usb 1-1: New USB device found, idVendor=0951, idProduct=1666, bcdDevice= 0.01
[ 2.735429] usb 1-1: New USB device strings: Mfr=1, Product=2, SerialNumber=3
[ 2.735432] usb 1-1: Product: DataTraveler 3.0
[ 2.735434] usb 1-1: Manufacturer: Kingston
[ 2.735435] usb 1-1: SerialNumber: 60A44C413B99F1A1B9980092
[ 2.747325] usb-storage 1-1:1.0: USB Mass Storage device detected
[ 2.747671] scsi host0: usb-storage 1-1:1.0
[ 3.361118] random: fast init done
[ 3.795668] scsi 0:0:0:0: Direct-Access Kingston DataTraveler 3.0 PQ: 0 ANSI: 6
[ 3.815183] sd 0:0:0:0: [sda] 30218842 512-byte logical blocks: (15.5 GB/14.4 GiB)
[ 3.826961] sd 0:0:0:0: [sda] Write Protect is off
[ 3.838672] sd 0:0:0:0: [sda] Write cache: disabled, read cache: enabled, doesn't support DPO or FUA
[ 3.920725] sda: sda1
[ 3.971403] sd 0:0:0:0: [sda] Attached SCSI removable disk
のように表示され、LKL側のUSBスタックに実際のUSBデバイスを認識させることができた。
何の役に立つのか
これ単体では何とも言えないが、例えばLKLではファイルシステムも使えるので、LinuxデバイスのHDDを直接読むツールとかは面白いかもしれない。LKL自体はWindowsでの動作実績もあり、USB/IPは単純なプロトコルなのでlibusbのようなユーザランドライブラリとのブリッジを書くのもそれほど難しくないと考えている。
またNetBSDのrumpkernelやDragonFly BSDのVKERNELのように、ドライバの開発用にユーザランド側にコードを持っていくのは有用な気がしている。今のところLKLが公式にサポートする外部との接続手段はvirtioのサポートするネットワークデバイスとブロックデバイスに限られているが、ここにUSBが加わることでさらに多くのデバイスドライバが使えるようになる。
ただ、本当に実用的にデバイスドライバを活用しようとするならば、どうしてもユーザランド側の制御daemonは必要になってしまう(ファームウェアの注入とかwpa_supplicant
のようなプロトコル処理など)。本格的な活用には、qemuのユーザランドエミュレーションと接続するとか、何か既存のバイナリをより良いエミュレーション精度で活用できる仕組みが必要になるだろう。