続C言語教室 - 第12回 同じファイルをいくつも開くと
C言語でファイルを使って読み書きする時には、ファイル名を指定して開いて、構造体へのポインタか整数のハンドルを受け取ってそれを使って読み書きします。ファイルを管理するファイルシステムからは、どのファイルが開かれているかを知る方法はあるのですが、普通にファイルを開く場合には、同じプログラムや他のプログラムでファイルが既に開かれていたとしても、新たにファイルを開くことは問題なく出来てしまいます。
以下のファイルは予め用意しておきます。
#include <stdio.h>
void main() {
FILE* f1 = fopen("hello.txt", "r");
if (f1 == NULL) {
perror("Error: at First Open \"hello.txt\"");
exit(1);
}
FILE* f2 = fopen("hello.txt", "r");
if (f2 == NULL) {
perror("Error: at Second Open \"hello.txt\"");
exit(1);
}
int ch;
printf("\nFirst\n");
while ((ch = fgetc(f1)) != EOF) {
if (ch == 'D') break;
putchar(ch);
}
printf("\nSecond\n");
while ((ch = fgetc(f2)) != EOF) {
if (ch == 'F') break;
putchar(ch);
}
fclose(f2);
fclose(f1);
exit(0);
}
このコードを実行すると
とそれぞれの処理が問題なく行われています。
何か所から開かれていたとしてもすべてが読み出しモードで開かれていれば、問題となることは無いのですが、書き込みモードや更新モードで開かれている場合、そこで何らかの書き込みが行われるとどうなってしまうのかといえば「どうなるかわからない」のが一般的です。
基本的には開かれたファイルそれぞれの動作が順に行われるだけではあるのですが、ややこしいのがストリーム入出力の場合は、それぞれのファイル構造体にバッファ領域を持っていて、このバッファと実際のファイルとのやりとりがどのタイミングで行われるかはコードからはわかりません。ですから先に書き込み操作が行われても、書き込んだ内容がファイルに反映されるよりも前に読み出されれば、書き込む以前の状態のファイルが読み込まれることもあります。さらに複数の場所でファイルを書いた場合は破滅的で、同時に同じファイル位置に異なる内容を書き込むと、そのどちらの内容がファイルに反映されるかは運次第です(基本的に後勝ち)。
#include <stdio.h>
void main() {
FILE* f1 = fopen("hello.txt", "w");
if (f1 == NULL) {
perror("Error: at First Open \"hello.txt\"");
exit(1);
}
FILE* f2 = fopen("hello.txt", "w");
if (f2 == NULL) {
perror("Error: at Second Open \"hello.txt\"");
exit(1);
}
fprintf(f1, "ABCDEFG\n");
fprintf(f2, "HIJKLMN\n");
fclose(f2);
fclose(f1);
FILE* fp = fopen("hello.txt", "r");
int ch;
while ((ch = getc(fp)) != EOF) {
putchar(ch);
}
fclose(fp);
exit(0);
}
fclose()の順序を入れ替えて
fclose(f1);
fclose(f2);
と修正すると今度は
と結果が変わります。
この「同時に書き込む」は、同じプログラムの中であっても異なるプログラムからであっても起こることは変わりなく、自分で書いたプログラムでいくら配慮しても解決はしません。これを避けるためにファイルを開く前に既にそのファイル名のファイルが存在しているか調べたり、(プロセス終了時までしかファイルを使わない場合は)自身のプロセスIDからファイル名を生成したり、tmpfile()やmkstemp()を使ったりして同じファイル名を使わないようにすることが一般的です。またファイルを作るディレクトリを引き数などで指定してプログラムを使う側で同じディレクトリを使わないように管理してもらって解決することもあります。
【C言語】一時ファイルを作成するtmpfile関数とmkstemp関数,一時ディレクトリを作成するmkdtemp関数の使い方
いずれにせよファイルを作ってしまってからも、他所で同じ名前のファイルを開くことを防ぐことは出来ないので、ファイルを「ロックする」という操作を行うこともあります。これはストリーム入出力ではなくシステムコール(低レベル入出力)のflock()を呼ぶことでロックをかけるのですが、システムコールですからこれが使えるかはOS次第ですし、その実装はマチマチで対象となるファイルシステムによっても機能しないこともあり、fcntl()を使ったりlockf()を使わなければならないこともあります。また使い方も少々難解で、正しい手順を守らないと今度は誰もそのファイルに書き込むことが出来なくなるというトラブルが起こることもあります。
ファイルに対してロックの適用・解除を行う
manページ — FLOCK
そんな訳でファイルを書き込んで、その結果が意図した通りになるかもなかなか大変なのですが、最近のC言語では fopen() に代わって fopen_s() という関数が追加されています。戻り値がFILE構造体へのポインタからerrno_t型の値に変更になっていて、FILE構造体へのポインタは引き数で渡した領域へ書いてもらう形になっています。大きな違いは書き込みモードで開いた場合に排他モードで開くことで、既にファイルが存在していると開くことが出来ません。
#include <stdio.h>
void main() {
FILE* f1;
errno_t e = fopen_s(&f1, "hello.txt", "w");
if (e != 0) {
perror("Error: at First Open \"hello.txt\"");
exit(1);
}
FILE* f2;
e = fopen_s(&f2, "hello.txt", "w");
if (e != 0) {
perror("Error: at Second Open \"hello.txt\"");
exit(1);
}
fprintf(f1, "ABCDEFG\n");
fprintf(f2, "HIJKLMN\n");
fclose(f2);
fclose(f1);
exit(0);
}
まだ、この fopen_s() が利用できない場合もあるかもしれませんが、使える場合にはこちらを使うと安心かもしれません。
C11 ライブラリ関数 (脆弱性対策)
しかしファイルロックの実装はロックファイルを作ったり、共有メモリにフラグを用意したりいろいろありますし、複数のマシンから同じファイルをアクセスすることもありますし、マルチスレッドなプログラムだとアトミックな処理を正しく行うための保護なんかもなかなか面倒です。そしてロックしたファイルはいずれ何らかの異常が発生してロックが外れなくなって、ロックファイルを手動で消したりプロセスを再起動したり、場合によればマシンの再起動が必要になるので本当に手強いです。
さてファイル入出力についても、なかなか面倒な話になってきたので、このあたりで切り上げて、次はヒープのメモリ管理について調べてみましょう。
ヘッダ画像は、AIに頑張ってもらいました。