Ricorsione del collegamento simbolico: cosa lo rende "ripristinato"?


64

Ho scritto un piccolo script bash per vedere cosa succede quando continuo a seguire un collegamento simbolico che punta alla stessa directory. Mi aspettavo che diventasse una directory di lavoro molto lunga o che si arrestasse in modo anomalo. Ma il risultato mi ha sorpreso ...

mkdir a
cd a

ln -s ./. a

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

Parte dell'output è

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

cosa sta succedendo qui?

Risposte:


88

Patrice ha identificato l'origine del problema nella sua risposta , ma se vuoi sapere come arrivare da lì al perché lo ottieni, ecco la lunga storia.

L'attuale directory di lavoro di un processo non è nulla che potresti pensare troppo complicato. È un attributo del processo che è un handle per un file di tipo directory da cui iniziano i percorsi relativi (nelle chiamate di sistema effettuate dal processo). Quando si risolve un percorso relativo, il kernel non ha bisogno di conoscere il (a) percorso completo di quella directory corrente, legge semplicemente le voci di directory in quel file di directory per trovare il primo componente del percorso relativo (ed ..è come qualsiasi altro file in tal senso) e continua da lì.

Ora, come utente, a volte ti piace sapere dove si trova quella directory nell'albero delle directory. Con la maggior parte degli Unices, l'albero delle directory è un albero, senza loop. Cioè, c'è un solo percorso dalla radice dell'albero ( /) a un dato file. Quel percorso è generalmente chiamato il percorso canonico.

Per ottenere il percorso della directory di lavoro corrente, ciò che un processo deve fare è semplicemente camminare su (ben giù se ti piace vedere un albero con la sua radice in basso) l'albero di nuovo alla radice, trovando i nomi dei nodi sulla strada.

Ad esempio, un processo che prova a scoprire che è la sua directory corrente /a/b/c, aprirebbe la ..directory (percorso relativo, quindi ..è la voce nella directory corrente) e cercherebbe un file di tipo directory con lo stesso numero di inode di ., scoprire che ccorrisponde, quindi si apre ../..e così via fino a quando non trova /. Non c'è ambiguità lì.

Questo è quello che fanno le funzioni getwd()o getcwd()C o almeno lo fanno.

Su alcuni sistemi come Linux moderno, c'è una chiamata di sistema per restituire il percorso canonico alla directory corrente che fa quella ricerca nello spazio del kernel (e ti permette di trovare la tua directory corrente anche se non hai accesso in lettura a tutti i suoi componenti) , ed è quello che getcwd()chiama lì. Su Linux moderno, puoi anche trovare il percorso della directory corrente tramite un readlink () su /proc/self/cwd.

Questo è ciò che fanno la maggior parte delle lingue e delle prime shell quando restituiscono il percorso alla directory corrente.

Nel tuo caso, si può chiamare cd acome può le volte che vuoi, perché è un link simbolico ., la directory corrente non cambia in modo che tutti getcwd(), pwd -P, python -c 'import os; print os.getcwd()', perl -MPOSIX -le 'print getcwd'sarebbe restituire la tua ${HOME}.

Ora, i collegamenti simbolici hanno complicato tutto ciò.

symlinksconsentire i salti nella struttura di directory. In /a/b/c, se /ao /a/bo /a/b/cè un collegamento simbolico, allora il percorso canonico di /a/b/csarebbe qualcosa di completamente diverso. In particolare, l' ..ingresso /a/b/cnon è necessariamente /a/b.

Nella shell Bourne, se lo fai:

cd /a/b/c
cd ..

O anche:

cd /a/b/c/..

Non c'è garanzia in cui finirai /a/b.

Proprio come:

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

non è necessariamente lo stesso di:

vi /a/b/d

kshintrodotto un concetto di una directory di lavoro corrente logica per aggirare in qualche modo quello. Le persone si sono abituate e POSIX ha finito per specificare quel comportamento, il che significa che anche la maggior parte delle shell lo fanno anche oggi:

Per i comandi cde pwdbuiltin ( e solo per loro (anche se anche per popd/ pushdsu shell che li hanno)), la shell mantiene la propria idea della directory di lavoro corrente. È memorizzato nella $PWDvariabile speciale.

Quando lo fai:

cd c/d

anche se co c/dsono collegamenti simbolici, mentre $PWDcontiene /a/b, si aggiunge c/dalla fine così $PWDdiventa /a/b/c/d. E quando lo fai:

cd ../e

Invece di farlo chdir("../e"), lo fa chdir("/a/b/c/e").

E il pwdcomando restituisce solo il contenuto della $PWDvariabile.

Questo è utile nelle shell interattive perché pwdgenera un percorso alla directory corrente che fornisce informazioni su come ci sei arrivato e fintanto che usi solo ..argomenti cde non altri comandi, è meno probabile che ti sorprenda, perché cd a; cd ..o cd a/..ti riprenderà in genere dove eri.

Ora, $PWDnon viene modificato a meno che non si esegua un cd. Fino alla prossima chiamata cdo pwd, potrebbero accadere molte cose, qualsiasi componente di $PWDpotrebbe essere rinominato. La directory corrente non cambia mai (è sempre lo stesso inode, anche se potrebbe essere eliminata), ma il suo percorso nella struttura della directory potrebbe cambiare completamente. getcwd()calcola la directory corrente ogni volta che viene chiamata camminando lungo l'albero delle directory in modo che le sue informazioni siano sempre accurate, ma per la directory logica implementata dalle shell POSIX, le informazioni in $PWDpotrebbero diventare obsolete. Quindi, correndo cdo pwd, alcune conchiglie potrebbero voler proteggerlo.

In quel particolare caso, vedi comportamenti diversi con diverse shell.

Ad alcuni piace ksh93ignorare completamente il problema, quindi restituiranno informazioni errate anche dopo aver chiamato cd(e non vedresti il ​​comportamento che stai vedendo bashlì).

Ad alcuni piace basho zshverifica che $PWDsia ancora un percorso della directory corrente su cd, ma non su pwd.

pdksh verifica entrambi pwde cd(ma su pwd, non aggiorna $PWD)

ash(almeno quello trovato su Debian) non controlla, e quando lo fai cd a, lo fa effettivamente cd "$PWD/a", quindi se la directory corrente è cambiata e $PWDnon punta più alla directory corrente, in realtà non cambierà nella adirectory nella directory corrente , ma quello in $PWD(e restituisce un errore se non esiste).

Se vuoi giocarci, puoi fare:

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)

in varie conchiglie.

Nel tuo caso, poiché stai usando bash, dopo un cd a, i bashcontrolli che $PWDpuntano ancora alla directory corrente. Per fare ciò, chiama stat()il valore di $PWDper verificare il suo numero di inode e confrontarlo con quello di ..

Ma quando la ricerca del $PWDpercorso implica la risoluzione di troppi collegamenti simbolici, ciò stat()restituisce un errore, quindi la shell non può verificare se $PWDcorrisponde ancora alla directory corrente, quindi la calcola di nuovo con getcwd()e si aggiorna di $PWDconseguenza.

Ora, per chiarire la risposta di Patrice, che il controllo del numero di collegamenti simbolici incontrati durante la ricerca di un percorso è di proteggersi dagli anelli dei collegamenti simbolici. Il loop più semplice può essere realizzato con

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

Senza quella guardia sicura, su un cd a/x, il sistema dovrebbe trovare dove si acollegano, trova bed è un collegamento simbolico a cui si collega ae che andrebbe avanti indefinitamente. Il modo più semplice per proteggersi è quello di rinunciare dopo aver risolto più di un numero arbitrario di symlink.

Ora torniamo alla directory di lavoro logica corrente e perché non è una funzionalità così buona. È importante rendersi conto che è solo per cdla shell e non altri comandi.

Per esempio:

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

non è sempre lo stesso di:

vi -- "$dir/$file"

Ecco perché a volte scoprirai che le persone raccomandano di usare sempre cd -Pnegli script per evitare confusione (non vuoi che il tuo software gestisca un argomento in modo ../xdiverso dagli altri comandi solo perché è scritto in shell anziché in un'altra lingua).

L' -Popzione è disabilitare la gestione della directory logica in modo da cd -P -- "$var"fare appello chdir()al contenuto di $var(tranne quando $varè -ma questa è un'altra storia). E dopo a cd -P, $PWDconterrà un percorso canonico.


7
Dolce Gesù! Grazie per una risposta così completa, è davvero abbastanza interessante :)
Lucas

Risposta fantastica, grazie mille! Mi sembra di conoscere un po ' tutte queste cose, ma non avevo mai capito o pensato a come si sono unite tutte. Ottima spiegazione
dimo414,

42

Questo è il risultato di un limite hardcoded nel sorgente del kernel Linux; per prevenire il denial-of-service, il limite sul numero di symlink nidificati è 40 (trovato nella follow_link()funzione interna fs/namei.c, chiamata da nested_symlink()nel kernel source).

Probabilmente otterresti un comportamento simile (e forse un altro limite di 40) con altri kernel che supportano i collegamenti simbolici.


1
C'è un motivo per "resettare", piuttosto che fermarsi. cioè x%40piuttosto che max(x,40). Immagino che tu possa ancora vedere che hai cambiato directory.
Lucas,

4
Un link alla fonte, per chiunque altro curioso: lxr.linux.no/linux+v3.9.6/fs/namei.c#L818
Ben
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.