Linuxプログラミング入門

97年11月14日執筆

from DOS to Linux(4)
ファイル処理(1)


はじめに

今回からファイル処理についての解説に入る。UNIXでは基本的に全てのリソー スがファイルとして抽象化されているので(最近は一部例外はある)、ファイル 処理がわかってしまうと、カーネルの機能の基本的な部分の解説はほぼ全部済 んだことになってしまう。だから、本当の意味で「Linux固有の機能」という ことになると、このファイル処理の解説で全てと言えないこともない。

フィルタ

痩せても枯れてもLinuxはUNIXの一種である。だから、UNIXプログラミングの 古典的名著と言われる「プログラム言語C」とか「プログラム作法」といった ような本に出ているCのコードは、全て正しく動くはずである(古い教科書だ とANSI Cで書かれていないので、ワーニングが出るかも知れないが)。だから、 フィルタの「各論」に関しては、それらの本に書かれていることを勉強すれば 良く、本稿で特別に解説するべき話題はない。だから、全く白紙の状態で Linux上でフィルタを作りたいと思っている人は、これらの本をそのまま読ん でプログラムの勉強をすれば良い。

ただ、この記事を読んでいる人の多くは、MS-DOSの上でプログラムを書いたこ とのある人がろうと思う。そうすると、ちょっとした常識の違いに留意する必 要が出て来る。

ファイルモード

LinuxとMS-DOSとの大きな違いは、何と言ってもファイルの行端記号であろう。 ここで、ちょっとこの辺をおさらいしてみよう。

Linux(UNIX全て)の行端記号は0x0Aになる。これがMS-DOSの場合は0x0D 0x0Aと なる。また、ファイルの終端記号というものは本来MS-DOSにはないものである が、CP/Mとの互換の関係で0x1Aというものが存在したり、これを考慮したりし ていたのが普通である。これらの問題は「これらはそれぞれの問題」「どうし ても必要ならファイル変換で」ということだけで終わっていれば良かったので あるが、Cのプログラムを書く上とかの都合で、MS-DOS上のCでは、「テキス トモード」と「バイナリモード」というものを用意してこの問題を解決しよう としたのである。つまり、「テキストモード」でファイルを読み書きしていれ ば、ソースコード上に0x0A(\n)と書かれていると、自動的に0x0D 0x0A(\r\n) に変換してくれ、またその逆もやってくれるという一見便利なものとなってい たのである。また、基本的に標準入出力もデフォルトだとテキストモードとなっ ているため、この変換が自動的に行われるようになっているのが普通である (そうでないようにも出来るのだが)。

この変換は別に「知的」に行われているわけではなく、単にテキストモードで 入力する時には\r\n -> \nという変換を行い、出力する時には\n -> \r\nとい う変換を行っているに過ぎない。だから、扱っているデータの中にたまたまこ れらの変換が行われるデータがあった場合は、プログラマの意思に関係なく勝 手に変換が行われる。バイナリデータを扱う時にこれが起きると良くないので、 バイナリデータを扱う時にはバイナリモードでファイルを使う... といったこ とはMS-DOSの場合は常識であった。

ところが、このようなものはLinuxには全く不要である。ソースコード上の\n はデータ上も\nである。だから、MS-DOS上で配慮していたこれらのことは逆に 邪魔になる。と言うか、そのような概念すら存在しないので、仮にそのような ことを考慮したMS-DOSのプログラムを移植しようとすると、その辺のコードは 外さなくてはコンパイルすら通らないと思われる。もっとも、fopenの場合は ファイルモードに"b"とか書いてあっても、それは無視されるだけなので、あっ てもとりあえずは問題にならない。

問題となるのはシステムコールであるopenや、その返り値であるファイル記述 子を使ったfcntlである。MS-DOSにあった、

            O_BINARY

といったものは全くなくなっているので、適当に対処しておかなくてはならな い。多くの場合、他のモードとセットで(つまりORで)使われることが多いので、

            #define O_BINARY  0

のようにしておけば良いだろう。

この辺の話は、言われれば当然のことなのであるが、実際にプログラムを書い ていると何となく嬉しい。ファイルの扱いにバイナリとかテキストとかの区別 がないので、「うっかりモード間違いでデータを壊した」ということも起きな いし、ライブラリが余分な処理をしないということは、フィルタを書く時の心 配事を一つ減らしてくれる。レコード長が固定のデータ並びになるファイルを 便宜的に「行=レコード」にする時につけるべき文字も1文字になるので、 structを考える時も楽である。

ところが、これはファイルの時には楽になるのであるが、ネットワークとなる と、またちょっと話が違う。それは、いわゆる「通信」の世界では、0x0D 0x0Aが行末になっていることが多い。だから、MS-DOSの時に問題が起きなかっ たものが、逆に問題が起きたりすることもないではない。それでも、妙な変換 がされてない分だけ、物事の見通しが良くなっていることは確かである。

rawとcocked

前節で述べたように、ファイルにバイナリとテキストの区別はなくなった。し かし、それでは何でもそのまま通るかと言うと、これは必ずしもそうではない。 これはcatでも何でも動かしてみるとわかるが、入力がttyになっていると、行 の編集が出来てしまう。つまり、いくつかの特殊文字に関しては、そのまま通 らないで編集に使われてしまうのである。このことについての詳細はまた後の 機会に譲るが、とにかくバイナリモードであるからと言って、何でもかんでも コードが通るというわけではない。

これは自分で特殊文字による動作を書きたい場合、たとえばエディタとか通信 ソフトとかを作る時には障害となったりする。このような「特殊文字が特別な 意味を持った」ファイル入力モードを`coocked'モードと言い、そのまま全て の文字を通すものを`raw'モードと言う。だから、ttyからの入力を本当に全て 受けようと思うと、rawモードにする必要がある。

バッファ領域

フィルタに限らず、ファイルを読んで行単位の処理をする時、「1行はどれく らいまで考慮すれば良いのだろうか」ということに悩んだことのない人は少な くないと思う。既に前段のプログラムで行の長さが制限されてしまうような場 合を除いては、「これで十分」という長さが考えにくいのである。そこで「え いやっ」と80文字とか255文字とかという長さで十分だろうと仮定してプログ ラムを書くのであるが、これで十分だとうい確証がないために、

            #define LINE_LENGTH   1024

とかといった定義をしてやって、それを使うということになる。あるいは、行 単位の処理を諦めてしまって、文字単位の処理を積み重ねて行を処理するよう なプログラムにしてしまうという手もある。何本もプログラムを書いた経験の ある人は、いずれの方法も試したことがあるのではないかと思う。

ここでフィルタの話からちょっと外れるが、「ファイルをコピーする」という プログラムを書く時に、皆さんはどんなコードにするだろうか? ありがちな のは、リスト1に示すような「ある程度の大きさのバッファを用意して、それ を中継して何度もファイルを読み書きする」という処理を書くのが普通ではな いかと思う。話を見えやすくするために意図的にエラー処理を省いていること を別にすれば、もちろんこれは正しいプログラムなのであるが、この時にも 「バッファの大きさはどうしたら良いか」という問題はついて回る。この場合 の処理単位は「行」ではないので、SIZE_BUFFをいくらにしようと「使われな いバッファを用意した」というような問題はないのであるが、小さくするとファ イル処理手続きが呼ばれる頻度が高くなるので、やはり無駄ではある。いずれ にしても、このような問題の時には、

            バッファサイズ < ファイルサイズ

という仮定の下に「何度か読み書きする」というプログラムになる。まぁこの 辺はどちらかと言えばDOS的というかメモリが少ない環境的な考え方である。

ところが現代のLinux環境の場合、メモリはもっと大胆に使っても良い場合が 多い。もちろんメモリ空間は無限ではないし、たいていはいくらメモリ空間が 広くしてあるからと言っても仮想空間の話だから、無制限に大きくすることは 出来ないが、それでも多くの場合

            バッファサイズ >= ファイルサイズ

という仮定を持ったプログラムを書くこともそう無茶な話ではない。実際、現 代的な多くのプログラムの中で「ファイルを一気に」といった感じのreadがあっ たりする(Elkのloadの処理はどはそれに近い)。そのようなわけで、「ある程 度の大きさ」という制約は当然あるものとしても、一気に読むということも無 茶なことではない。一般的なファイル処理に使うことは出来ないが、数MB程度 のファイルを扱う場合には有効な手段である。

「そんなことをして何の役に立つのだ?」という疑問もあるかも知れない。し かし、「ファイルの中を何度もスキャンする」ようなプログラムを書く必要に かられた人は少なくないだろうと思う。データベースシステムを使う程ではな いにしても、そこそこのマスタを更新するようなプログラムはこれにあてはま る。

そのような時には、一々ファイルを読むようなプログラムだと面倒な上に遅く なるので、ファイルを全部読み込んで処理するようにすると効率が良い。シス テムコール等も全く発行しないメモリ上だけの処理は、非常に高速に実行出来 る。

ところが、フィルタとして機能させるプログラムの場合、読み込むべきファイ ルの大きさは事前にわからないし、ゆっくりデータが作られるプログラムの出 力を処理する場合には、バッファがいっぱいになるまで実際の処理が始まらな いために、即時性が損なわれる。だから、このような場合には無闇に大きなバッ ファを用意するのは、あまり嬉しいことではない。この辺のことも留意する必 要がある。

そうなると、「結局どうすれば良いのか」ということになるのだが、「具体的 にいくら」という問いに直接答えることは出来ない。一番正確な答えは、「そ のアプリケーションに必要なだけ用意する」というごくあたりまえの答えにな る。「それは答えではない」と思われるかも知れないが、今までどうやってバッ ファの大きさを決めて来たかと言えば、「i86のセグメントの大きさ」だった り、「実装メモリの大きさ」だったりしたわけで、それは「アプリケーション に必要な大きさ」とは直接関係のない理由である。ところが今はアプリケーショ ンが本当に必要とする大きさの領域を使うことが可能になったのである。 MS-DOSやi86のしがらみ、古典的アルゴリズムのしがらみから離れ、「本当は どうするべきか」ということをベースにプログラムを設計し直すと良いだろう。 現代のWindowsのアプリケーションのようにメモリを浪費的に使うのもどうか と思うが、せっかく大きなメモリ空間が使えるのだから、それを有効に使わな いというのももったいない話である。

リスト1:copy1.c
#include    

#define SIZE_BUFF       256

main(
    int     argc,
    char    **argv)
{
    FILE    *fpFrom
    ,       *fpTo;
    unsigned char   buff[SIZE_BUFF];
    size_t  size;

    fpFrom = fopen(argv[1],"r");
    fpTo = fopen(argv[2],"w");

    do  {
        if      (  ( size = fread(buff,1,SIZE_BUFF,fpFrom) )  >  0  )   {
            fwrite(buff,1,size,fpTo);
        }
    }   while   (  size  ==  SIZE_BUFF  );
    fclose(fpFrom);
    fclose(fpTo);
}

コラム
Breaking the Code!

直接プログラミングの話ではないが、今JLUGの有志でRC5という暗号を破るコ ンテストに参加している。これはRC5という暗号でキーが64bit長のものを破る というものである。現在のところRC5を解く有効な手段は発見されていないの で、64bit全てのキー(つまり0x0000000000000000から0xFFFFFFFFFFFFFFFFまで) をしらみ潰しに調べて行き、それで破ろうというプロジェクトである。実は先 日Dual P6のマシンを入手して、それでこの暗号解きのプログラムを走らせて みたのだが、このマシンだと約100万個のキーを試すことが出来る。このマシ ンで破ろうとすれば、18x10^13秒。つまり、約60万年くらいかかってしまうの である。そこで、たくさんのマシンを集めて手分けしてキーを調べれば、それ だけ時間が短縮出来るだろうということで、大規模分散的にやっているのが、 今回のコンテストの参加なのである。

実は、このコンテストには前段階があって、キー長が56bitのものに関しての コンテストが前にあったのである。この時は「キー長が56bitくらいじゃ破ら れちゃいますよ」ということの実証のためのコンテストだったのであるが、今 回はどちらかと言うと、「マシンパワー集め競争」的な面がある。もちろん 「たとえ64bitでも破ろうと思えば不可能ではないですよ」という意味もある。 ちなみに56bitの時には250日くらいで破られているので、単純計算でその256 倍で解けるはずであるから、200年くらいで解けるということになるが、今回 はエントリしている人も多いので、いくらなんでもそこまではかからないだろ う。

このコンテストに参加するのは簡単で、進捗管理しているサーバにアクセス出 来る環境で、プログラムを走らせてやれば良い。「アクセス出来る」と言って も、ダイアルアップPPPでも良いし、壁の中なら適当な抜け穴(ポート)から外 が見えていればそれで良い。このプログラムは非常に良く出来ていて、強制停 止しても再起動すれば続きからやってくれるし、進捗管理サーバにアクセス出 来なければ、それなりに処理をしてからアクセス出来た時に物事を解決するよ うになっている。このあたりの詳しいことは、http://www.linux.or.jp/~rc5 に書いてあるので、そこを見て欲しい。また、このページを見ることが出来る 人というのは、このコンテストに参加することが可能なはずである。

11月11日の時点で、我らがJapan Linux Users Groupチームは世界第2位の計 算機パワーが集まり、世界第1位のチームの約半分のパワーになっている。や はりせっかくやっているのだから、一度くらいはトップに立ってみたいもので ある。

と言うわけで、これからも参加する人を募集しているので、遊ばせているマシ ンや、負荷的に暇そうなマシンのある人は、ぜひ挑戦してみて欲しい。なお、 このプログラムはniceが低く設定されているので、完全な空き時間にしか走ら ないようになっている。

skkinput

先月号で「skkinput はDp/noteで使えないのが残念だ」と書いたのだが、その ことを作者である阪本@京大情報さんに報告したら、さっそく対応してもらえ た。また、かなり本質的問題を含んでいたバグらしく、これで安心して使える プログラムも増えたのではないかと思う。

ついでに言うと、Dp/noteはキーのカスタマイズが出来る。これを利用して 「 よしだともこ」さんが「Emacs配列の定義」をあるMLで教えて下さった。こ れとskkinputを組み合わせると、「Emacsのキー操作でSKKで日本語入力をする ワープロ」という妖しげなものが出来上がる。会社では変態呼ばわりされたが、 なかなかキュートである。


もどる