ぷるぷるの雑記

低レイヤーがんばるぞいなブログ. 記事のご利用は自己責任で.

fprintf()とファイル構造体とバッファリングについて

fprintf()は出力データをバッファリングするという記述をよく見かけますが、実際にバッファリングされていることを確かめたかったのでC言語で実験してみました。printf()系の関数は多岐にわたりますが、fprintf()がわかれば十分なので以下ではfprintf()に絞って話をしていきます。

バッファリングとは

fprintf()は最終的にはwriteシステムコールというOSのシステムコールを通して文字を出力します。このシステムコールの発行にはそれなりに多くの処理が必要となり、できる限り回数を抑えたいというのがシステムのお気持ちです。例えば、"abc...xyz"という26文字の文字列をファイルに出力する場合、一文字ずつ出力しようとすると計26回writeシステムコールを呼ばなければなりません。一方、"abc...xyz"を一文字ずつバッファーに蓄えておき、バッファーの先頭ポインタをwriteシステムコールに教えてあげれば、1回のシステムコールで文字列を出力することができます。このような仕組みを、出力データをバッファリングすると言ったりします。

バッファリングされたデータがフラッシュされるのは、主に以下の4つの場合*1らしいです。

  1. バッファサイズ以上のデータがバッファリングされようとしたとき
  2. 改行コードがバッファリングされたとき
  3. fflush()されたとき
  4. fcloseが実行されたり、プログラムが正常終了したとき

fprintf()は直接ファイルに出力する関数ではなく、あくまでファイルのバッファにデータを書き込む関数です。上記のいずれかの場合にOSがバッファリングされたデータを実際にファイルに書き込んでくれます。

つまり、ファイルを編集したいときはアプリケーション側ではバッファにデータを書き込めばOSが良い感じにファイルに書き込んでくれます。

同様に、ファイルを読み込みたいときはOSが良い感じにファイルのデータをバッファに取り込んでくれて、アプリケーション側ではバッファからデータを読み込むだけで良いです。

ファイル構造体の構造

次の構造体の型の定義はLinuxのファイル構造体のうち重要そうな部分を抜き出したものになります。

/* 
 *       /usr/include/x86_64-linux-gnu/bits/stdio.h 
 */

struct _IO_FILE
{
  .
  .

  /* The following pointers correspond to the C++ streambuf protocol. */
  char *_IO_read_ptr;   /* Current read pointer */
  char *_IO_read_end;   /* End of get area. */
  char *_IO_read_base;  /* Start of putback+get area. */
  char *_IO_write_base; /* Start of put area. */
  char *_IO_write_ptr;  /* Current put pointer. */
  char *_IO_write_end;  /* End of put area. */
  char *_IO_buf_base;   /* Start of reserve area. */
  char *_IO_buf_end;    /* End of reserve area. */

  .
  .
};

_IO_buf_baseや_IO_buf_endという名前からわかるように、何気なく使っていたFILE構造体にはバッファリングのためにchar型へのポインタが含まれているみたいですね。fseek()は_IO_read_baseや_IO_write_baseの値をいじっているのだという予想ができます。

標準出力への出力だとしてもファイル構造体を使っているので、バッファリングが有効になっているわけですね。

コードを書いて実験

以下のコードは"hoge"は直ちに画面に表示されますが、"fuga"はsleep(3)の後に表示されます。printf()で出力した文字列が直接画面に表示されるのではなくバッファリングされていることがよくわかります。

#include<stdio.h>
#include<unistd.h>
int main(void)
{
        printf("hoge");
        fflush(stdout);
        printf("fuga");
        sleep(3);
        fflush(stdout);
        printf("\n");
        return 0;
}

実行結果

f:id:prupru_prune:20220417003001g:plain
printf()のタイミングとフラッシュのタイミングをずらした場合

標準出力への出力のバッファリングだけではなく、通常のファイルへのデータのバッファリングもみてみましょう。

#include<unistd.h>
#include<stdio.h>
#include<stdlib.h>

int main()
{
        FILE *fpr, *fpw;
        char line[20]={'e','m','p','t','y','\n'};
        fpw = fopen("test","w");

        // 末尾に\nがないので、出力バッファにバッファリングされるだけでフラッシュされない
        fprintf(fpw,"%s","abcde");
        fpr = fopen("test","r");
        fread(line, sizeof(char), 20, fpr);

        // "empty"が出力される
        printf("%s\n",line);

        // 強制的にバッファリングされたデータをフラッシュ
        fflush(fpw);

        // fpr->_IO_read_base を先頭に戻す
        fseek( fpr, 0, SEEK_SET );

        fread(line, sizeof(char), 20, fpr);

        // "abcde"が出力される
        printf("%s\n",line);


        fclose(fpr);
        fclose(fpw);
        return 0;
}

参考

a-hisame.hatenadiary.org

atmarkit.itmedia.co.jp

*1:参考にしたサイトとは計上の仕方を変えている