Đệ quy liên kết tượng trưng - điều gì làm cho nó thiết lập lại thành công?


64

Tôi đã viết một tập lệnh bash nhỏ để xem điều gì sẽ xảy ra khi tôi tiếp tục theo một liên kết tượng trưng chỉ đến cùng một thư mục. Tôi đã hy vọng nó sẽ làm cho một thư mục làm việc rất dài, hoặc bị sập. Nhưng kết quả làm tôi ngạc nhiên ...

mkdir a
cd a

ln -s ./. a

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

Một số đầu ra là

${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

có chuyện gì ở đây vậy?

Câu trả lời:


88

Patrice đã xác định nguồn gốc của vấn đề trong câu trả lời của anh ta , nhưng nếu bạn muốn biết làm thế nào để giải quyết vấn đề đó, thì đây là câu chuyện dài.

Thư mục làm việc hiện tại của một quá trình không có gì bạn nghĩ quá phức tạp. Đây là một thuộc tính của quy trình xử lý một tệp có thư mục loại trong đó các đường dẫn tương đối (trong các cuộc gọi hệ thống được thực hiện bởi quy trình) bắt đầu từ đó. Khi giải quyết một đường dẫn tương đối, kernel không cần biết (a) đường dẫn đầy đủ đến thư mục hiện tại, nó chỉ đọc các mục nhập thư mục trong tệp thư mục đó để tìm thành phần đầu tiên của đường dẫn tương đối (và ..giống như bất kỳ đường dẫn nào khác tập tin về vấn đề đó) và tiếp tục từ đó.

Bây giờ, là một người dùng, đôi khi bạn muốn biết thư mục đó nằm ở đâu trong cây thư mục. Với hầu hết các Unices, cây thư mục là một cây, không có vòng lặp. Đó là, chỉ có một đường dẫn từ gốc của cây ( /) đến bất kỳ tệp nào. Con đường đó thường được gọi là con đường kinh điển.

Để có được đường dẫn của thư mục làm việc hiện tại, một quá trình phải làm là chỉ cần đi lên ( xuống nếu bạn muốn thấy một cây có gốc ở phía dưới) cây trở lại gốc, tìm tên của các nút trên đường.

Ví dụ, một quá trình cố gắng tìm ra thư mục hiện tại của nó /a/b/c, sẽ mở ..thư mục (đường dẫn tương đối, ..mục nhập trong thư mục hiện tại) và tìm một tệp có thư mục loại có cùng số inode như ., tìm ra rằng cphù hợp, sau đó mở ../..và như vậy cho đến khi nó tìm thấy /. Không có sự mơ hồ ở đó.

Đó là những gì các hàm getwd()hoặc getcwd()C làm hoặc ít nhất được sử dụng để làm.

Trên một số hệ thống như Linux hiện đại, có một lệnh gọi hệ thống để trả về đường dẫn chính tắc cho thư mục hiện tại tìm kiếm trong không gian kernel (và cho phép bạn tìm thư mục hiện tại ngay cả khi bạn không đọc quyền truy cập vào tất cả các thành phần của nó) , và đó là những gì getcwd()gọi đó. Trên Linux hiện đại, bạn cũng có thể tìm đường dẫn đến thư mục hiện tại thông qua readlink () trên /proc/self/cwd.

Đó là những gì hầu hết các ngôn ngữ và shell đầu tiên làm khi trả lại đường dẫn đến thư mục hiện tại.

Trong trường hợp của bạn, bạn có thể gọi cd anhư thể lần như bạn muốn, bởi vì đó là một liên kết tượng trưng đến ., thư mục hiện hành không thay đổi vì vậy tất cả các getcwd(), pwd -P, python -c 'import os; print os.getcwd()', perl -MPOSIX -le 'print getcwd'sẽ quay trở lại của bạn ${HOME}.

Bây giờ, symlink đã làm phức tạp tất cả điều đó.

symlinkscho phép nhảy trong cây thư mục. Trong /a/b/c, nếu /ahay /a/bhoặc /a/b/clà một liên kết tượng trưng, sau đó con đường kinh điển của /a/b/csẽ là một cái gì đó hoàn toàn khác nhau. Đặc biệt, các ..mục trong /a/b/ckhông nhất thiết phải /a/b.

Trong vỏ Bourne, nếu bạn làm:

cd /a/b/c
cd ..

Hoặc thậm chí:

cd /a/b/c/..

Không có gì đảm bảo bạn sẽ kết thúc /a/b.

Giống như:

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

không nhất thiết phải giống như:

vi /a/b/d

kshđã giới thiệu một khái niệm về một thư mục làm việc hiện tại hợp lý để bằng cách nào đó làm việc xung quanh đó. Mọi người đã quen với nó và POSIX cuối cùng đã xác định rằng hành vi đó có nghĩa là hầu hết các shell hiện nay cũng làm điều đó:

Đối với các lệnh cdpwdlệnh dựng sẵn ( và chỉ cho chúng (mặc dù cũng cho popd/ pushdtrên các vỏ có chúng)), shell duy trì ý tưởng riêng của nó về thư mục làm việc hiện tại. Nó được lưu trữ trong $PWDbiến đặc biệt.

Khi bạn làm:

cd c/d

ngay cả khi choặc c/dlà liên kết tượng trưng, trong khi $PWDcontaines /a/b, nó gắn thêm c/dđến cùng để $PWDtrở thành /a/b/c/d. Và khi bạn làm:

cd ../e

Thay vì làm chdir("../e"), nó làm chdir("/a/b/c/e").

pwdlệnh chỉ trả về nội dung của $PWDbiến.

Điều đó hữu ích trong các shell tương tác vì pwdxuất ra một đường dẫn đến thư mục hiện tại cung cấp thông tin về cách bạn đến đó và miễn là bạn chỉ sử dụng ..trong các đối số cdvà không phải các lệnh khác, điều đó sẽ ít làm bạn ngạc nhiên hơn, bởi vì cd a; cd ..hoặc cd a/..nói chung sẽ khiến bạn quay lại đến nơi bạn đã ở.

Bây giờ, $PWDkhông được sửa đổi trừ khi bạn làm a cd. Cho đến lần tiếp theo bạn gọi cdhoặc pwd, rất nhiều điều có thể xảy ra, bất kỳ thành phần nào của $PWDcũng có thể được đổi tên. Thư mục hiện tại không bao giờ thay đổi (luôn luôn là cùng một nút, mặc dù nó có thể bị xóa), nhưng đường dẫn của nó trong cây thư mục có thể thay đổi hoàn toàn. getcwd()tính toán thư mục hiện tại mỗi lần nó được gọi bằng cách đi xuống cây thư mục để thông tin của nó luôn chính xác, nhưng đối với thư mục logic được triển khai bởi shell POSIX, thông tin trong đó $PWDcó thể trở nên cũ. Vì vậy, khi chạy cdhoặc pwd, một số đạn pháo có thể muốn bảo vệ chống lại điều đó.

Trong trường hợp cụ thể đó, bạn thấy các hành vi khác nhau với các vỏ khác nhau.

Một số người thích ksh93bỏ qua vấn đề hoàn toàn, do đó sẽ trả lại thông tin không chính xác ngay cả sau khi bạn gọi cd(và bạn sẽ không thấy hành vi mà bạn đang thấy bashở đó).

Một số thích bashhoặc zshkiểm tra $PWDvẫn là một đường dẫn đến thư mục hiện tại cd, nhưng không phải trên pwd.

pdksh không kiểm tra cả hai pwdcd(nhưng khi pwd, không cập nhật $PWD)

ash(ít nhất là cái được tìm thấy trên Debian) không kiểm tra và khi bạn thực hiện cd a, nó thực sự làm cd "$PWD/a"như vậy, vì vậy nếu thư mục hiện tại đã thay đổi và $PWDkhông còn trỏ đến thư mục hiện tại, nó thực sự sẽ không thay đổi athư mục trong thư mục hiện tại , nhưng lỗi trong $PWD(và trả về lỗi nếu nó không tồn tại).

Nếu bạn muốn chơi với nó, bạn có thể làm:

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)

trong các vỏ khác nhau.

Trong trường hợp của bạn, vì bạn đang sử dụng bash, sau một cd a, bashkiểm tra xem $PWDvẫn còn trỏ đến thư mục hiện tại. Để làm điều đó, nó gọi stat()giá trị của $PWDđể kiểm tra số inode của nó và so sánh nó với giá trị của ..

Nhưng khi tìm kiếm $PWDđường dẫn liên quan đến việc giải quyết quá nhiều liên kết tượng trưng, stat()sẽ trả về một lỗi, vì vậy trình bao không thể kiểm tra xem có $PWDcòn tương ứng với thư mục hiện tại hay không, do đó, nó sẽ tính toán lại getcwd()và cập nhật $PWDtương ứng.

Bây giờ, để làm rõ câu trả lời của Patrice, việc kiểm tra số lượng liên kết tượng trưng gặp phải trong khi tìm đường dẫn là để bảo vệ chống lại các vòng lặp liên kết. Vòng lặp đơn giản nhất có thể được thực hiện với

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

Nếu không có sự bảo vệ an toàn đó, theo một cd a/x, hệ thống sẽ phải tìm nơi aliên kết đến, tìm thấy nó bvà là một liên kết tượng trưng liên kết đến a, và điều đó sẽ tiếp diễn vô tận. Cách đơn giản nhất để bảo vệ chống lại điều đó là từ bỏ sau khi giải quyết nhiều hơn một số liên kết tượng trưng tùy ý.

Bây giờ trở lại thư mục làm việc hiện tại hợp lý và tại sao nó không phải là một tính năng tốt. Điều quan trọng là phải nhận ra rằng nó chỉ dành cho cdshell chứ không phải các lệnh khác.

Ví dụ:

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

không phải lúc nào cũng giống như:

vi -- "$dir/$file"

Đó là lý do tại sao đôi khi bạn sẽ thấy rằng mọi người khuyên bạn nên luôn sử dụng cd -Pcác tập lệnh để tránh nhầm lẫn (bạn không muốn phần mềm của mình xử lý một đối số ../xkhác với các lệnh khác chỉ vì nó được viết bằng vỏ thay vì ngôn ngữ khác).

Các -Plựa chọn là để vô hiệu hóa các thư mục logic xử lý để cd -P -- "$var"thực hiện gọi chdir()về nội dung của $var(trừ khi $var-nhưng đó là một câu chuyện khác). Và sau một cd -P, $PWDsẽ chứa một con đường chính tắc.


7
Chúa Giêsu ngọt ngào! Cảm ơn câu trả lời toàn diện như vậy, nó thực sự khá thú vị :)
Lucas

Câu trả lời tuyệt vời, cảm ơn rất nhiều! Tôi cảm thấy như mình đã biết tất cả những điều này, nhưng tôi chưa bao giờ hiểu hay nghĩ về việc tất cả chúng kết hợp với nhau như thế nào. Giải thích tuyệt vời.
dimo414

42

Đây là kết quả của giới hạn mã hóa cứng trong nguồn nhân Linux; để ngăn chặn từ chối dịch vụ, giới hạn về số lượng liên kết tượng trưng lồng nhau là 40 (được tìm thấy trong follow_link()hàm bên trong fs/namei.c, được gọi bởi nested_symlink()trong nguồn kernel).

Bạn có thể sẽ có một hành vi tương tự (và có thể là giới hạn khác hơn 40) với các hạt nhân khác hỗ trợ các liên kết tượng trưng.


1
Có một lý do để nó "thiết lập lại", thay vì chỉ dừng lại. tức x%40là hơn max(x,40). Tôi đoán bạn vẫn có thể thấy bạn đã thay đổi thư mục.
Lucas

4
Một liên kết đến nguồn, cho bất kỳ ai khác tò mò: lxr.linux.no/linux+v3.9.6/fs/namei.c#L818
Ben
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.