Enlace recursivo simbólico: ¿qué lo hace "restablecer"?

64

Escribí un pequeño script bash para ver qué sucede cuando sigo un enlace simbólico que apunta al mismo directorio. Esperaba que hiciera un directorio de trabajo muy largo o que se bloqueara. Pero el resultado me sorprendió ...

mkdir a
cd a

ln -s ./. a

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

Parte de la salida es

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

¿que está sucediendo aquí?

Lucas
fuente

Respuestas:

88

Patrice identificó la fuente del problema en su respuesta , pero si desea saber cómo llegar desde allí y por qué lo obtiene, aquí está la larga historia.

El directorio de trabajo actual de un proceso no es nada que pueda parecer demasiado complicado. Es un atributo del proceso que es un identificador de un archivo de directorio de tipo desde donde comienzan las rutas relativas (en las llamadas al sistema realizadas por el proceso). Al resolver una ruta relativa, el núcleo no necesita conocer la (a) ruta completa a ese directorio actual, solo lee las entradas del directorio en ese archivo de directorio para encontrar el primer componente de la ruta relativa (y ..es como cualquier otro archivo en ese sentido) y continúa desde allí.

Ahora, como usuario, a veces le gusta saber dónde se encuentra ese directorio en el árbol de directorios. Con la mayoría de los Unices, el árbol de directorios es un árbol, sin bucle. Es decir, solo hay una ruta desde la raíz del árbol ( /) a cualquier archivo dado. Ese camino generalmente se llama el camino canónico.

Para obtener la ruta del directorio de trabajo actual, lo que un proceso tiene que hacer es caminar hacia arriba (bien hacia abajo si desea ver un árbol con su raíz en la parte inferior) el árbol de regreso a la raíz, encontrando los nombres de los nodos en camino.

Por ejemplo, un proceso que intenta descubrir que su directorio actual es /a/b/c, abriría el ..directorio (ruta relativa, también lo ..es la entrada en el directorio actual) y buscaría un archivo de tipo directorio con el mismo número de inodo que ., descubra que ccoincide, luego se abre ../..y así sucesivamente hasta que encuentra /. No hay ambigüedad allí.

Eso es lo que las funciones getwd()o getcwd()C hacen o al menos solían hacer.

En algunos sistemas como Linux moderno, hay una llamada al sistema para devolver la ruta canónica al directorio actual que realiza esa búsqueda en el espacio del kernel (y le permite encontrar su directorio actual incluso si no tiene acceso de lectura a todos sus componentes) , y eso es lo que getcwd()llama allí. En Linux moderno, también puede encontrar la ruta al directorio actual a través de un enlace de lectura () en /proc/self/cwd.

Eso es lo que hacen la mayoría de los idiomas y los primeros shells al devolver la ruta al directorio actual.

En su caso, se le puede llamar cd acomo mayo veces como quiera, porque es un enlace simbólico a ., el directorio actual no cambia por lo que todos getcwd(), pwd -P, python -c 'import os; print os.getcwd()', perl -MPOSIX -le 'print getcwd'volvería a su ${HOME}.

Ahora, los enlaces simbólicos complicaron todo eso.

symlinkspermitir saltos en el árbol de directorios. En /a/b/c, si /ao /a/bo /a/b/ces un enlace simbólico, entonces la ruta canónica de /a/b/csería algo completamente diferente. En particular, la ..entrada /a/b/cno es necesariamente /a/b.

En el shell Bourne, si lo haces:

cd /a/b/c
cd ..

O incluso:

cd /a/b/c/..

No hay garantía de que termines /a/b.

Al igual que:

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

no es necesariamente lo mismo que:

vi /a/b/d

kshintrodujo un concepto de un directorio de trabajo lógico actual para evitarlo de alguna manera. La gente se acostumbró y POSIX terminó especificando ese comportamiento, lo que significa que la mayoría de los proyectiles actuales también lo hacen:

Para los comandos cdy pwdbuiltin ( y solo para ellos (aunque también para popd/ pushden shells que los tienen)), el shell mantiene su propia idea del directorio de trabajo actual. Se almacena en la $PWDvariable especial.

Cuando tu lo hagas:

cd c/d

incluso si co c/dson enlaces simbólicos, mientras $PWDcontiene /a/b, se agrega c/dal final así se $PWDconvierte /a/b/c/d. Y cuando lo haces:

cd ../e

En lugar de hacerlo chdir("../e"), lo hace chdir("/a/b/c/e").

Y el pwdcomando solo devuelve el contenido de la $PWDvariable.

Eso es útil en shells interactivos porque pwdgenera una ruta al directorio actual que brinda información sobre cómo llegó allí y siempre que solo use ..argumentos cdy no otros comandos, es menos probable que lo sorprenda, porque cd a; cd ..o cd a/..generalmente lo recuperaría a donde estabas

Ahora, $PWDno se modifica a menos que haga una cd. Hasta la próxima vez que llame cdo pwd, podrían suceder muchas cosas, cualquiera de los componentes de $PWDpodría cambiar su nombre. El directorio actual nunca cambia (siempre es el mismo inodo, aunque podría eliminarse), pero su ruta en el árbol de directorios podría cambiar por completo. getcwd()calcula el directorio actual cada vez que se llama caminando por el árbol de directorios para que su información sea siempre precisa, pero para el directorio lógico implementado por los shells POSIX, la información $PWDpuede volverse obsoleta. Entonces, al correr cdo pwd, algunos proyectiles pueden querer protegerse contra eso.

En ese caso particular, ve diferentes comportamientos con diferentes shells.

A algunos les gusta ksh93ignorar el problema por completo, por lo que devolverá información incorrecta incluso después de llamar cd(y no vería el comportamiento que está viendo bashallí).

A algunos les gusta basho zshcomprueban que $PWDtodavía hay una ruta hacia el directorio actual cd, pero no sobre pwd.

pdksh comprueba ambos pwdy cd(pero pwdno actualiza $PWD)

ash(al menos el que se encuentra en Debian) no marca, y cuando lo hace cd a, en realidad lo hace cd "$PWD/a", por lo que si el directorio actual ha cambiado y $PWDya no apunta al directorio actual, en realidad no cambiará al adirectorio en el directorio actual , pero el que está dentro $PWD(y devuelve un error si no existe).

Si quieres jugar con él, puedes hacer:

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)

en varias conchas.

En su caso, dado que está utilizando bash, después de un cd a, bashverificaciones que $PWDtodavía apuntan al directorio actual. Para hacer eso, invoca stat()el valor de $PWDpara verificar su número de inodo y compararlo con el de ..

Pero cuando la búsqueda de la $PWDruta implica resolver demasiados enlaces simbólicos, eso stat()devuelve un error, por lo que el shell no puede verificar si $PWDaún corresponde al directorio actual, por lo que lo calcula nuevamente getcwd()y se actualiza en $PWDconsecuencia.

Ahora, para aclarar la respuesta de Patrice, esa verificación de la cantidad de enlaces simbólicos encontrados al buscar un camino es evitar los bucles de enlaces simbólicos. El bucle más simple se puede hacer con

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

Sin esa protección segura cd a/x, el sistema tendría que encontrar dónde se avincula, encontrarlo by es un enlace simbólico al que se vincula a, y eso continuaría indefinidamente. La forma más sencilla de protegerse contra eso es darse por vencido después de resolver más de un número arbitrario de enlaces simbólicos.

Ahora regrese al directorio de trabajo lógico actual y por qué no es una característica tan buena. Es importante darse cuenta de que es solo para cdel shell y no para otros comandos.

Por ejemplo:

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

no siempre es lo mismo que:

vi -- "$dir/$file"

Es por eso que a veces encontrarás que las personas recomiendan usar siempre cd -Pen scripts para evitar confusiones (no quieres que tu software maneje un argumento de manera ../xdiferente a otros comandos solo porque está escrito en shell en lugar de otro idioma).

La -Popción es deshabilitar el manejo del directorio lógico, por lo que en cd -P -- "$var"realidad invoca chdir()el contenido de $var(excepto cuando $vares -pero esa es otra historia). Y después de un cd -P, $PWDcontendrá un camino canónico.

Stéphane Chazelas
fuente
77
¡Dulce Jesús! Gracias por una respuesta tan completa, es realmente bastante interesante :)
Lucas
Impresionante respuesta, muchas gracias! Siento que un poco sabía todas estas cosas, pero nunca había entendido o pensado cómo se unieron. Gran explicación
dimo414
42

Este es el resultado de un límite codificado en la fuente del kernel de Linux; Para evitar la denegación de servicio, el límite en el número de enlaces simbólicos anidados es 40 (que se encuentra en la follow_link()función interna fs/namei.c, llamada nested_symlink()en la fuente del núcleo).

Probablemente obtendría un comportamiento similar (y posiblemente otro límite que 40) con otros núcleos que admiten enlaces simbólicos.

Patrice Levesque
fuente
1
¿Hay alguna razón para que se "reinicie", en lugar de simplemente detenerse? es decir, en x%40lugar de max(x,40). Supongo que todavía puedes ver que has cambiado de directorio.
Lucas
44
Un enlace a la fuente, para cualquier persona curiosa: lxr.linux.no/linux+v3.9.6/fs/namei.c#L818
Ben