Linuxプログラミング入門

97年12月9日執筆

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もその例外ではないという ことである。

ところが、実際のファイルの更新までには、

等のバッファが存在している。だから、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を読めば良いだろう。

dirent関数一覧
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 *);

と言ってしまうと少し寂しいので、簡単なサンプルをリストに挙げておく。

と、ここまで書いて予定の枚数になってし まったので、システムコールでファイル操 作をする話は次回に譲ろうと思う。

direntの例
#include 
#include 

void
test(
    char    *name)
{
    DIR             *dirp;
    struct dirent   *entp;

    dirp = opendir(name);
    while ((entp = readdir(dirp)) != NULL)
    printf("%s\tfile number %lu\n",
            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パソコン」である。

IT100のスペック
品名 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の代用として使うということはちょっと難しい。 しかし、そういった欠点が欠点として表面化しない用途に限定すれば、なかな か使えるマシンではある。


もどる