unix は幾つかの醜い面を持っている。(もちろん Windows に比べると遥かに美しい世界ではある。)
その中の1つが SYMLINK である。例えば僕の Mac (unix の1変種である)では
-bash$ ls -l /usr/bin/vi lrwxr-xr-x 1 root wheel 3 11 18 2014 /usr/bin/vi -> vim -bash$
図1: vi
SYMLINK が相手だとアウトプットのフィールドがイレギュラーである。このようなイレギュラーなアウトプットが含まれていると、これを何かのプログラムのインプットとして使いたい時に面倒なことになる。unix の良さは、プログラムのアウトプットが、他のプログラムのインプットとなるように設計しやすい点にあるのだが、これを台無しにするのである。
このアウトプットは /usr/bin/vi
の実体は vim
であることを伝えている。従って /usr/bin/vi
について知りたければ vim
を見なくてはならない。この vim
の場所は、SYMLINK が作成された vi
の場所と同じである。そこで vim
の属性を
-bash$ ls -l /usr/bin/vim -rwxr-xr-x 1 root wheel 1530240 6 24 2016 /usr/bin/vim -bash$
図2: vim
で手に入れることができる。これは舞台裏における vi
の顔である。表舞台では、こうした仕掛けが隠されて vi
は一人前の役者となる。表舞台の様子は ls
の L
オプションで表示される。
-bash$ ls -lL /usr/bin/vi -rwxr-xr-x 1 root wheel 1530240 6 24 2016 /usr/bin/vi -bash$
図3: vi
仕組みは簡単だ。/usr/bin/vi
の実体は小さなファイルで、この場合には
ls -l /usr/bin/vi
/usr/bin/vi -> vim
vim
を見なさいと言っている。/usr/bin
に置かれた案内標識なので、/usr/bin
の中で vim
を探すことになる。
SYMLINK は仕組みから言えばファイルなのだが、以下では単にファイルと言えば SYMLINK を含めないことにする。特性があまりにも他のファイルと異なっているからである。
SYMLINK の基本的な作り方は
ln -s bar foo
foo
にはパス名を書く。絶対パスでもよいし、作業場所からの相対パスでもよい。他方 bar
は何でもよい。この部分には案内標識に書き込む内容を書く。例えば "I love you"
だって許されるが、間違った案内標識は混乱を招くので、十分に気を付ける必要がある。
観客が見る世界、すなわち、表舞台で大切な点は次の通りである: /usr/bin/vi
が SYMLINK であることが隠され、ファイルと同じ扱いを受けている。すなわち、/usr/bin/vi
をコマンドとみなしても不都合は生じない。
表舞台では SYMLINK
の持つ醜さを隠してしまう。表舞台は幸せな世界のように見えるが... ここは仮装舞台であって、奇妙奇天烈な舞台になり得るのである。
案内標識を辿ると、先にはまた案内標識があり、それに従って辿るとまた案内標識があり... 結局最終的には次の4つのケースがあり得る。
(a) ファイルに行き着く
(b) ディレクトリに行き着く
(c) 何も無い (以下 broken link と言う)
(d) 循環している (以下 cyclic link と言う)
ln
は多数のオプションを備えているが、これ以外の形式は必要の無いものである。ところで筆者は時々 bar
と foo
の関係が分からなくなる。そこで link の持つ英語の語感をこの際に整理しておく。コマンドを英語に直すと、link bar to foo
bar
を linked file、foo
を linked-to file と表現している。日本人には分かりにくいが bar
と foo
の関係は対等ではなく、ちょうどadd bar to foo
link a file to the directory
link a directory to the file
s
オプションのない link
に現れている。
正しく作られた SYMLINK で、ケース(a)の例である。
-bash$ ls -l total 32 lrwxr-xr-x 1 arisawa staff 5 4 17 11:55 file1 -> file2 lrwxr-xr-x 1 arisawa staff 5 4 17 12:39 file2 -> file3 -rw-r--r-- 1 arisawa staff 2231 4 17 12:39 file3 -rw-r--r--@ 1 arisawa staff 76 4 17 12:40 memo.txt -bash$ ls -Ll total 32 -rw-r--r-- 1 arisawa staff 2231 4 17 12:39 file1 -rw-r--r-- 1 arisawa staff 2231 4 17 12:39 file2 -rw-r--r-- 1 arisawa staff 2231 4 17 12:39 file3 -rw-r--r--@ 1 arisawa staff 76 4 17 12:40 memo.txt -bash$
file1
も file2
も file3
を表している正しいリンクである。
簡単な cyclic link
-bash$ ls -l total 24 lrwxr-xr-x 1 arisawa staff 5 4 17 11:55 file1 -> file2 lrwxr-xr-x 1 arisawa staff 5 4 17 11:53 file2 -> file1 -rw-r--r--@ 1 arisawa staff 68 4 17 12:00 memo.txt -bash$ ls -Ll total 24 -rw-r--r--@ 1 arisawa staff 68 4 17 12:00 memo.txt -bash$
ls
コマンドは L
オプションの下では cyclick link になっているファイルは表示していない。これは正しい考え方である。
長い cyclic link の例
-bash$ ls -l total 32 lrwxr-xr-x 1 arisawa staff 5 4 17 11:55 file1 -> file2 lrwxr-xr-x 1 arisawa staff 5 4 17 12:39 file2 -> file3 lrwxr-xr-x 1 arisawa staff 5 4 17 13:05 file3 -> file1 -rw-r--r--@ 1 arisawa staff 114 4 17 13:05 memo.txt -bash$ ls -Ll total 32 -rw-r--r--@ 1 arisawa staff 114 4 17 13:05 memo.txt -bash$
ls
は正しく働いている。
broken link の例
-bash$ ls -l total 32 lrwxr-xr-x 1 arisawa staff 5 4 18 13:24 file1 -> file2 lrwxr-xr-x 1 arisawa staff 5 4 18 13:24 file2 -> file3 lrwxr-xr-x 1 arisawa staff 6 4 18 13:25 file3 -> blabla -rw-r--r--@ 1 arisawa staff 114 4 18 13:21 memo.txt -bash$ ls -Ll total 32 -rw-r--r--@ 1 arisawa staff 114 4 18 13:21 memo.txt -bash$
ls
は L
オプションの下では broken link は表示しない。これは正しい考え方である。
次に cyclick link ではないが、下図に示すようにディレクトリを循環的に繋ぐリンクを調べる。
これはケース(b)の例であるが、素直ではない。ディレクトリへのリンクの場合には、このように意地悪くリンクを貼ると、表舞台ではあたかも無限に深いディレクトリであるかのように見せることが可能になる。
-bash$ ls -l total 8 drwxr-xr-x 6 arisawa staff 204 4 18 16:15 dir1 drwxr-xr-x 6 arisawa staff 204 4 18 16:15 dir2 -rw-r--r--@ 1 arisawa staff 123 4 17 18:36 memo.txt -bash$ ls -l dir1 total 8 -rw-r--r-- 1 arisawa staff 0 4 18 16:15 bar1 -rw-r--r-- 1 arisawa staff 0 4 18 16:15 foo1 -rw-r--r-- 1 arisawa staff 0 4 18 16:15 hoge1 lrwxr-xr-x 1 arisawa staff 7 4 17 18:36 ln1 -> ../dir2 -bash$ ls -l dir2 total 8 -rw-r--r-- 1 arisawa staff 0 4 18 16:15 bar2 -rw-r--r-- 1 arisawa staff 0 4 18 16:15 foo2 -rw-r--r-- 1 arisawa staff 0 4 18 16:15 hohe2 lrwxr-xr-x 1 arisawa staff 7 4 17 18:36 ln2 -> ../dir1 -bash$ ls -lL dir1 total 0 -rw-r--r-- 1 arisawa staff 0 4 18 16:15 bar1 -rw-r--r-- 1 arisawa staff 0 4 18 16:15 foo1 -rw-r--r-- 1 arisawa staff 0 4 18 16:15 hoge1 drwxr-xr-x 6 arisawa staff 204 4 18 16:15 ln1 -bash$ ls -lL dir2 total 0 -rw-r--r-- 1 arisawa staff 0 4 18 16:15 bar2 -rw-r--r-- 1 arisawa staff 0 4 18 16:15 foo2 -rw-r--r-- 1 arisawa staff 0 4 18 16:15 hohe2 drwxr-xr-x 6 arisawa staff 204 4 18 16:15 ln2 -bash$ ls -lL dir1/ln1 total 0 -rw-r--r-- 1 arisawa staff 0 4 18 16:15 bar2 -rw-r--r-- 1 arisawa staff 0 4 18 16:15 foo2 -rw-r--r-- 1 arisawa staff 0 4 18 16:15 hohe2 drwxr-xr-x 6 arisawa staff 204 4 18 16:15 ln2 -bash$ ls -lL dir2/ln2 total 0 -rw-r--r-- 1 arisawa staff 0 4 18 16:15 bar1 -rw-r--r-- 1 arisawa staff 0 4 18 16:15 foo1 -rw-r--r-- 1 arisawa staff 0 4 18 16:15 hoge1 drwxr-xr-x 6 arisawa staff 204 4 18 16:15 ln1 -bash$ ls -lL dir1/ln1/ln2/ln1/ln2 total 0 -rw-r--r-- 1 arisawa staff 0 4 18 16:15 bar1 -rw-r--r-- 1 arisawa staff 0 4 18 16:15 foo1 -rw-r--r-- 1 arisawa staff 0 4 18 16:15 hoge1 drwxr-xr-x 6 arisawa staff 204 4 18 16:15 ln1 -bash$
ディレクトリを隈なく調べたいことがしばしば発生して、それに役に立つツールが存在する。例えば R
オプションを添えた ls
、find
などである。しかし、SYMLINK の下ではそうしたツールの作成が難しくなる。特に難しくなるのは L
オプション(follow link option)をつけた場合である。
こうしたツールの設計には
(a) 表示の欠落を発生させない
(b) 無限ループに陥いらない
(c) 同じファイルを余剰に表示しない
などの要求事項が考えられる。
欠落を発生させるツールはそもそも役に立たない。従って (a) は当然である。(b) の要求はいわばツールが暴走しないための保護である。安全性が確認されている環境、あるいは使い方の下では
(b) の対策を取らなくてもよいであろう。意地の悪い SYMLINK の下でも (c) の要求を満たす既存のものは僕は知らない。そもそも無限ループに陥るような SYMLINK を張る方が悪いのであるから、(c) に関しては正しい SYMLINK の下で満たされていれば良しとして割り切っても良いと思える。
ディレクトリ test1
から test5
をどのように処理できたかを、ls
と find
について調べる。
なお、正しく SYMLINK が貼られているのは test1
だけで、
test2
、test3
は cyclic link、test4
は broken link になっている。
test5
はディレクトリへのリンクで、リンクをフォローしていくと、無限に深いディレクトリ(一種の無限ループ)になっている。
SYMLINK をフォローさせない場合には、いずれも正しく処理している。
-bash$ ls -RLl test[1-5] test1: total 32 -rw-r--r-- 1 arisawa staff 2231 4 17 12:39 file1 -rw-r--r-- 1 arisawa staff 2231 4 17 12:39 file2 -rw-r--r-- 1 arisawa staff 2231 4 17 12:39 file3 -rw-r--r--@ 1 arisawa staff 76 4 17 12:40 memo.txt test2: total 24 -rw-r--r--@ 1 arisawa staff 68 4 17 12:00 memo.txt ls: file1: Too many levels of symbolic links ls: file2: Too many levels of symbolic links test3: total 32 -rw-r--r--@ 1 arisawa staff 114 4 17 13:05 memo.txt ls: file1: Too many levels of symbolic links ls: file2: Too many levels of symbolic links ls: file3: Too many levels of symbolic links test4: total 32 -rw-r--r--@ 1 arisawa staff 114 4 18 13:21 memo.txt ls: file1: No such file or directory ls: file2: No such file or directory ls: file3: No such file or directory test5: total 8 drwxr-xr-x 6 arisawa staff 204 4 18 16:15 dir1 drwxr-xr-x 6 arisawa staff 204 4 18 16:15 dir2 -rw-r--r--@ 1 arisawa staff 123 4 17 18:36 memo.txt test5/dir1: total 0 -rw-r--r-- 1 arisawa staff 0 4 18 16:15 bar1 -rw-r--r-- 1 arisawa staff 0 4 18 16:15 foo1 -rw-r--r-- 1 arisawa staff 0 4 18 16:15 hoge1 drwxr-xr-x 6 arisawa staff 204 4 18 16:15 ln1 test5/dir1/ln1: total 0 -rw-r--r-- 1 arisawa staff 0 4 18 16:15 bar2 -rw-r--r-- 1 arisawa staff 0 4 18 16:15 foo2 -rw-r--r-- 1 arisawa staff 0 4 18 16:15 hohe2 drwxr-xr-x 6 arisawa staff 204 4 18 16:15 ln2 ls: ln2: directory causes a cycle test5/dir2: total 0 -rw-r--r-- 1 arisawa staff 0 4 18 16:15 bar2 -rw-r--r-- 1 arisawa staff 0 4 18 16:15 foo2 -rw-r--r-- 1 arisawa staff 0 4 18 16:15 hohe2 drwxr-xr-x 6 arisawa staff 204 4 18 16:15 ln2 test5/dir2/ln2: total 0 -rw-r--r-- 1 arisawa staff 0 4 18 16:15 bar1 -rw-r--r-- 1 arisawa staff 0 4 18 16:15 foo1 -rw-r--r-- 1 arisawa staff 0 4 18 16:15 hoge1 drwxr-xr-x 6 arisawa staff 204 4 18 16:15 ln1 ls: ln1: directory causes a cycle -bash$
正しく cyclic link を防いでいるが、test5
では同じファイルを多重に表示している。
find
は SYMLINK をフォローするオプションとして L
と H
を持つ。
-bash$ find -L test[1-5] test1 test1/file1 test1/file2 test1/file3 test1/memo.txt test2 test2/file1 test2/file2 test2/memo.txt test3 test3/file1 test3/file2 test3/file3 test3/memo.txt test4 test4/file1 test4/file2 test4/file3 test4/memo.txt test5 test5/dir1 test5/dir1/bar1 test5/dir1/foo1 test5/dir1/hoge1 test5/dir1/ln1 test5/dir1/ln1/bar2 test5/dir1/ln1/foo2 test5/dir1/ln1/hohe2 test5/dir1/ln1/ln2 test5/dir2 test5/dir2/bar2 test5/dir2/foo2 test5/dir2/hohe2 test5/dir2/ln2 test5/dir2/ln2/bar1 test5/dir2/ln2/foo1 test5/dir2/ln2/hoge1 test5/dir2/ln2/ln1 test5/memo.txt -bash$
find
は何故か cyclick link や broken link も表示している。test5
では無限ループを防いだが、ls
と同様に同じファイルを多重に表示している。
lr
は筆者の自作ツールで、もともとは plan9 用に作成したのであるが、今回 unix 版で SYMLINK をサボートした1。この記事はその経験に基づいている。
-bash$ lr -l test[1-5] drwxr-xr-x arisawa staff 0 2017/04/17 12:40:02 test1 lrw-r--r-- arisawa staff 2231 2017/04/17 12:39:48 test1/file1 lrw-r--r-- arisawa staff 2231 2017/04/17 12:39:48 test1/file2 -rw-r--r-- arisawa staff 2231 2017/04/17 12:39:48 test1/file3 -rw-r--r-- arisawa staff 76 2017/04/17 12:40:02 test1/memo.txt drwxr-xr-x arisawa staff 0 2017/04/17 12:00:38 test2 -rw-r--r-- arisawa staff 68 2017/04/17 12:00:38 test2/memo.txt drwxr-xr-x arisawa staff 0 2017/04/17 13:05:32 test3 -rw-r--r-- arisawa staff 114 2017/04/17 13:05:01 test3/memo.txt drwxr-xr-x arisawa staff 0 2017/04/18 13:25:00 test4 -rw-r--r-- arisawa staff 114 2017/04/18 13:21:59 test4/memo.txt drwxr-xr-x arisawa staff 0 2017/04/17 18:36:56 test5 drwxr-xr-x arisawa staff 0 2017/04/18 16:15:17 test5/dir1 -rw-r--r-- arisawa staff 0 2017/04/18 16:15:17 test5/dir1/bar1 -rw-r--r-- arisawa staff 0 2017/04/18 16:15:17 test5/dir1/foo1 -rw-r--r-- arisawa staff 0 2017/04/18 16:15:17 test5/dir1/hoge1 lrwxr-xr-x arisawa staff 0 2017/04/18 16:15:50 test5/dir1/ln1 drwxr-xr-x arisawa staff 0 2017/04/18 16:15:50 test5/dir2 -rw-r--r-- arisawa staff 0 2017/04/18 16:15:50 test5/dir2/bar2 -rw-r--r-- arisawa staff 0 2017/04/18 16:15:50 test5/dir2/foo2 -rw-r--r-- arisawa staff 0 2017/04/18 16:15:50 test5/dir2/hohe2 lrwxr-xr-x arisawa staff 0 2017/04/18 16:15:17 test5/dir2/ln2 -rw-r--r-- arisawa staff 123 2017/04/17 18:36:56 test5/memo.txt -bash$
SYMLINK の表示法が ls
と異なっていることに注意しよう。すなわち、SYMLINK 自体の属性ではなく、リンク先のファイルの属性が表示されている。
SYMLINK をフォローさせると次の結果になる。
-bash$ lr -lL test[1-5] drwxr-xr-x arisawa staff 0 2017/04/17 12:40:02 test1 -rw-r--r-- arisawa staff 2231 2017/04/17 12:39:48 test1/file1 -rw-r--r-- arisawa staff 2231 2017/04/17 12:39:48 test1/file2 -rw-r--r-- arisawa staff 2231 2017/04/17 12:39:48 test1/file3 -rw-r--r-- arisawa staff 76 2017/04/17 12:40:02 test1/memo.txt drwxr-xr-x arisawa staff 0 2017/04/17 12:00:38 test2 # test2/file1: broken or cyclic link # test2/file2: broken or cyclic link -rw-r--r-- arisawa staff 68 2017/04/17 12:00:38 test2/memo.txt drwxr-xr-x arisawa staff 0 2017/04/17 13:05:32 test3 # test3/file1: broken or cyclic link # test3/file2: broken or cyclic link # test3/file3: broken or cyclic link -rw-r--r-- arisawa staff 114 2017/04/17 13:05:01 test3/memo.txt drwxr-xr-x arisawa staff 0 2017/04/18 13:25:00 test4 # test4/file1: broken or cyclic link # test4/file2: broken or cyclic link # test4/file3: broken or cyclic link -rw-r--r-- arisawa staff 114 2017/04/18 13:21:59 test4/memo.txt drwxr-xr-x arisawa staff 0 2017/04/17 18:36:56 test5 drwxr-xr-x arisawa staff 0 2017/04/18 16:15:17 test5/dir1 -rw-r--r-- arisawa staff 0 2017/04/18 16:15:17 test5/dir1/bar1 -rw-r--r-- arisawa staff 0 2017/04/18 16:15:17 test5/dir1/foo1 -rw-r--r-- arisawa staff 0 2017/04/18 16:15:17 test5/dir1/hoge1 drwxr-xr-x arisawa staff 0 2017/04/18 16:15:50 test5/dir1/ln1 -rw-r--r-- arisawa staff 0 2017/04/18 16:15:50 test5/dir1/ln1/bar2 -rw-r--r-- arisawa staff 0 2017/04/18 16:15:50 test5/dir1/ln1/foo2 -rw-r--r-- arisawa staff 0 2017/04/18 16:15:50 test5/dir1/ln1/hohe2 # test5/dir1/ln1/ln2: cyclic directory link drwxr-xr-x arisawa staff 0 2017/04/18 16:15:50 test5/dir2 -rw-r--r-- arisawa staff 0 2017/04/18 16:15:50 test5/dir2/bar2 -rw-r--r-- arisawa staff 0 2017/04/18 16:15:50 test5/dir2/foo2 -rw-r--r-- arisawa staff 0 2017/04/18 16:15:50 test5/dir2/hohe2 # test5/dir2/ln2: cyclic directory link -rw-r--r-- arisawa staff 123 2017/04/17 18:36:56 test5/memo.txt -bash$
test5
については、余剰表示の個数が ls
や lr
に比べて減少している。他方、表示結果は dir1
と dir2
について非対称である。この非対称性は採用したアルゴリズムの結果である。
このアルゴリズムは以下のようなものである。
ディレクトリにリンクする SYMLINK で出会ったら、置かれているディレクトリをマークする。SYMLINK の先が既にマークされていれば、その SYMLINK は辿らない。
lr
は ls
と同様に broken link や cyclic link は表示しない。 代わりに警告メッセージを出すのみである。
余剰表示を完全に防ぐには、辿ったすべてのディレクトリをマークしなくてはならなくなるであろう。これは保護にしては大袈裟すぎる。
一応、余剰表示を完全に防ぐオプション R
(no redundant output) を入れてみた。次はそのアウトプットである。
-bash$ lr -lLR test5 drwxr-xr-x arisawa staff 0 2017/04/17 18:36:56 test5 drwxr-xr-x arisawa staff 0 2017/04/18 16:15:17 test5/dir1 -rw-r--r-- arisawa staff 0 2017/04/18 16:15:17 test5/dir1/bar1 -rw-r--r-- arisawa staff 0 2017/04/18 16:15:17 test5/dir1/foo1 -rw-r--r-- arisawa staff 0 2017/04/18 16:15:17 test5/dir1/hoge1 drwxr-xr-x arisawa staff 0 2017/04/18 16:15:50 test5/dir1/ln1 -rw-r--r-- arisawa staff 0 2017/04/18 16:15:50 test5/dir1/ln1/bar2 -rw-r--r-- arisawa staff 0 2017/04/18 16:15:50 test5/dir1/ln1/foo2 -rw-r--r-- arisawa staff 0 2017/04/18 16:15:50 test5/dir1/ln1/hohe2 # test5/dir1/ln1/ln2: cyclic directory link drwxr-xr-x arisawa staff 0 2017/04/18 16:15:50 test5/dir2 -rw-r--r-- arisawa staff 123 2017/04/17 18:36:56 test5/memo.txt -bash$
どのファイルも1度だけ表示されている。
R
オプションは廃止されるかも知れない。自分はオプションを増やすのを好まない。大切なのは間違った SYMLINK に対して警告メッセージを出し、間違いが訂正されるのに役に立つことであって、間違いをそのままにして余剰を防ぐことではない。その点で find
は失格である。
lr
のソースコードは http://p9.nyx.link/netlib/cmd/lr/
に置かれている。ただしコンパイルには Plan9port が必要である。
ファイルに対する ls
コマンドの例
-bash$ ls -l /usr/bin/vim -rwxr-xr-x 1 root wheel 1530240 6 24 2016 /usr/bin/vimを考える。このうち、
-rwxr-xr-x 1 root wheel 1530240 6 24 2016
sb
を stat 構造体として)stat("/usr/bin/vim",&sb)
lsb
を stat 構造体として)lstat("/usr/bin/vim",&lsb)
SYMLINK の場合には事情が異なる。stat(path,&sb)
と lstat(path,&lsb)
では得られるものが異なっている。例を示す。/usr/bin/vi
は SYMLINK である。その場合、命令
lstat("/usr/bin/vi",&lsb)
-bash$ ls -l /usr/bin/vi lrwxr-xr-x 1 root wheel 3 11 18 2014 /usr/bin/vi -> vimの
lrwxr-xr-x 1 root wheel 3 11 18 2014
lsb
に与えられる。他方stat("/usr/bin/vi",&sb)
sb
には、/usr/bin/vi
がリンクされている /usr/bin/vim
の stat 情報stat("/usr/bin/vim",&sb)
ls
の L
オプションで確認できる。-bash$ ls -lL /usr/bin/vi -rwxr-xr-x 1 root wheel 1530240 6 24 2016 /usr/bin/vi
なお "lstat
" の "l
" は link の意味である。
/dev/
の中のファイルには、SYMLINK でないにも関わらず、stat(path,&sb)
と lstat(path,&lsb)
で得られる sb
と lsb
が異なるものが存在する。例えば MacOS の場合 /dev/io8log*
がそうである。これらは character device であるが、他の character device では両者は一致している。
筆者は Plan9 の愛好家であって unix のプログラミングには慣れていない。久しぶりに unix でプログラムをしたら色々な発見があった。何しろ Plan9 には SYMLINK は存在しない。
SYMLINK は構造的には単なるテキストファイルである。パスを path
とすると、SYMLINK か否かの判定は次のようにすればよい。
# include <sys/stat.h> char *path; struct stat lsb; ... if(lstat(path,&lsb)<0){ /* stat 情報が取れない (ファイルが存在しない) */ ... } else{ if(S_ISLNK(lst->st_mode)){ /* then path is SYMLINK */ ... } else{ /* then path is a file or a dir */ ... } }
ここに S_ISLNK
は sys/stat.h
で
#define S_ISLNK(m) (((m)&S_IFMT) == S_IFLNK)
path
が SYMLINK であれば
# include <sys/stat.h> char *path; struct stat sb; ... if(stat(path,&sb)<0){ /* stat 情報が手に入らなかった。 * 原因は path が存在しない、 * あるいは broken or cyclic SYMLINK など。 * sb はそのまま */ ... } else{ /* この場合 SYMLINK チェーンを辿った先の * file や dir の sb が設定される */ if(S_ISDIR(st->st_mode)){ /* dir である */ ... } else{ /* file の類 */ ... } }
stat
構造体には ls -l
コマンドで表示されている内容(ただしファイル名を除く)が含まれている。MacOS の場合には次のようになっている。
struct stat { /* when _DARWIN_FEATURE_64_BIT_INODE is defined */ dev_t st_dev; /* ID of device containing file */ mode_t st_mode; /* Mode of file (see below) */ nlink_t st_nlink; /* Number of hard links */ ino_t st_ino; /* File serial number */ uid_t st_uid; /* User ID of the file */ gid_t st_gid; /* Group ID of the file */ dev_t st_rdev; /* Device ID */ struct timespec st_atimespec; /* time of last access */ struct timespec st_mtimespec; /* time of last data modification */ struct timespec st_ctimespec; /* time of last status change */ struct timespec st_birthtimespec; /* time of file creation(birth) */ off_t st_size; /* file size, in bytes */ blkcnt_t st_blocks; /* blocks allocated for file */ blksize_t st_blksize; /* optimal blocksize for I/O */ uint32_t st_flags; /* user defined flags for file */ uint32_t st_gen; /* file generation number */ int32_t st_lspare; /* RESERVED: DO NOT USE! */ int64_t st_qspare[2]; /* RESERVED: DO NOT USE! */ };
MacOS の stat 構造体。OS によって異なる。
st_mode
の各 bit の意味は
#define S_IFMT 0170000 /* type of file */ #define S_IFIFO 0010000 /* named pipe (fifo) */ #define S_IFCHR 0020000 /* character special */ #define S_IFDIR 0040000 /* directory */ #define S_IFBLK 0060000 /* block special */ #define S_IFREG 0100000 /* regular */ #define S_IFLNK 0120000 /* symbolic link */ #define S_IFSOCK 0140000 /* socket */ #define S_IFWHT 0160000 /* whiteout */ #define S_ISUID 0004000 /* set user id on execution */ #define S_ISGID 0002000 /* set group id on execution */ #define S_ISVTX 0001000 /* save swapped text even after use */ #define S_IRUSR 0000400 /* read permission, owner */ #define S_IWUSR 0000200 /* write permission, owner */ #define S_IXUSR 0000100 /* execute/search permission, owner */
16bit で定義されているのだね〜
unix が生まれたのは、こんな時代だから...
bit ごとに独立した意味を与えていないことが、判定を(少しだけ)面倒にしている。
ディレクトリのパス path
を与えて、その中に含まれるファイル名を知る。
#include <dirent.h> char *path; DIR *d; struct dirent *de; ... d = opendir(path); if(d == NULL){ /* error */ ... } else{ while ((de = readdir(d)) != NULL) { printf("%s\n",de->->d_name);/* or something else */ ... } /* dont't free(de) */ } closedir(d);
ここで使われている構造体の内容を次に示す。
typedef struct { int __dd_fd; /* file descriptor associated with directory */ long __dd_loc; /* offset in current buffer */ long __dd_size; /* amount of data returned */ char *__dd_buf; /* data buffer */ int __dd_len; /* size of data buffer */ long __dd_seek; /* magic cookie returned */ long __dd_rewind; /* magic cookie for rewinding */ int __dd_flags; /* flags for readdir */ __darwin_pthread_mutex_t __dd_lock; /* for thread locking */ struct _telldir *__dd_td; /* telldir position recording */ } DIR; struct dirent { /* when _DARWIN_FEATURE_64_BIT_INODE is defined */ ino_t d_fileno; /* file number of entry */ __uint64_t d_seekoff; /* seek offset (optional, used by servers) */ __uint16_t d_reclen; /* length of this record */ __uint16_t d_namlen; /* length of string in d_name */ __uint8_t d_type; /* file type, see below */ char d_name[1024]; /* name must be no longer than this */ };
MacOS の DIR 構造体と dirent 構造体。OS によって異なる。
Linux の場合には d_namlen は dirent 構造体に含まれていない。(困らない)
SYMLINK チェーンの先がファイルであれば、この path
はファイルと同等の、ディレクトリであればディレクトリと同等の、扱いを受けるのである。
従って SYMLINK ファイルの中身を覗くには open(path,...)
では覗けない。(リンク先のファイルが open されるから)
#include <unistd.h> int n; char *path; char buf[256];/* 適当な十分な大きさのサイズで */ ... n = readlink(path,buf,sizeof(buf)) if(n<0){ /* error */ ... } buf[n] = 0; printf("%s\n",buf); /* or something */
再巡回を避けるために、巡回済(あるいは巡回中)のディレクトリは必要に応じてリストに登録する。リストに登録する情報はディレクトリをユニークに特徴付けるものでなくてはならない。巡回中のプロセスから見えるディレクトリのパスではダメである。ここは一種の仮想空間であり、本来のパス名が分からないからである。では何を使うか?
Plan9 では qid.path
なるものをその目的のために使っている。path
と名前が付いているが、ディレクトリをユニークに識別する数字である。Russ Cox1 は Plan9 のソフトウェアを unix に移植するにあたって、qid.path の代用として stat 構造体の st_ino
を採用している。
Plan9 には SYMLINK なるものは存在しない。システムのメンテ作業においては木構造でないと制御しにくいからである。しかし他方では木構造を基にして変幻自在な仮想空間をアプリケーションに提供する仕組みを持っている。そのためにファイルを識別する情報を必要としているのである。
プロセスとは実行中のプログラムのことである。
ps aux
chroot とはコマンドの名前であり、foo
をディレクトリの名前であるとする。このコマンドは
cd foo sudo chroot .
foo
をうまく作ると foo
の中に閉じ込められたプロセスを生成できる。いい加減に foo
を作ると、(FreeBSD の場合には)chroot: /bin/csh: No such file or directoryと怒られる。このメッセージは
foo/bin/csh
が無いと言っている。なぜ csh
かと言えば FreeBSD では root プロセスは csh
を使うからである1。そこで csh
を foo の中に追加する。そこでmkdir bin cp /bin/csh bin
foo
の下にプロセスを閉じ込める檻を作る方針であり、/bin/csh
は檻の外にあるからである。
foo/bin/csh
を作って再度
sudo chroot .
ELF interpreter /libexec/ld-elf.so.1 not found, error 2と言われる。そしてまた追加する。今度は
ld-elf.so.1: Shared object "libncursesw.so.9" not found, required by "csh"と言われた。この作業が延々と続く。そこで結局どうしたらよいの? 面倒だから、過剰でもよいから... 結局
bsd$ mkdir bin lib libexec bsd$ cp /bin/csh bin bsd$ cp /lib/*.so.* lib bsd$ cp /libexec/ld-elf.so.1 libexec bsd$ sudo chroot . # # OK. However we can do almost nothing # exit bsd$root のプロセス
csh
が foot
の中に入ることに成功したが、事実上何もできない。コマンドのプログラムが入っていないから。檻の外のプログラムは実行できない。檻の外はそもそも見えないのである。楽しくない。はじめから /bin
のプログラムぐらいは foo/bin
にコピーしておけば、もう少し楽しかったのに... (読者の課題とする)
Linux の場合、chroot コマンドのエラーメッセージを頼りに foo
の内容を増やそうとしても上手くいかない。エラーメッセージが不親切で、何が不足しているのかを知らせてくれない。単に /bin/bash
が無いと言われるだけである。この場合 ldd
コマンドが役に立つ2。
maia$ cd $t maia$ sudo chroot . chroot: failed to run command ‘/bin/bash’: No such file or directory maia$ ldd /bin/bash linux-vdso.so.1 (0x00007ffe2431f000) libtinfo.so.6 => /lib/x86_64-linux-gnu/libtinfo.so.6 (0x00007f0d058fb000) libdl.so.2 => /lib/x86_64-linux-gnu/libdl.so.2 (0x00007f0d058f5000) libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f0d05703000) /lib64/ld-linux-x86-64.so.2 (0x00007f0d05a70000) maia$ mkdir -p lib/x86_64-linux-gnu lib64 maia$ cp /lib/x86_64-linux-gnu/* lib/x86_64-linux-gnu maia$ cp /lib64/ld-linux-x86-64.so.2 lib64 maia$ sudo chroot . bash: warning: setlocale: LC_ALL: cannot change locale (ja_JP.UTF-8) bash-5.0#
ここでも、ダイナミックライブラリは過剰である。
目的に応じて必要なプログラムが異なる。そのためのツールもまた存在するようである。
macOS は FreeBSD からコマンド環境を受け継いでいる。従って僕の現在の環境では chroot も使える。「現在の」と書いたのは、chroot を使ったプログラムをコンパイルすると "deplecated" のメッセージを出すからである。Apple は将来 chroot を廃止するらしい。将来ではなく、もう既に廃止されているかもしれらい。「プロセス閉じ込め」はセキュリティの保持にとって有用な技術である。chroot に代わって、Apple は何かを既に持っているのだろうか? 注目して行きたい。
ldd
の最初の表示 linux-vdso.so.1
に関しては、locate
コマンドで調べても、このファイルは表示されない。どうやらファイルシステムの中には存在しないらしい。文献[1]を見よ。
[1] VDSO
https://linuxjm.osdn.jp/html/LDP_man-pages/man7/vdso.7.html
檻の中に必要な備品を全て配置するのは資源の無駄遣いである。同じ備品であっても檻の数だけ準備しなくてはならないだけではなく、そんなことをやっていれば、檻をダイナミックに構成できない。
この問題に関して Plan9 は極めて上手にやっている。この手法が FreeBSD や Linux でも(ある程度)使えるようになっているので、それを紹介する。
FreeBSD には mount_nullfs
が存在する:
bsd$ ls -l /sbin/mount* -r-xr-xr-x 1 root wheel 26304 Apr 9 2021 /sbin/mount -r-xr-xr-x 1 root wheel 12768 Apr 9 2021 /sbin/mount_cd9660 -r-xr-xr-x 1 root wheel 17840 Apr 9 2021 /sbin/mount_fusefs -r-xr-xr-x 2 root wheel 18672 Apr 9 2021 /sbin/mount_mfs -r-xr-xr-x 1 root wheel 14152 Apr 9 2021 /sbin/mount_msdosfs -r-xr-xr-x 1 root wheel 27896 Apr 9 2021 /sbin/mount_nfs -r-xr-xr-x 1 root wheel 9248 Apr 9 2021 /sbin/mount_nullfs -r-xr-xr-x 1 root wheel 11120 Apr 9 2021 /sbin/mount_udf -r-xr-xr-x 1 root wheel 10720 Apr 9 2021 /sbin/mount_unionfs bsd$
実験をしてみる。$HOME/tmp
を既存のディレクトリとする。新たにディレクトリ $HOME/mp
を作り
bsd$ cd bsd$ sudo mount -t nullfs tmp mp bsd$ ls tmp ... bsd$ lr mp ... bsd$
マウントされた mp
の内容は tmp
と全く同じになっていることが解る。
これは Plan9 の bind
コマンドの真似である。ただし Plan9 の方は
mount_bind
にしなかったのか? 類似のマウントは Linux にも存在し、こちらは率直にsudo mount --bind foo bar
FreeBSD においても mount_bind
にしていれば
sudo mount -t bind foo bar
調べていくと、FreeBSD の nullfs
は Linux の "--bind
" とは起源が違うらしく、基本的な違いがあるかも知れない。気がついたら追記する。
なお mount
の取り消しは、マウントポイント bar
に対して
sudo umount bar
Plan9 流の bind のサポートは、chroot を始めとしたプロセス閉じ込めに革命を齎すだけのポテンシャルを秘めている。FreeBSD の場合には次のようになる:
bsd$ cd $t # $t is the dir to chroot bsd$ mkdir bin lib libexec # one time action bsd$ sudo mount -t nullfs /bin bin bsd$ sudo mount -t nullfs /lib lib bsd$ sudo mount -t nullfs /libexec libexec bsd$ sudo chroot . # # you can do something # exit
つまり、ファイルのコピーをしなくてもやっていける。Linux の場合も同様である。
地上の世界は檻で閉ざされている。ならば地下空間(カール空間)を通じて外部の参照を(必要に応じて)許しましょうというのが bind の精神である。
mount -t nullfs
" や "mount --bind
" の記事は 2011 にまで遡る。この頃から注目されるようになったのだろうか? さらに調べていくと、この nullfs
の名前の起源は 4.4 Release(2001) まで遡るらしい。僕の macOS は古く(10.12.6)、サポートされていない。
FreeBSD も Linux も、Plan9 の影響を受けて、カーネルに新しい仕組みが徐々に導入されつつある。カーネルは OS の土台である。カーネルのバグは破壊力が大きい。上部の建物を一瞬のうちに吹き飛ばすだけの破壊力を持つ。そこで次の実験をしてみた。
FreeBSD の mount -t nullfs
や Linux の mount --bind
によるマウントは symlink と似たところがある。すなわち循環可能である。循環マウントすると何が起こるか? 暴走しなければ合格としよう。
どこかに実験用のディレクトリを作る。これを $t
とする。
以下の実験で lr
は僕の自作のツールで、ディレクトリを再帰的に表示する。unix の ls -lR
よりも見やすいと思うので、今回はこれで示す。ディレクトリは "/
" で終わる。
bsd$ cd $t bsd$ mkdir dir bsd$ mkdir dir/next bsd$ touch dir/foo dir/bar bsd$ lr dir dir/ dir/bar dir/foo dir/next/ bsd$ sudo mount -t nullfs dir dir/next bsd$ lr dir dir/ dir/bar dir/foo dir/next/ dir/next/bar dir/next/foo dir/next/next/ bsd$ sudo umount dir/next bsd$ lr dir dir/ dir/bar dir/foo dir/next/ bsd$ sudo mount -t nullfs dir/next dir bsd$ lr dir dir/ bsd$ sudo umount dir bsd$
オペレーション
mount -t nullfs dir dir/next
ln -s dir/next dir
mount -t nullfs dir/next dir
dir/next
が空ディレクトリであるから、dir
も空ディレクトリとなっている。
今度はさらに複雑な例として、2つのディレクトリの中での相互循環を挙げる。
bsd$ cd $t bsd$ mkdir dir1 dir2 bsd$ touch dir1/foo1 dir1/bar1 bsd$ touch dir2/foo2 dir2/bar2 bsd$ mkdir dir1/next1 dir2/next2 bsd$ sudo mount -t nullfs dir1 dir2/next2 bsd$ sudo mount -t nullfs dir2 dir1/next1 bsd$ lr dir1 dir2 dir1/ dir1/bar1 dir1/foo1 dir1/next1/ dir1/next1/bar2 dir1/next1/foo2 dir1/next1/next2/ dir2/ dir2/bar2 dir2/foo2 dir2/next2/ dir2/next2/bar1 dir2/next2/foo1 dir2/next2/next1/ bsd$ sudo umount dir1/next1 dir2/next2
実験1と同様循環は起きない。オペレージョンの向きを変えて実験すると
bsd$ sudo mount -t nullfs dir1/next1 dir2 bsd$ lr -l dir1 dir2 bsd$ lr dir1 dir2 dir1/ dir1/bar1 dir1/foo1 dir1/next1/ dir2/ bsd$ sudo mount -t nullfs dir2/next2 dir1 mount_nullfs: /usr/home/arisawa/TEST/bind/dir2/next2: No such file or directory bsd$
最後のオペレーションは当然エラーになり、循環は起きない。
Linux の場合も同様である。また Plan9 の場合も同様である。
エラーになった理由は
mount -t nullfs dir1/next1 dir2
dir2/next2
が隠されたからである。
「循環」と書いたが、bind では循環は起きるはずがないのである。挿し木を想像すればよい。挿される枝はいわゆる木構造をしている。それを継ぎ足しても全体として木構造を保つ。Plan9 の開発者たちは、名前空間が木構造を保つことを極めて重視している。木構造を破る unix のリンク(symlink を含む)を嫌ったのである。
bind によって、ファイルシステムは本来の姿から他の姿に変る。
従ってファイルシステムのバックアップを採るときには bind は邪魔である。その時には bind を外す必要がある。
Plan9 の場合には、この点についても良くできていて、bind を外さなくても、生の姿を見せる工夫がある。この工夫は Plan9 のキーコンセプトである private namespace に由来する。unix にも private namespace の類似概念が導入される日が来るかもしれないが、今の所その気配は無い。