-title: ファイル操作とファイル・システム --目的 この実験では、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 /* stderr */ #include /* 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 /* 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 /* stderr */ #include /* 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 #include #include #include 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 */ { 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 /* stderr */ #include /* open(2) */ #include /* getdirentries(2) */ #include /* 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)でテストせよ。