BitVisorの、BroadcomギガビットEthernetネットワークインターフェイスデバイス (以下bnx) のpara pass-through driver (drivers/net/bnx.c) には、デバイスが受信したデータがゲストオペレーティングシステムにすぐに届かないというバグがありましたが、2020年から2021年にかけて修正されています。修正は以下の3つあります。
- ステータスワードの扱いに関する修正
- 一度に扱う最大パケット数の上限をなくす修正
- 割り込みクリア後にレジスターのダミー読み取りを行う修正
これらの修正について紹介します。
ステータスワードの扱いに関する修正
BitVisor / Code / Commit [bd8164]
bnxはRAM上にステータスワードを持つ特徴があります。このステータスワードには、受信データの到着、リンク状態の変化、エラー、の3つをビットで表していて、デバイスがセット、ソフトウェアがクリアすることになっています。
個人的な印象として、普通ならこういう情報はレジスターにあるものかなぁと思うのですが、レジスターアクセスが遅いのを嫌ったのかも知れません。しかし、デバイス側がDMAでこの情報を更新し (少なくともクリアされていないことを確認するための読み取りが毎回必要)、CPU側のアクセスとの競合もありますから、それはそれでトリッキーさがあって、どうなのかなぁと思います。
それで、修正前の実装では、それぞれビットがセットされていることを確認したら、その内容をチェックした後でビットをクリアしていました。例えば受信データが到着したことを確認したら、受信バッファーを見に行って、その後受信データ到着のビットをクリアしていました。これにはわかりやすい競合状態があって、それは、受信バッファーを見に行っている間に次の受信データが到着したらどうなるか? という話です。デバイスとしては受信を正しく終えてビットをセットし、CPU側は最新のデータを受け取らないままビットをクリアしてしまうということがありえて、この時の受信データがゲストオペレーティングシステムに届くのは、次の受信データ到着まで遅延されてしまっていました。
修正では先にクリアをするようにしています。さらにその、デバイス側との競合をできるだけ避けるため、lockも併用しています。クリアするというのは、メモリーから読み込む、論理積を取る、メモリーに書き込む、ですから、CPUが読み込みをしてから書き込みが始まるまでの間に、デバイス側がガリッと新たなビットをセットすれば、それはなかったことにされてしまうわけです。Lockによって、その間の他からのアクセスがされないようにすることを期待しています。やっぱり、このデバイスの仕様はいまいちだと思うんですけどね...
一度に扱う最大パケット数の上限をなくす修正
BitVisor / Code / Commit [f213af]
これは、もともとどうして上限を入れていたかの理由はわからないのですが、16パケットずつしか転送しないように実装されていました。上に書いたステータスワードのクリアは、16個を超えるパケットが届いていたとしても実行されていたため、そのような状況では、新たな受信データ到着のたびに16パケットずつしかゲストオペレーティングシステムに届かない、という状態になってしまっていました。
他のネットワークインターフェイスデバイス用のpara pass-through driverではそのような上限を設定していませんので、それに合わせて上限をなくして解決しました。
割り込みクリア後にレジスターのダミー読み取りを行う修正
BitVisor / Code / Commit [587a85]
これは、一見意味がないように見えるダミー読み取りの追加によって問題を解決したものです。
割り込みクリアというのは、CPUの割り込み処理が終わったことをデバイス側に通知することを指します。割り込み要求を出したデバイスは、割り込みクリアが来るまで次の割り込み要求を出しません。レベルトリガー割り込みを用いているデバイスには一般的に存在する概念です。
もともと、割り込みクリア後に受信バッファーをチェックする実装を入れてあります。これは、上に出てきた、受信バッファーを見に行っている間に次の受信データが到着したらどうなるか? という話のためです。行き違いでCPUが受信しそこねたデータが残る時には、別の割り込みを出してくれよ、ということです。
で、問題は、割り込みクリアはレジスター書き込みによって行われる、というところにあります。レジスターはキャッシュ禁止領域にありますから、CPUとしては、書き込みサイクルが終了するまで次の命令の実行には移りません。ISAバスの時代ならそれで話は終了なのですが、今はPCI Expressバスです。CPUの書き込み要求は、PCI Expressバスのブリッジを経由し、シリアライズされてデバイスに届きます。そうすると... どうやら、デバイスに届く頃にはCPUの書き込みサイクルは終了してしまっているようなのです。
それで何が起きるかというと、CPUとしては割り込みクリアしたつもりで受信バッファーを見に行きます。同時に、デバイスとしては割り込みクリアされていないつもりで受信バッファーを更新し、割り込み要求を出しません。実際にこのような事象が発生し、この時の受信データがゲストオペレーティングシステムに届くのは、次の受信データ到着まで遅延されてしまっていました。
ダミー読み取りでは何が起こるかというと、読み取り要求がPCI Expressバスのブリッジを経由していくのは書き込みと同じですが、デバイスがその読み取り要求に応答するまでCPUは動けません。CPUが動けるようになった時には、その前の書き込み要求が確実にデバイスに届いていることになります。
IntelのギガビットEthernetでは、割り込みクリアの概念は一緒ですが、受信バッファーのhead位置をレジスターから読み取る必要があるため、この問題は発生しません。bnxは、ステータスワードとともに受信バッファーの情報もRAM上に置かれているため、こんな手間が必要になってしまっています。