Perl Hackers Hub

第6回UNIXプログラミングの勘所(4)

ファイルの保存

ここまでいくつかの見落としがちな処理を見てきましたが、最後にどんなプログラマでも必ず行う「ファイルの保存」についての注意点を考察してみたいと思います。

ファイルを確実に書き込むためには、どのようなコードを書けばよいでしょうか。⁠fsync」を呼べばいい。それだけではありません。実際には、次の2点も必要になってきます。

  • (内容の差し替えだった場合に)書き換え途中の状態にならないこと
  • ユーザに対して書き込み完了を返したあとは、ディスクがクラッシュしない限り、データが消えないこと

UNIX系OSでこれらの要件を満たすには、まずテンポラリファイルに新しいデータを書いてディスクに同期し①②⁠、⁠必要ならば)古いファイルをバックアップファイル名を使っても参照できるよう別名を付与し⁠、次にテンポラリファイルのファイル名を差し替え対象になっているファイルの名前に切り替え⁠、最後にディレクトリの情報をディスクに同期する⁠、というステップを踏む必要があります。

# 適当なテンポラリファイル名(格納先と同ディレクトリ)
my $newfn = "tmp.$$";
# ファイルを書いてfsync
open my $fh, '>', $newfn
    or die "failed to open file:$newfn:$!";
print $fh $data;
IO::Handle::flush($fh);
    or die "flush failed:$!";
IO::Handle::sync($fh);
    or die "fsync failed:$!";
close $fh;

# 古いファイルに別名を付与(必要ならば)
link $fn, "$fn~"
    or die "failed to link $fn to $fn~:$!";

# 新しいファイルに差し替え
rename $newfn, $fn
    or "failed to rename $newfn to $fn:$!";

# ディレクトリエントリをfsync
sync_dir('.');

また、このコードで呼んでいるディレクトリエントリをfsyncする関数の実装は次のとおりになります[13]⁠。

sub sync_dir {
    my $dir = shift;
    sysopen my $d, $dir, O_RDONLY
        or die "failed to open directory:$dir:$!";
    open my $d2, '>&', fileno($d)
        or die "dup(2) failed:$!";
    IO::Handle::sync($d2)
        or die "fsync(2) failed:$!";
}

これではあまりに複雑だということで、最近のLinuxではでディレクトリの同期を自動的に行うようになったとのことです。しかしCentOS 5以前の環境では上のとおりの処理を行う必要があります[14]⁠。

まとめ

ファイルの保存は、一見簡単なようで正しく実装しようとすると面倒です。また、停電やOSクラッシュが起きるまで、実装にバグがあっても問題が表面化しない類いのものなので、テストを書きづらいというのも悩ましい点です。まあ呪文のようなものだと諦めて、コードを書き写すしかないでしょう。

おわりに

以上、PerlでUNIXプログラムを書く際に筆者が重要だと考える事項を4点、紹介しました。本稿が、より品質の高いプログラミングにほんの少しでも寄与するところがあれば、著者としてこれに勝る喜びはありません。

OSを制御する知識はPerlでプログラムを書く場合以外にも、たとえば障害解析をする場合など、さまざまな場面で必要になってきます。UNIXのプログラミングについて、より詳しく勉強したい方は、⁠詳解UNIXプログラミング』注15などの書籍をあたることをお勧めします。

次回の執筆者はacotieこと横山彰子さんで、テーマは「仕事で使えるPerlの基礎知識」です。お楽しみに。

おすすめ記事

記事・ニュース一覧