Programmer's Highの最近の記事

Programmer's High

ファイルオープンと新フラグ

|
http://www.oreilly.co.jp/community/blog/images/union/pg_high_logo.png

多人数により日々改善が加えられるLinuxカーネルですが、その中にはまったく新しい機能もあれば、既存機能を拡張したものもあります。本記事ではopen(2)に加えられた新フラグについて取り上げます。

O_TMPFILEフラグ ---- linux-3.11

2014年9月にリリースされたlinux-3.11では、ファイルオープン時に指定可能なO_TMPFILEフラグが追加されました。目的は従来のmkstemp(3)tmpfile(3)と同様ですが、カーネルレベルで対応するため、効率とアトミック性が強化されます。glibcでは2014年2月にリリースされたv2.19でO_TMPFILEに対応しました。

従来のmkstemp(3)ファミリ、tmpfile(3)を用いる場合では、

  1. 一意な(と期待できる)ファイル名の生成
  2. そのファイル名でファイルを作成/オープン

という手順を踏みますが、一意性を保証するにはO_EXCLを用いたopen(2)を実行する必要があり、open(2)がエラーを返せば再度ファイル名の生成からやり直さなければなりません。

一意なファイル名は(一般的に)乱数を基に生成されます。重複する恐れは多くないかもしれませんが、この動作は効率的ではありません。ファイル名をユーザ空間で生成するため、アトミック性はほぼ皆無と言えます

O_TMPFILEはそもそもファイル名を与える必要がないという点に特長があり、ディレクトリ名しか指定せず、このディレクトリ下にファイルが作成されます。ファイル名は、Linuxカーネルが自動的に一意な名前を生成しますが、作成完了時には既に削除された状態となります。すなわち、リンクカウントはゼロであり、ファイルディスクリプタを用いた操作は可能でもファイル名による操作は不可能です。

Programmer's High

番外編: LiveCDのファイルシステム(2)

|
http://www.oreilly.co.jp/community/blog/images/union/pg_high_logo.png

今回は、前回取り上げたsquashfs,aufs,cloop,dm-snapshotの各機能を用いた、LiveCDに必要な機能を実現するための組合せを考えてみます。 なお、本記事はhttp://sourceforge.net/mailarchive/forum.php?thread_name=20986.1293537718%40jrobl&forum_name=aufs-usersを元にしています。

方式ごとの性能比較

ここまで、squashfs,aufs,cloop,dm-snapshotの各機能を簡単に紹介しました。次にLiveCDに必要な機能を実現するための組合せを考えてみます。組合せ要素には圧縮された読み取り専用下位レイヤ、書き込み可能上位レイヤ、両レイヤの結合方法の三種類がありますが、前述のようにdm-snapshotの場合にはファイルシステム種類に条件が加わるため、次のようになります(表1)。

Programmer's High

番外編: LiveCDのファイルシステム(1)

|
http://www.oreilly.co.jp/community/blog/images/union/pg_high_logo.png

Linuxをハードディスクへインストールせずに使用するLiveCD/DVDも広く利用されるようになり、多くのディストリビューションが従来のインストール媒体/方法に加え、LiveCDをリリースしています。ほとんどのLiveCDではsquashfs、tmpfs、更にAUFSを用い、ハードディスクへインストールしない形態を実現していますが、本記事ではその他の方法も取り上げ、考察します。 なお、本記事はhttp://sourceforge.net/mailarchive/forum.php?thread_name=20986.1293537718%40jrobl&forum_name=aufs-usersを元にしています。

古代の構成

一般的にLinux LiveCDではシステムをインストールした状態のファイルシステムを圧縮し、読み取り専用ファイルシステムイメージとして収納してあります。この際に用いられるのがsquashfsなどのファイルシステムですが、ここからそのままシステムを起動しても読み取り専用ですから、使用できない部分が生まれます。

圧縮の反対動作を指す言葉は伸長とも表現されますが、本記事では展開と表現します。

例えば、システムを起動すれば各種ログファイルや/var/wtmpファイルに対する書き込みが発生しますし、ファイルシステムをマウントすれば/etc/mtabというファイルも更新されます。デーモンが起動されればサービスに必要な各種ファイルなども作成されます。それぞれを一つづつ調査し、書き込みが発生しないように対処して行くことも不可能ではないかもしれませんが、もちろん手間がかかります。そもそもLinux(UNIX)システムは書き込み可能ファイルシステム上で動作することを前提としているのですから、この努力は有意義とは言えないでしょう。

かつてはシャドウディレクトリ方式で対応しようという時代もありました。シャドウディレクトリとはやや古い呼び方かもしれませんが、X.Org/XFree86以前のX11 Window Systemのビルドなどにも用いられていた(と思うけど、記憶に自信がなくなってきた )形態です。例えば、ソースファイルが置かれたsrc/下でそのままビルドすると、他のアーキテクチャ用にビルドする際には全オブジェクトファイルを削除しなければなりません。そうすると、先のアーキテクチャ用のリビルド時にはまた初めからすべてをコンパイルしなければならず、この手間暇を節約するための方策として、obj/を別に用意し、全てのソースファイルのシンボリックリンクをobj/下に作成する方法が採られました。この形態がシャドウディレクトリで、ビルドされるオブジェクトファイルはobj/下に作成され、ソースファイルを書く場合にもディレクトリの差異を気にする必要もありません。lndir(1)を用いると、子ディレクトリ、孫ディレクトリも含んだシャドウディレクトリを作成できます。

Programmer's High

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

|
http://www.oreilly.co.jp/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)に対応します。

Programmer's High

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

|
http://www.oreilly.co.jp/community/blog/images/union/pg_high_logo.png

前回までファイル 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;     /* スレッド属性 */
};

Programmer's High

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

|
http://www.oreilly.co.jp/community/blog/images/union/pg_high_logo.png

プロセスがブロックする要因の一つにファイルI/Oがあります。これを同期I/Oと言いますが、POSIXではAIO(非同期 I/O、Asynchronous I/O)も定義しており、I/O中でもプロセスがブロックせず他の処理を進められるようになります。 今回は、バッファキャッシュを意識したさまざまなファイルI/Oについて解説します。

メモリマップ I/O

ファイルI/Oの一種にメモリマップI/O、mmap(2)があります。mmap(2)(およびmmap2(2))はオープンされたファイルをプロセスアドレス空間へマッピングするもので、例えばアプリケーション内に領域を用意し、ファイルを読み取る動作は次のようにも実装できます。

3mmap.c 要約

{
    char a[N];

    fd = open(path, O_RDONLY);
    read(fd, a, N)
    printf("%.*s\n", N, a);
}

{
    char *p;

    fd = open(path, O_RDONLY);
    p = mmap(NULL, sysconf(_SC_PAGESIZE), PROT_READ, MAP_SHARED, fd, 0);
    printf("%.*s\n", N, p);
}

上記2つの関数は同じ内容を表示します。read(2)を用いる場合は、通常静的に宣言した文字配列やmalloc(3)により割り当てたメモリ領域を事前に用意しますが、mmap(2)を用いると用意した領域がそのままファイルの内容になります。このためメモリ間のコピー(上の例ではバッファキャッシュからアプリケーション内の配列aへのコピー)を削減できます。対象ファイルがまだ読み取られておらず、バッファキャッシュに存在しない場合は、mmap(2)が返したメモリ領域を参照すると、システム側でディクスから自動的に読み取ります。

上の例では読み取りしか行っていませんが、書き込みも同様に可能です。open(2)mmap(2)のパラメータを書き込み用に変え、得られたメモリ領域内を変更すると、ファイル書き込みと同等の動作となります。この場合、メモリ領域がディスクへ書き戻す必要があることを通知するのはmsync(2)です。MS_ASYNCを指定すると、通知するだけでシステムコールは終了します。MS_SYNCを指定すると、通知に加え、内部でfsync(2)相当の処理も行われ、ディスクにまで書き戻します。

mmap(2)したメモリ領域は、アプリケーション終了時に自動的にmunmap(2)され、またmunmap(2)msync(2)を包含するため、mmap(2)したメモリ領域を更新後にアプリケーションが終了するならば、明示的なmsync(2)は省略可能です。しかし、メモリ更新からmunmap(2)まで時間があく可能性がある、またはすぐにデータをディスクにまで保存したいなどの場合にはmsync(2)を発行します。mmap(2)のパラメータにMAP_SHAREDを指定すると、アドレスこそ変換されますがこの領域の参照はシステムが管理するバッファキャッシュの直接参照に相当します。

さらに、ファイルポジションを移動するlseek(2)もユーザ空間だけで解決するアドレス演算で代替でき(上の例ではp+1すればファイル内の2バイト目が参照できる)、発行システムコール数を削減できます。

mmap(2)は効率的なI/Oに大きく貢献しますが、若干の注意点があります。

  • システムのメモリ管理に近付くという性質上、パラメータのaddresslengthoffsetはメモリページサイズの倍数でなければならない(lengthにアラインメントを必要としないシステムもある)。
  • ファイルサイズが小さい場合はプロセスアドレス空間が無駄になる。例えばページサイズが4,096バイトの場合にサイズが1バイトしかないファイルをmmap(2)すると、内容があるのは1バイトだけであり、残り4,095バイトはゼロで埋められ、プロセスアドレス空間を無駄に消費する。
  • 現代では32ビットアドレス空間は不足する場合もある。 上述の無駄に加え、フラグメンテーションが発生する恐れもあり、大きな空き領域が必要な場合に連続した領域が得られなくなることが考えられる。

このため、一般的にはある程度のサイズ(少なくともメモリページサイズ以上)を持つファイルでなければ、mmap(2)の効果は薄れると考えられます。やはりLinux固有のシステムコールですが、remap_file_pages(2)というのもあり、同じファイルに対しmmap(2)を複数回発行する場合、2回目以降はremap_file_pages(2)を用いた方が効率向上が期待できます。mmap(2)を発行すると、システム内部ではメモリ管理用の構造体を新たに作成しますが、remap_file_pages(2)を用いると既存の内部構造体を用いるためです。

Programmer's High

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

|
http://www.oreilly.co.jp/community/blog/images/union/pg_high_logo.png

プロセスがブロックする要因の一つにファイルI/Oがあります。これを同期I/Oと言いますが、POSIXではAIO(非同期 I/O、Asynchronous I/O)も定義しており、I/O中でもプロセスがブロックせず他の処理を進められるようになります。 本記事ではバッファキャッシュからファイル I/Oを解説し、Linuxのio_submit(2)を用いたPOSIX準拠のAIOライブラリを試作してみます。

ファイルI/Oとバッファキャッシュ

io_submit(2)ではDirect I/Oを用いますが、ライブラリの試作へ進む前にまずファイルI/Oのバッファ(バッファキャッシュ)について整理します。実は単にバッファと言ってしまうと誤解される場面が多くあり、例えばプログラミング入門一般としてファイルI/Oを取り上げる際には、

  • CPUの動作は速い。ディスクの動作は遅い。
  • 両者の間に速度差を緩和する緩衝地帯を設けないと、CPUの速度が犠牲にされてしまう。
  • このため、ファイルバッファを用いる。

といった説明があり、よく以下のようなサンプルコードが提示されます。

ファイルI/Oサンプルコード

main()
{
    # define SZ 16
    int fd;
    char buffer[SZ];

    fd = open(argv[1], O_WRONLY | O_CREAT | O_TRUNC, 0644);

    memset(buffer, 'a', SZ);
    write(fd, buffer, SZ);
}

先ほどの説明の後でbufferという名前の配列を見ると、これがCPUとディスクの速度差を緩和する緩衝体(バッファ)の役割をするもなのかと思ってしまいそうです。しかし現代の一般的な環境では、このbufferにはあまりそのような意味合いはなく、単なる作業用のメモリ領域に過ぎません。細部を説明する前に、あと2つサンプルコードを挙げます。以降のサンプルコードは付属のソースファイル一式に含まれているので、詳細はそちらを参照してください。

Programmer's High

epollインタフェースとsignalfd(2)

|
http://www.oreilly.co.jp/community/blog/images/union/pg_high_logo.png

>> (1)よりつづく

本記事のサンプルコードは、以下のリンクよりダウンロードすることができます。記事と合わせてご参照ください。
[ サンプルコード ]

子プロセスの同期/非同期

4epoll、5epoll-multiへのダミー処理追加には、いくつかの注意点があります。1baseと同じように、epollによるイベントループがその場で子プロセスの終了を待つようにすると、1つのダミー処理の終了を他のセッションが待つことになってしまいます。 これはepollによる、イベントループのI/Oの多重性を損なう大きな問題です( 図1.9 )。

図1.9 epoll の多重性を損ねるダミー処理追加

epoll2-fig9.png

(セッションCは割愛)

Programmer's High

epollインタフェースとsignalfd(1)

|
http://www.oreilly.co.jp/community/blog/images/union/pg_high_logo.png

「インターネットサーバでのPthreadとepoll」の記事(以下、前記事と呼びます)を書いた時点では、手元の環境がプアなためマルチプロセス/マルチスレッドを採用したサンプルプログラムの真価を発揮させられず、適切に比較できませんでしたが、その後デュアルコアマシンを借りることができたので、改めて比較してみました。 また、比較の際にサンプルプログラムに追加したダミー処理ではシグナルも使用したので、やはりLinuxに追加された signalfd(2) もepollによるイベントループで処理してみました。

本記事のサンプルコードは、以下のリンクよりダウンロードすることができます。記事と合わせてご参照ください。
[ サンプルコード ]

前記事のサンプルプログラム

前記事 ではHTTPサーバを例に並列性/多重性のサンプル実装を5種類提示しました。簡単に振り返ります。サンプルプログラムがデュアルコアシステム上で動作しており、HTTPリクエストがほぼ同時に3件届いた場合のインタリーブ例を挙げてみます。毎回必ずこの通りにインタリーブされるわけではなく、あくまでもそれぞれの動作の違いを示すための一例です( 図1.1 から 図1.5 )。 また、5epoll-multiはCPU数-1個の4epoll(相当)を子プロセスとして起動しますが、デュアルコアシステムでは子プロセスの4epollを1つしか起動せず、4epollとの比較がしにくくなるため、今回は測定用にCPU数分の4epollの子プロセスを起動することにします。

Programmer's High

インターネットサーバでのPthreadとepoll(2)

|
http://www.oreilly.co.jp/community/blog/images/union/pg_high_logo.png

>>(1)よりつづく
前回は単純な実装からマルチスレッド、スレッドプールと順に見て行きました。今回はいよいよepollを使った実装を紹介します。

epoll例- 4epoll.c

多重I/Oすなわち select(2) / poll(2) によるイベントループはマルチスレッドが普及する以前から利用されていました。 select(2) / poll(2) は複数のファイルディスクリプタ(ソケット)を調べ、I/O可能なものを返すシステムコールです。ソケットに対する読み取りはデフォルトではデータがなければブロック(データが到着するまで待つ)しますが、事前にI/O可能かを確認しておけばブロックすることはありません。1システムコールで複数のソケットを調べられる点も重要で、1プロセスで複数のクライアントに並行して対応できるようになります。しかし当然ながら、対象ソケット数の増加に応じて処理量が増えます。システムコールの実行時間がかかるだけではなく、ユーザ空間でもその結果を1ソケットづつ確認しながら処理を進めるため、性能劣化が問題となり、以前からこの問題が指摘されていました。Linuxでは poll(2) を拡張したepollインタフェース( epol_create(2) , epoll_ctl(2) , epoll_wait(2) )を実装しており、ファイルディスクリプタ数が多い場合の性能劣化を防いでいます。

前掲のもっとも単純なサンプル1base.cを、epollを用いて多重I/O化してみます。

アーカイブ