はじめに
Linux(というかPOSIX)ではファイルを移動するときにmvコマンドを使います。
このmvコマンドはファイルのリネームもできます。POSIXにはrn(ReName)のようなコマンドはありません。1
ところで何故mvコマンドでリネームができるのでしょうか?初心者の人に聞かれた場合に適切な回答ができるでしょうか。というわけで(聞かれたわけではありませんが)自分が答えるならこうするなというものを段階を踏んで説明していこうと思います。
コマンドラインでの説明
中身に入る前にまずコマンドラインで考えてみましょう。
「ディレクトリAにあるファイルaをディレクトリBに移動する」とします。この場合、移動した後のファイルパスはB/aとなります。コマンドで言うと以下のようになります。
$ mv A/a B/
また、mvコマンドでは移動と同時に名前を変えることができます。より正確には「ディレクトリBにファイルbという名前で移動する」とすることができます。つまり以下のような感じです。
$ mv A/a B/b
移動先を「ディレクトリB」ではなく「ディレクトリA」としましょう。つまり、「ディレクトリAにあるファイルaをディレクトリAにファイルbという名前で移動する」となります。
$ mv A/a A/b
「ディレクトリA」ではなくカレントディレクトリにあるファイルだとしましょう。
$ mv a b
はい、「ファイルa」を「ファイルb」に移動する(リネームする)ことができました。
「移動する」とはどういうことか
ところで、mvコマンドは1Gバイトのファイルでも一瞬で移動できます(できない場合もありますがそれについては後で)。「移動」とは結局何をしているのでしょうか。ここで出てくるのがファイル実体、iノード、ディレクトリエントリの概念です。
「ファイル」という入れ物があった場合、その中には「ファイル名」「ファイルの中身」「属性(作成日とか権限とか)」がすべて入っているわけではありません。「ファイル名」があり、「ファイル名」が「属性」を参照し、「属性」情報の一部として「ファイルの中身」がどこにあるかというリンクがあります。図で書くとこんな感じです。「属性」と言ったものがiノードです。2
移動(リネーム)する場合、変更するのは「ファイル名」に関する情報のみです。この「ファイル名」に関する情報は「ディレクトリ」に対応する「ファイル」の「ファイルの中身」に入っておりそれを書き換えます。ディレクトリ内に入ってるファイルの量によって書き換えに時間はかかるかもしれませんが少なくとも1Gバイトの「ファイルの中身」を読み書きするよりは速そうです。
ハードリンク
話は移動(リネーム)から逸れますが何故、このような三段構成になっているのでしょうか。そこで登場するのがハードリンクです。
Linuxではある一つのファイル実体に対して複数のファイル名を対応させることができます。つまり、こんな感じです。
「ファイルA」を実行するとプログラムには「ファイルA」という名前が渡ります。
「ファイルB」を実行するとプログラムには「ファイルB」という名前が渡ります。
当たり前のように聞こえるかもしれませんがここで重要なのは「ファイルの中身」は一つだけということです。つまり、関連した動作を行う二つのプログラムが「ファイルの中身」二つディスクを占有しないで済むということです。プログラムは呼ばれた名前から行う動作を決定します。この仕組みは実際にgzipとgunzipなどで使われています。3
mvのソースを見てみる
概念がわかったところでmvコマンドの中身を覗いてみましょう。Linuxであればcoreutilsのmvが使われているでしょう。バージョンは一応固定したものをリンクしておきます。
mv a b
とした場合(a, bは通常のファイルとする)、大まかな流れは以下のようになっています。
- main関数
- movefile関数
- do_move関数
- copy関数(copy.cへ続く)
え?copy?
copy関数に踏み込む前にdo_moveの続きを確認しましょう。まずcopy関数は以下のように呼ばれています。
bool ok = copy (source, dest, false, x, ©_into_self, &rename_succeeded);
copy_into_selfはcopy呼び出し後の処理のところにコメントで書いてありますがどうにも変な風に移動しようした場合な感じなので無視します。
rename_succeededが真ならばもうやることはないということになっています。え?rename?(しつこい)
copy_into_self、rename_succeededいずれでもない場合はSOURCE、移動元を削除しています。状況はコメントに書かれていますがこれについてはまた後から説明します。
今のところわかったこととしては以下となります。
- copyといいつつrenameしてる(場合もある)らしい。
- renameできなかった場合はcopyしているらしい。さらにmoveなので元はrmしているようだ。
ではcopy関数を見てみましょう。
copy_internal
copy関数は入口ですぐにcopy_internal関数が呼ばれます。copy_internal関数は1000行近くあり大量の条件分岐がありますが今回状況(通常ファイルのリネーム)でいうと結局以下の行が実行されます。
if (rename (src_name, dst_name) == 0)
このrenameはcoreutilsに書かれている関数ではなくシステムコールです。
rename() はファイルの名前を変更し、必要ならばディレクトリ間の移動を行なう。
ということでmvは究極的にはシステムコールを呼んでファイルの名前を変えている(ディレクトリエントリを変更している)だけということになります。あ、実際には移動先が存在するか、存在する場合はディレクトリなのかファイルなのかとかいろんなことはしていますが。
renameがうまくいかないとき
さて、以上で終了、とはいきません。いやまあ同じディレクトリ内でファイル名を変える場合の話は終わりですがせっかくなのでもう少し見ていきましょう。
copy_internal関数はポインタとして渡されたrename_succeededにrenameが成功したかを返します(まんまですが)。では、失敗する場合とはどのような場合でしょうか。
いろいろ考えられますがrenameシステムコールのman、エラーセクションを見てみると
EXDEV
oldpath と newpath が同じマウントされたファイルシステムに存在しない。
とあります。つまり、別のファイルシステムにmvしようとした場合はEXDEVとなり、copy_internal関数では以降コピー処理が行われています(EXDEV以外はエラー終了です)
copy_internal関数の結果を受けて、do_move関数はrename_succeededじゃない場合(別ファイルシステムにmvした場合)移動元を削除しています。どうにも処理が回りくどいですがコピー処理を使い回しているためでしょう。(と言いつつmoveも考慮したコピー処理になっていますが)
ところで、何故一回renameして失敗したらコピーしているのでしょうか。別の言い方をするとrenameしないでも移動先が別ファイルシステムであることを認識して初めからコピーしてしまえばいいのではないでしょうか。
多分できそうな気がしますがおそらく「とりあえずrenameしてみてEXDEVになったらコピーする方が(プログラムで)チェックする手間がなくて速い」ということなのかなと思います。
まとめ
さて話をまとめます。
- コマンドラインだけで言うと「同じディレクトリに違う名前で移動する」ということである。
- mvコマンドがやっているのはrenameシステムコールの呼び出しである(別ディレクトリにもrenameできる)。その際に出てくる話がファイル実体、iノード、ディレクトリエントリである。
- 別のファイルシステムにはrenameできない。その場合はコピーした後に消している。
まあ、初心者の人にここまで深い話をしたら挫折しそうなので一つ目ぐらいで止めておくべきでしょうね(笑)