POSIX準拠の非同期I/O

これは、ライブラリ関数として実装されており、以下の関数群を持ちます。

aio_read(),aio_write(),aio_fsync(),aio_error()
aio_return(),aio_cancel(),aio_suspend()

詳しい使い方は説明しませんが、aio_read(),aio_write()を使い、I/O要求を発行します。I/O要求は、それぞれの関数中で要求を発行した段階で復帰してくれます。

では次のような、aio_readを行うプログラムを書いて、このライブラリ関数がどのように実装されているかを見てみます。aio_readの動作が見たいだけなので、たぶん、この非同期I/Oライブラリの使い方としては、だめだめです。

#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/time.h>
#include <fcntl.h>
#include <aio.h>

#define _POSIX_PRIORIZED_IO

int main(int argc, char** argv)
{
        int fd;
        void* buf;
        struct aiocb aioc;

        if ((fd = open(argv[0], O_RDONLY)) <= 0 ){
                return -1;
        }

        if ((buf = malloc(1024 * 1024)) == NULL) {
                close(fd);
                return -1;
        }
        aioc.aio_fildes = fd;
        aioc.aio_buf = buf;
        aioc.aio_nbytes = 1024 * 1024;
        aio_read(&aioc);

        free(buf);
        close(fd);
        return 0;
}

これをsample_aio.cとして、次のようにコンパイルします。

gcc -o sample_aio.o -g -lrt sample_aio.c

で、これをgdbで、ブレークポイントをmainにし、aio_readが実行されるまで、ステップ実行していきます。

$ gdb ./sample_aio.o
GNU gdb 6.4.90-debian
Copyright (C) 2006 Free Software Foundation, Inc.
GDB is free software, covered by the GNU General Public License, and you are
welcome to change it and/or distribute copies of it under certain conditions.
Type "show copying" to see the conditions.
There is absolutely no warranty for GDB.  Type "show warranty" for details.
This GDB was configured as "i486-linux-gnu"...Using host libthread_db library "/lib/tls/i686/cmov/libthread_db.so.1".

(gdb) b main
Breakpoint 1 at 0x8048498: file sample_aio.c, line 17.
(gdb) start hoge
Breakpoint 2 at 0x8048498: file sample_aio.c, line 17.
Starting program: /home/taka/io/file_io/sample_aio.o hoge
Failed to read a valid object file image from memory.
[Thread debugging using libthread_db enabled]
[New Thread -1209805120 (LWP 2258)]
[Switching to Thread -1209805120 (LWP 2258)]

Breakpoint 1, main (argc=2, argv=0xbf955a34) at sample_aio.c:17
17              if ((fd = open(argv[0], O_RDONLY)) <= 0 ){
(gdb) s

21              if ((buf = malloc(1024 * 1024)) == NULL) {
(gdb)
25              aioc.aio_fildes = fd;
(gdb) s
26              aioc.aio_buf = buf;
(gdb)
27              aioc.aio_nbytes = 1024 * 1024;
(gdb)
28              aio_read(&aioc);
(gdb) s
[New Thread -1208419408 (LWP 2261)]
30              free(buf);

New Threadというメッセージがでて、スレッドが生成されているのがわかります。aio_read()は、スレッドを生成し、その中で通常のI/O要求を発行することによって、非同期でI/Oを行っているのです。

実をいいますと、POSIX準拠の非同期I/Oの簡易的な実装は、スレッドによって実装されているのは、詳解Linuxカーネルで説明されているのですが、実際どうなっているのか、gdbで確認いたしました。ふつうは、こういうのを確認するにはstraceを使うのがよいのでしょうが、ライブラリ関数等を読み込むところも表示されるので、あえてgdbにしました。また、コンパイル時につけている、rtというのは、たしかリアルタイムスレッドのライブラリです。

Linuxの非同期I/Oについて

最近、LinuxのI/Oまわりについて調べていた延長で、ちょうど一年ぐらい前にlighttpdの1.5が採用して性能があがったといわれた、非同期I/Oまわりについて調べてみたので、これから現状自分がわかっている範囲でのまとめと疑問点を書いてみます。ひとつのエントリに書いてしまうと、ものすごく長くなりそうなので、linux_aioというタグをつけていきます。

というわけで、よくわかっていない部分もあるので、間違い等あれば指摘いただけると、うれしいです。

LinuxのI/Oは同期?非同期? 2007-10-13
なぜ非同期I/OのAPIが存在するのか 2007-10-13
POSIX準拠の非同期I/O 2007-10-13
2007-10-15 - takkan_mのNo planな日常 2007-10-15
どうやってI/Oの完了をうけとっているか(準備編) - takkan_mのNo planな日常2007-10-21

LinuxのI/Oは同期?非同期?

まず、ここでLinuxのI/Oについて、簡単に説明します。
基本的に、LinuxのI/Oは、非同期で行われます。非同期というのは、I/O要求とディスクアクセスは同期していないということです。ふつうにwrite()を呼んでも、ディスクに書き込み要求がいきません。read()要求をだしても、ディスクにread()要求が即時にいくとは、限りません。
ここら辺の説明は、id:naoyaさんのまとめが、非常にわかりやすいと思います。
Linux のページキャッシュ - naoyaのはてなダイアリー
Linux I/O のお話 write 編 - naoyaのはてなダイアリー
つまり、ページキャッシュのみで操作できるときは、ディスクにアクセスにいかないということです。はしょりすぎてすみません。俗に言う、遅延書き込みや先読みと呼ばれる部分です。ページキャッシュがやりとりをするので、データの転送は、カーネルモードのプロセスがカーネル空間のメモリをつかうことになります。

ただし、open()で、O_DIRECT(直接転送のためのフラグ)がたてられていたりすると、ページキャッシュによるデータ転送を行わず、ユーザー空間のメモリをカーネルモードのプロセスが操作することになります。つまり、read()/write()を行うとディスクまでアクセスが行われることになります。また、アクセスを行っても、ページキャッシュを生成したりしません。O_DIRECTについて、以前簡単にまとめたものをどうぞ。
O_DIRECTで遊ぶ - takkan_mのNo planな日常

なので、LinuxでのI/O操作では、意図的にO_DIRECTを指定したりなど、同期I/Oを発行するような操作を行わない限りは、非同期に行われるといえます。

なぜ非同期I/OのAPIが存在するのか

Linuxでは、ユーザー空間で使用できる非同期I/Oを使う手段としては、2種類あります。

  1. aio_readなどを使うPOSIX準拠の非同期I/O
  2. io_setupなどを使うシステムコールによる非同期I/O

しかし、LinuxのI/Oは同期?非同期?で、LinuxのI/Oがたいていの場合、非同期であると説明しましたのに、何故、非同期I/OのAPIというのがあるのでしょうか?

ここで注意しなければいけないのは、前のエントリでいっていた非同期と非同期I/OのAPIの非同期とはの意味が若干変わるということです。前のエントリでの非同期では、ディスクへのアクセスがユーザー空間からの要求と同時に行われるかどうかでしたが、非同期I/O APIによる非同期という意味は、要求をだしてから、read系ならその要求をだした関数から返ってきたら、すぐにそのメモリにデータが使える状態になるようなことを同期、要求を出し関数から返ってくるのとI/O操作(ここでのI/O操作はページキャッシュに対する操作を含む)の完了が一致しないことを非同期と呼びます。

では、I/O要求とI/O操作が非同期であると何がうれしいのでしょうか。

それは、I/O操作を行っている間に、他の仕事をさせることができるということです。

まず、ディスクに対するアクセスが行われるとき、ディスクとメモリのデータの転送には、CPUは使われず、DMAによる転送になります。DMA転送を行っている間、CPUは遊んでしまいます。

ページキャッシュに対する操作も、ディスクに対する操作がたいていの場合、非同期に行われるとはいえ、ページキャッシュの検索や、writeの場合であれば、dartyフラグをたてなければいけないと、いろいろ操作を行わなければなりません。

非同期I/O APIによるI/O要求をだすと、ページキャッシュやディスクに対する操作要求をだすだけで、プロセスがCPUを使うことができます。ページキャッシュやディスクにアクセスする操作は、I/O要求を出したプロセスとは別のコンテキストで行われることになります。そうすることで、要求をだしたプロセスはI/O操作の完了するまで、他の操作を行うことができるわけです。

ページキャッシュにのっていないようなファイルや、I/Oの直接転送を行うときなどI/O操作に時間がかかるようなときに、非同期I/Oを用いることでCPUを有効に使えます。特に、マルチプロセスなどの環境では、特に有効になってきます(そのプロセス自身の実行とそのプロセスが要求した非同期I/Oの操作が同時に複数のプロセスで行われる可能性があるため)。

ということで、これかのエントリでは、上で紹介した2つの非同期I/Oについて書いていきます。