Home日記コラム書評HintsLinks自己紹介
 

フィンローダのあっぱれご意見番 第103回「稀に見るもの」

← 前のをみる | 「フィンローダのあっぱれご意見番」一覧 | 次のをみる →

世間では、何か重大なトラブルがあったときに「調査して改善した」と発表することがある。 発表にもかかわらず、 その後またトラブルが発生するということも、よくある。

実は何も改善していなかった、というのは言語道断としても、 システムに完全性を求めるのはかなり厳しい。 あなたがプログラマーなら、 予期しなかった操作やデータによって予期しないトラブルが発生した経験は、 一度や二度じゃないと思う。 そういった意味では、相次ぐトラブルというのも、 プログラマー的な感じとしてはやや同情的になってしまうものだ。

  

最近ハマったのが、バックアップの問題。 皆さん、pcのデータとかバックアップしていますか? 最近はバックアップしないままで使っていて、 クラッシュして全滅したら、それもまた人生、 という悟りのような使い方が流行っているような気もするのだが、 とりあえず私の場合、VAIOで使っているデータはできるだけこまめにバックアップしている。 とはいえ、単にXCOPYでSolaris 8にコピーするだけだが。

 

※ Solaris 8: Sun Microsystems が発表した UNIX 系のOSで、 当時のバージョンが8だった。 x86版は、無償または手数料程度で get できた。 xcopy は Windows のコマンドラインで使うコマンドで、 ディレクトリをツリーごとコピーする。 Solaris 側は samba を使っている。

さて、そのSolaris 8に作ったデータはどうやってバックアップするか。 cpioで別のハードディスクにコピーしている。 本当はRAIDとかで対応する時代だとは思うが、なにしろ予算がないので。

 

※ エラーフリーのハードディスクを使えばいいかというと、 人為的なミスでうっかり消す、という事故もある。

とあるディレクトリの下のデータをどこか別に全部コピーするにはどうするか? 多分UNIXの初心者向けのFAQだろう。 cp -r というコマンドを使ってコピーすることは可能だが、 いろいろ困ったことが起きる。 そこで、実際はtarだとかcpioを使ってコピーするのが裏技、というかもしかすると正統派だ。 cpioでコピーするって? man cpioとか見たら拒絶反応を起こすかもしれないが、基本は簡単である。 /tmp/original のファイルを全部 /tmp/copy にコピーするには、リスト1のようなことをする。

---- List 1 (cpio を使ってコピー) ----

  cd /tmp/original
  find . -depth -print | cpio -pdm /tmp/copy

---- List 1 end ----

ちなみに、cpioが威力を発揮するのは、 ネットワーク上にある異なるファイルサーバ間の転送であるが、今回は省略。 さて、fig.1 のような構造のディレクトリがあるとしましょう。 矢印が意味しているのはシンボリックリンクである。 UNIX環境下ではそれほど珍しい状況でもない。

---- fig.1 (状態1) ----

   /tmp ---- original --+-- file
                        +    ↑
                        +-- symlink
                        +-- dir --+-- foo.c
                        +    ↑
                        +-- src

---- fig.1 end ----

試しにこのようなファイルを作成するには、例えばSolaris 8なら、 List 2のコマンドを順番に実行すればいい。

---- List 2 ----

 cd /tmp
 mkdir original copy
 cd original
 echo file > file
 ln -s file symlink
 mkdir dir
 echo foo.c > dir/foo.c
 ln -s dir src

---- List 2 ----

期待通りのものができているかどうかは、 ls -lR の表示結果で確認できる。 fig.2 のようになっていればokである。

---- fig. 2 (サンプルディレクトリの内容) ----

 % ls -lR
 .:
 合計 32
 drwxrwx---   2 phinloda staff        107 11月  2日  02:02 dir
 -rw-rw----   1 phinloda staff          5 11月  2日  02:01 file
 lrwxrwxrwx   1 phinloda staff          3 11月  2日  02:02 src -> dir
 lrwxrwxrwx   1 phinloda staff          4 11月  2日  02:01 symlink -> file

 ./dir:
 合計 8
 -rw-rw----   1 phinloda staff          6 11月  2日  02:02 foo.c

---- fig.2 end ----
  

もちろん、少しUNIXをかじった人なら、 これを他のディレクトリにコピーするなんて実に簡単だといいたいだろうが、 まあとりあえず一例だ。 この状態で、original の下を全て copy にバックアップするには、 既に List 1 で紹介したようなコマンドを実行すればいいのである。 実行後に cd /tmp/copy して ls -lR で内容を確認すれば、確かにコピーされていることが分かると思う。

 

※ SELinux なんかだと、 もう少しややこしい話が出てくるかもしれない。

§

さて、ここからが問題である。 その後あれこれ作業した結果、original の状態がfig.3のようになった。

---- fig.3 (状態2) ----

   /tmp/original --+-- file
                   +-- file2
                   +     ↑
                   +-- symlink
                   +-- dir --+-- foo.c
                   +
                   +-- src ---+-- foo.c

---- fig.3 end ----

大きなポイントは、前回シンボリックリンクだった src が実体を持つディレクトリになったことだ。 初期状態からこの状態にするコマンドの例を List 3 に示す。

---- List 3 ----

 cd /tmp/original
 echo file2 > file2
 rm symlink
 ln -s file2 symlink
 rm src
 mkdir src
 echo "new version" > src/foo.c

---- List 3 end ----

このようになった /tmp/original を、/tmp/copy にコピーするのが目標だ。 もちろん、/tmp/copy の下を全部削除してから先ほどと同じコマンドを実行すれば目標は達成できる。 ただし、今回は、全削除をしないという条件をつける。 つまり、変更のあった個所だけ上書きコピーして欲しい。

  

これは例えば、IT革命が成就すれば無意味かもしれないが、 コピー先が遠隔地にあって、回線速度がボトルネックになってしまうような場合。 膨大なファイルを全部コピーするととんでもない時間がかかるが、 更新されたファイルだけをコピーすれば処理時間が短縮できるという場合は割とある。 指定日以内に更新されたものだけリストアップするには、 find のオプションで -mtime を指定すればよい。 実はこれにはちょっと問題があるのだが今回は気が付かないことにして、 とにかく、作業後の状態はfig.3のようになったとする。

 

※ フェイルセーフという意味でも、 全部消してから上書きするというのは、 消した瞬間にシステムが停止したらヤバいという意味でよくない。
「ちょっと問題」というのは、 アーカイブから日付の古いファイルを展開したようなのがあると、 いきなり古いファイルが出現してしまって、 コピーし損ねるという話。

更新されたものだけコピーしてもいいが、 どうせ全てのファイルの作成時から1日たってないから、 先ほどと同じコマンドを実行しても同じことになるはず。 更新されていないファイルを上書きするところでエラーが発生するが、 このエラーは予想通りなので無視してよい。 しかし、別の大問題が発生している。 /tmp/copy の状態は /tmp/original とは異なり、fig.4 のようになっているはずだ。

---- fig. 4 (状態2をコピーした結果) ----

   /tmp/copy --- --+-- file
                   +-- file2
                   +     ↑
                   +-- symlink
                   +-- dir --+-- foo.c
                   +     ↑
                   +-- src

---- fig. 4 end ----

一体何が起きたのだろうか? cpio は、original にある src/bar.c を、コピー先の src/bar.c にコピーしようとした。 src というディレクトリは、シンボリックリンクとして既に存在しているから、 何も考えずに src/bar.c を作成すると、 それは実は src の指している先のディレクトリ、すなわち dir に foo.c を作ることになってしまう。 シンボリックリンクであったsrcが実体のあるディレクトリに変更された、 という情報はどこかに行ってしまったのだ。 そして、最初にあった foo.c はこの世から消滅した。

というわけで、この時点での問題は、状態1から状態2に変化したとして、 /tmp/copy の下も状態2のようにするためには、 どのようなコマンドを使えばよいか、という話になるが、 もっと本質的な問題は、 この状況を最初にコマンドを作成する時点で想定できなかったか、という所にある。

§

ちなみに、現在使っているコマンドは、 他の雑多な問題も含めて、ほぼどんな状況であってもコピーできるようになっている。 「ほぼ」というのがポイント。 見落としがないことを証明するというのは、あまりにも難しい。 しかも、今回はレアだが確実に失敗するケースが分かっているのだ。 もし「私の修正版なら完璧だ」という方がいれば、実に頼もしいと思うし、 本当にあらゆるケースに対して正しい結果が得られることを検証したのであれば、 お見事としか言いようがない。 そこまで自信のある方ならまず大丈夫だとは思うが、 久しぶりのCのプログラムですが、 List. 4のプログラムをコンパイル、 ランさせて生成したファイルがコピーされることを確認してください。

---- List 4 (testfile.c) ----

#include <stdio.h>

int main(void)
{
    FILE *fp;

    fp = fopen("newline\n.c", "w");
    if (fp == NULL) {
        fprintf(stderr, "can't create\n";
        exit(0);
    }
    fprintf(fp, "test file\n");
    fclose(fp);
    exit(0);
}

---- List 4 end ----
  

私の環境ではこの問題は残念ながらまだ解決していない。 findでいきなりprintしないでexecを使ってフィルタを通せば何とかなりそうだが、 そこまでするよりも、 現実的には、 ファイル名にヘンなコードが含まれないようにするという制限事項を付けて、 消極的に解決した方が楽だったのだ。

 

※ 元のスクリプトは、 find で生成した内容を1行ずつコピーの処理にまわす、 という仕組みになっているから、 ファイル名に改行が含まれていたら、そこで破綻する。

ちなみに、なぜperlじゃなくてCなの? 実は、 perlで単純にファイル名に改行を含むようなファイルを作成しようとしたらエラーになってしまったのだ。 perlの作者はファイル名がヘンな場合にどうなるかを理解していて、 ファイル作成時にエラーにしているのではないだろうか。 だとすると、本来この程度までなら事前に想定すべきということかもしれない。 世の中厳しい。

 

※ もちろん、そんなヘンな名前のファイルを作った覚えはないのだが、 できてしまったものは仕方ない【格言】。

(C MAGAZINE 2001年1月号掲載)
内容は雑誌に掲載されたものと異なることがあります。

修正情報:
2006-03-11 裏ページに転載。

(C) Phinloda 2000-2006, All rights reserved.