前回までファイル I/O 全般について簡単に振り返りました。いよいよ本題のAIOに取り掛かります。今回は、POSIXのAIOインタフェースと、LinuxカーネルのAIOサポートについて紹介します。
POSIX AIO インタフェース
バッファキャッシュにより緩和されるとはいえ、ファイル I/Oの最終到達地点はディスクですから、同期的なI/Oはやはりその時間が問題視されることがあります。まだバッファキャッシュに存在しないデータを読み取る場合には遅いディスク必ず待たなければなりません。この動作を非同期に行い、待っている間に他の処理を進められるようにするのが非同期 I/O、AIO(Asynchoronous I/O)です。POSIXではaio_read(3)、aio_write(3)、aio_suspend(3)、aio_fsync(3)、aio_return(3)、aio_cancel(3)、aio_error(3)、lio_listio(3)を定義しています。例えばread(2)に対応するAIOインタフェースは次のように定義されています。
aio_read(3)
# include <aio.h>
int aio_read(struct aiocb *);
struct aiocb {
int aio_fildes; /*ファイルディスクリプタ */
off_t aio_offset; /* オフセット */
volatile void *aio_buf; /* バッファ */
size_t aio_nbytes; /* バイト数 */
int aio_reqprio; /* 相対優先度 */
struct sigevent aio_sigevent; /* 通知方法 後述 */
int aio_lio_opcode; /* I/O種類 */
};
# include <signal.h>
struct sigevent {
int sigev_notify; /* 通知方法 */
int sigev_signo; /* シグナル番号 */
union sigval sigev_value; /* ユーザデータ */
void(*sigev_notify_function)(union sigval); /* スレッド関数 */
pthread_attr_t *sigev_notify_attributes; /* スレッド属性 */
};
通常のread(2)の3つのパラメータは、struct aiocbのメンバaio_fildes、aio_buf、aio_nbytesにそれぞれ対応します。非同期という性質のため、I/O時にファイルポジションが維持されていることは期待できず、pread(2)同様にaio_offsetも指定します。aio_reqprioは内部で非同期に実行されるI/Oコンテキストの優先度を決定する際に使用されます。aio_lio_opcodeはlio_listio(3)以外では使用しません。read(2)とaio_read(3)の関係が分かれば、aio_write(3)、aio_fsync(3)については説明は不要でしょう。 :-)AIOの完了を待ち合わせるのがaio_suspend(3)です。まずaio_read(3)をコールし、ファイルを読み取っている際中に別の処理を進め、読み取ったファイル内容を参照する際にaio_suspend(3)をコールします。もちろん、すでにAIOが完了していることが分かっていればaio_suspend(3)は省略できます。待ち合わせずに完了しているかだけを確認するにはaio_error(3)を用います。戻り値がEINPROGRESSならばまだAIO未完了、それ以外ならば、正常終了、異常終了にかかわらず、AIOは完了しています。正常終了の場合には戻り値、aio_read(3)ならば読み取ったバイト数を返すのがaio_return(3)です。この値はAIO完了後でなければ意味を持ちません。
発行済みAIOをキャンセルするのはaio_cancel(3)です。もちろんすでにAIOが完了していればエラーとなります。
lio_listio(3)は複数のAIOを一度に発行できる強力なシステムコールです。aio_lio_opcodeにはLIO_READ、LIO_WRITE、LIO_NOPを指定でき、一度struct aiocbの配列を用意すればaio_lio_opcodeを操作するだけで済み、配列全体の再作成は省略できます。多数のファイルディスクリプタを繰り返し処理する場合には、効率が期待できます(しかし、後述するようにGlibcの実装ではスレッドを多用する点に注意が必要です)。またすべての処理完了を待つか否かも指定できます(LIO_WAIT、LIO_NOWAIT)。
lio_listio(3)も大きな特徴ですが、最大の特徴はstruct aiocbのメンバaio_sigeventです。非同期に実行したI/Oの完了通知を受け取る方法を指定するもので、sigev_notifyにシグナルによる通知(SIGEV_SIGNAL)、スレッド起動による通知(SIGEV_THREAD)のどちらからを指定します。通常はあまり使用しないかもしれませんが、通知なし(SIGEV_NONE)も定義されています。サンプルコードを2つ挙げます。
スレッド起動によるAIOの完了通知 ― 5aio_thread.c 要約
struct {
pthread_cond_t cond;
pthread_mutex_t mtx;
int flag;
} notified = {
.cond = PTHREAD_COND_INITIALIZER,
.mtx = PTHREAD_MUTEX_INITIALIZER,
.flag = 0
};
void thread_func(union sigval sv)
{
printf("%s : aio_read from fd %d completed \n",
__func__, sv.sival_int);
pthread_mutex_lock(¬ified.mtx);
notified.flag = 1;
pthread_cond_signal(& notified.cond);
pthread_mutex_unlock(& notified.mtx);
}
main ()
{
char a[BUFSIZ];
struct aiocb aio = {
.aio_offset = 0,
.aio_buf = a,
.aio_nbytes = sizeof(a),
.aio_reqprio = 0,
.aio_sigevent = {
.sigev_notify = SIGEV_THREAD,
.sigev_notify_function = thread_func,
.sigev_notify_attributes = NULL
}
};
aio.aio_fildes = open(argv[1], O_RDONLY);
aio.aio_sigevent.sigev_value.sival_int = aio.aio_fildes;
aio_read(&aio);
/* do other jobs */
pthread_mutex_lock(¬ified.mtx);
while (!notified.flag)
pthread_cond_wait(¬ified.cond, ¬ified.mtx);
pthread_mutex_unlock(¬ified.mtx);
}
シグナルによるAIOの完了通知 ― 6aio_signal.c 要約
main ()
{
char a[BUFSIZ];
struct aiocb aio = {
.aio_offset = 0,
.aio_buf = a,
.aio_nbytes = sizeof(a),
.aio_reqprio = 0,
.aio_sigevent = {
.sigev_notify = SIGEV_SIGNAL,
.sigev_signo = SIGRTMIN + 1
}
};
sigset_t mask;
struct signalfd_siginfo ssi;
aio.aio_fildes = open(argv[1], O_RDONLY);
aio.aio_sigevent.sigev_value.sival_int = aio.aio_fildes;
sigemptyset(&mask);
sigaddset(&mask, aio.aio_sigevent.sigev_signo);
sigprocmask(SIG_BLOCK, &mask, NULL);
sigfd = signalfd(-1, &mask, /* flags unused */0);
aio_read(&aio);
/* do other jobs */
read(sigfd, &ssi, sizeof(ssi));
printf("%s: aio_read from fd %d completed\n",
__func__, ssi.ssi_int);
}
5aio_thread.c、6aio_signal.cいずれもAIOの通知方法を示すためだけのサンプルで、動作に非同期性を必要とするものではありません。本来は/* do other jobs */というコメント部分に並列に実行する処理を記述します。
Glibc(RT)のAIO実装
GNU Libc(厳密には同梱のlibrt)はすでにPOSIX AIOインタフェースを実装しています。そのv2.7でのaio_read(3)の動作を要約してみます。
- 内部キューのメモリ確保
- pthread_create(3)をコールし、内部スレッドを起動
- AIOリクエストをキューへつなぐ
内部キューへAIO リクエストをつなげばaio_read(3)はリターンします。内部スレッドは次のように動作します。
- キューからAIOリクエストを取り出す
- 内容に応じpread(2)などを発行
- 内容に応じ通知
- 上記処理の繰り返し
内部スレッドがキューからAIOリクエストを取り出そうとした時にもうリクエストが一つも存在していなければ、内部スレッドは短時間の眠りに着きます。起床してもまだAIOリクエストがなければ終了します。これはスレッドを終了/再作成/起動するコストを節約するため、今は用がなくても近い将来出番が来るかもしれないので、少しの間は待つという動作です。
逆に起床したら複数のAIOリクエストが溜っていた場合には、自分一人では手が足りないため、内部スレッドが別の内部スレッドを起動します。新たに起動された内部スレッドも同じように動作します。
POSIXでは定義されていませんが、Glibcではstruct aioinit、aio_init(3)を定義し、内部動作も制御可能としています。例えば内部スレッド数、キューに保持可能なAIOリクエスト数の上限を設定可能です。
この実装で注目すべき点は、AIOが多用されることにより、内部では複数回pthread_create(3)がコールされ、複数の内部スレッドが起動される点でしょう。上限を設定可能とはいえ(デフォルトでは20)、スレッド数を不用意に多くしてしまうと性能劣化を招く恐れがあります。
LinuxカーネルのAIO対応
LinuxはカーネルレベルでAIOをサポートしています。すなわち上述のGlibcによる ユーザ空間でのスレッドによる実装ではありません。Glibcの実装ではライブラリ内部でpread(2)など対応するシステムコールを発行しますが、カーネルレベルのAIOサポートではio_submit(2)一つで全種類のI/Oに対応します。
LinuxのAIO システムコール
# include <linux/aio_abi.h>
long io_setup(unsigned nr_events, aio_context_t *ctxp);
long io_submit(aio_context_t ctx_id, long nr, struct iocb **iocbpp);
long io_getevents(aio_context_t ctx_id, long min_nr, long nr,
struct io_event *events, struct timespec *timeout);
long io_destroy(aio_context_t ctx);
long io_cancel(aio_context_t ctx_id, struct iocb *iocb,
struct io_event *result);
struct iocb {
/* アプリケーションが使用しないメンバについては省略 */
__u64aio_data; /* io_eventのdataへ代入される */
__u16 aio_lio_opcode; /* IOCB_CMD_を代入する */
__s16 aio_reqprio; /* 未使用 */
__u32 aio_fildes;
__u64 aio_buf;
__u64 aio_nbytes;
__s64 aio_offset;
__u32 aio_flags; /* aio_resfdを使用するか否か */
__u32 aio_resfd; /* eventfd (使用する場合) */
};
struct io_event {
__u64 data; /* iocbのaio_dataが代入される */
__u64 obj; /* iocbポインタ */
__s64 res; /* I/Oの戻り値 */
__s64 res2; /* gadgetfsしか使用していない */
};
- io_setup(2)
- パラメータのnr_events 個の非同期 I/Oに対応したメモリ領域をシステム内に確保し、aio_context_tディスクリプタ(ハンドル)としてctxpに返します。渡すctxpは事前に0で初期化しておきます。また、システムワイドの非同期I/O数の上限は/proc/sys/fs/aio-max-nrから設定/参照可能です。
- io_submit(2)
AIOリクエストをシステム内部のキューへつなぎます。AIO発行の本体に相当します。厳密に言うと、キューへつないだ直後にI/Oが開始されますが、この動作は非同期性を損なう恐れがあります。LinuxがAIOを実装した当初は動作は違ったと思うのですが。また、このため、異常なファイルディスクリプタ(EBADF)などのエラーはio_submit(2)が検知します。後述するように、この点には注意が必要となる場面があり得ます。
struct iocbはAIOリクエストごとに一つづつ必要で、I/O完了前に再利用はできません。このパラメータが構造体配列ではなく構造体ポインタ配列である点も一つの特徴と言えます。リクエストの内容、構造体メンバは前述のPOSIX AIOと同様ですが、aio_reqprioは使用されません。GlibcのAIOでは、おかしな値が代入されていた場合はエラーとされますが、io_submit(2)では無視します。またaio_lio_opcodeには次の値が指定可能です(表1)。
表1 io_submit(2)のI/O種類
iocb.aio_lio_opcode |
相当システムコール |
IOCB_CMD_PREAD |
pread(2) |
IOCB_CMD_PWRITE |
pwrite(2) |
IOCB_CMD_FSYNC |
fsync(2) |
IOCB_CMD_FDSYNC |
fdatasync(2) |
IOCB_CMD_PREADV |
preadv(2) |
IOCB_CMD_PWRITEV |
pwritev(2) |
aio_flags、aio_resfdについては後述します。非同期動作はDirect I/Oの場合にのみ有効となり、O_DIRECTを指定しないファイルディスクリプタに対するio_submit(2)は非同期にもエラーにもならず、通常のI/Oとして処理されます(表2)。Direct I/Oの場合の動作については3節で述べました。ただし、io_submit(2)、またはO_DIRECTに対応していないものも存在し(パイプや/dev/nullなど)、これらに対するio_submit(2)はエラーとなります。
表2 Linux AIOの非同期動作条件
|
|
アラインメント |
|
|
なし |
あり |
O_DIRECT |
なし |
同期 |
同期 |
|
あり |
エラー |
非同期 |
言い方を換えると、io_submit(2)はAIO専用ではなく、O_DIRECTを指定しない同期的ファイルI/Oの場合にも使用でき、大量のI/Oを一度に発行できるという強力なシステムコールです。
- io_getevents(2)
- 処理を完了したAIO リクエストの情報を読み取ります。POSIX AIOとは異なりシグナルなどの通知はなく、アプリケーションがio_getevents(2)を発行するか、または後述するeventfdを用い、AIO完了を自ら問い合わせます。
- io_destroy(2)
- 渡されたaio_context_tハンドルに対応するシステム資源を解放します。未実行のAIOリクエストがキュー内にあればキャンセルされます。
- io_cancel(2)
- io_submit(2)に渡したAIOリクエストをキャンセルします。キャンセルできた場合は成功(0)を、できなかった場合はエラー(EAGAIN)を、それぞれ返します。Linux AIOのサンプルコードを挙げます。
Linux AIOによるファイル読み取り ― 7raw_laio.c要約
main ()
{
aio_context_t laio;
struct io_event ev;
struct iocb *pcb, laiocb = {
.aio_lio_opcode = IOCB_CMD_PREAD,
.aio_nbytes = BUFSIZ,
.aio_offset = 0
};
laiocb.aio_fildes = open(argv[1], O_RDONLY | O_DIRECT);
laiocb.aio_data = laiocb.aio_fildes;
posix_memalign((void *)&laiocb.aio_buf, 512, BUFSIZ);
memset(&laio, 0, sizeof(laio));
io_setup(1, &laio);
pcb = &laiocb;
io_submit(laio, 1, &pcb);
/* do other jobs */
// err = io_getevents(laio, 0, 1, &ev, NULL);
err = io_getevents(laio, 1, 1, &ev, NULL);
printf("%s: io_submit from fd %llu completed\n",
__func__, ev.data);
printf("%s, %lld read\n", argv[1], ev.res);
}
GlibcはLinux AIOシステムコールに対応していませんので、前述のio_submit(2)などのシステムコールを発行するには、システムコールラッパーを別途定義する必要があり、7raw_laio.cではsyscall(2)を用いています。
LinuxカーネルレベルAIOはPOSIX AIOとは異なり、シグナルまたはスレッド起動による通知機能はありません。7raw_laio.cではio_getevents(2)のmin_nrに1を渡しているため、ここでAIO完了を待ち合わせる動作になります。min_nrを0にすると、まだ完了していなくても待ち合わせません。
min_nrを変更すればブロックする/しないを制御できるので、これだけでも用は足りると思われますが、Linux AIOではeventfd(2)を経由する方法も提供されています。7raw_laio.cにeventfd(2)を追加した8raw_laio_ev.cもサンプルコード一式に含めておきます。
Glibcはすでにeventfd(2)に対応しており、eventfd_read(3)、eventfd_write(3)なども追加されていますが、定義/実装されたのがv2.7から、しかしヘッダファイルが提供された(インストールされるようになった)のはv2.8からという経緯があります。実は筆者はGlibc v2.7 環境(えぇ、古典派と呼ばれようがDebian stable(lenny)を使っていますとも)なので、ちと不便があります。v2.7ではeventfd(2) を使おうとsys/eventfd.hをincludeしようとしても存在しないためコンパイルエラーになりました。一時しのぎですが8raw_laio_ev.cではGlibc v2.7のソースからsys/eventfd.hの一部をコピーすることでこの問題を回避しています。
Glibcのaio_read(3)の動作を要約したように、Linux AIOの動作も要約してみます。io_submit(2)は複数のAIOリクエストを受け取れます。その一つづつに対し次のように処理します。
- AIOリクエストのメモリ領域を確保
- ファイルシステムへAIOリクエストを渡す
- O_DIRECTが指定されていない、またはサイズを拡張する書き込みの場合は完了を待つ。すなわち非同期動作にならない
- それ以外の場合は完了を待たず非同期動作になる
ここで処理の流れはひとまず終了し、アプリケーションへ制御が返り、処理を進められます。以降の動作は基本的に前述のDirect I/Oと同じです。ファイルシステムの下位に位置するブロックデバイスがAIOリクエストを処理し、その後ブロックデバイスからI/O完了の通知を受けると、AIOはシステム内部のリングバッファ内に完了情報を蓄えます。
ここで前述のaio_flags、aio_resfdが関係する動作になります。aio_resfdにeventfdを、aio_flagsにIOCB_FLAG_RESFDをそれぞれ指定すると、そのeventfdに完了通知が届きます。eventfdへの完了通知はio_getevents(2)で得られる詳細な情報 ではなく単なる完了カウンタですが、select(2)、poll(2)、epoll(2)などでeventfdが読み取り可能か見張るイベントループに使用できます。eventfdは面白い動作を備えており、カウンタが0の場合にeventfdから読み取ろうとするとブロックし、epoll(2)などでも読み取り可能とは判断されません。
io_getevents(2)はシステム内部に蓄えられた完了情報を取り出します。情報の型は前掲のstruct io_eventです。I/Oしたバイト数など対応するシステムコールの戻り値はresメンバに代入されます。詳細は7raw_laio.c, 8raw_laio_ev.cを参照してください。
Linux AIOではaioという名前のカーネルスレッドも使用しますが、現状ではファイルクローズくらいしか主な仕事がありません。これはアプリケーションがio_submit(2)後、かつAIO完了前にO_DIRECTのファイルをclose(2)した場合に対応する処理で、アプリケーションが対象ファイルをもう使用しないと言ってもAIOリクエストが生きている限りはシステムはファイルをクローズしません。io_submit(2)時に参照カウンタをインクリメントしてあるためです。AIO完了時に参照カウンタをデクリメントし、0になればファイルをクローズし、対応するシステム資源を解放します。それ以外にaioカーネ ルスレッドを使用するのはgadgetfs(USB Gadget。LinuxをUSB master(host)ではなく、slaveとして使用する場合に使うものらしい)しかないようです。
Libaio ライブラリ
Glibcでは上記io_submit(2)などに対応しておらず(少なくともv2.7では)、現状では別途開発されたlibaioを用いることになります。繰り返しになりますが、POSIXとは異なる独自インタフェースのため、やや使い勝手が異なります。しかし、前述のLinux AIOを理解しておけば有効に活用できるでしょう。
libaioインタフェース
# include<libaio.h>
/* libaioコア(と言って良いと思う) */
/* その他Linux AIOシステムコールも利用可能な様に定義されている */
int io_queue_init(int maxevents, io_context_t *ctxp);
int io_queue_release(io_context_t ctx);
int io_queue_run(io_context_t ctx);
/* もう一つのコア:コールバック */
typedef void(*io_callback_t)(io_context_t ctx, struct iocb *iocb,
long res, long res2);
void io_set_callback(struct iocb *iocb, io_callback_t cb);
/* 初期化関数 */
void io_prep_pread(struct iocb *iocb, int fd, void *buf, size_t count,
long long offset);
void io_prep_pwrite(struct iocb *iocb, int fd, void *buf, size_t count,
long long offset);
void io_prep_preadv(struct iocb *iocb, int fd, const struct iovec *iov,
int iovcnt, long long offset);
void io_prep_pwritev(struct iocb *iocb, int fd, const struct iovec *iov,
int iovcnt, long long offset);
void io_prep_fsync(struct iocb *iocb, int fd);
void io_prep_fdsync(struct iocb *iocb, int fd);
/* その他 */
int io_fsync(io_context_t ctx, struct iocb *iocb, io_callback_t cb, int fd);
int io_fdsync(io_context_t ctx, struct iocb *iocb, io_callback_t cb, int fd);
void io_set_eventfd(struct iocb *iocb, int eventfd);
io_queue_init()、io_queue_release()は単なるio_setup(2)、io_destroy(2)のラッパです。また、io_prep_で始まる6つの初期化関数は、渡されたパラメータを前述のstruct iocbへ代入するだけのものです。io_fsync()、io_fdsync()も単なる便利関数で、初期化、io_set_callback()、io_submit(2)をまとめただけのもので、あまり存在意義がわかりません。これを用意するならばio_pread()、io_pwrite()なども用意するものではなかろうかとも思います。
libaioの特徴はio_set_callback()、io_queue_run()にあります。io_queue_run()はio_getevents(2)を発行し、完了したAIOリクエストに設定されたユーザ関数をコールします(コールバック)。AIOリクエストにコールバック関数を設定するのがio_set_callback()です。libaioではコールバック関数が必須とされており、設定しないと実行時にエラーが発生します。この、コールバック関数が必須な点、またコールバックされる契機は自動ではなく、自らio_queue_run()をコールした時点であるという2点はlibaioの大きな特徴だと言えるでしょう。また、struct iocbのaio_dataはlibaioが使用するため、ユーザが任意のデータを渡すことはできません。
libaioを用いたサンプルコードを挙げます。
libaioを用いたサンプルコード ― 9libaio_sample.c 要約
#include <libaio.h>
char *fname;
void libaio_cb(io_context_t ctx, struct iocb *iocb, long res, long res2)
{
printf("%s: io_submit from fd %d completed\n",
__func__, iocb -> aio_fildes);
printf("%s, %ld read\n", fname, res);
}
int main (int argc, char *argv[])
{
int fd;
char *p;
io_context_t libaio;
struct iocb *pcb, laiocb;
fd = open(argv[1], O_RDONLY | O_DIRECT);
posix_memalign((void *)&p, 512, BUFSIZ);
fname = argv[1];
io_queue_init(1, &libaio);
io_prep_pread(&laiocb, fd, p, BUFSIZ, 0);
io_set_callback(&laiocb, libaio_cb);
pcb = &laiocb;
io_submit(libaio, 1, &pcb);
/* do other jobs */
io_queue_run(libaio);
}
9libaio_sample.cではio_queue_run()を終えてもAIOが完了しているとは限りません。このライブラリはio_getevents(2)のmin_nrに0を渡しており、ブロックしません(というか、後述するように自分でシステムコール発行前に完了情報の有無を直接調べるので、完了していなければシステムコールを発行すらしません)。libaioではコールバックされたか否かで完了を判断するというアプローチです。
Linux AIOが自動通知機能を備えていない以上、アプリケーションがライブラリをコールして初めてコールバックされる動作は当然とも言えますが、プログラマからは敬遠されるかもしれません。この点はアプリケーションをマルチスレッド化し、常駐する別スレッドがio_queue_run()相当の処理(ただしブロックする)を実行することで対応可能と思われますが、マルチスレッド化に伴うプログラミング上の注意点が増えます。これもまたプログラマからは敬遠される要素になるかもしれません。
libaioにはもう一つ隠れた特徴があります。io_setup(2)はシステム内にメモリ領域を確保しますが、実はユーザ空間にもマッピングしてくれ、得られるaio_context_tはそのポインタです。この動作は、例えばopen(2)がシステム内に確保したメモリ領域をユーザには見せず、単にファイルディスクリプタを返す動作とは似て異なるもので、善し悪しの意見が分かれるところかもしれません。libaioではこの動作を利用し(というかLinux AIOとlibaioは同じ人物が開発したので、自分用にこんなマッピングを実装したのかもしれません)、io_getevents(2)をラッピングし、事前にシステム内のメモリ領域を参照し、完了したAIOリクエストが存在する場合にのみ本当のシステムコールを発行するようにしています。これはシステムコール発行回数を削減し効率化を狙った動作と思われます。
Linux AIOがeventfdに対応しているように、libaioからも利用できます。サンプルコード10libaio_sample_ev.cを一式に含めておきます。eventfdを用いると、マルチスレッド化 やio_queue_run()相当の処理を実装せずとも、ブロックする動作が可能となります(もちろん必要ならばですが)。
(初出: H22/4)
Copyright © 2010 SENJU Jiro