kakts-log

programming について調べたことを整理していきます

stdioバッファの方式について

概要

fork()による子プロセスを作成するプログラムにおいて、printfなどのstdioライブラリの関数を実行した場合、標準出力が端末とファイルの場合で出力結果が異なる場合があります。
この挙動について整理します。

(参考: LINUXプログラミングインタフェース25.4)

fork()とprintf()を利用した例

下記のように、fork()の前にprintfで文字列出力させ、その後fork()によって子プロセスを作成するプログラムを例とします。

#include "tlpi_hdr.h"
#include <stdio.h>
#include <unistd.h>

int main(int argc, char const *argv[])
{
    printf("Hello world\n");
    write(STDOUT_FILENO, "Ciao\n", 5);

    if (fork() == -1)
        errExit("fork");

    // 親子プロセスともにここまで実行する
    exit(EXIT_SUCCESS);
}

これを、標準出力を端末にして実行すると

$ ./fork_stdio_buf
Hello world
Ciao

という、予想通りの結果が得られます。

しかし、標準出力をファイルにしてリダイレクトすると、挙動が変わります。

root@3d890c1239c5:/tlpi/procexec# ./fork_stdio_buf >a.txt
root@3d890c1239c5:/tlpi/procexec# cat a.txt 
Ciao
Hello world
Hello world

ファイルに出力された結果を確認すると Hello worldが2回出力されてしまいます。

printf()実行時に内部で保持しているstdioバッファの扱いが変わるようです。

解説

上記の挙動を理解するために、まっずstdioバッファのバッファリング方式の違いを知る必要があります。

まず、stdioバッファは、カーネル空間でなく、ユーザ空間で扱われるデータとなります。

そして下記のように標準出力先によって、バッファリング方式が変わります。

  • 端末: 行バッファリング
  • ファイル: ブロックバッファリング

行バッファリングの場合、上記のプログラムでprintfに対して改行文字を含んだ文字列を渡しているため、改行によりその場で表示されます。
逆に、ブロックバッファリングの場合だと、改行があっても表示されず親プロセスのstdioバッファにデータが止まります。

そして、fork()の実行により子プロセスにも、親プロセスのstdioバッファの内容が複製されます。
最後に、親子プロセス両方でexit()を実行する際に、それぞれがもっているstdioバッファの内容をフラッシュすることになり、結果として同じ文字列が二度表示されることになります。

write()による出力では、データはユーザ空間でなく、カーネルのバッファへ直接送信されることになります。 fork()では、カーネルのバッファの内容は複製されないため、2度出力されることはありません。

まとめ

まとめると、同じファイルに対するstdioの関数とシステムコールの併用をする場合には注意が必要となります。 stdioがユーザ空間で扱っているバッファの扱いと、カーネル空間のバッファを直接扱うシステムコールの挙動の理解が重要です。