低水準ファイル入出力関数を使おう

前へ << DNS クライアントを作ってみよう (3) C 言語で HTTP クライアントを作ってみよう (1) >> 次へ

低水準ファイル入出力関数

perl でプログラミングするにしても、 基礎知識としてC言語のファイルディスクリプタの概念を知っておくにこしたことはありません。 特に 4引数 select を使うときには、 ファイルディスクリプタの知識があると理解が早いでしょう。

C言語でファイルを扱うときは、一般的に fopenfreadfwritefclose などの ライブラリ関数を使います。これらは stdio.h で宣言されており、 標準入出力ライブラリ (標準入出力関数) と呼ばれます。 ファイルをオープンするときは

FILE *fp;
fp = fopen("file.dat", "r");
として FILE 構造体を得ます。それに対して fread・fwrite を実行します。

しかし、これら fopenfread などのライブラリ関数も、 内部では openreadwriteclose などの、より低レベルな システムコールを呼び出しているのです。

標準入出力ライブラリではファイルを管理するのに FILE 構造体を使用していましたが、 システムコールではファイルディスクリプタと呼ばれる整数値を使います。 プロセスが実行されると同時に

  • 標準入力が 0
  • 標準出力が 1
  • 標準エラー出力が 2
を OS が自動的にオープンします。さらにファイルをオープンしたり、socket 関数を使うと 3、4…と順番にディスクリプタが割り当てられます。

では、標準入出力ライブラリを一切使わないサンプルプログラムを紹介します。

% cc -o file-open file-open.c
でコンパイル。
% ./file-open filename
とすることで、ファイル filename の内容を標準出力へ出力します。

file-open.c

    1: /* $Id: file-open.c,v 1.4 2004/07/18 11:37:55 68user Exp $ */
    2: 
    3: #include <fcntl.h>
    4: #include <sys/types.h>
    5: #include <sys/uio.h>
    6: #include <unistd.h>
    7: #include <stdio.h>
    8: #include <string.h>
    9: #include <errno.h>
   10: 
   11: int main(int argc, char *argv[]){
   12:     int fd;       /* ファイルディスクリプタ */
   13: 
   14:     /* 引数が指定されていなかったらエラー */
   15:     if ( argc != 2 ){
   16:         char err_message[] = "ファイル名を指定して下さい。\n";
   17:         write(2, err_message, strlen(err_message));
   18:         return 1;
   19:     }
   20: 
   21:     /* ファイルをオープン。オープンできなかったらエラー */
   22:     fd = open(argv[1], O_RDONLY);
   23:     if ( fd < 0 ){
   24:         char err_message[] = "ファイルをオープンできません。";
   25:         write(2, err_message, strlen(err_message));
   26:         write(2, strerror(errno), strlen(strerror(errno)));
   27:         write(2, "\n", 1);
   28:         return 1;
   29:     } else {
   30:         char message[256];
   31:         sprintf(message,
   32:                 "ファイル %s をオープンしました。ファイルディスクリプタは %d です。\n",
   33:                 argv[1], fd);
   34:         write(1, message, strlen(message));
   35:     }
   36: 
   37:     /* 256 バイト単位でファイルから読み込み、標準出力に書き出す */
   38:     while (1){
   39:         char buf[256];
   40:         int read_size;
   41: 
   42:         read_size = read(fd, buf, sizeof(buf));
   43: 
   44:         if ( read_size > 0 ){
   45:             write(1, buf, read_size);
   46:         } else if ( read_size == 0 ){
   47:             break;
   48:         } else {
   49:             char err_message[] = "read(2) でエラーが発生しました。";
   50:             write(2, err_message, strlen(err_message));
   51:             write(2, strerror(errno), strlen(strerror(errno)));
   52:             write(2, "\n", 1);
   53:             return 1;
   54:         }
   55:     }
   56:     close(fd);
   57:     return 0;
   58: }

解説

最初から見ていきましょう。 まずは、引数でファイル名が指定されなかった場合のエラー処理です。
   15:     if ( argc != 2 ){
   16:         char err_message[] = "ファイル名を指定して下さい。\n";
   17:         write(2, err_message, strlen(err_message));
   18:         return 1;
   19:     }
ファイルディスクリプタ 2番は標準エラー出力を指しますので、 この write(2) は標準エラー出力宛に出力します。 printf(3) などの標準入出力ライブラリは、 文字列の中で文字列の終端を意味する '\0' を見付けるとそこで処理をやめてくれるので、 いちいち文字数を指定する必要はありません。 しかし write(2) は何文字出力するのかをプログラマが管理しなくてはいけません。 指定した文字数を出力し終わるまでは、途中で '\0' があっても処理を続けます。

上記の部分を標準入出力ライブラリで書き直すと

if ( argc != 2 ){
    fprintf(stderr, "ファイル名を指定して下さい。\n");
    return 1;
}
に相当します。
次にファイルのオープンです。
   22:     fd = open(argv[1], O_RDONLY);
   23:     if ( fd < 0 ){
   24:         char err_message[] = "ファイルをオープンできません。";
   25:         write(2, err_message, strlen(err_message));
   26:         write(2, strerror(errno), strlen(strerror(errno)));
   27:         write(2, "\n", 1);
   28:         return 1;
open(2) の第一引数はファイル名、第二引数はフラグです。 フラグのうち基本的なものを以下に示します。
  • O_RDONLY 読み込み専用でオープン
  • O_WRONLY 書き込み専用でオープン
  • O_RDWR 読み込みと書き込み用にオープン
  • O_APPEND 書き込みのたびに末尾に追加する
  • O_CREAT ファイルが存在しない場合、新たなファイルを作成する
  • O_TRUNC サイズを 0 バイトに切り捨てる
ここでは読み込み専用のフラグしか使っていませんが、
fd = open(argv[1], O_WRONLY | O_APPEND | O_CREAT);
などと論理 OR を取ることで複数のフラグを指定することもできます。

open(2) はオープンに成功するとファイルディスクリプタ (整数値) を返しますが、失敗すると -1 が返されます。このとき errno を参照することでエラーになった原因がわかります。 errno には

  • No such file or directory なら 3
  • Permission denied なら 13
といった値が入ります。しかし、これではわかりづらいため、 errno の値を strerror(3) というライブラリ関数に渡すと
  • No such file or directory
  • Permission denied
といった文字列に変換してくれます。このサンプルソースではその文字列を出力しているので、
% file-open hoge.txt
ファイルをオープンできません。No such file or directory
などと出力されます。日本語ロケールを使用している場合は、
% file-open hoge.txt
ファイルをオープンできません。ファイルまたはディレクトリがありません。
などと日本語で表示される場合があるかもしれません。

なお、標準入出力ライブラリで書きなおすと

FILE *fp;
fp = fopen(argv[1], "r");
if ( fp == NULL ){
    fprintf(stderr, "ファイルをオープンできません。%s\n", strerror(errno));
    return 1;
}
となります。
   29:     } else {
   30:         char message[256];
   31:         sprintf(message,
   32:                 "ファイル %s をオープンしました。ファイルディスクリプタは %d です。\n",
   33:                 argv[1], fd);
   34:         write(1, message, strlen(message));
   35:     }
これは処理内容とは全く関係なく、ただのデバッグ情報です。 ここではファイルディスクリプタが何番かを表示しています。 使用中のディスクリプタは 0〜2 (それぞれ stdinstdoutstderr) なので fd の値は 3 になる OS が多いと思われます。
   38:     while (1){
   39:         char buf[256];
   40:         int read_size;
   41: 
   42:         read_size = read(fd, buf, sizeof(buf));
   43: 
   44:         if ( read_size > 0 ){
   45:             write(1, buf, read_size);
   46:         } else if ( read_size == 0 ){
   47:             break;
   48:         } else {
   49:             char err_message[] = "read(2) でエラーが発生しました。";
   50:             write(2, err_message, strlen(err_message));
   51:             write(2, strerror(errno), strlen(strerror(errno)));
   52:             write(2, "\n", 1);
   53:             return 1;
   54:         }
   55:     }
read(2) で、ファイルディスクリプタ fd から sizeof(buf) の長さだけ読み込み、buf に格納します。 つまり、オープンしたファイルから 256 バイトを読み込み、buf に格納するわけです。 read(2) は実際に読み込んだバイト数を返します。

そして、buf から read_size バイトだけ ファイルディスクリプタ1番 (stdout) に出力します。 もし read_size が 0 なら、 読み込むべきデータがなかった (EOF) ということなので終了します。

もし指定したファイルの長さが 600 バイトなら、

  • 1回目のループ: read_size == 256
  • 2回目のループ: read_size == 256
  • 3回目のループ: read_size == 88
  • 4回目のループ: read_size == 0
となります。

また、read(2) でエラーが発生した場合は -1 を返しますので、 その場合は errno に相当する文字列を strerror(3) で取得し、 表示します。


   56:     close(fd);
   57:     return 0;
   58: }
最後に close(2) でファイルをクローズして終了です。 close のエラー処理は省略しています。
前へ << DNS クライアントを作ってみよう (3) C 言語で HTTP クライアントを作ってみよう (1) >> 次へ

ご意見・ご指摘は Twitter: @68user までお願いします。