新卒で就職したばかりの頃、UNIX/Linuxの研修で習ったような気がするけど忘れてしまっていた話を改めて調べたのでまとめておきます。
ちなみに、この記事はMacで検証しています。
概要説明
Windowsでは特定のファイルやフォルダーを別のフォルダーから開けるようにするショートカットという仕組みがあります。Macを含むUNIX系OSにも同様の仕組みがあり、それがリンクです。(正確には、ディスク上のファイルの実体と、ファイルパスを繋げる仕組みをリンクと言います。)
ここで、Windowsと違うのは、UNIX系のリンクにはハードリンクとシンボリックリンクの2種類がある点です。(実はWindowsでもハードリンクができるという話も聞きますが、あまり一般的ではないと思います。)
この後、実際にファイルを操作しながら具体的な挙動の違いを見ていきますが、ざくっと二つのリンクの違いを説明すると次のようになります。
ハードリンクは、一つの実体を持つファイルを表現するパスを複数作る方法です。
マニュアルにも、ハードリンクでは、リンクと元のファイルの区別はつかないと書かれています。
man ln から抜粋。
> A hard link to a file is indistinguishable from the original directory entry
それに対してシンボリックリンクは、リンク先のファイルへの参照を含みます。Windowsのショートカットに近いのはこちらです。
わかりにくいですね。
例えば、 file01.txt というファイルがあったとします。これは当然、ディスク上にあるデータが存在し、それに対して、file01.txt という名前でアクセスできることを意味します。
これに対して、file01.txt を file02.txtに「ハードリンク」したとします。すると、そのディスク上の同じデータが file02.txt というファイル名でもアクセスできるようになります。
一方で、file01.txt を file03.txt に「シンボリックリンク」したとします。すると、file03.txt は、file01.txt というパスへのリンクになります。その結果、file03.txtにアクセスしようとすると、file01.txtにアクセスすることになり、結局同じ実体のファイルにアクセスできることになります。
2種類のリンクの作成
まだわかりにくいのでやってみましょう。実際にファイルとリンクを作ってみます。
まず、サンプルのデータから。
# 検証用ディレクトリを作って移動
$ mkdir linktest
$ cd linktest
# サンプルデータ作成
$ echo "サンプルファイル1行目" > file01.txt
# 作成したファイルの情報を確認
$ ls -li
total 8
8795955 -rw-r--r-- 1 {owner} {group} 34 6 12 17:51 file01.txt
ls コマンドで作ったデータを見るときに、-iオプションをつけてiノード番号も表示させました(先頭の8795955がそれ)。iノードというのは、ファイルの属性が記録されているデータのことで、iノード番号というのはそのデータについている番号のことです。このデータは実体のファイルと対応して存在しているので、実質的にディスク上のファイルの実体と対応している番号だと考えて大丈夫です。iノード番号が同じなら同じファイルを表しています。
さて、ここからリンクを作っていきましょう。リンクは ln コマンドで作ります。文法は次のとおりです。
# ハードリンクを作成する
$ ln リンク先 リンク名
# シンボリックリンクを作成する
$ ln -s リンク先 リンク名
やってみます。
# ハード/シンボリックでそれぞれリンクを作成する
$ ln file01.txt file02_hard.txt
$ ln -s file01.txt file03_symbolic.txt
# 確認
$ ls -li
total 16
8795955 -rw-r--r-- 2 {owner} {group} 34 6 12 17:51 file01.txt
8795955 -rw-r--r-- 2 {owner} {group} 34 6 12 17:51 file02_hard.txt
8796478 lrwxr-xr-x 1 {owner} {group} 10 6 12 18:03 file03_symbolic.txt -> file01.txt
はい、できました。
ls の結果を見ていきましょう。まずハードリンクの方(file02_hard.txt)です。
着目するべきは、最初のiノード番号で、元のファイルと全く同じになっています。これはfile02_hard.txtがfile01.txtと全く同じ実体ファイルにリンクしていること意味しており、同じデータに対して2個ファイル名があるような状態にになっています。ファイルサイズも同じ34バイトですね。
権限(-rw-r–r–) の後ろに 2 という数字がありますが、実はこれ、そのファイルへのリンク数です。元々1だったのがハードリンクを作成したことで2になっています。そして、ここからわかるのですが、シンボリックリンクの方はノーカウントです。(カウントされるなら3になるはず。) ちなみに、この数字がリンク数だっていう情報は、lsのマニュアル(man ls)のThe Long Format ってセクションに書かれています。
次は、シンボリックリンクの方(file03_symbolic.txt)を見ていきましょう。 -> で、元ファイルへのパスが書かれており、いかにもリンクって感じの表示になっていますね。
権限の最初にlがついて、lrwxr-xr-x となっていますが、このlはシンボリックリンクであることを示しています。そして、iノード番号は元のファイルと違うものが振られています。また、ファイルサイズも異なっていますね。file01.txtへの参照だけなのでサイズが小さいです。
当然ですが、全部同じデータを見てるので、中身を表示したら一致します。
$ cat file01.txt
サンプルファイル1行目
$ cat file02_hard.txt
サンプルファイル1行目
$ cat file03_symbolic.txt
サンプルファイル1行目
リンク元ファイルに変更が発生した場合の挙動
ここから、元ファイルに変更が発生した時のハードリンクとシンボリックリンクの挙動の違いを見ていきます。
まず、元ファイルに修正が入った場合、これはどちらのリンク方法でもそれぞれ反映されます。同じファイル見てるだけだからですね。1行追記してみてみましょう。
$ echo "サンプルファイル2行目" >> file01.txt
$ ls -li
total 16
8795955 -rw-r--r-- 2 {owner} {group} 64 6 12 18:23 file01.txt
8795955 -rw-r--r-- 2 {owner} {group} 64 6 12 18:23 file02_hard.txt
8796478 lrwxr-xr-x 1 {owner} {group} 10 6 12 18:03 file03_symbolic.txt -> file01.txt
# 念のため中身も見る
$ cat file01.txt
サンプルファイル1行目
サンプルファイル2行目
$ cat file02_hard.txt
サンプルファイル1行目
サンプルファイル2行目
$ cat file03_symbolic.txt
サンプルファイル1行目
サンプルファイル2行目
はい、 echo で追記したのはfile01.txtだけですが、3ファイルとも2行目が増えましたね。ファイルサイズが増えたのはfile01.txt/ file02_hard.txt だけで、file03_symbolic.txtはそのままなので想定通りです。ここまでは、ハードリンク、シンボリックリンク共に違いはありません。
ここからが差が発生することです。もし、このfile01.txtがリネームされたり削除されたりして、そのパスに存在しなくなったらどうなるでしょうか。
試しに消してみます。
$ rm file01.txt
# 1ファイル消えて2ファイル残っている。
$ ls -li
total 8
8795955 -rw-r--r-- 1 {owner} {group} 64 6 12 18:23 file02_hard.txt
8796478 lrwxr-xr-x 1 {owner} {group} 10 6 12 18:03 file03_symbolic.txt -> file01.txt
# ハードリンクの方は元ファイルがなくなっていても開ける
$ cat file02_hard.txt
サンプルファイル1行目
サンプルファイル2行目
# シンボリックリンクの方はリンク先ファイルがなくなると開けない
$ cat file03_symbolic.txt
cat: file03_symbolic.txt: No such file or directory
はい、ここで大きな差が生じました。元ファイルが消えてしまったのですが、ハードリンクのファイルは、元ファイルのパスと関係なくディスク上のファイルの実体にリンクされていたので元ファイルのパスが消えてしまっても問題なく開くことができます。(消したと思ったファイルが残ってる、というのがデメリットになるケースもありそうですが。)
ちなみに、ls -li の結果のリンク数は1個になっていますね。
一方で、シンボリックリンクの方は、元ファイルへの参照しか情報を持っていなかったので、元ファイルのパスがなくなってしまうとデータにアクセスができなくなってしまっています。
さて、次はその逆の操作です。元々存在してたファイルと同じパスで、再度ファイルが作成されたらどうなるでしょうか。この時の挙動もそれぞれ異なります。やってみましょう。
# あたらめてリンク先ファイル作成
$ echo "新規作成したファイル1行目" > file01.txt
$ ls -li
total 16
8798109 -rw-r--r-- 1 {owner} {group} 38 6 12 18:37 file01.txt
8795955 -rw-r--r-- 1 {owner} {group} 64 6 12 18:23 file02_hard.txt
8796478 lrwxr-xr-x 1 {owner} {group} 10 6 12 18:03 file03_symbolic.txt -> file01.txt
新しい file01.txt は 元のと異なるiノード番号で作成されましたね。これは要するに、ファイル名は同じだけど実データとしては元々存在してたfile01.txtとは異なるファイルであることを意味します。そいて、よく見ていただきたいのは、file01.txt と file02_hard.txt のファイルサイズが違うことです。もはや同じファイルは指し示しておらず、file02_hard.txtは元々のファイルにリンクされていますね。それぞれ開くと明らかです。
$ cat file01.txt
新規作成したファイル1行目
$ cat file02_hard.txt
サンプルファイル1行目
サンプルファイル2行目
$ cat file03_symbolic.txt
新規作成したファイル1行目
はい、ハードリンクしていたfile02_hard.txtはもう完全にfile01.txtとは別ファイルになってしまいました。
一方で、リンク先がなくなって開けなくなっていたシンボリックリンクの方(file03_symbolic.txt)は、同じパスのファイルができたら自動的にそこにリンクされてfile01.txtと同じデータが参照できるようになりました。
ハードリンク、シンボリックリンクのどちらを選ぶかは、これらの挙動を踏まえて決めるのが良いと思います。
その他の違い
以上で元ファイルの編集に関するリンクごとの挙動の違いを書いて来ましたが、他にも少し違いがありますのでまとめておきます。
まず、ファイルではなくディレクトリに対しては、ハードリンクは作成できず、シンボリックリンクのみ作成できます。
# 実験用ディレクトリ作成
$ mkdir subfolder01
# ハードリンクは作れない
$ ln subfolder01 subfolder02
ln: subfolder01: Is a directory
# シンボリックリンクは作れる
$ ln -s subfolder01 subfolder02
$ ls -li
8799037 drwxr-xr-x 2 {owner} {group} 64 6 12 18:46 subfolder01
8799038 lrwxr-xr-x 1 {owner} {group} 11 6 12 18:46 subfolder02 -> subfolder01
また、別の違いとして、ハードリンクはパーティションなどファイルシステムを跨いで作ることはできないというものもあります。ハードリンクを作れるのは同じパーティション内のみだけです。理由はiノード番号の管理が違うからだそうです。
一方でシンボリックリンクはどこでも作れます。
あとは、ハードリンク(元のファイルパスも含む)を全部消すと、ファイル自体が削除されてしまいますが、シンボリックリンクは消しても元のファイルに影響がないとか細々した違いがあります。
ハードリンクしているファイルを探す方法
ls コマンドでそのファイルへのリンク数が表示されますが、 どこからリンクされているか探したくなることはあると思います。そのファイルを確実に消したい時などは一通り洗い出す必要ありますし。
良い探し方を調べていたのですが、今のところ、find コマンドで iノード番号を調べて検索する以外になさそうです。-inum 引数で指定できます。
今やっているサンプルは同じディレクトリ配下なので楽勝ですが、遠いパスに作っていたらかなり広範囲をfindで探さないといけないですね。
# 実験のためハードリンクを作成する
$ ln file01.txt file04.txt
# iノード番号を調べる
$ ls -i file01.txt
8798109 file01.txt
# find コマンドで該当のiノード番号を持つファイルを探す
$ find . -inum 8798109
./file04.txt
./file01.txt
余談: 何が発端でこれを調べていたのか
なんで今になってこんなのを調べているのかというと、実は個人的に開発してるプロジェクトがあって、そのコード管理に使いたかったからです。
ほとんどのソースコードはプロジェクトのディレクトリ配下に格納されていてそこでgit管理されているのですが、ごく一部/etc/の配下とか、ホームディレクトリのドット付き隠しフォルダの下とかで作成する必要上がります。
これらをどうやって管理しようかなと思ったときに、プロジェクトのディレクトリ内に実体ファイルを作って、それらの本来の配置場所にリンクを貼れば単一のリポジトリで管理できるじゃないかと思いつきました。で、その時のリンク方法が2種類あったのでどっちがいいのかというのが発端になります。
この用途だと、git管理してるファイルと、稼働してるファイルを確実に同期させたいのでシンボリックリンクの方が良さそうですね。
まぁ、その他、brewで入れたソフトウェアとかMeCabの辞書のファイルたちとか自分の環境内でシンボリックリンクで稼働しているファイルはいろいろ存在し、これらについても理解を深められたのは良かったです。