LoginSignup
4
5

More than 3 years have passed since last update.

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

Last updated at Posted at 2019-06-24

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

tl;dr

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カーネルをビルドできるようにしている。

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ドメインソケット等が含まれるようにする。

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

適当な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はreadwriteのようなAPIで読み書きできるが、LKLのfdは専用のAPIである、lkl_sys_readなどを使用する必要がある。このため、 socketpair でできたfdの片方をUSB/IPドライバに渡し、もう片方は転送スレッドを用意してそこでデータを転送する。

これは単に write できたら lkl_sys_read で書き出す、またはその逆を行えば良いだけになる。

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

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

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

やることは:

  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のソースコードに書かれていて:

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/busnumdevnum を事前にcatして調べておく必要がある。

LKLでやってみる

SnapCrab_NoName_2019-6-24_20-35-35_No-00.png

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

4
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
5