Rekursi tautan simbolik - apa yang membuatnya "diatur ulang"?

64

Saya menulis skrip bash kecil untuk melihat apa yang terjadi ketika saya terus mengikuti tautan simbolik yang menunjuk ke direktori yang sama. Saya mengharapkannya untuk membuat direktori kerja yang sangat panjang, atau crash. Tetapi hasilnya mengejutkan saya ...

mkdir a
cd a

ln -s ./. a

for i in `seq 1 1000`
do
  cd a
  pwd
done

Beberapa outputnya adalah

${HOME}/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a
${HOME}/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a
${HOME}/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a
${HOME}/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a
${HOME}/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a
${HOME}/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a
${HOME}/a
${HOME}/a/a
${HOME}/a/a/a
${HOME}/a/a/a/a
${HOME}/a/a/a/a/a
${HOME}/a/a/a/a/a/a
${HOME}/a/a/a/a/a/a/a
${HOME}/a/a/a/a/a/a/a/a

apa yang terjadi disini?

Lucas
sumber

Jawaban:

88

Patrice mengidentifikasi sumber masalah dalam jawabannya , tetapi jika Anda ingin tahu bagaimana mendapatkan dari sana ke mengapa Anda mendapatkannya, inilah cerita panjangnya.

Direktori proses yang sedang berjalan bukanlah hal yang Anda anggap terlalu rumit. Ini adalah atribut dari proses yang merupakan pegangan untuk file direktori jenis di mana jalur relatif (dalam panggilan sistem yang dibuat oleh proses) mulai dari. Ketika menyelesaikan path relatif, kernel tidak perlu mengetahui path lengkap (a) ke direktori saat ini, hanya membaca entri direktori dalam file direktori untuk menemukan komponen pertama dari path relatif (dan ..seperti yang lainnya file dalam hal itu) dan berlanjut dari sana.

Sekarang, sebagai pengguna, Anda terkadang ingin tahu di mana direktori itu berada di pohon direktori. Dengan sebagian besar Unices, pohon direktori adalah pohon, tanpa loop. Artinya, hanya ada satu jalur dari root tree ( /) ke file yang diberikan. Jalur itu umumnya disebut jalur kanonik.

Untuk mendapatkan path dari direktori kerja saat ini, apa yang harus dilakukan oleh suatu proses adalah berjalan ( turun jika Anda ingin melihat pohon dengan akarnya di bawah) pohon kembali ke root, mencari nama-nama node dalam perjalanan.

Sebagai contoh, suatu proses mencoba untuk mengetahui bahwa direktori saat ini adalah /a/b/c, akan membuka ..direktori (path relatif, begitu ..juga entri dalam direktori saat ini) dan mencari file direktori tipe dengan nomor inode yang sama seperti ., cari tahu bahwa ccocok, lalu buka ../..dan seterusnya sampai ditemukan /. Tidak ada ambiguitas di sana.

Itulah yang getwd()atau getcwd()fungsi C melakukan atau setidaknya digunakan untuk melakukan.

Pada beberapa sistem seperti Linux modern, ada panggilan sistem untuk mengembalikan jalur kanonik ke direktori saat ini yang melakukan pencarian dalam ruang kernel (dan memungkinkan Anda untuk menemukan direktori Anda saat ini bahkan jika Anda tidak memiliki akses baca ke semua komponennya) , dan itulah yang getcwd()disebut di sana. Di Linux modern, Anda juga dapat menemukan jalur ke direktori saat ini melalui readlink () pada /proc/self/cwd.

Itulah yang dilakukan sebagian besar bahasa dan shell saat mengembalikan jalur ke direktori saat ini.

Dalam kasus Anda, Anda dapat memanggil cd asebagai mungkin kali seperti yang Anda inginkan, karena itu symlink ke ., direktori saat ini tidak berubah sehingga semua getcwd(), pwd -P, python -c 'import os; print os.getcwd()', perl -MPOSIX -le 'print getcwd'akan kembali Anda ${HOME}.

Sekarang, symlink menjadi rumit semua itu.

symlinksmemungkinkan lompatan di pohon direktori. Dalam /a/b/c, jika /aatau /a/batau /a/b/cmerupakan symlink, maka jalur kanonik /a/b/cakan menjadi sesuatu yang sama sekali berbeda. Secara khusus, ..entri /a/b/cbelum tentu /a/b.

Dalam shell Bourne, jika Anda melakukannya:

cd /a/b/c
cd ..

Atau bahkan:

cd /a/b/c/..

Tidak ada jaminan Anda akan berakhir di /a/b.

Seperti:

vi /a/b/c/../d

belum tentu sama dengan:

vi /a/b/d

kshmemperkenalkan konsep direktori kerja saat ini logis untuk entah bagaimana mengatasi itu. Orang-orang terbiasa dengan hal itu dan POSIX akhirnya menentukan perilaku yang berarti sebagian besar shell saat ini melakukannya juga:

Untuk perintah cddan pwdbuiltin ( dan hanya untuk mereka (meskipun juga untuk popd/ pushdpada shell yang memilikinya)), shell mempertahankan idenya sendiri dari direktori kerja saat ini. Ini disimpan dalam $PWDvariabel khusus.

Saat kamu melakukan:

cd c/d

bahkan jika catau c/dyang symlink, sementara $PWDcontaines /a/b, itu menambahkan c/dsampai akhir sehingga $PWDmenjadi /a/b/c/d. Dan ketika Anda melakukannya:

cd ../e

Alih-alih melakukan chdir("../e"), itu benar chdir("/a/b/c/e").

Dan pwdperintah hanya mengembalikan konten $PWDvariabel.

Itu berguna dalam shell interaktif karena pwdmenampilkan jalur ke direktori saat ini yang memberikan informasi tentang bagaimana Anda sampai di sana dan selama Anda hanya menggunakan ..argumen untuk cddan bukan perintah lain, itu cenderung mengejutkan Anda, karena cd a; cd ..atau cd a/..biasanya akan membuat Anda kembali ke tempat Anda berada.

Sekarang, $PWDtidak dimodifikasi kecuali Anda melakukan cd. Sampai saat Anda menelepon lagi cdatau pwd, banyak hal bisa terjadi, komponen apa pun dari itu $PWDbisa diganti namanya. Direktori saat ini tidak pernah berubah (selalu inode yang sama, meskipun bisa dihapus), tetapi jalurnya di pohon direktori bisa berubah sepenuhnya. getcwd()menghitung direktori saat ini setiap kali dipanggil dengan berjalan di pohon direktori sehingga informasinya selalu akurat, tetapi untuk direktori logis yang diterapkan oleh shell POSIX, informasi di $PWDmungkin menjadi basi. Jadi saat menjalankan cdatau pwd, beberapa kerang mungkin ingin berjaga-jaga terhadap itu.

Dalam contoh khusus itu, Anda melihat perilaku yang berbeda dengan kulit yang berbeda.

Beberapa suka ksh93mengabaikan masalah sepenuhnya, sehingga akan mengembalikan informasi yang salah bahkan setelah Anda menelepon cd(dan Anda tidak akan melihat perilaku yang Anda lihat di bashsana).

Beberapa suka bashatau zshtidak memeriksa bahwa $PWDmasih jalan ke direktori saat ini di atas cd, tetapi tidak di atas pwd.

pdksh memeriksa keduanya pwddan cd(tetapi setelah pwd, tidak memperbarui $PWD)

ash(setidaknya yang ditemukan di Debian) tidak memeriksa, dan ketika Anda melakukannya cd a, itu benar-benar terjadi cd "$PWD/a", jadi jika direktori saat ini telah berubah dan $PWDtidak lagi menunjuk ke direktori saat ini, itu sebenarnya tidak akan berubah ke adirektori di direktori saat ini , tetapi yang ada di $PWD(dan mengembalikan kesalahan jika tidak ada).

Jika Anda ingin bermain dengannya, Anda dapat melakukan:

cd
mkdir -p a/b
cd a
pwd
mv ~/a ~/b 
pwd
echo "$PWD"
cd b
pwd; echo "$PWD"; pwd -P # (and notice the bug in ksh93)

dalam berbagai kerang.

Dalam kasus Anda, karena Anda menggunakan bash, setelah cd a, bashperiksa yang $PWDmasih menunjuk ke direktori saat ini. Untuk melakukan itu, dibutuhkan stat()nilai $PWDuntuk memeriksa nomor inode dan membandingkannya dengan ..

Tetapi ketika mencari $PWDjalan melibatkan penyelesaian terlalu banyak symlink, yang stat()kembali dengan kesalahan, sehingga shell tidak dapat memeriksa apakah $PWDmasih sesuai dengan direktori saat ini, jadi ia menghitungnya lagi dengan getcwd()dan memperbarui $PWDsesuai.

Sekarang, untuk mengklarifikasi jawaban Patrice, pemeriksaan jumlah symlink yang ditemui saat mencari jalan adalah untuk menjaga terhadap loop symlink. Loop paling sederhana dapat dibuat dengan

rm -f a b
ln -s a b
ln -s b a

Tanpa pelindung yang aman itu, pada a cd a/x, sistem harus menemukan ke mana atautan menuju, menemukannya bdan merupakan symlink ke mana tautannya a, dan itu akan berlangsung tanpa batas. Cara paling sederhana untuk mencegah hal itu adalah menyerah setelah menyelesaikan lebih dari jumlah symlink yang sewenang-wenang.

Sekarang kembali ke direktori yang berfungsi saat ini logis dan mengapa itu tidak begitu baik fitur. Sangat penting untuk menyadari bahwa itu hanya untuk cddi shell dan bukan perintah lainnya.

Misalnya:

cd -- "$dir" &&  vi -- "$file"

tidak selalu sama dengan:

vi -- "$dir/$file"

Itulah sebabnya kadang-kadang Anda akan menemukan bahwa orang merekomendasikan untuk selalu menggunakan cd -Pdalam skrip untuk menghindari kebingungan (Anda tidak ingin perangkat lunak Anda menangani argumen yang ../xberbeda dari perintah lain hanya karena itu ditulis dalam shell, bukan bahasa lain).

The -Ppilihan adalah dengan menonaktifkan direktori logis penanganan sehingga cd -P -- "$var"benar-benar tidak memanggil chdir()pada isi $var(kecuali bila $varadalah -tapi itu cerita lain). Dan setelah a cd -P, $PWDakan berisi jalur kanonik.

Stéphane Chazelas
sumber
7
Yesus yang manis! Terima kasih atas jawaban yang begitu komprehensif, sungguh sangat menarik :)
Lucas
Jawaban yang luar biasa, terima kasih banyak! Saya merasa agak tahu semua hal ini, tetapi saya tidak pernah mengerti atau berpikir tentang bagaimana mereka semua berkumpul. Penjelasan yang bagus.
dimo414
42

Ini adalah hasil dari batasan kode-keras dalam sumber kernel Linux; untuk mencegah penolakan layanan, batas jumlah symlink bersarang adalah 40 (ditemukan dalam follow_link()fungsi di dalam fs/namei.c, dipanggil oleh nested_symlink()dalam sumber kernel).

Anda mungkin akan mendapatkan perilaku yang serupa (dan mungkin batas lain dari 40) dengan kernel lain yang mendukung symlink.

Patrice Levesque
sumber
1
Apakah ada alasan untuk itu "reset", bukan hanya berhenti. yaitu x%40bukan max(x,40). Saya kira Anda masih dapat melihat Anda telah mengubah direktori.
Lucas
4
Tautan ke sumbernya, untuk orang lain yang ingin tahu: lxr.linux.no/linux+v3.9.6/fs/namei.c#L818
Ben