from DOS to Linux(5)
ファイル処理(2)
はじめに
今年初めて「オンラインソフト大賞」の審査員をさせてもらった。何でも、「Linux界からも」ということらしい。これはMacやWindowsと同じ土俵でLinux が語られるようになったということで、非常に喜ばしい。
ところが、さて何を出そうかと言う段になって困ってしまった。パッチの類は多数あって、それらにお世話になっていることは確かなのであるが、それらは推薦出来ないことになっている。ところが、オリジナルはそれ程多くもない。
実はこの辺の状況は他のOSでも同じらしく、全体の小粒なソフトばかりであって、結局「大賞」は不在ということになった。何もこういった賞を目標に頑張れと言おうとは思わないが、やはりこの状況は寂しいので、ぜひ頑張って欲しい。私も今年は大きなフリーソフトを書きたいなと思っているところだ。
ファイル操作の基本
ファイル操作あたりの話は、前回も書いたのであるが、今回は主に標準入出力ではないファイルの操作についての話、つまり普通のファイルについての話をしようと思う。
いきなりの否定的な話になるが、ファイル操作を移植性を少しでも考慮したり、簡単に操作したいと思うのなら、いわゆるシステムコールを使うのではなく、ライブラリ関数を使うべきである。具体的に言えば、open, close, read,writeの類を使うのではなく、fopen, fclose, fread, fwriteの類を使うということである。ごく普通のファイル操作に限って言うなら、システムコールでもライブラリ関数でも、出来ることに大きな違いがあるわけではないし、結果も同じである。ライブラリ関数も結局はシステムコールになるわけであるから、この辺の事情は当然のことである。
それではなぜシステムコールを使うことを勧めず、ライブラリ関数を使えと言うかと言えば、「ライブラリ関数はOSを越えた標準であり、システムールはせいぜいPOSIXの範囲の中だけの標準である」という移植上の問題と、「ライブラリ関数は便利に作ってあるが、システムコールは生でOSを触っている」という使い勝手上の問題である。
これらのことから考えると、おのずと使い分けもわかって来ると思う。つまり、「ある程度抽象化されたファイルを操作したければ、ライブラリ関数を使う」ということであるし、「エグいことしたければ、システムコールの方が細いことが出来る」ということである。これはプログラム言語を何を使うかということと似ている。つまり、ライブラリ関数でファイル操作をするということは、「高級言語的」であるし、システムコールでファイル操作をするということは、「アセンブラ的」だということである。確かにアセンブラは何でも出来るが、普通のプログラムでは使いたくない。同じようなことがシステムコールにも言える。いわゆる「業務プログラム」的なものは、ライブラリ関数を使うべきであろうし、細かなファイル操作を要するものは、システムコールを使う方が楽である。
と言うことで、本稿ではシステムコールの話を中心にしたいと思うが、ライブラリ関数の中にもちょっと気をつけるものがあるので、その辺をちょっと紹介しよう。
fflush
fflushは「ファイルバッファの中身を吐き出す」処理をするものである。通常このような動作は、ファイルをcloseする時にのみ行うが、それ以外の時に行うために使われる。
例えば「デバッグの時にメッセージを出す」といった、いわゆる「printfデバッグ」をすることを考えよう。この時、デバッグ用のprintfの直後でプログラムが異常終了してしまった場合、「メッセージがどこにも出力されずに終わる」ということが起きることがある。そのような時にprintfの直後にfflushを入れておけば、そのメッセージは正しく出力される。
あるいは、B treeの実装のように、「インデクス領域から実体へのポインタを持っている」といった構造を持ったファイルの場合、インデクスが破壊されると、ファイルが参照出来なくなる。そういった時に「念のためにポインタはファイルに書き込んでから」といったロジックにすることがよくあるが、そのような時にfflushを呼ばないでいると、「ファイルを更新したつもりだけど、実際にはバッファ内の更新だけだった」ということが起こり、プログラムが異常終了した時の用心のためにせっかく書き込み処理を書いてもムダになってしまう。そうならないためにも、fflushを呼び出して、明示的にバッファの吐き出しをしておくと良い。
ただ、ここで注意しなくてはならないことは、fflushとは「バッファの内容を吐き出す」だけであって、実際のファイルの更新が行われるかどうかは、また別の問題であるということである。これは、FILE構造体を使う関数はプロセス内に独自のバッファを用意していて、全てのFILE構造体を操作する関数は、このバッファに対する操作を行うだけであり、fflushもその例外ではないということである。
ところが、実際のファイルの更新までには、
- read, writeで使われるシステムコール用のバッファ
- ファイルの格納されているデバイス操作用のバッファ
- デバイス自身が持っているキャッシュ
等のバッファが存在している。だから、fflushしてもsyncされるか、カーネル内のバッファが満杯になるまでは、カーネル内のバッファに貯えられるだけである。だから、fflushしてから電源断のような事象が起きれば、当然その結果はディスク上のファイルには反映されていない。あくまでも「当該プロセスが異常終了」した時のための用心にのみ意味を持つ。
実際にfflushのソースを見ると、その中ではfsyncは呼び出していない。ちょっと考えると呼び出せば良さそうなものであるが、fsyncは想像を絶する重さを持ったシステムコールであるから(大量にファイルを操作した後のsyncコマンドにかかる時間を想像して欲しい)、fflushくらいの気軽さで呼ぶことは出来ないし、処理の趣旨から言って呼び出すことは適当ではない。そういったわけで、自動的にはfsyncは呼び出されないので、どうしても確実にファイルを更新させたい場合は、自分でfsyncを呼び出す必要がある。
fsyncはシステムコールであるから、FILE*を引数で使うことは出来ない。そのような時には、filenoを呼び出してやると、fsyncの引数に使うべきファイル デスクリプタ(file descriptor)を求めることが出来る。
このファイルデスクリプタは、read, write等の操作でも使うことが出来るものである。しかし、FILE構造体の中には、fread, fwrite等で使うための情報を持っているため、勝手にread, write等を行うと、その情報と矛盾を起こすので、そのようなことは絶対にしてはならない。また、ライブラリのソースコードを読めば、「こういった操作は大丈夫」といったことがわかると思うが、このようなことを行うとポータブルでなくなるので、やるには注意が必要である。
なお、多くの場合、printfでは`n'を含んだ文字列をttyに出力した場合は、自動的にfflushされるようである。また、exitで終了する場合は、ファイルは正しくcloseされるという仕様になっているので、一々fflushをする必要はない。もちろん正しくfcloseされた場合も、バッファの内容は正しく出力されているはずである。
fpurge
fflushと同じようなバッファ操作の関数でfpurgeというのもある。これはman page上もfflushと同じところに書いてある。
動作は逆で「バッファを無効にする」ためのものである。つまり、何らかの事情で、freadやfwriteの時に使っているバッファを「なかったこと」にするために使う。具体的には、コンソールから読み込んだものを1行単位で捨てる時に使う(cf. Elkのread.c)。もっとも、このような場合は、自分で捨てるような処理をしても同じなので、かなり特殊な使い方をしている時でないと、実際には使わないであろう。
fread, fwrite
freadもfwriteも、ごく普通のファイル入出力関数であるから、特別に難しいことはない。使い方はMS-DOS等のものと全く同じであるから、特別に説明することもない。
ところで、freadやfwriteでバッファの大きさに関する引数は2つある。それは第2引数のブロックサイズと、第3引数のブロック数である。入出力バッファはその積のバイト数が必要になる。ところが多くの場合、バッファはある決まった大きさを用意しておき、それを単に入出力するだけというプログラムは多い。つまり、システムコールであるreadやwriteの第2引数のように、「バッファの大きさ」だけを指定して使いたくなる場面の方が、ブロック数やブロックサイズを意識して書く場合よりも、ずっと多いはずである。
このような時には、「果して、どっちに大きさを指定しようか」と考えてしまった経験はないだろうか? 実は私もこれはよく悩むことなので、今回ライブラリのソースを読んでみた。
結論としては、「UNIX上で使う場合は、どちらに指定しても結果は同じ」なようである。何しろfreadにしてもfwriteにしても、処理の先頭で第2引数と第3引数を掛け算して、その結果を内部処理に使っているくらいであるので、どっちに何を指定しても同じである。まぁ強いて言うなら、第2パラメータが除数となる割り算があるので、第2引数が1となる指定をすれば高速になる可能性がないではないが、違いはその計算だけなので、全体から見れば誤差の範囲に過ぎない。
蛇足ながら、freadやfwriteがなぜこのような仕様になっているかと言えば、これはメインフレームが使う「古典的」なファイル形式に対応するためである。このような場合にはブロックサイズとかブロック数が意味を持っていたのである。もっとも、現在のメインフレームは、UNIXやMS-DOSのような扱いの出来るファイル形式を使うことも増えたようである。
dirent
システムコールの話の前に、direntの話を簡単にしておこう。
direntというのは、「ディレクトリ操作のための関数群」である。これは、ディレクトリをファイルのように操作をするためのものである。
古来、このような処理は、ベタベタにシステム依存したものであったのだが、これもPOSIXの影響で標準化され、「準標準ライブラリ」的になって来た。
実際のプログラムで、direntを使うことはまずない。使う必要があると言えば、「ファイルの一覧に対して操作を行う」ようなプログラムの場合である。操作と言っても、dirent自体は基本的にディレクトリを読むことしか出来ないので、項目を削除(つまりファイル削除)したり、変更(つまりファイル名の変更)をしたりするためのは、それぞれファイル名を引き数とするファイル操作関数を呼び出す必要がある。
図にdirent関数の一覧を示す。名前を見ると、だいたい動作が想像出来るようなものばかりだし、あまり使うものでもないので、「ああ、こういった関数があるのだ」程度のことだけ知っておいて、必要な時にmanを読めば良いだろう。
| DIR | *opendir(DIR *); |
| int | closedir(DIR *); |
| struct dirent | *readdir(DIR *); |
| void | rewinddir(DIR *); |
| void | seekdir(DIR *, off_t); |
| off_t | telldir(DIR *); |
| int | scandir(const char *, struct dirent ***t, int (*)(const struct dirent *), int (*)(const struct dirent **, const struct dirent **)); |
| int | alphasort(const struct dirent **, const struct dirent **); |
| ssize_t | getdirentries(int, char *, size_t, off_t *); |
と言ってしまうと少し寂しいので、簡単なサンプルをリストに挙げておく。
と、ここまで書いて予定の枚数になってしまったので、システムコールでファイル操作をする話は次回に譲ろうと思う。
#include <stdio.h>
#include <dirent.h>
void
test(
char *name)
{
DIR *dirp;
struct dirent *entp;
dirp = opendir(name);
while ((entp = readdir(dirp)) != NULL)
printf("%stfile number %lun",
entp->d_name, (unsigned long int) entp->d_fileno);
closedir(dirp);
}
}
extern int
main(
int argc,
char **argv)
{
if ( argc == 1 ) {
test(".");
} else {
test(argv[1]);
}
}
コラム
IT100
会社の業務でthin clientとして使えるPCを組み立てることをいろいろ検討していたのであるが、ちょうどそんな折、日立ソフトからIT100という NC(Network Computer)が発表された。
スペックを表に挙げる。これを見ると、HDD等がないことを除けば、「一昔前の普通のPC」とほぼ同等であることがわかると思う。いわゆるthin clientとしては、十分なスペックである。これでメーカ希望小売り価格が本体で6万円だということなので、文字通りの「$500パソコン」である。
| 品名 | ILIOS IT100 |
| 型名 | K-NC00-02200 |
| CPU/クロック | AMD ElanSC400(486/100MHz相当) |
| 内部キャッシュ | 8KB |
| 主メモリ | EDO 16MB (70ns) |
| ビデオ解像度/最大色数 | 640x480/16M,800x600/64K,1024x768/256 |
| LAN | 10BASE-T仕様 |
| サウンド | Sound Blaster Pro互換 |
| 入出力ポート | |
| 外部ポート | シリアルポート(9ピン、DSUB)またはIrDA |
| オーディオポート | Line in/out(stereo), MIC in(mono) |
| LAN用コネクタ | 10BASE-T用RJ45コネクタ |
| 拡張スロット | PCMCIA TYPE II×2またはTYPEIII×1 |
| VGAポート | DSUB 15ピン |
| その他 | マウス用PS/2コネクタ、キーボード用PS/2コネクタ |
| 電源 | |
| 入力電圧 | 100V AC |
| 消費電力 | 35W |
| 寸 法 | 30(W)×200(D)×250(H)mm |
| 重 量 | 約1.8kg |
外部インターフェイス等を見ると、必要にして十分なものが使えることがわかると思う。普通に使っていて「出来ればあったら嬉しいな」と思うのは、パラレルポートくらいなものである。もっとも、通常ネットワーク環境のクライアントとして使う場合は、プリンタはネットワーク上のものを使うので、たいていはなければないで済むものである。
そして、このマシンはNCであるので、ネットワーク経由でブートアップする。しかも、そのOSはLinuxやFreeBSDが利用可能である(作った日立SKも、その辺を主力と考えているフシがある)。さらに、PCMCIAのデバイスからもブート可能だそうである。また、内部には2.5インチのHDDを入れることが可能になっており、そこからもブート可能である。もちろん、このHDDは通常のHDDとして利用することも可能であるから、スタンドアロンで使うことも出来なくはない。
日立SKよりサンプルが借用出来たので、会社でいろいろ試してみたのであるが、ちゃんとネットワークからブートアップ出来るし、ルートディレクトリもネットワーク上に持つことが出来るし、スワップもネットワーク上に持つことが出来る。PentiumPROとかを使い慣れてしまった身には、若干遅さを感じるのだが、「どうしようもなく遅い」ということはない。そんなことよりも、机の上を占有することなく、電源断のタイミングを心配することなく使えるLinuxマシンというメリットの方をずっと大きく感じる。
誰でも使えるというタイプのマシンではないし、マシンパワーも大したことはないものであるから、普通のPCの代用として使うということはちょっと難しい。しかし、そういった欠点が欠点として表面化しない用途に限定すれば、なかなか使えるマシンではある。