The Design and Implementation of
the Gracious Days

FreeBSD 10から追加されたunmapped I/Oという機能がある。これが入ってからSSDのIOPSが格段に改善したが、技術解説が見当たらないのでメモを兼ねてまとめてみた。そもそも筋があまりよくなかった部分を直したようなものなので、新機能というよりはバグ修正と呼んだほうが良いのかも知れない。

FreeBSDのI/Oの問題点

FreeBSD 9までのI/Oは、I/O処理の前に必ずKVA(カーネル仮想アドレス空間)に物理メモリをマップするようになっていた。これは他のBSD由来のOSにはないFreeBSD特有の特徴で、デバイスドライバからデータをカーネル空間のバッファで受け取るために行なわれる。デバイスドライバはマップした仮想アドレス空間を受け取り、対応する物理アドレスに対してDMAを実行するか、あるいはCPUを使ってデータをコピー(いわゆるprogrammed I/O)する。

たとえばユーザランドからread(2)システムコールを発行すると、次のような流れでデータがユーザランドアドレス空間のバッファに移される。

  1. カーネルは、I/O用のバッファをKVAにマップしてデバイスドライバに伝える。
  2. デバイスドライバはデータを読み取ってバッファに転送する。
  3. カーネルはバッファにデータが入った後に、read(2)システムコールで渡されたユーザランド空間のバッファにデータを転送する。
  4. バッファをアンマップしてシステムコールから復帰する。

この流れの問題点は 1 と 4 にある。カーネルは必ずI/O用のバッファをKVAにマップするため、SMP環境ではTLBをクリアするためのIPIが発生する。IPIはアンマップする時にも発生するため、I/Oが発生するたびにTLB shootdownが発生してしまう。

カーネルI/Oバッファの削除

もともとこのバッファのマッピングはバウンスバッファなどの目的で用意されているものだった。近年のアーキテクチャ(特に64-bitのもの)は仮想アドレス空間をダイレクトマップにしているか、TLBをクリアしなくてもダイレクトマップが使えるものが多い。また、デバイスやバスコントローラ側がMMUを備えていることも増えていて、物理メモリ空間へのアクセス範囲が制限されるようなことが少なくなった。つまりI/Oのためのバッファがカーネル空間にマップされていなくても、デバイスドライバからは任意の物理アドレスにDMAを実行することができる。したがって、1 のマップを省いて直接ユーザランド空間のバッファにデータを転送すれば無駄がない。また、この処理はI/Oのコードパスに存在するため、md(4)(メモリディスク)のようにI/Oをvnode以外に行なうドライバも影響を受けて本来不要なデータの転送が発生してしまう。unmapped I/O の変更は、基本的にはこういった動作を実装する修正だ。

デバイスドライバが unmapped I/O に対応している場合(対応しているかどうかを示すフラグがある)、カーネルはバッファをKVAにマップせず、デバイスドライバはI/Oの要求アドレスに対して直接データ転送を行なう。ここで、デバイスやバスコントローラ側でアドレス変換ができるなら仮想アドレスが、できないのであれば物理アドレスが渡される。ほとんどのI/OはCAMサブシステムを通過するので、物理アドレスが必要な場合はCAMのCCB(CAM Control Block)からアドレスを受け取ることになる。ユーザランドプログラムが発行したread(2)システムコールの場合、DMAの転送先はユーザランド空間にマップされた物理アドレスだ。

mapped なのか unmapped なのかは I/O 要求を発行する側で選択できるようになっている。次の場合はmappedを要求する必要がある。簡単に言えば、KVAにマップする必要があるのは、カーネルからデータを触る必要がある場合だけだ。

  • 単にユーザランド空間にデータをコピーするのではなく、カーネルの中でデータを解釈したり書き換える必要がある場合
  • カーネルの中で何度も参照され、長い間保持する必要がある場合

たとえば UFS のコードは、メタデータは mapped を、ファイルの内容については unmapped I/O を使うようになった。変わったのは「カーネルのI/OバッファをKVAにマップするかどうか」の部分だけなので、vnode pagerで管理されているバッファキャッシュの機能は変わらない。

得られた性能向上

この変更で、I/Oの大部分で無駄なIPIが発生しなくなった。ひとつひとつのI/Oのスループットはそれほど変わらないが、KVAマップとTLBのクリアによる遅延量が減ったので、CPU負荷の低減が期待できる。開発者が行なった40コアのCPUとSSDを使ったベンチマークでは、IOPSが300Kから1Mを超える値になることが確認されている。CPU boundだったIOPSが向上したわけだ。

FreeBSD 10 以降であれば、この変更がすでに適用されている。何も設定せずとも有効になっているので、ユーザは特に存在を意識する必要はない。Unmapped I/Oが有効な場合と無効な場合との性能差を知りたければ、次のような設定用の loader tunable があるので、それを操作すると良い。デフォルトは1になっているが、0にすると無効になる。

vfs.unmapped_buf_allowed

Next post: OSSコミュニティの継続性

Previous post: read(2) vs mmap(2) の迷信