Linux I/O のお話 write 編

  • write はページに dirty フラグを立てるだけなので決してユーザープロセスを待たせない

って、本当にそうなんでしょうか?(否定しているわけではなく、純粋な疑問です。)

と質問をもらったので、最近追ったことをここでまとめます。かなり長文です、すいません。また、まだまだ不勉強なので間違っているところもあるかもしれません。ツッコミ大歓迎です。

まず、オライリーカーネル本の 15章 ページキャッシュ 15.3 汚れたページのディスクへの書き込み から引用。

ご存知のように、カーネルは、ブロック型デバイスのデータを含むページをページキャッシュに蓄えています。プロセスが何らかのデータを更新した場合は、必ず対応するページに汚れている印をつけます。すなわち、PG_dirty フラグを設定します。

UNIX システムでは、汚れたページのブロック型デバイスへの書き込みを遅延することができます。この方法によって、システムの性能を著しく向上させることができます。キャッシュ内のページへの複数の書き込み操作を、対応する低速なディスクセクタに対して1回だけの物理的更新で済ませることができます。さらに書き込み操作は、読み取り操作ほどほかの処理に影響を与えません。というのも、通常、書き込みが遅延することによってプロセスが休止することはないからです。 一方、読み取りが遅延されるとほとんどの場合、プロセスは休止してしまいます。書き込みを遅延することによって、物理的なブロック型デバイスは、平均的に書き込み要求よりも読み取り要求の方を多く処理できるようになります。

だそうです。書き込みがプロセスを休止させないのはそうなんだろう。でもなんでそうなのか、が問題ですよね。ということで深追いの時間です。

Linux の書き出し処理のおおまかな仕組み

Linux が write(2) なり何なりでプロセスから書き込み要求を受け取ったあと、どのようにしてそれがブロック型デバイスに反映されるか、ですがざっくりまとめると

  • ページ(仮想メモリ上の最小単位。カーネル内データ構造)にそのページは汚れてます = 後でブロック型デバイスに書き出す必要がありますとフラグを立てる。フラグを立てたらすぐプロセスに処理は戻る。
  • カーネルスレッドの pdflush が定期的に汚れたページを検索して汚れたページとブロック型デバイスと同期を取る

というカーネルスレッドが別スレッドで処理する非同期書き込み方式になってます。(ここで言う非同期というのは AIO API の"非同期"ではないです。)

本当にそうなのか、検証してみましょう。

#!/usr/local/bin/perl
use strict;
use warnings;
use IO::File;
use Time::HiRes qw/usleep/;

use constant DATA => "12345678" x 128;

my $io = IO::File->new("/home/naoya/tmp/test.dat", 'w') or die $!;

while (1) {
    $io->syswrite(DATA);
    usleep(100);
}

という、0.1 秒毎に特定のファイルにひたすらテキストを追記していくプログラムを実行する。これを実行しつつ、別の端末で vmstat を1秒間隔で見てみます。

% vmstat -a 1
procs -----------memory---------- ---swap-- -----io---- --system-- ----cpu----
 r  b   swpd   free  inact active   si   so    bi    bo   in    cs us sy id wa
 2  0 262136  17696 261772 681116    0    1     5    16  104    31 11  1 88  0
 0  0 262136  17632 261768 681168    0    0     0     0  104   115 35  1 64  0
 0  0 262136  17568 261768 681220    0    0     0     0  102   108 33  0 67  0
 0  0 262136  17504 261772 681268    0    0     0   424  102   114 45  0 55  0
 0  0 262136  17440 261768 681320    0    0     0     0  102   109 50  0 50  0
 0  0 262136  17376 261768 681372    0    0     0     0  102   108 45  0 55  0
 0  0 262136  17312 261768 681420    0    0     0     0  102   110 42  0 58  0
 0  0 262136  17312 261768 681472    0    0     0     0  102   109 41  0 59  0
 0  0 262136  17248 261772 681520    0    0     0   276  102   109 34  0 66  0
 0  0 262136  17184 261768 681572    0    0     0     0  102   108 38  0 62  0
 0  0 262136  17120 261768 681624    0    0     0     0  102   109 44  0 56  0
 0  0 262136  17056 261772 681672    0    0     0     0  102   111 40  0 60  0
 0  0 262136  17056 261768 681724    0    0     0     0  102   108 45  0 55  0
 0  0 262136  16992 261764 681780    0    0     0   292  102   110 44  0 56  0
 0  0 262136  16928 261764 681828    0    0     0     0  102   109 42  0 58  0
 0  0 262136  16864 261764 681880    0    0     0     0  102   108 47  0 53  0
 0  0 262136  16800 261768 681928    0    0     0     0  102   108 36  0 64  0
 0  0 262136  16800 261764 681980    0    0     0     0  102   109 46  0 54  0
 0  0 262136  16736 261760 682036    0    0     0   276  102   109 41  0 59  0

ここで見るべきは bo = Blocks sent to a block device (blocks/s). です。プロセスが書き込み処理をするたびにブロック型デバイスにデータを書き出すとしたら、ここの値は常に 0 以上になるはずですが、実際には 5 秒に一回だけ要求が発生してるのがわかります。

5秒、というのはカーネルスレッドの pdflush が汚れたページを書き出す間隔です。これは sysctl の vm.dirty_writeback_centisecs で設定されています。

% /sbin/sysctl vm.dirty_writeback_centisecs
vm.dirty_writeback_centisecs = 500

確かに5秒になってますね。

ブロック型デバイスとの同期は遅延していて、プロセスから書き込み要求を受け取った段階ではカーネルはページキャッシュに書き込みフラグを立てているだけというのはこれで分かります。ただしこれは比較的システムリソースに余裕があるときの状態で、ディスクへの書き出し処理は pdflush の定期的な処理以外にも発生します。

書き出し処理が発生する条件

書き出し処理が発生する条件を Linux カーネル 2.6 解読室 P.308 の表 17-6 ディスクへの書き出し処理を参考にリストアップしてみます。

  • ユーザープロセスによる明示的な同期書き出しの指定
    • ファイルを O_SYNC モードでオープンする
    • ファイルシステムが -o sync オプションでマウントする
    • fsync(2) / fdatasync(2) を発行する
    • sync(2) を発行する
  • カーネルスレッド (pdflush) によるバックグラウンドでの書き出し
    • 一定時間ごとに起床して汚れたページを書き出す。 (旧 kupdate カーネルスレッド)
    • 空きページが少なくなってきたときに起床して書き出す backgroud_writeback() (旧 bdflush カーネルスレッド)
    • 汚れたページキャッシュの割合があまり増えないようにするためのページキャッシュ書き出し balance_dirty_pages_ratelimited()

以上になります。

ユーザーが明示的に同期を命令しない限り、ブロック型デバイスの書き出しはカーネルスレッドが非同期に行ってるのがわかります。従ってこの辺からも通常の write 時にはカーネルは決してユーザープロセスをブロックしない...という結論になります。

read は待ち合わせる

話は変わって、write ではなく read の話。

read がページキャッシュにまだ載ってないデータを読み取ろうとしたとき、ブロック型デバイスとの I/O を待ち合わせる必要があるのはまあ明らかです。その read 処理がどのような流れになっているかは同じくカーネル解読室の P.298 が分かりやすい。

  • まずページキャッシュを検索
  • ページキャッシュがあった場合
    • そのページキャッシュにデータがちゃんと載ってるか確認 (同時に同じページにアクセスしてきた他の誰かがデータを読み込み中なのを割けるため。)
      • 載ってなかった場合はほかの誰かが完了するまで待ち合わせ
    • ページキャッシュのデータをプロセス空間にコピー
  • ページキャッシュがなかった場合
    • 新規ページキャッシュをアロケート
    • そのページをページキャッシュの管理オブジェクト (address_space 構造体) に追加
    • ファイルシステムにデータの読み取り命令を発行。
    • I/O の完了を待ち合わせる

という流れになっているそうです。つまり、

  • 他の誰かが同じデータを読み取ろうとしてるとき
  • ページキャッシュに該当データがなかったとき

に待ちが発生します。このときプロセスは休止していて、ハードウェアの読み取りを待っている状態 = TASK_UNINTERUPPTIBLE = 割り込み不可能なタスクの一時停止状態です。

ちょっと脱線するけど 負荷とは何か - naoyaのはてなダイアリー で調べたように TASK_UNINTERUPPTIBLE なプロセスはロードアベレージとして換算されます。なので http://naoya.g.hatena.ne.jp/naoya/20070518/1179467301 で遭遇したような、バグでなんらかのプロセスが TASK_UNINTERUPPTIBLE 状態から返れなかったりするとロードアベレージが常に 1.00 とかおかしなことになったりもします。

とにかく read は write と違ってプロセスを待たせます。

「書き込み処理」とは / write と read + write は区別すること。

  • write はプロセスをブロックしない
  • read は待ち合わせが必要な場合プロセスをブロックする

なわけですが、ここで write とは何かのそもそも論を考えてみます。

ここまでに議論している "write" とは、あくまでカーネルの視点でみた場合の write。書き出し処理それだけ。一方アプリケーションのレベルで "write" "書き込み" と言った場合、必ずしも「書き込み」だけとは限りません。

例えばファイルの一部を更新する場合。この場合、書き込みは書き込みですが、一度書き込み位置までファイルオフセットを seek させる必要があるかもしれません。更にこの seek する場所を特定するためにもしかしたら何かしらの検索が必要かもしれません。するとその時 read が発生しますよね。

こういった点に注意。アプリケーション内の書き込み処理、と一言にいってもそれが本当に write だけなのか、それともある程度の read 処理を伴う一連の処理なのかは区別して考える必要があります。

Apache のログ

相当なアクセスのあるウェブサーバーではアクセスログへの書き込みが結構あります。tail -f access_log がまったく目で追えないぐらい速い、というのもざらです。Apache のログの書き込みって結構すごいけど、別にそれでシステムが重くなったりとかしないよなー、と不思議に思ったことがある方は多いかもしれません。

このアクセスログについて先の視点で考えてみましょう。ファイルの末尾にひたすら追記していくだけで良いので read が発生しない、純粋に write しまくりな状態です。ここで仮に write のたびにディスクと同期しているとしたら大変そうですね。

実際はログが結構な勢いで書き込まれてるウェブサーバーで vmstat を見てみましょう。

% vmstat 1
procs -----------memory---------- ---swap-- -----io---- --system-- ----cpu----
 r  b   swpd   free   buff  cache   si   so    bi    bo   in    cs us sy id wa
 0  0    128  24644  33552 3686236    0    0     2    17    2     0  0  0 99  0
 0  0    128  24644  33552 3686236    0    0     0   416  347    67  3  1 95  2
 0  0    128  24652  33552 3686332    0    0     0     0  316   105  0  0 100  0
 0  0    128  24652  33552 3686332    0    0     0     0  323    99  0  0 100  0
 1  0    128  24660  33552 3686404    0    0     0     0  315    91  0  0 100  0
 0  0    128  24660  33552 3686404    0    0     0     0  322   102  0  0 100  0
 0  0    128  24544  33552 3686492    0    0     0   320  357   109  1  1 96  3
 0  0    128  24544  33552 3686492    0    0     0     0  307    78  0  0 100  0
 0  0    128  24552  33552 3686564    0    0     0     0  311    88  0  0 100  0
 1  0    128  24552  33552 3686564    0    0     0     0  310    55  0  0 100  0
 0  0    128  24552  33552 3686624    0    0     0     0  306    50  0  0 100  0
 1  0    128  24552  33556 3686620    0    0     0   288  353    88  0  1 96  3

やっぱりブロックデバイスへの書き込みは 5秒おきです。Apache のプロセスはログがディスクへ書き出されたかどうかは関係なく動きますし、ログの書き込み命令でブロックされることはありません。

「え、でも tail -f すると、5 秒に一回しかブロックデバイスと同期されないはずなのに、ログは常に流れまくってるよ」と思った方。いえいえ、いままさに tail -f が端末に流している出力はページキャッシュからコピーされたデータです。

カーネルは書き込み要求を受け取ってページ(キャッシュ)をアロケートしてデータをページにコピーして、あとは 別スレッドで動いている pdflush 任せ。ブロック型デバイスからではなく ページキャッシュにコピーされたものから tail -f のプロセスのバッファにコピーされたものがあなたの目に届いている。

...と、Apache のログについて考えてみると write はプロセスを待たせない、というのがよくわかります。

ページキャッシュに確保できるメモリ量と書き込みの関係

では、書き込み処理が多いホストでディスクI/O待ちが発生しがちなのはなんででしょうか。たぶん原因はページキャッシュ用のメモリが足りない、というところにあるんじゃないかなと思います

Linux のページキャッシュ - naoyaのはてなダイアリー でも触れたようにディスクの内容をページキャッシュに展開するのに十分なメモリがあれば I/O wait はほとんど発生しないのは OK。これを前提に考えていきます。

  • ページキャッシュに載せるのに十分なメモリがない場合、まずページキャッシュに載ってないページの読み出し時に read が待たされます。
  • write が頻繁に起こっている環境でページキャッシュ分のメモリが足りないと、「書き出し処理が発生する条件」で挙がっている background_writeback() を実行するカーネルスレッドが頻繁に起き上がり、5秒に一回の定期書き出し以外でのブロック型デバイスへの書き込み要求が多くなります。
  • ただでさえディスクに対して read が発生してそこでディスクは忙しいのに、write も頻繁に発生するようになって、ますます read は遅くなっていきます。
  • 書き込み用にページを確保するために、すでにキャッシュされたページを解放してやらないといけない。 → ますますディスクに read が。
  • 結果、I/O 待ちが多発する。

ということになるかと思います。

また、こういう状況に対処するにはディスクからの read を発生させないようメモリを積めばよいのと同じく、write もメモリを増やせばよいのが分かります。(あとは pdflush 周りの sysctl パラメータをいじるのでもある程度は緩和できるかもしれません。) 実際最近、はてなでも write が多くてひーひー言ってたあるホストに十分なメモリを積んだところ、I/O 待ちを無くすことができました。

さて、

また、free の結果はこんな感じなので、メモリに余裕はあるみたいです。

ということですが free の結果では、プロセスに割り当てるためのメモリが足りてるかどうかはわかりますが、ページキャッシュ用にメモリが足りてるかどうかはわかりません。

# free
             total       used       free     shared    buffers     cached
Mem:       2043756    2011160      32596          0     351016     847776
-/+ buffers/cache:     812368    1231388
Swap:      1052248

を見るにページキャッシュに割り当てられてるメモリは 800 MB強。実際そのサーバーが主な仕事で使っているデータは、合計でどのぐらいのサイズでしょう。おそらく、キューに溜まったメールその他 OS 上で必要なデータのサイズをあわせると 800MB を超えるんではないかなあと思います。外してたらごめん。

あと id:hirose31 さんがコメントしてますが、アプリケーションが SYNC モードでファイルを開いてたり、明示的に fsync() してたりするとそこで wait が発生するのは言わずもがな、です。

write はプロセスを待たせないのをコードで見る

最後にカーネルのコードを実際に追って、write がプロセスを待たせていないというのを見ていきます。ext2 を題材に。

操作対象のファイルオブジェクトにセットするコールバック一覧であるところの file_operations 構造体を見ると、read や write が発行されたときどういう関数が呼ばれるかがすぐわかります。

/*
 * We have mostly NULL's here: the current defaults are ok for
 * the ext2 filesystem.
 */
const struct file_operations ext2_file_operations = {
        .llseek         = generic_file_llseek,
        .read           = do_sync_read,
        .write          = do_sync_write,
        .aio_read       = generic_file_aio_read,
        .aio_write      = generic_file_aio_write,
        .ioctl          = ext2_ioctl,
#ifdef CONFIG_COMPAT
        .compat_ioctl   = ext2_compat_ioctl,
#endif
        .mmap           = generic_file_mmap,
        .open           = generic_file_open,
        .release        = ext2_release_file,
        .fsync          = ext2_sync_file,
        .sendfile       = generic_file_sendfile,
        .splice_read    = generic_file_splice_read,
        .splice_write   = generic_file_splice_write,
};

と、write のときは do_sync_write() が呼ばれます。do_sync_write() は filp->f_op->aio_write のラッパ。filp->f_op->aio_write は上記の aio_write = generic_file_aio_write()。AIO API に使うためのコールバックを実行して、結果を待ち合わせることで同期処理としています。

generic_file_aio_write() の中を見てみます。

ssize_t generic_file_aio_write(struct kiocb *iocb, const struct iovec *iov,
                unsigned long nr_segs, loff_t pos)
{
        struct file *file = iocb->ki_filp;
        struct address_space *mapping = file->f_mapping;
        struct inode *inode = mapping->host;
        ssize_t ret;

        BUG_ON(iocb->ki_pos != pos);

        mutex_lock(&inode->i_mutex);
        ret = __generic_file_aio_write_nolock(iocb, iov, nr_segs,
                        &iocb->ki_pos);
        mutex_unlock(&inode->i_mutex);

        if (ret > 0 && ((file->f_flags & O_SYNC) || IS_SYNC(inode))) {
                ssize_t err;

                err = sync_page_range(inode, mapping, pos, ret);
                if (err < 0)
                        ret = err;
        }
        return ret;
}
  • iノードオブジェクトのロックを獲得(write(2) を一度に1プロセスしか発行できないように)
  • __generic_file_aio_write_nolock() = 関連するページに PG_dirty フラグを設定する (とカーネル本に書いてました.)
  • 何かしらの条件で SYNC が明示的に命令されているときのみ sync_page_range() でページとブロック型デバイスを同期する
  • 終わり

ということで、明示的に同期命令が加えられているとき意外はブロック型デバイスとページを同期させることはしていないのがわかります。

まとめ

  • write はプロセスを待たせない
  • ただし明示的に sync する場合は待たせる
  • pdflush が別スレッドでブロック型デバイスへの書き出しを行っている。デフォルトでは5秒おき。
  • Apache のログが書き込みまくっててもシステムは平気な理由はページキャッシュにあり
  • 十分なメモリを足せば書き込み性能も向上させられる
  • generic_file_aio_write() を見れば明示的に sync する場合以外はブロック型デバイスと同期しない、というのがコードで分かる

以上がわかりました。これで答えになってるかな?

参考

詳解 Linuxカーネル 第3版

詳解 Linuxカーネル 第3版

Linuxカーネル2.6解読室

Linuxカーネル2.6解読室