Linux

Linux: ハードリンクと inode

More than 3 years have passed since last update.

ハードリンクと inode についての説明です。

説明は、Linux 以外の UNIX/POSIX にもあてはまると思いますが、OS(特にファイルシステム)の実装等によっては異なる場合があるかも知れません。

説明内容は、Ubuntu Linux 14.04 で確認しています。
ファイルシステムは ext4 です。

$ uname -a
Linux linux01 3.13.0-39-generic #66-Ubuntu SMP Tue Oct 28 13:30:27 UTC 2014 x86_64 x86_64 x86_64 GNU/Linux

※ Mac OS X 10.10.1 (Yosemite) でも確認しました。

ハードリンクと inode

ファイルを2つ作って、「ls -l」でその情報を見てみます。

$ pwd
/home/user1

$ echo hello >file1.txt

$ echo hello >file2.txt

$ ls -l file?.txt
-rw-rw-r-- 1 user1 user1 6 12月 10 20:33 file1.txt
-rw-rw-r-- 1 user1 user1 6 12月 10 20:33 file2.txt

inode 番号を表示するには「ls」に「-i」オプションをつけます。
inode 番号は最左のカラムに表示されます。(下の例では「3809597」と「3809598」)

$ ls -li file?.txt
3809597 -rw-rw-r-- 1 user1 user1 6 12月 10 20:33 file1.txt
3809598 -rw-rw-r-- 1 user1 user1 6 12月 10 20:33 file2.txt

ハードリンク1.png

「inode」 は、いわばファイルの実体です。
メタ情報(ファイル種別、オーナ、グループ、パーミッション情報など)と、データ(ファイルの中身)から成り、「inode 番号」で識別されます。

ファイル名は、ファイルシステム階層に作られた、いわばラベルです。
ファイル名と inode の関係(上図では赤い線)が「リンク」です。
このリンクを、シンボリックリンク(ソフトリンク)との対比で「ハードリンク」とも呼びます。

「ls -l」の出力でパーミッション情報の右に表示される数値が「リンク数」です。
上の例では、file1.txt、file2.txt ともに「1」になっています。
普通のファイルは作成時にリンク数が 1 になります。

ファイルを消してみる

file2.txt を「rm」で消してみます。

$ rm -f file2.txt

$ ls -li file?.txt
3809597 -rw-rw-r-- 1 user1 user1 6 12月 10 20:33 file1.txt

ハードリンク2.png

「rm でファイルを消す」と表現しますが、実際に消すのはファイル名です。リンクも消滅します。
リンクが 0 になった(どこからも参照されなくなった) inode はシステムに回収されます。
これは、Java などのオブジェクト指向言語でリファレンスカウンタが 0 になったオブジェクトがGC(ガレージコレクション)されるのに似ています。

ハードリンクを作ってみる

file1.txt(のinode)に対してリンクを作ってみます。
リンクは「ln」で作りますが、シンボリックリンクをつくる「-s」はつけません。

$ ln file1.txt file3.txt

$ ls -li file?.txt
3809597 -rw-rw-r-- 2 user1 user1 6 12月 10 20:33 file1.txt
3809597 -rw-rw-r-- 2 user1 user1 6 12月 10 20:33 file3.txt

両ファイルの inode番号が同じで、リンク数が「2」であることに着目してください。
この時、ファイル名 file1.txt と file3.txt は、どちらも inode「3809597」の"本名"です。どちらかが本名で、どちらかが別名という関係ではありません。

ハードリンク3.png

inode を共有しているので、file1.txt の内容を更新すると両ファイルに反映されます。

$ echo world >>file1.txt

$ cat file1.txt
hello
world

$ cat file3.txt
hello
world

file3.txt の inode 情報を変更しても両ファイルに反映されます。

$ chmod o-r file3.txt

$ ls -li file?.txt
3809597 -rw-rw---- 2 user1 user1 13 12月 10 20:38 file1.txt
3809597 -rw-rw---- 2 user1 user1 13 12月 10 20:38 file3.txt

リンクされているファイルを消してみる

file1.txt を消してみます。
file3.txt のリンク数が「1」になります。

$ rm file1.txt

$ ls -li file?.txt
3809597 -rw-rw---- 1 user1 user1 13 12月 10 20:38 file3.txt

ハードリンク4.png

inode にはリンクが残っているので、システムに回収されません。

mv してみる

「mv」で file3.txt の名前を file4.txt に変えてみます。
ファイル名が変わってもリンクは保持されます。なので、inode 番号は変わりません。

$ mv file3.txt file4.txt

$ ls -li file?.txt
3809597 -rw-rw---- 1 user1 user1 13 12月 10 20:38 file4.txt

ハードリンク5.png

以下のような場合、上と下の記述では微妙に効果が異なります。

$ sed 's/foo/bar/' orig.file >new.file
$ mv -f new.file orig.file  
                      #  new.file(新しく作られたinode)が orig.file になる

$ sed 's/foo/bar/' orig.file >new.file
$ cat new.file >orig.file
                      # orig.file(元からあるinode)のデータが new.file のもので更新される

元のファイルのパーミッション情報を保持したい等の場合は問題になります。

cp してみる

「cp」で file4.txt を file5.txt にコピーします。
「cp」は新しくファイル(inode)を作って、元のファイルの内容(データ)をコピーします。

$ cp file4.txt file5.txt

$ ls -li file?.txt
3809597 -rw-rw---- 1 user1 user1 13 12月 10 20:38 file4.txt
3809598 -rw-rw---- 1 user1 user1 13 12月 10 20:51 file5.txt

ハードリンク6.png

シンボリックリンクを作ってみる

「ln -s」で file4.txt のシンボリックリンクを作ってみます。

$ ln -s file4.txt file6.txt

$ ls -li file[46].txt
3809597 -rw-rw---- 1 user1 user1 13 12月 10 20:38 file4.txt
3809599 lrwxrwxrwx 1 user1 user1  9 12月 10 20:55 file6.txt -> file4.txt

inode 番号「3809599」の新しいファイルができています。ファイルの種別(「lrwxrwxrwx」の1文字目)が「l」になっています。(シンボリックリンクであることを示す)

ハードリンク7.png

シンボリックリンクファイルはリンク先情報を「ファイルパス」として持っています。
inode番号ではありません。
リンク先の inode が変わったり、ファイルが削除されてもシンボリックリンクファイルには無関係です。(もちろん、リンク先にアクセスしようとした時にファイルが無いなどの場合は、その時点でエラーになります。)

inode 番号の一意性

inode はファイルシステム単位で管理されます。
ここでいうファイルシステムは、一口に言ってマウントされる単位です。(「df」で表示される一行と考えてください)

$ df -H
Filesystem      Size  Used Avail Use% Mounted on
/dev/sda4        68G   20G   45G  30% /
none            4.1k     0  4.1k   0% /sys/fs/cgroup
udev             17G   13k   17G   1% /dev
tmpfs           2.2G  1.4M  2.2G   1% /tmp
tmpfs           3.4G  898k  3.4G   1% /run
none            5.3M     0  5.3M   0% /run/lock
none             17G   14M   17G   1% /run/shm
none            105M   46k  105M   1% /run/user
/dev/sda1       534M   12M  523M   3% /boot/efi
/dev/sdb6       275G   55G  221G  20% /home1

inode 番号はファイルシステム単位でユニーク(一意)になります。

このような事情からだと思いますが、ハードリンクはファイルシステムを越えて作成することはできません。
対して、シンボリックリンクは「ファイルパス」でリンク先情報を持っているので、このような制約はありません。

$ cd /tmp

$ ln ~/file4.txt fileX.txt
ln: `fileX.txt' から `/home/user1/file4.txt' へのハードリンクの作成に失敗しました: 無効なクロスデバイスリンクです

ハードリンク10.png

異なるファイルシステムへの mv

同様の理由から、異なるファイルシステムへの mv もできません。
。。。と思いきや、やってみるとエラーにはならないと思います。
昔の UNIX ではエラーになったのですが、Linux で使われている「GNU mv」や最近の mv コマンドは、このような場合には「コピーして元のファイルを削除する」動作をします。

inode に3つハードリンク (A,B,C) を作り、B を同じ、C を異なるファイルシステムに mv して、B, C それぞれを更新し A の変化を観察してみると分かると思います。ご興味があればやってみてください。(ここでは割愛します。)

ディレクトリのハードリンク

ディレクトリを作ってみます。
「ls」で、ディレクトリ自身の情報を表示する場合は「-d」をつけます。

$ cd ~
$ mkdir dir1

$ ls -lid dir1
3809601 drwxrwxr-x 2 user1 user1 4096 12月 10 23:27 dir1

普通のファイルは作成時のリンク数が 1 でしたが、ディレクトリは「2」です。
もうひとつのリンクは「dir1/.」です。

$ ls -lid dir1/.
3809601 drwxrwxr-x 2 user1 user1 4096 12月 10 23:27 dir1/.

ディレクトリは元の(親)ディレクトリにある名前と自分自身にある「.」の2つの名前をもっているので、作成時のリンク数が 2 になります。

ハードリンク8.png

サブディレクトリを作ってみる

dir1/ にサブディレクトリを作ってみます。

$ mkdir dir1/sub1

$ ls -lid dir1 dir1/.
3809601 drwxrwxr-x 3 user1 user1 4096 12月 10 23:28 dir1
3809601 drwxrwxr-x 3 user1 user1 4096 12月 10 23:28 dir1/.

dir1/ のリンク数が 3 に増えました。新しく作られたリンクは「dir1/sub1/..」です。

$ ls -lid dir1/sub1/..
3809601 drwxrwxr-x 3 user1 user1 4096 12月 10 23:28 dir1/sub1/..

ディレクリが作られると、親ディレクトリを示す「..」が作られます。
なので、サブディレクトリを作ると親ディレクトリではリンク数がその分増えます。
(もちろん、サブディレクトリを消すとその分減ります。)

ハードリンク9.png

ディレクトリのリンク数と直下のディレクトリの数

(以下の話は、ファイルシステムの実装次第と思いますが、大抵そうです)
普通のファイルと異なり、ディレクトリのハードリンクは、ユーザが「ln」で作成することができせん。

ディレクトリのリンク数は、作成時は 2 で、増減するのはサブディレクトリが作られた時のみです。
ということは、

  • 直下のディレクトリ数 = (リンク数 - 2)

になります。
ご興味があれば、お試しください。

名前が「.」で始まるファイルを「ls」で表示するには「-a」をつけます。
また、この時「.」(ディレクトリ自身)と「..」(親ディレクトリ)も表示されます。
直下のディレクトリ (と「.」と「..」)を表示するコマンドラインは以下のようになります。

$ ls -la dir1/ | grep ^d

$ ls -la dir1/ | grep ^d | wc -l       # 行数を数える場合は「wc -l」を使う

ルートディレクトリの「..」

ルートディレクトリには親ディレクトリがないから、上の話は当てはまらないでしょうか?
確かに、親ディレクトリにエントリがありませんが、ルートディレクトリは「..」が自分自身へのリンクになっています。

$ cd /..              # ルートディレクトリに cd される
$ pwd
/

$ ls -lid / /. /..    # 「/」と「/.」と「/..」は同じ inode 番号
2 drwxr-xr-x 24 root root 4096 11月 24 09:52 /
2 drwxr-xr-x 24 root root 4096 11月 24 09:52 /.
2 drwxr-xr-x 24 root root 4096 11月 24 09:52 /..

なので、「直下のディレクトリ数 = (リンク数 - 2)」は当てはまります。

参考

参考書籍

(参考書籍といっても、現在は手元にありません。ですが、昔はこの本で勉強しました)

  • The UNIX Super Text <上巻> (技術評論社 1992/08)
    • ISBN-10: 4874085059
    • ISBN-13: 978-4874085059

改定版が出ていますが、お勧めはこの旧版の上巻です。
UNIX の基礎的な事柄が書かれています。現在でも参考になると思います。
下巻がありますが、こちらは(出版当時の)発展的話題が中心です。
改定版は新しい話題が盛り込まれた代わりに、この手の話題が割愛されているようです。