ファイル操作とファイル・システム

Menu Menu


目的

この実験では、Unixのシステム・コールを通じて、ファイル・システムの利用について学ぶ。具体的には、ファイルの作成、書き込み、読み込み、開く閉じる操作および、ディレクトリの読み込み操作について実習を行う。同時に、標準入出力の概念を学ぶ。


関連科目

情報204 オペレーティングシステム 必修、2単位


C言語

Cのプログラムは、ANSI-C で統一すること。(例題ではわざと K&R C で記述してあるので、正しく ANSI-C に修正する)

   int
   main( int argc, char *argv[] )
   {
      ....
   }

Windows でもMac OS X, Linux でも gcc を使うことによりコンパイラの違いを気にすることなくプログラムを作成することができる。しかし、どのような ccでもコンパイルできるよう可搬性(portablity)の高い記述が望ましい。しかし、OSの異なる場合も含めてportabilityを確保することは一般的には難しい。

gcc の最新版で、-O2 -Wall で エラー及び warning が出ないようにすること。(例題では、warining が出るのでそれを修正する)


報告書

それぞれの実験に付いて、

    作成したプログラムへのポインタ 
        学科のCVS に登録し、repositoryに登録する。repsitory名を乗せること。
	他の人がcheckoutして読めるようにすること。
    その説明(説明に必要なソースのコピーは最小限にすること)
	(Makefileについても説明すること)

および、

    その実行結果を付けなさい。

この実験では、プログラムの説明では、フローチャートを付加する必要はない。開発環境と実行環境 (計算機、オペレーティング・システムのバージョン、コンパイラ)を載せなさい。それぞれの実験について、プログラム作成に要した時間を書きなさい。


一般的な注意

報告書は、日本語または英語で記述すること。プログラム、表、図、数式の羅列は、報告書とは認めない。図は必ず本文から参照すること。数学における証明のように、示すべき結論、用いる仮定と前提、推論の詳細について論理的に記述しなさい。

メールはHTMLで送らない。ソースのencodingは、UTF-8 を使う。


source の場所

ソース

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

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: /net/home/cvs/teacher/kono/os/ex/file/file.ind,v 1.1.1.1 2007/12/18 03:39:43 kono 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()という関数を呼ぶ。

バッファの大きさは、stdio.h に定義されている BUFSIZ というマクロを使う。BUFSIZの大きさがいくつか調べよ。

    file_copy( from_name,to_name )
        char *from_name, *to_name ;
    {
        int from_fd,to_fd ;
        char buff[BUFSIZ] ;
        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変数を解析して、より詳しいエラー・メッセージを表示する関数である。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引数で与えられる。結果として読み込んだバイト数を返す。通常は、BUFSIZ が返される。read() システム・コールの結果、ファイル上の読み書きする位置が、実際に読み込んだバイト数だけずれる。ファイルの末尾近くや、ファイルが端末の時、BUFSIZ 以下の値が返される。ファイルの末尾に行き着くと 0 が返される。エラーが起きると、-1 が返される。

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

        while( (rcount=read(from_fd,buff,BUFSIZ)) > 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: /net/home/cvs/teacher/kono/os/ex/file/file.ind,v 1.1.1.1 2007/12/18 03:39:43 kono 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番)として渡される。

    stdio_thru()
    {
        char buff[BUFSIZ] ;
        int rcount ;

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

            while( (rcount=read(0,buff,BUFSIZ)) > 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: /net/home/cvs/teacher/kono/os/ex/file/file.ind,v 1.1.1.1 2007/12/18 03:39:43 kono 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 ;
    {
    /*
    課題 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コマンドの実行結果を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 オプションは、定期的にファイルの最後をチェックし、最後が拡張されていたら、その部分を書き出す。いろんなUnixで、
        % tail -f /var/log/maillog

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

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


課題 3 ls -l コマンドの仕組み

stat()システム・コールを用いて、ls -l filename と似たような結果を出力するプログラムを作りなさい。このプログラムの骨格をmyls-l.c に示す。

    /*
            myls-l.c -- ls -l filename とにた動きをするプログラム
            $Header: /net/home/cvs/teacher/kono/os/ex/file/file.ind,v 1.1.1.1 2007/12/18 03:39:43 kono Exp $
            Start: 1995/03/07 20:59:12
    */
    #include <sys/types.h>
    #include <sys/stat.h>
    #include <stdio.h>
    #include <pwd.h>
    main( argc,argv )
        int argc ;
        char *argv[] ;
    {
            if( argc != 2 ) {
                fprintf( stderr,"Usage:%% %s filename \n",argv[0] );
                exit( 1 );
            }
            ls_l( argv[1] );    /* 引数はファイル */
    }
    ls_l( path )
        char *path ;
    {
    /*
    実験4.2の課題は、この関数を完成させることである。下請け関数として、以
    下で定義されている uid_print() を使ってもよい。stat.c を参考にして、ファ
    イルの属性を取りだし、そのうちのいくつかを表示しなさい。
    */
    }
    /*
    struct stat の st_uid フィールドには、uid が入っている。これを数値とし
    てではなく、ログイン名として表示するためには、次の関数 uid_print() を
    使うとよい。引数は、uid_t 型(unsigned short)である。
    */
    uid_print( uid )
        uid_t uid ; /* unsigned short, in <sys/types.h > */
    {
        struct passwd *pwd ;
            pwd = getpwuid( uid );
            if( pwd == NULL ) {
                printf("%d ",uid );
            } else {
                printf("%s ", pwd->pw_name );
	    }
    }

ls -l では、3つの時刻のうち、どの時刻が表示されているかを調べなさい。また、他の二つの時刻を表示させる方法を調べなさない。また、その時刻を変更する方法について考察し実際に変更して見なさい。

myls-l の表示形式は、ls -l と完全に一致しなくてもよい。例えば、時刻の表示は、上のstat.c と同じでも良い。localtime(), strftime() ライブラリ関数を利用すると、時刻の表示をより簡単に、ls -l の表示に近づけることができる。

プログラムの引数となるファイルの数は一つとする。複数のファイルについてls -l と同様の表示をするように拡張しても良い。

引数とsてディレクトリの名前が与えられた場合にも、ディレクトリの内容ではなくディレクトリ自身の属性を表示する。シンボリックリンクには対応しなくて良い。(これは、ls -ldL の動作と似ている)


課題3 ディレクトリの検索

dir-list.c は、ディレクトリの内容を表示させるプログラムである。

    /*
	    dir-list.c -- ディレクトリの内容を表示するプログラム
	    $Header: /net/home/cvs/teacher/kono/os/ex/file/file.ind,v 1.1.1.1 2007/12/18 03:39:43 kono Exp $
	    Start: 1995/03/07 21:44:51
    */
    #include <stdio.h>	/* stderr */
    #include <fcntl.h>	/* open(2) */
    #include <sys/types.h>	/* getdirentries(2) */
    #include <sys/dirent.h>	/* getdirentries(2) */

open(), getdirentries()システム・コールを使うためのヘッダ・ファイルを読み込む。

getdirentries() は古いので、opendir() を使って書き換えて下さい。

    extern char *malloc();
    main( argc,argv )
	int argc ;
	char *argv[] ;
    {
	    if( argc != 2 ) {
		fprintf( stderr,"Usage:%% %s dirname \n",argv[0] );
		exit( 1 );
	    }
	    dir_list( argv[1] );
    }

このプログラムは、引数として、ディレクトリ名一つを取る。エラーメッセージできるだけ丁寧に出すようにしよう。それがデバッグを用意にすることが多い。

    dir_list( dirname )
	char *dirname ;
    {
	int fd ;
	struct dirent *p ;
	char *buff ;
	int rcount ;
	long pointer;
     * O_RDONLY: 読み込み専用でディレクトリ・ファイルを開く
	    fd = open( dirname,O_RDONLY );
	    if( fd == -1 )	/* エラーが起きたとき */
	    {
		perror( dirname );	/* ファイル名とエラーメッセージを表示し、 */
		exit( 1 );		/* プロセスを終了する。 */
	    }

ここでは、malloc() を用いて BUFSIZ 分のメモリを確保する。

	    buff = malloc( BUFSIZ );
	    if( buff == 0 )
	    {
		perror("memory");
		exit( 1 );
	    }

getdirentries() システム・コールは、ファイルに対する read() システム・コールとよく似ている。第1引数で指定されたファイル記述子のディレクトリ・ファイルの内容を読み込み、それを第2引数の番地へ保存する。読み込むバイト数は、第3引数で与えられる。getdirentries() は、結果として読み込んだバイト数を返す。通常は、BUFSIZ が返される。ファイルの末尾やファイルが端末の時、BUFSIZ 以下の値が返される。ファイルの末尾に行き着くと 0 が返される。最後の引数は、次のentryのfile pointerを書き込む場所である。
 getdirentries(int fd, char *buf, int nbytes, long *basep);

man によると、

    DESCRIPTION
     Getdirentries() reads directory entries from the directory referenced by
     the file descriptor fd into the buffer pointed to by buf, in a filesystem
     independent format.  Up to nbytes of data will be transferred.  Nbytes
     must be greater than or equal to the block size associated with the file,
     see stat(2).  Some filesystems may not support getdirentries() with
     buffers smaller than this size.

とある。nbytes がBUFSIZ よりも小さい場合には何が起きるかも知れないと書いてあるか説明せよ。

	    while((rcount=getdirentries(fd, buff, BUFSIZ, &pointer))>0) 
                          ^^^^^^^^^^^^^^   これは時代遅れ。opendir に直す。
	    {
		for( p = (struct dirent *)buff ; (char *)p < &buff[rcount] ;
		     p=(struct dirent *) ((int)p+(p->d_reclen)) )
		{
		    printf("p:%d,\t", (char *)p - buff );
		    /*  printf("off:%d, ", p->d_off ); */
		    printf("fileno:%d,\t", p->d_fileno ); 
		    printf("reclen:%d,\t", p->d_reclen );
		    printf("namlen:%d,\t", p->d_namlen );
		    printf("name:%s\n", p->d_name );
		}
		printf("base:%ld\n", pointer);
	    }

ここでは、次のような内容を画面に表示している。

    p:            ポインタ p がバッファの先頭からどれだけずれているか
    filneno:      i-ノード番号。stat()システム・コールのinoと同じ。
    reclen:       レコード長。このディレクトリ・エントリの長さ
                  次のディレクトリ・エントリがどこにあるかを計算する時に
                  使われる。
    namelen:      次のnameに含まれているファイル名の長さ
    name:         ファイル名の本体。文字列として最後に'\0'がある。

最後に、ディレクトリ・ファイルを閉じ、確保したメモリを解放している。

	    close( fd );	/* ファイルを閉じる。*/
	    free( buff );
    }

次のように dir-list をコンパイルし、実行して見なさい。余裕があれば、BSD/OS とSolaris の両方でプログラムを動かして見よ、

    % make dir-list
    % ./dir-list /usr
    %

name: の後に、指定されたディレクトリ(上の例では/usr)に登録されているファイル名が現れていることがわかる。それ以外の意味は、上で説明したようなディレクトリファイルの内容が表示されていることがわかる。

ここでdirentは、/usr/include/sys/dirent.h に次のように定義されている構造体である。

    struct dirent {
	u_int32_t d_fileno;		/* file number of entry */
	u_int16_t d_reclen;		/* length of this record */
	u_int8_t  d_type; 		/* file type, see below */
	u_int8_t  d_namlen;		/* length of string in d_name */
    #ifdef _POSIX_SOURCE
	char	d_name[255 + 1];	/* name must be no longer than this */
    #else
    #define	MAXNAMLEN	255
	char	d_name[MAXNAMLEN + 1];	/* name must be no longer than this */
    #endif
    };

d_name[]の大きさが255+1であることにより、ファイル名の長さが255文字であることがわかる。+1 バイトは、文字列の終端を表す'\0'のためのものである。しかしながら、実際のファイル名は短いものが多い。よって、すべてのディレクトリエントリについて255文字分の領域を割り当てることは無駄が多い。よって実際のディレクトリでは、d_name[]として255文字分の領域が使われているのではなく、必要最小限の領域しか割り当てられない。d_name[]は、次のようなものが並んでいる。
       名前の文字列、d_namelen バイト
       文字列の終端を表す1バイト
       4バイト境界に合わせるための埋め文字(padding), 0-3バイト

埋め文字、または、終端を表す'\0'の次には、次のディレクトリ・エントリが続いている。よって、p++ ではなく、プログラムにあるように、
		 p=(struct dirent *) ((int)p+(p->d_reclen)) 

とすることで、次のディレクトリ・エントリを得ることができる。

dirent.h の場所と中身はシステム依存である。中身を確認して、ディレクトリの中のファイル名の最大長を調べよ。


課題4 正規表現を用いたディレクトリの内容の検索

与えられたディレクトリの中から与えられた名前のファイルを検索し、そのiノード番号を表示するプログラムを作りなさい。そのプログラムの名前をdir-lookup とする。dir-lookup の実行例を示す。

       % ./dir-lookup '?*.html'
       49614 local.html

ここで 49614 はiノード番号である。''が使ってあるのは、?*などをshell escape から保護するためである。

dir-lookup コマンドの骨格部分を dir-lookup.c に 含める。

     regcomp, regexec, regerror, regfree -- regular-expression library

というC libaryを用いて、regular expression を受け付ける検索を可能にしたい。

このプログラムを実装し、shell のファイル名の展開と、regcomp による展開の違いを示す例を示せ。


課題6 Binary Data の読み書き

read()/write()を用いて、Binary Data の交換をしたい。

構造体を使って、今日の日付と時間を書き出そう。

     time_t time(time_t *tloc);
     struct tm * gmtime(const time_t *clock);

を使用して、tm 構造体にデータを取り出し、これをBinaryのまま、ファイルに書き出す。例えば、
    write(fd, (char*)tmptr, sizeof(struct tm));

とする。書き出したデータを読み出すプログラムを作成せよ。

ntohl() や、htonl() を使って、他のシステムとの互換性計りたい。特に、Java で書いたプログラムと互換性があるように、プログラムを書き、Intel 及び、PowerPC (iBook G4 や Xserve )、Mac OS X, Linux (可能ならばWindows)でテストせよ。


Shinji KONO / Thu Oct 21 18:17:37 2010