USB/IPとLKLを使ってLinuxのUSBスタックをユーザランドで使う実験

OSを作っていて一番面倒なのはデバイスドライバを揃えることなんじゃないかという気がしている。スケジューラとかメモリ管理はまぁまぁ面白いが、デバイスドライバはできれば避けて通りたい。というわけで、Linuxが流用できないか検討することにした。

# tl;dr

<blockquote class="twitter-tweet" data-lang="en"><p lang="ja" dir="ltr">転倒で顔面直撃というアホみたいな怪我で仕事できずLKL(Linuxカーネルをユーザランドライブラリにしたもの)にUSB/IPを付けてLinuxのUSBスタックをアプリから使う実験をしている <a href="https://t.co/DWAW1nvCSo">https://t.co/DWAW1nvCSo</a> 。USBメモリは見えた。次はホスト側のlibusb化かな。。dmesg: <a href="https://t.co/VWKZEypHRf">https://t.co/VWKZEypHRf</a> <a href="https://t.co/hQtDaxSzU6">pic.twitter.com/hQtDaxSzU6</a></p>&mdash; okuoku (@okuoku) <a href="https://twitter.com/okuoku/status/1142768332791173120?ref_src=twsrc%5Etfw">June 23, 2019</a></blockquote>

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スタックの一部になっている。

ドライバの内部構造をほぼそのまま使用するため、ドライバの使用方法も非常にシンプルで、

1. 通信の相手先とsocketを確立する
2. 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ドメインソケット等が含まれるようにする。

- https://gist.github.com/okuoku/ba845c80065df71ee6a5d435572eff66#file-defconfig
  - 今回使用した `defconfig`

とくに、 `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` は存在しないので自前で用意する必要がある。

- https://github.com/okuoku/usbip-localbridge/blob/90fbf42eab76e311946d34f6e6161eec33e9a3db/lklbridge.c#L82

``` 適当なsocketpairスタブ.c
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` で書き出す、またはその逆を行えば良いだけになる。

- https://github.com/okuoku/usbip-localbridge/blob/90fbf42eab76e311946d34f6e6161eec33e9a3db/lklbridge.c#L30

```工学部を出た人間が書いてはいけないコード.c
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のコンテキストが確保される。

- https://github.com/lkl/linux/blob/0a4ebeadad12b94db665b8daf30e44e949a02d90/arch/lkl/kernel/syscalls.c#L111

```syscalls.c
	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カーネルでの試行

![SnapCrab_NoName_2019-6-24_20-35-20_No-00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/67446/23191562-5b3d-4bd8-8e1b-6c22373bc716.png)

まずは、`usbip`コマンドと`usbipd` daemonがやっている操作を手動でやってみることにした。そもそもUNIXドメインソケットで良いのかどうかもよくわからなかったので。

- https://github.com/okuoku/usbip-localbridge/blob/90fbf42eab76e311946d34f6e6161eec33e9a3db/localbridge.c
  - 実際のコード

やることは:

1. 事前にデフォルトのドライバを切断し、USB/IPの提供するstubドライバに置き換えておく。これは `usbip bind -b 1-1` コマンドで行える。また、 **置き換え後にbusnum/devnumを調べる** (後述)
2. `socketpair` syscallでfdを2つ用意する
3. 片方を `echo -n "4" > /sys/bus/usb/drivers/usbip-host/1-1/usbip_sockfd` のようにしてstubドライバに渡す
4. もう片方を `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のソースコードに書かれていて:

- https://github.com/lkl/linux/blob/0a4ebeadad12b94db665b8daf30e44e949a02d90/drivers/usb/usbip/vhci_sysfs.c#L317

```vhci_sysfs.c
	 * @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でやってみる

![SnapCrab_NoName_2019-6-24_20-35-35_No-00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/67446/1644f023-c75a-8d1b-372d-8c2a5f1a13a5.png)

- https://github.com/okuoku/usbip-localbridge/blob/90fbf42eab76e311946d34f6e6161eec33e9a3db/lklbridge.c
  - 実際のコード

LKLでは、仮想ホストコントローラとしてLKL側のものを使用する以外は、特に違いはない。

で、上のコードを実行すると、

- https://gist.github.com/okuoku/ba845c80065df71ee6a5d435572eff66#file-lkl-usbip-mass-txt-L97

```dmesg
[    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のユーザランドエミュレーションと接続するとか、何か既存のバイナリをより良いエミュレーション精度で活用できる仕組みが必要になるだろう。