BitVisorの準パススルードライバーのデータ転送タイミング

BitVisorの準パススルードライバーは、ゲストオペレーティングシステムが準備したデータをシャドウバッファーへ転送したり、デバイスが書き込んだシャドウバッファーのデータをゲストオペレーティングシステムのバッファーへ転送したりします。そのような転送が必要な例としては、たとえば、ディスク暗号化機能で、シャドウバッファーには暗号化されたデータが書き込まれていて、ゲストオペレーティングシステムのバッファーには暗号化されていないデータが書き込まれている形になります。これは、ゲストオペレーティングシステムから見れば、デバイスのDMA転送にあたる動作が行われているということになります。

そこで、どのようなタイミングでデータ転送を行っているのか、方向別に簡単に紹介します。

# ホストからデバイスへの転送

ホストからデバイスへの転送は、ほとんどの場合デバイスへのレジスター書き込みで行われます。例外はUSBのUHCIとEHCIです。

例えばAHCIの準パススルードライバーの実装 (drivers/ata/ahci.c) では、ゲストオペレーティングシステムがPxCI (Port x Command Issue) レジスターへ書き込みを行ったタイミングで、ahci_cmd_start() 関数が呼び出され、シャドウバッファーの確保とデータ転送が行われています。また、virtio-netの実装 (drivers/net/virtio_net.c) では、ゲストオペレーティングシステムがキューへ書き込んだことを通知する、ベースアドレスからオフセット0x10のポート番号への書き込みで、virtio_net_recv() 関数が呼び出され、シャドウバッファーへの転送が行われています。

USBのUHCIとEHCIでは、新たなデータを渡す際にはメモリー上でリストの付け替えが行われるだけで、レジスター書き込みがありません。そのためリストの変更がないかをスレッドでポーリングし、変更が見つかるとシャドウを作ります。このような仕組みになっているのは、ハードウェアの仕様のためです。USBバスでは、マウスのようなヒューマンインターフェイスデバイスであってもデバイス側からホストコントローラーへプッシュすることができず、必ずホストコントローラーからメッセージを送り、それに対してデバイス側が返事することになっているそうです。そのためホストコントローラーがポーリングを行うのであり、ポーリングに必要なデータがリスト構造としてメモリー上に置かれます。ホストコントローラーは定期的にそのリスト構造をたどっていくので、オペレーティングシステムは新たなデータを渡す際はリストに追加するだけで、ホストコントローラーが一定時間後にはそのデータにたどり着くということです。

また、USBホストコントローラーは多数のUSBデバイスを相手にするため、USBデバイスのアドレスを見て、特に必要がない場合はデータバッファーのシャドウは作らない仕組みになっているのが特徴です。

# デバイスからホストへの転送

デバイスからホスト (ゲストオペレーティングシステム) への転送タイミングについては、レジスターアクセスをきっかけとするものと、ポーリングのものと、外部割り込みをきっかけとするものがあります。

## レジスターアクセス

ゲストオペレーティングシステムによるレジスターアクセスをきっかけとして転送を行うものです。ほとんどのデバイスは割り込みでオペレーティングシステムに通知するのですが、その割り込みハンドラー内で各デバイスの状況確認のためのレジスターアクセスが行われることを利用して、仮想マシンモニター側でも状況確認を行い、転送を行います。

例えばAHCIの準パススルードライバーの実装 (drivers/ata/ahci.c) では、ahci_cmd_complete() 関数がコマンドの完了チェックをして、データ転送が必要な場合は転送します。シンプルな実装ですが実は難しい問題があります。UEFIのファームウェアに含まれるAHCIのデバイスドライバーは、割り込みを使わずポーリングで完了待ちをします。ところが、ポーリングするのがデバイスのレジスターではなく、D2H(Device to Host) と呼ばれる、デバイスから届いたステータス等が書き込まれるメモリー領域をポーリングします。AHCIの準パススルードライバーの実装がD2H領域を変更しておらず、追加のコマンドを発行することもないため、ポーリングによる完了待ち自体は正常に終わりますが、その時点では読み取りデータは転送されていません。その後のレジスターアクセスのタイミングで初めてデータ転送が行われます。これは、AHCIのストレージで複雑な処理をしようとすると問題になります。

ネットワークデバイスの準パススルードライバーでは、drivers/net/pro1000.cが、Interrupt Cause Read Registerのアクセスのタイミングで転送しています。まさに、ゲストオペレーティングシステムの割り込みハンドラーがこのレジスターにアクセスすることをあてにした実装です。

## ポーリング

仮想マシンモニターに制御が移ってきた時にポーリングで完了を検出するものもあります。vmm.no_intr_intercept=0の場合、任意の外部割り込みによっても制御が移ってくるので、割り込みのタイミングで転送しているのと近いものがあります。

USBのUHCIとEHCIの準パススルードライバーは、ホストからデバイスへの転送と同様に、ポーリングで完了チェックをしています。また、ネットワークドライバーのdrivers/net/bnx.cもポーリングによる完了チェックをしています。TCP/IPスタックの中のスレッドでポーリングルーチンが呼び出されます。

## 割り込み

外部割り込みによって仮想マシンモニターに制御が移ってきた時に完了チェックをします。pci_register_intr_callback() 関数を使ってコールバック関数を登録しておき、そのコールバック関数内でチェックをします。これは以前のバージョンではなかったので、代わりにポーリングになっているものもあります。

例えばxHCIやNVMeの実装はこの方法を使用しています。詳しいことはわかりませんが、どうも割り込みの際にゲストオペレーティングシステムが必ずアクセスしてくれるというレジスターがなく、データ転送のタイミングが他にないのが理由のようです。pci_register_intr_callback() 関数は任意の外部割り込みでコールバック関数を呼び出すので、無関係の割り込みでも排他制御やチェックのために待ち時間が発生したりプロセッサの負荷が上がったりするという問題はありますが、ゲストオペレーティングシステムの割り込みハンドラーが完了キューを見る前に確実にデータ転送を済ませておくことができます。

任意の外部割り込みで処理をしている理由は、BitVisorはPCIの割り込みラインの検出をしていないためです。MSIやMSI-Xになっていれば比較的簡単に割り込み番号を取得できるのですが、レガシーなPCI割り込みの場合は、割り込みラインが割り込みコントローラーのどの部分につながっているかを調べていく必要があるそうです。BitVisorではそれを調べていないだけでなく、割り込みコントローラーすらパススルーにしているので、ゲストオペレーティングシステムがどのように割り込みを割り当てたかまったくわからない状態です。