4.1 ファイルの作成と読み書き

Menu Menu

file-copy.c と stdio-thru.c は、いずれもファイルのコピーを行うプログラムである。それぞれの使い方を、以下に示す。

   % make file-copy
   % ./file-cpoy 既存のファイル 新しいファイル
   %
   % make stdio-thru
   % ./stdio-thru < 既存のファイル > 新しいファイル
   %

このように、file-copy.c は、引数で指定されたファイルを開き、その内容をコピーするプログラムである。stdio-thru.c は標準入力で指定されたファイルの内容を標準出力で指定されたファイルへコピーするプログラムである。

以下にfile-copy.c のプログラムを示す。

    /*
            file-copy.c -- ファイルをコピーする簡単なプログラム
            $Header: /home/h1/yas/slab-info1-os/3-file/RCS/file-copy.c,v 1.2 1995/03/08 13:57:05 yas Exp $
            Start: 1995/03/04 16:40:24
    */
    #include <stdio.h>  /* stderr */
    #include <fcntl.h>  /* open(2) */

fprintf()関数、stderr変数を使うためには、stdio.h というヘッダ・ファイルを読み込まなければならない。open()システム・コールを使うためには、fcntl.h を読み込まなければならない。必要なヘッダ・ファイルは、マニュアルを引けば出ている。

        main( argc,argv )
            int argc ;
            char *argv[] ;
        {
                if( argc != 3 )
                {
                    fprintf( stderr,"Usage: %s from to\n", argv[0] );
                    exit( 1 );
                }
                file_copy( argv[1],argv[2] );
        }

main() は引数のチェックをしてfile_copy()という関数を呼ぶ。

        #define BUFFSIZE        1024

あとで変更することを考えて、バッファの大きさをマクロを使って定義する。これは、stdio.h に定義されている BUFSIZ というマクロを使った方が良い。

    file_copy( from_name,to_name )
        char *from_name, *to_name ;
    {
        int from_fd,to_fd ;
        char buff[BUFFSIZE] ;
        int rcount ;
    /*
     * O_RDONLY: 読み込み専用でファイルを開く
     */
            from_fd = open( from_name,O_RDONLY );

ここでは、入力用のファイルを開いている。open()は、フィアルを開くシステム・コールである。UNIXではファイルを読み書きするためには、まずファイルを開かなければならない。O_RDONLYとは読み込み専用でファイルを開くことを意味している。open()システム・コールは、結果としてファイル記述子を返す。ファイル記述子は、負でない小さい整数(1-127の範囲)である。ファイル記述子は、read()システム・コールやwrite()システム・コールで実際にファイルを読み書きする時に使われる。

            if( from_fd == -1 ) /* エラーが起きたとき */
            {
                perror( from_name );/* ファイル名とエラーメッセージを表示し、 */
                exit( 1 );              /* プロセスを終了する。 */
            }

エラーが起きた時には、open()システム・コールは、-1 を返す。この時、エラーの番号がerrnoという変数に格納される。perror()は、errno変数を解析して、より詳しいエラー・メッセージを表示する関数である。(Solaris では日本語、BSD/OSでは英語で表示される) perror()の引数は、エラーメッセージともに表示される文字列である。

    /* O_WRONLY: 書き込み専用でファイルを開く。
     * O_CREAT:  ファイルが存在しなければ作られる。
     * O_TRUNC:  ファイルが存在している時には、その大きさを0にする。
     * 0666:     ファイルを作る時のモード。実際には、umask の分落とされる。
     */
            to_fd = open( to_name,O_WRONLY|O_CREAT|O_TRUNC,0666 );
            if( to_fd == -1 )
            {
                perror( to_name );
                exit( 1 );
            }

ここでは出力用のファイルを開いている。O_WRONLYは、書き込み専用でファイルを開くことを意味している。O_CREATは、ファイルが存在しなければ作るように指示するものである。ファイルが存在する場合、上書きされる。O_TRUNCは、ファイルが存在している時には、その大きさを0にすることを指示するものである。0666 (C言語で0から始まる数は、8進数)は、ファイルを作る時のモードである。この数値にしたがって、ファイルのモード(ls -l でrwxrwxrwx と表示される部分)が決定される。作成されるファイルのモードは、ここで指定されたモードから現在のumaskが落された値となる。

ファイル記述子は、小さな整数である。次のように、printf() の %d 書式で画面に表示される。次のように表示されるであろう。

    from_fd == 3, to_fd == 4

これより、以下のプログラムでは read(), write() の引数として直接 3, 4 と記述しても同じ結果になる。ただし、open(), close() を繰り返し行うと、この値が異なってくる。よって普通は、このように変数で指定しなければならない。

        printf("from_fd == %d, to_fd == %d\n", from_fd, to_fd );

read() システム・コールは、第1引数で指定されたファイル記述子のファイルを読み込み、それを第2引数の番地へ保存する。読み込むバイト数は、第3引数で与えられる。結果として読み込んだバイト数を返す。通常は、BUFFSIZE が返される。read() システム・コールの結果、ファイル上の読み書きする位置が、実際に読み込んだバイト数だけずれる。ファイルの末尾近くや、ファイルが端末の時、BUFFSIZE 以下の値が返される。ファイルの末尾に行き着くと 0 が返される。エラーが起きると、-1 が返される。

write() システム・コールは、第1引数で指定されたファイル記述子のファイルへデータを書き込む。書き込まれるデータは、第2引数で与えられた番地から、第3引数で与えらた大きさである。write() システム・コールは、結果として書き込んだバイト数を返す。ファイル上の読み書きする位置が、実際に読み込んだバイト数だけずれる。通常は、BUFFSIZE が返される。空き容量不足などで書き込みが失敗した時には、-1 を返す。エラーが起きると、-1 が返される。

        while( (rcount=read(from_fd,buff,BUFFSIZE)) > 0 )
        {
            if( write(to_fd,buff,rcount)== -1 )
            {
                 perror( to_name );
                 exit( 1 );
            }
        }
        close( from_fd );       /* ファイルを閉じる。*/
        close( to_fd );         /* ファイルを閉じる。*/
    }

close() システム・コールを実行してしまうと、もうそのファイル記述子は無効である。ここで、read() や write() を行うと、エラーが返される。


標準入出力を使ったコピー

stdio-thru.c は、標準入力で指定された ファイルの内容を標準出力で指定されたファイルへコピーするプログラムである。

    /*
            stdio-thru.c -- 標準入力から標準出力へのコピー
            $Header: /home/h1/yas/slab-info1-os/3-file/RCS/stdio-thru.c,v 1.2 1995/03/08 13:58:59 yas Exp $
            Start: 1995/03/04 16:40:24
    */
    #include <stdio.h>  /* stderr */
    main( argc,argv )
        int argc ;
        char *argv[] ;
    {
            if( argc != 1 )
            {
                fprintf( stderr,"Usage: %s\n", argv[0] );
                exit( 1 );
            }
            stdio_thru();
    }

このプログラムは、引数を取らない。"<"や">"は、シェルにより解釈され、このプログラムには、既に開いたファイルのファイル記述子(0番と1番)として渡される。

    #define     BUFFSIZE        1024
    stdio_thru()
    {
        char buff[BUFFSIZE] ;
        int rcount ;

file_copy() とは違って、stdio_thru() では、ファイルを開く操作(open())を行うことなく、入出力(read(),write)を行っている。このような事が可能な理由は、UNIXでは、ファイル記述子 0, 1, 2 は、シェルにより開かれているからである。

            while( (rcount=read(0,buff,BUFFSIZE)) > 0 )
            {
                if( write(1,buff,rcount)== -1 )
                {
                     perror("stdout");
                     exit( 1 );
                }
            }
            close( 0 ); /* ファイルを閉じる。*/
            close( 1 ); /* ファイルを閉じる。*/
    }

次のように file-copy と stdio-thru をコンパイルし、実行してみなさい。

        % make file-copy
        % ./file-copy 既存のファイル 新しいファイル
        %
        % make stdio-thru
        % ./stdio-thru < 既存のファイル > 新しいファイル

stdio-thru を使ってファイルをコピーするためには、上で示したようにシェルの標準入出力切り替え機能を使わなければならない。


課題1 tee プログラム

file-copy.c, stdio-thru.c の 二つのプログラムを参考にして、UNIXの tee プログラムと同じ機能を持つプログラムを作りなさい。プログラムの名前はteeではなくmytee としなさい。その骨組みを mytee.c に示す。

    /*
            mytee.c -- tee コマンドと似た動きをするコマンド
            $Header: /home/h1/yas/slab-info1-os/4-file/RCS/mytee.c,v 1.2 1995/05/31 11:43:32 yas Exp $
            Start: 1995/03/08 22:54:31
    */
    #include <stdio.h>  /* stderr */
    #include <fcntl.h>  /* open(2) */
    main( argc,argv )
        int argc ;
        char *argv[] ;
    {
            if( argc != 2 )
            {
                fprintf( stderr,"Usage: %s filename\n", argv[0] );
                exit( 1 );
            }
            mytee( argv[1] );
    }
    mytee( filename )
        char *filename ;
    {
    /*
    課題 4.1 は、この部分を完成させることである。file-copy.c の 
    file_copy() と stdio-thru.c の stdio_thru() を参考にしなさい。
    */
    }

mytee は、次のようにして、標準入力を標準出力にコピーしならがら、同時にファイルにも保存するものである。

        % grep pattern file1 | ./mytee rresult

この結果として、画面には、次のように grep コマンドを実行した時と同じ結果が表示される。

        % grep pattern file1

同時に、ファイル result には、画面に表示された結果とまったく同じものが保存される。

tee コマンドの名前は、アルファベットの「T」に由来する。図形的に考えれば動きを理解することができる。tee コマンドは、左(パイプラインで標準入力) から入ったデータをした(引数で与えられたファイル)に保存しながら右に(標準出力)にも出力する。

       入力   ----------  出力
                  |
                  |
                ファイル

tee コマンドは、時間のかかる処理の結果や、後で参照したい中間結果をファイルに保存するために利用される。次の例は、makeコマンドの実行けっkをmake.out に保存すると同時に、それをuserにメールで送るものである。

        % make |& tee make.out | Mail -s 'make result' user

これにより、userは、メールが届いたことで、make コマンドの実行終了を知ることができる。同時に、作業していたディレクトリでその結果を参照することができる。

もう一つの方法として、ファイルを経由してtailを使って出力を二つに分けることができる。例えば、

        % make >& make.out &
        % tail -f make.out

とすると、make.out を書き込みながら同時にmake.outの内容を標準出力に書き出すことができる。tail の -f オプションは、定期的にファイルの最後をチェックし、最後が拡張されていたら、その部分を書き出す。BSD/OSDで、
        % tail -f /var/log/maillog

としながら、そこでメールを送って見よう。

この二つの方法の利点と欠点について考察して見よ。


課題 2 cat コマンド

UNIXの cat コマンドと同様の働きをするプログラムを作りなさい。このプログラムの名前をmycatとする。引数として、何も与えられなかった時には、標準入力を標準出力にコピーしなさい。また、引数として - が与えられた時には、その位置で標準入力をコピーしなさい。例えば、次のような場合、file1 と file2 の内容の間に、標準入力の内容をコピーしなさい。

tail -f に相当する機能は、fseek やusleepを使って作ることができるが、この時点ではかなり難しい。


Shinji KONO / Fri May 1 00:39:29 1998