home ホーム search 検索 -  login ログイン  | reload edit datainfo version cmd icon diff delete  | help ヘルプ

C言語系/「デーモン君のソース探検」読書メモ/09, mv(1)

C言語系/「デーモン君のソース探検」読書メモ/09, mv(1)

C言語系 / 「デーモン君のソース探検」読書メモ / 09, mv(1)
id: 553 所有者: msakamoto-sf    作成日: 2010-01-14 01:07:29
カテゴリ: BSD C言語 

お題:メールスプールのコピーを取るプログラムを作成する場合に注意すべき点を、mvコマンドのソースと絡めて調査せよ。

※「デーモン君のソース探検」の方では、メールスプールのコピーを取るプログラムの作成で"mvコマンドをexecしてはならない"という条件をヒントに探検が始まります。こちらの読書メモの方では、プログラムは作成せず、mvコマンドのソース探検だけに留めておきます。
というのは・・・メールスプールのコピー周りでのファイルロックがもう一つのテーマになっているのですが、そちらの方は調査対象のソースがかなり限定されている上に、解説も書籍の方だけで充分だと思ったからです。

まぁぶっちゃけ、mvのソース調査だけで時間がかなり掛かってしまったというのもあります(APUE:Advanced Programming in the UNIX Environment まで引っ張りだす羽目になったし・・・)。

それでは、どうぞ。


mv(1)のコマンドのmanページをおさらい

まずはmvコマンドのソースを探検してみる。本では"rename()"関数を使っている部分を中心に調べている。

$ locate mv.c
/usr/src/bin/mv/mv.c
...

ソースが特定出来た。ざっと見てみると

main() -> do_move() (-> fastcopy() or copy() )

の流れが見える。ロジックだが、移動元と移動先の属性であったりアクセス権限であったりを検査するコードが頻出している。
ここで一旦、mv(1)のmanページを元に動作パターンをおさらいしておく。これにより実際のコードリーディングがmanページとの対比で分かりやすくなる筈。

mvの使い方:
mv [-fi] source target
mv [-fi] source ... directory
  1. 最初の形式ではsourceで指定されたファイル名をtargetに変更する。ただしtargetが既存のディレクトリで無い事が前提。
  2. 2番目の形式では、sourceで指定されたファイル群をdirectoryで指定された既存のディレクトリ内へ移動する。なおsourceファイル群の移動先のファイル名は、"directory/" + 各ファイル群の最後のパス名となる。

2番目の形式で、移動先のファイル名云々は何が言いたいのかというと、

$ mv dir1/file1 dir2/dir3/file2 dir4/file3  directory

が実行されたら、以下に示すようにdir1 - dir4 のディレクトリ階層は無視され、最終的なファイルだけがdirectoryの下に移動すると言うこと。

dir1/file1      -> directory/file1
dir2/dir3/file2 -> directory/file2
dir4/file3      -> directory/file3

NetBSD1.6の場合は次のコマンドラインオプションが提供されている。

-f : 移動先を上書きする時に確認しない。
-i : 移動先を上書きする時に標準エラー出力にプロンプトを表示する。
     標準入力から入力された文字列が"y"で始まっていれば上書きを実行する。

例えば"-f"と"-i"が複数回指定された場合は、最後に指定された方が有効になる。

$ mv -i -f -i file1 file2
→上書き確認プロンプト有り
$ mv -f -i -f file1 file2
→上書き確認プロンプト無し

続けてエラーになるパターンを二つ。

  • source側で一つでも、存在しないファイルorディレクトリを指定された場合
  • sourceが既存のディレクトリを指していて、target側がディレクトリ以外で既存の場合

また、destinationに対して書き込み権限が無い場合、"-i"オプションが指定されたのと同様な確認プロンプトを表示する。

$ touch file1
$ touch file2
$ chmod 444 file2
$ su
# su -m nobody -c "/bin/mv file1 file2"
override r--r--r--  msakamoto/wheel for file2? y
mv: rename file1 to file2: Permission denied

mvコマンドはrename(2)システムコールを使う。sourceとtargetでファイルシステムをまたぐとrename(2)は失敗し、mvはその場合、以下のコマンドの組み合わせを実行する。

rm -f destination_path && \
cp -PRp source_file destination_path && \
rm -rf source_file

以上がmv(1)のmanページからまとめなおした、各動作パターンとなる。なおrename(2)が失敗するケースをもう少し厳密に書くと以下のパターンになる。

  1. ファイルシステムが異なる場合(fat -> ffsなど)
  2. ファイルシステムが同じでも、パーティションが異なる場合(NetBSD1.6の場合)

mountポイントが異なるディレクトリ間でのrename(2)は失敗すると覚えて置けば良い。システムのmanページによっては、「ファイルシステムが異なる場合失敗」と書いてあったりして、「ファイルシステムは同じだがmountポイントが異なる場合」はどうなるのかについては曖昧な場合もある。
なおあくまでも後者のパターンはNetBSD1.6の場合である。その他のBSD系列、SystemV系列、Linuxでどうなっているのかはそれぞれのmanを参照してほしい。
APUE:"Advanced Programming in the UNIX Environment" にも、rename(2)の説明では特にファイルシステムについては書かれていない。なので、OSによってはこの辺の挙動が違う可能性がある。一応Linuxのrename(2)も、ファイルシステムをまたぐ場合はEXDEVエラーになり、NetBSD1.6と同じ挙動らしい。

mv.cソース解析:main()関数

準備が整った所でmv.cのソースに挑戦していきたい。

#include "pathnames.h"

int fflg, iflg;
int stdin_ok;

まずpathnames.hをインクルードし、ファイル内グローバルな変数 fflg, iflg, stdin_okを宣言している。

$ cat /usr/src/bin/mv/pathnames.h
...
#define _PATH_RM        "/bin/rm"
#define _PATH_CP        "/bin/cp"

pathnames.h内で定義されているrm, cpへのパスは、前述のrename(2)失敗時にrm, cpコマンドを組み合わせる時に使用している(後述)。

mv.cでは関数のプロトタイプ宣言が続き、main()関数となる。読みやすいので、細かい部分は省略してポイントとなるところだけ紹介していきたい。
まず"-i"と"-f"コマンドラインオプションの指定を、fflg, iflgに写し取る。排他的になっているのと、最後に指定されたのが優先されるようになっている点に注意。

while ((ch = getopt(argc, argv, "if")) != -1)
    switch (ch) {
    case 'i':
        fflg = 0;
        iflg = 1;
        break;
    case 'f':
        iflg = 0;
        fflg = 1;
        break;
    case '?':
    default:
        usage();
    }

続けてisatty()で端末経由か判断している。端末経由ならstdin_okは1になり、cronなど端末に繋がっていないバッチジョブなどから呼ばれた場合は0になる。

stdin_ok = isatty(STDIN_FILENO);

これは後でプロンプト表示の箇所で使われる。

続いて次のブロック:

/*
 * If the stat on the target fails or the target isn't a directory,
 * try the move.  More than 2 arguments is an error in this case.
 */
if (stat(argv[argc - 1], &sb) || !S_ISDIR(sb.st_mode)) {
    if (argc > 2)
        usage();
    exit(do_move(argv[0], argv[1]));
}

stat(2)でtargetのstat構造体を取得し、"sys/stat.h"で定義されたS_ISDIRマクロを使ってディレクトリか判別。ディレクトリで無い場合はそのままdo_move()を呼び出す。なお、この時点でargvが3つ以上の場合はエラーとしている(意味的に曖昧になってしまうし、argv[argc-1]とdo_move()に渡しているargv[0], argv[1]がずれてしまう)。

main()関数ではこれに続いて、targetがディレクトリの場合にsourceで指定されたファイル群それぞれに対してdo_move()を呼び出すwhileループが定義されている。whileループの中の殆どの処理は、移動元のファイル名の末尾要素をtargetディレクトリ名につなげる文字列処理となっており、これについては解説は省略する。

mv dir1/dir2/file1  target_directory

で、移動先のファイル名を以下のように生成する処理であることを押さえていればOK。ソースも読みやすいと思う。

target_directory/file1

mv.cソース解析:do_move()関数

main()関数は見終わったので、do_move()関数に進む。
do_move()関数も、条件分岐のif文に詳細なコメントが書かれている為非常に読みやすくなっていると思う。予めmanページの方でmv(1)の挙動をおさらいしておけば、特に難しい所は無いと思う。

ここではファイルシステムに関連する関数を使っている部分や、rename(2)周辺をまとめておくに留める。

まずこのifブロック。ifの直ぐ上にコメントが書かれている(本記事では省略)が、"-f"オプションが指定されていない場合&移動先(to)が存在する場合にプロンプトを表示するか判別+表示処理を行っている。access(2)では"F_OK"で存在チェックを行い、存在する場合は0を返す。

if (!fflg && !access(to, F_OK)) {
    int ask = 1;
    int ch;
    if (iflg) {
        /* 【1】"-i"指定時 */
        if (access(from, F_OK)) {
            /* 移動元(from)が存在しない場合は警告表示+return(1) */
        }
        (void)fprintf(stderr, "overwrite %s? ", to); /* 上書き確認プロンプト */
    } else if (stdin_ok && access(to, W_OK) && !stat(to, &sb)) {
        /* 【2】端末有り+"-i"未指定+移動先で書き込み権限が無い場合 */
        if (access(from, F_OK)) {
            /* 移動元(from)が存在しない場合は警告表示+return(1) */
        }
        strmode(sb.st_mode, modep);
        (void)fprintf(stderr, "override %s%s%s/%s for %s? ", ... );
        /* ↑"override r--r--r--  msakamoto/wheel for file2?"確認プロンプト */
    } else
        ask = 0; /* 【3】それ以外 */
    if (ask) {
        /* 【1】、【2】の場合はask=1のままなのでここでユーザ入力取得 */
        if ((ch = getchar()) != EOF && ch != '\n')
                while (getchar() != '\n'); /* 最初の1文字以外は読み飛ばし */
        if (ch != 'y' && ch != 'Y')
                return (0);
        /* 'y' or 'Y'なら処理続行 */
    }
}

そしていよいよrename(2)を実行する。

if (!rename(from, to))
    /* rename(2)が成功→0返す→"!"で反転→"return(0)"でdo_move()から抜ける */
    return (0);

/* 以下、rename(2)失敗時 */

if (errno != EXDEV) {
    /* EXDEV(mountポイントが異なる)以外のエラーの場合は警告表示+return(1) */
    warn("rename %s to %s", from, to);
    return (1);
}

/* 以下、mountポイントが異なる為ファイルシステムをまたいでの移動処理に続く */

以降は、前にmv(1)のmanのおさらいでも振り返ったが、以下のコマンドの組み合わせを実現していく。

rm -f destination_path && \
cp -PRp source_file destination_path && \
rm -rf source_file

まず次のifブロックで、rmdir(2)またはunlink(2)で上の"rm -f destination_path"を実現する。つまり、ターゲットが存在していれば削除してしまう。

if (!lstat(to, &sb)) {
    if ((S_ISDIR(sb.st_mode)) ? rmdir(to) : unlink(to)) {
/* 以下略 */

ここから stat(2) から lstat(2) になっている点にも注意しておきたい。lstat(2)の場合、ファイルがシンボリックリンクだとシンボリックリンク自身のstat構造体を取得する。stat(2)だとシンボリックリンクの参照を追った先のファイルのstat構造体を取得する。

この後移動元ファイルの存在をチェックし、

  1. テキストやバイナリなどの通常(regular)ファイル(=S_ISREGマクロ == true) → fascopy()を呼ぶ
  2. 通常ファイル以外(ディレクトリやリンク、デバイスファイルなど) → copy()を呼ぶ
if (lstat(from, &sb)) {
        warn("%s", from);
        return (1);
}
return (S_ISREG(sb.st_mode) ?
    fastcopy(from, to, &sb) : copy(from, to));

fastcopy(), copy() については割愛する。いずれも、以下の後半2コマンドを実現している。

cp -PRp source_file destination_path && \
rm -rf source_file

簡単に紹介すると、fastcopy()の場合は移動元・先ともにテキストやバイナリなどの通常ファイルなので、open(2)/read(2)/write(2)でデータをコピーした上でfchown(2)/fchmod(2)/fchflg(2)などファイル属性もコピーし、unlink(2)で移動元を削除している。
copy()の場合は対象が通常ファイル以外なのでfastcopy()のようにシステムコールだけで処理するのが厳しい。そこでcp, rmコマンドをvfork(2)で起動している。pathnames.hで定義されているcp,rmコマンドのパス名がここで使われる。

ここまででmv.cのソース解析が完了した。
「デーモン君のソース探検」ではメールスプールのロック機構に話題が進んでいるが、本記事ではそこまで探索しない。書籍の方を見てみて下さい。

今回のお題については、ここまで。



プレーンテキスト形式でダウンロード
現在のバージョン : 1
更新者: msakamoto-sf
更新日: 2010-01-14 01:10:33
md5:6d266791325f0ff36a26802765b96d69
sha1:709c1425f381fe65eadec4ee4ec5ed3010a66812
コメント
コメントを投稿するにはログインして下さい。