システムコールによる非同期I/O API

このAPIは、カーネル2.6以降にカーネルに追加された機能をつかうためのシステムコールになります。
カーネルは、非同期I/Oをサポートするために、VFS層のファイルオブジェクト(簡単に説明すると、実際のファイルシステムがファイルに対する操作を保持するオブジェクトだと思います。この中に、f_opというファイル操作のための関数テーブルをもっています。オブジェクト指向におけるスーパークラスのようなものです。)のf_opに、ファイル操作のaio_read()とaio_write()というエントリポイントがもうけられるようになりました。(個人的に、POSIXAPIと名前がかぶるので、ここで一度混乱しました。Linuxカーネル周りは、日本語に直しているせいかもしれませんが、同じ名前で呼べるけど実は別物というものがいくつかあります。まぁ、でもそれはおいおい)

このAPIをもちいると、I/O処理をカーネルスレッドで実行することになります。ここでいうカーネルスレッドとは、別名カーネルデーモンと呼ばれるもので、POSIXの非同期I/Oで生成されるものとは別物です。どう違うかというと、POSIXAPIで生成されるスレッドは、ユーザー空間で実行されるのに対し、このカーネルスレッドは名前が表すように、カーネル空間で実行されます。(他のUnixでは、ユーザー空間で動くスレッドをカーネルスレッドと呼ぶみたいですが、Linuxでは、このカーネルデーモンというのが存在するため(他のUnixでは無いみたい。)、これをカーネルスレッドと表現することが多いような気がします。このスレッドを実行する関数自体が、kernel_thread()という関数を実行しますし。)

ps -efを実行すると

[aio/0]

のように表示されているやつです。こ

システムコールによる非同期I/Oは、以下のシステムコールをもちます。

io_setup(),io_submit(),io_getevents()
io_cancel(),io_destroy()

これらの関数群を使って、どうやってI/O要求を行うのかを簡単に説明すると、

io_setup()で非同期I/Oを使う準備。
 ↓
iocb型というI/O要求用の構造体を準備する
 ↓
要求を発行したい分のiocb型のポインタの配列をつくり、準備したiocb型の配列をつくる
 ↓
iocb型の配列を引数にし、io_submit()で要求を発行
 ↓
io_getevents()で未終了の非同期I/Oの完了状態を取得する
 ↓
io_destroy()でio_setup()で準備したもの削除をおこなう

のようになります。

io_submit()関数で非同期I/Oコンテキストというのを生成し、そこでI/O要求が開始されていきます。

で、このシステムコールを利用するには、どうやらKernel Asynchronous I/O (AIO) Support for Linuxというのを使わないといけないみたいです(参考:moratorium | libaio(Linuxの非同期I/Oライブラリ)の使い方lab.klab.org - MediaWiki - 社内勉強会)。libaio.hには、システムコールの関数宣言や、io_prep_readなどのiocb構造体を簡単にセットアップするためのラッパー関数が定義されています。

ただ、ちょっと気になっているのは、詳解LinuxカーネルLinuxカーネル解読室やlibaioのページには、直接転送の時のみ有効のように書かれているのですが、参考としてあげたページやlibaioのmanページには、ふつうにopenしたようなファイルに対しても有効のように書かれています。どちらが正しいのでしょうか?

まぁ、どちらか置いておいて、ここではO_DIRECTのときの処理を追っていきます。そうでないときに、本当に行われないかは、別の機会に(または別の人に(ry))。

今回は、非同期I/O要求が、I/O完了の待ち合わせを行わず、復帰するかをみます。

io_submit()関数には、I/O要求を表すiocb型のポインタの配列がわたされます。iocb型に入れる情報としては、POSIX版の非同期I/Oと同様にファイルディスクリプタ(aio_fildes)、バッファ(aio_buf)、バッファサイズ(aio_nbytes)、オフセット(aio_offset)に加え、I/O要求の種類を表す要素(aio_lio_opcode)を持ちます。

/* kernel2.6.23 include/linux/aio_abi.h より */
78 struct iocb {
 79     /* these are internal to the kernel/libc. */
 80     __u64   aio_data;   /* data to be returned in event's data */
 81     __u32   PADDED(aio_key, aio_reserved1);
 82                 /* the kernel sets aio_key to the req # */
 83 
 84     /* common fields */
 85     __u16   aio_lio_opcode; /* see IOCB_CMD_ above */
 86     __s16   aio_reqprio;
 87     __u32   aio_fildes;
 88 
 89     __u64   aio_buf;
 90     __u64   aio_nbytes;
 91     __s64   aio_offset;
 92 
 93     /* extra parameters */
 94     __u64   aio_reserved2;  /* TODO: use this for a (struct sigevent *) */
 95 
 96     /* flags for the "struct iocb" */
 97     __u32   aio_flags;
 98 
 99     /*
100      * if the IOCB_FLAG_RESFD flag of "aio_flags" is set, this is an
101      * eventfd to signal AIO readiness to
102      */
103     __u32   aio_resfd;
104 }; /* 64 bytes */

このaio_lio_opcodeをもとに、io_submit()の実体であるsys_io_submit()のなかで、kiocbという構造体のki_retryという関数ポインタに要求に対する処理関数がセットされます。writeであればファイルシステムがもつaio_write、readであればaio_readとなります。

たいていのファイルシステムでは、自分で実装せずに最終的には、__generic_file_aio_[read/write]が呼ばれることになります。

話はそれますが、ファイルシステムがもつ関数には、read/writeという関数ポインタもあるのですが、最終的には同じ、__generic_file_aio_[read/write]がよばれることになります。

__generic_file_aio_read/__generic_file_aio_writeに来た後、O_DIRECTの直接転送の場合、write系の場合はgeneric_file_direct_writeの中で、共通のgeneric_file_direct_IOに行き着きます。

generic_file_direct_IOのなかで呼ばれる、__blockdev_direct_IOの中で、直接転送のための構造体(直接転送要求の処理状態を表している)であるdio構造体をセットアップしてきます。dio構造体の中には、このI/Oが非同期のI/Oなのかを表すis_asyncというフラグがあり、ここでセットされます。

/* kernel2.6.23 fs/direct_io.c より */
1215     /*
1216      * For file extending writes updating i_size before data
1217      * writeouts complete can expose uninitialized blocks. So
1218      * even for AIO, we need to wait for i/o to complete before
1219      * returning in this case.
1220      */
1221     dio->is_async = !is_sync_kiocb(iocb) && !((rw & WRITE) &&
1222         (end > i_size_read(inode)));

このフラグをたてた後に、direct_io_worker関数でI/Oの転送処理を行っていきます。I/Oの転送要求を出した後に、通常、要求の完了を待ち合わせて復帰を行いますが、非同期の要求の場合は、完了を待たないようになっています。

/* kernel2.6.23 fs/direct_io.c より */
1065     /*
1066      * The only time we want to leave bios in flight is when a successful
1067      * partial aio read or full aio write have been setup.  In that case
1068      * bio completion will call aio_complete.  The only time it's safe to
1069      * call aio_complete is when we return -EIOCBQUEUED, so we key on that.
1070      * This had *better* be the only place that raises -EIOCBQUEUED.
1071      */
1072     BUG_ON(ret == -EIOCBQUEUED);
1073     if (dio->is_async && ret == 0 && dio->result &&
1074         ((rw & READ) || (dio->result == dio->size)))
1075         ret = -EIOCBQUEUED;
1076 
1077     if (ret != -EIOCBQUEUED)
1078         dio_await_completion(dio);

dio->asyncがたっているという条件がふくまれるifの条件式でretに-EIOCBQUEDがいれられ、次のifでdio_await_completionという完了待ち合わせ関数がよばれないので、そのまま復帰となります。

これで、O_DIRECTで非同期で呼ばれた場合は、完了を待ちをしないということがわかりました。

では、どうやってI/O要求の完了をプロセスは知ることができるのでしょうか?
というわけで、次回はI/O要求の完了を知る方法を紹介します。