バッファキャッシュとAIO(4)

jirou senju
2010/08/10 17:10
/community/blog/images/union/pg_high_logo.png

前回はPOSIX AIOとLinuxカーネルのAIOサポートについて解説しました。今回は、このAIOの使い勝手を良くするため、POSIX AIOインタフェース準拠のライブラリを作成しています。

LinuxネイティブAIOライブラリliblaioの試作

Linux AIOを使用する場合、現在では前述のlibaioの利用が第一候補になりますが、やや使い勝手が悪いため、本記事でPOSIX AIOインタフェース準拠のライブラリを試作してみます。Linux AIOではO_DIRECTが前提となるため、この点もやや使い勝手が悪いのですが、SSDなどメモリベースのファイルシステムもありますし、動作は非同期になりませんがio_submit(2)O_DIRECTがなくとも使用可能ですから、まぁ試しにやってみましょう。

ライブラリ設計要点を挙げます。

Linux AIOにPOSIX AIOインタフェースをかぶせる

簡単に対応をとってみると表3の様になります。

表3 Linux AIOシステムコールとPOSIX AIOインタフェースの対応

POSIX AIOインタフェース Linux AIOシステムコール
aio_read io_submit
aio_write
aio_fsync
lio_listio
aio_suspend io_getevents
aio_return
aio_error
aio_cancel io_cancel

io_setup(2)などの前処理はライブラリ内で自動的に行いますが、プログラマが同時AIO数を拡張するなどの場合も考えられます。このため、Glibc拡張であるaio_init(3)を流用/対応すると同時に、laio_fin()も新設しio_destroy(2)に対応します。

AIO完了通知

シグナル、スレッド起動の通知機能に対応するため、本ライブラリ内部でスレッドを起動します。

Linux AIO 非同期動作条件

表2に示した条件(O_DIRECT、アラインメント)はそのまま受け継ぎます。このため多くの場合で、量は多くないとはいえ、アプリケーションの変更とリビルドが必要になると思われます。しかし、非同期動作にならないbufferd I/Oの場合にも、io_submit(2)が対応しているので、本ライブラリも対応します。

前掲の表1で述べたように、IOCB_CMD_FSYNCIOCB_CMD_FDSYNCに対応しているファイルシステムは現在存在しないため、aio_fsync(3)の実装として、本ライブラリ内部でio_submitIOCB_CMD_FSYNC)、io_submitIOCB_CMD_FDSYNC)を発行するとエラーになってしまいます。このため、本ライブラリでは特例として扱うことにします。すなわち、O_DIRECTを使用する場合、aio_fsync(3)の必要性はないため、処理せず常に成功を返すことにします。O_DIRECTを指定せず本ライブラリを使用する場合は省略できません。ライブラリ内部でサイズが0のIOCB_CMD_PWRITEを発行し、別スレッドでio_getevents(2)後にfsync(2)fdatasync(2)を発行することにします。

また、/dev/nullfullなどもAIOに対応していません。これらに対するio_submit(2)はエラーとなります。

ソースレベル互換

ヘッダファイルのインクルードなど、量は少なくても、多くの場合アプリケーションの変更およびリビルドが必要になると思われます。この点は、変更量を最小限に抑えるため、ライブラリヘッダファイル内でマクロによりPOSIXインタフェースシンボルを本ライブラリのものへ置き換えることで対応します。

Glibc AIO 実装との非互換

POSIXによればaio_cancel(3)にはパラメータstruct aiocbを渡す場合とそうでない場合(NULLを渡す)があります。しかしio_cancel(2)ではNULLはエラーとなります。すなわちLinux AIOは指定されたファイルディスクリプタに対する全AIOリクエストをキャンセルする方法を提供していません。本ライブラリ内で対応策が考えられないこともないのですが、手間がかかるため(現時点では)本ライブラリの制限事項とします。

Glibcのlio_listio(3)は不正なファイルディスクリプタ(EBADF)などのエラーのチェックを別のI/Oスレッドで行っているので、lio_listio(3)はエラーを返しません。もちろん最終的にはエラーになり、aio_error(3)EBADFが返されます。一方、本ライブラリのlio_listio()io_submit(2))はAIOリクエストをキューにつなぎ、直後にI/Oを開始するためこの種のエラーを検知します(早期発見ということです)。

liblaioの使用

上記を踏まえ、試作したものが同梱ソースファイル一式に含まれるlib/laio/です。前掲の6aio_signal.cを書き換え、試作したliblaioをリンクしてみます。

6aio_signal.cの書き換え ― 11laio_signal.c 要約

    :::
+#include <stdlib.h>
    :::
+#include "lib/laio/laio.h"
    :::
+#ifdef Laio
+#define OpenFlag O_DIRECT
+#else
+#define OpenFlag 0
+#endif
+
int main(int argc, char *argv[])
{
    int err, sigfd;
-   char a[BUFSIZ];
    ssize_t ssz;
    struct aiocb aio = {
        .aio_offset = 0,
-       .aio_buf = a,
-       .aio_nbytes = sizeof(a),
+       .aio_nbytes = BUFSIZ,
        .aio_reqprio = 0,
        .aio_sigevent = {
            .sigev_notify = SIGEV_SIGNAL,
        :::
-   aio.aio_fildes = open(argv[1], O_RDONLY);
+   aio.aio_fildes = open(argv[1], O_RDONLY | OpenFlag);
    assert(aio.aio_fildes >= 0);
    aio.aio_sigevent.sigev_value.sival_int = aio.aio_fildes;
+   err = posix_memalign((void *)&aio.aio_buf, 512, BUFSIZ);
+   assert(!err);

    sigemptyset(&mask);
    sigaddset(&mask, aio.aio_sigevent.sigev_signo);
    (以下略)

11laio_signal.cのコンパイルと実行

$ cc -I../lib/.. - L../lib/laio 11laio_signal.c -llaio -lrt \
    -o 11laio_signal
$ ./11laio_signal /mnt/test
main: aio_read from fd 3 completed
/mnt/test, 1466 read

$ ln -f 11laio_signal.c 11 laio_signal_dio.c
$ cc -I ../lib/.. -DLaio -L ../lib/ laio11laio_signal_dio.c -llaio -lrt \
    -o 11laio_signal_dio
$ ./11laio_signal_dio /mnt/test
main: aio_read from fd 3 completed
/mnt/test, 1466 read

書き換えのポイントはlaio.hのインクルードです。このヘッダファイルがaio_read(3)などをlaio_read()へ変換するため、書き換え量は極小で済みます。ただし、動作は非同期にはなりません。

非同期動作にするためにはO_DIRECTとアラインメントを加える必要があります。11laio_signal.cではコンパイル時に-DLaioを加えると非同期になるようにしました。同様の方法はliblaioに添付のテストプログラムのlaiotest.hでも用いています。

簡単に性能比較

本ライブラリの実行性能をGlibcと比較してみましたが、Direct I/Oを用いると、やはり速度面では不利でした。通常のbuffered I/Oではそれほど差は見られませんでした。

有意な差が見えたのはlio_listio(3)です。POSIX AIOの中では、いやシステムコール、ライブラリ一般から言っても、lio_listio(3)は非常に強力です。一度に多数のI/Oを発行する機能は実装も悩ましいところです。Glibcの実装では指定された分だけスレッドからI/Oする、複数のaio_read(3)のように処理するという比較的単純なアプローチですが、Linux AIOのio_submit(2)ではネイティブに複数I/Oに対応しているため、本ライブラリのlio_listio(3)はこの機能を活用しています。少数のAIOを発行する場合は、Glibcの複数スレッドアプローチでも、スレッド起動は通常充分に軽いため、満足な性能が得られるでしょう。しかし、大量になると単発のaio_read(3)を複数繰り返すよりもlio_listio(3)が威力を発揮します。この場合の性能はGlibcの実装では不満を感じるかもしれません。libaioや本記事で試作したliblaioの有効性が活きる場面です。liblaio付属テストプログラムでもlio_listio(3)の性能は、大きくとは言えませんが、Glibcよりも常に良好な結果を示しました。ただし、 この簡単なテストは同じファイルに 対する読み取りを一度に多数発行しただけで、差が見えにくい内容です。大量に発行して、やっとこれだけの差が見えたという程度の結果で、性能測定としては信頼に足るものではありません。

テストプログラムaio3_lioの実行結果部分だけ抜粋します。aio3_lioがGlibcを、nonlaio3_lioがO_DIRECTを指定せず本ライブラリを、laio3_lioがO_DIRECTを指定して本ライブラリを、それぞれ用いた場合です。

liblaio付属テストプログラム実行結果(抜粋)

$ make -s test
aio2_fsync + suspend
    :::
aio3_lio
2.65user 0.06system 0:03.42elapsed 79%CPU (0 avgtext+0 avgdata 0maxresident)k
0inputs+0outputs (0major+31412minor)pagefaults 0swaps
nonlaio3_lio
0.16user 1.68system 0:02.68elapsed 68%CPU (0avgtext+0 avgdata 0maxresident)k
0inputs+0outputs (0major+41636minor)pagefaults 0swaps
laio3_lio
0.17user 4.25system 0:05.57elapsed 79%CPU (0avgtext+0avgdata 0maxresident)k
4915200inputs+0outputs (0major+41718minor)pagefaults 0swaps
aio1_read + cancel
    :::

本記事ではファイルI/O全般からAIOまで解説し、さらにAIOライブラリを試作してみました。ライブラリには制限事項も複数残っており、まだ試用レベルですが、将来発展すればpthreadのようにNATL(Native POSIX AIO Library)と名乗る日が来るかもしれません。

もちろん興味を失えばこれで終わりになってしまうかもしれません :-)

(初出: H22/4)

Copyright © 2010 SENJU Jiro

本記事のサンプルコードは、以下のリンクよりダウンロードすることができます。記事と合わせてご参照ください。
[サンプルコード]
また、本稿で取り上げた内容は、記事の執筆時の情報に基づいており、その後の実装の進展、変更等により、現在の情報とは異なる場合があります。
本連載「プログラマーズ・ハイ」では読者からのご意見、ご感想を切望しています。今後取り上げるテーマなどについてもご意見をお寄せください。

Bookfair

O'Reilly Japanのセレクションフェア、全国の書店さんで開催
[ブックフェアのページへ]

Feedback

皆さんのご意見をお聞かせください。ご購入いただいた書籍やオライリー・ジャパンへのご感想やご意見、ご提案などをお聞かせください。より良い書籍づくりやサービス改良のための参考にさせていただきます。
[feedbackページへ]