安全なファイル書き換えにはディレクトリも fsync すべき。だけど Perl でどう書くか

間違ってたらツッコミお願いします。

ext4 が出たタイミングで話題になったことだけど、(ext4 に関係なく一般論として) ファイルを安全に書き換えるためには、いくつかの手順を踏む必要がある。で、Perl だとだいたい以下のようになる。

# 1) 適当なテンポラリファイル名 (格納先と同ディレクトリ)
my $newfn = "tmp.$$";

# 2) ファイルを書いて 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;

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

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

でも、この処理は、ファイルがディスクに書き込まれたことを保障するけど、ファイルがディレクトリ内に書かれたことは保障しない。具体的に言うと、4 のあとでディレクトリエントリの更新がディスクに書き込まれる前に OS がクラッシュすると、新しいファイルは lost+found 内で見つかったりする。

なので、ファイルがディレクトリ内に書かれたことを保障するには、C だと、上の 4 のあとで (jounalling fs じゃない場合は 3 のあとにもなのかな)、

int sync_dir(const char* dir)
{
  int d = open(dir, O_RDONLY), r = -1;
  if (d != -1) {
    r = fsync(d);
    close(d);
  }
  return r;
}

みたいな関数を呼ぶことになる。

んだけど、Perl だと O_RDONLY なファイルハンドルを IO::Handle::fsync すると Invalid Argument って言われる。ファイルハンドルが O_RDONLY かどうかチェックしてるんだと思う (余計なお世話 :-()。しょうがないから、

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:$!";
}

みたく書いて、ごまかすことに。

参考: Comment #54 : Bug #317781 : Bugs : linux package : Ubuntu, Ext3のコミット間隔を当てにしたアプリケーションは、Ext4でデータロスの恐れあり | スラド, Release History Of SQLite の 2.8.6