Recursão de link simbólico - o que o torna "redefinido"?

64

Eu escrevi um pequeno script bash para ver o que acontece quando continuo seguindo um link simbólico que aponta para o mesmo diretório. Eu esperava criar um diretório de trabalho muito longo ou travar. Mas o resultado me surpreendeu ...

mkdir a
cd a

ln -s ./. a

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

Parte da produção é

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

o que esta acontecendo aqui?

Lucas
fonte

Respostas:

88

Patrice identificou a fonte do problema em sua resposta , mas se você quiser saber como chegar a esse ponto, aqui está a longa história.

O diretório de trabalho atual de um processo não é nada que você pensaria muito complicado. É um atributo do processo, que é um identificador para um arquivo do tipo diretório onde os caminhos relativos (nas chamadas do sistema feitas pelo processo) começam. Ao resolver um caminho relativo, o kernel não precisa conhecer o (a) caminho completo para o diretório atual, apenas lê as entradas do diretório nesse arquivo de diretório para encontrar o primeiro componente do caminho relativo (e ..é como qualquer outro arquivo a esse respeito) e continua a partir daí.

Agora, como usuário, às vezes você gosta de saber onde esse diretório se encontra na árvore de diretórios. Na maioria dos Unices, a árvore de diretórios é uma árvore, sem loop. Ou seja, existe apenas um caminho da raiz da árvore ( /) para qualquer arquivo. Esse caminho é geralmente chamado de caminho canônico.

Para obter o caminho do diretório de trabalho atual, o que um processo precisa fazer é apenas subir (bem abaixo, se você quiser ver uma árvore com sua raiz na parte inferior) da árvore de volta à raiz, localizando os nomes dos nós a caminho.

Por exemplo, um processo que tenta descobrir qual é seu diretório atual /a/b/cabriria o ..diretório (caminho relativo, assim ..como a entrada no diretório atual) e procuraria por um arquivo do tipo diretório com o mesmo número de inode que .descobriria ccorresponde, abre ../..e assim por diante até encontrar /. Não há ambiguidade lá.

É o que as funções getwd()ou getcwd()C fazem ou pelo menos costumavam fazer.

Em alguns sistemas como o Linux moderno, há uma chamada do sistema para retornar o caminho canônico ao diretório atual, que faz essa pesquisa no espaço do kernel (e permite encontrar o diretório atual, mesmo que você não tenha acesso de leitura a todos os seus componentes) , e é isso que getcwd()chama lá. No Linux moderno, você também pode encontrar o caminho para o diretório atual através de um readlink () em /proc/self/cwd.

É o que a maioria dos idiomas e shells iniciais fazem ao retornar o caminho para o diretório atual.

No seu caso, você pode chamar cd acomo pode vezes quiser, porque é um link simbólico para .o diretório atual não muda assim todos getcwd(), pwd -P, python -c 'import os; print os.getcwd()', perl -MPOSIX -le 'print getcwd'voltaria seu ${HOME}.

Agora, os links simbólicos foram complicando tudo isso.

symlinkspermitir saltos na árvore de diretórios. Em /a/b/c, se /aou /a/bou /a/b/cé um link simbólico, o caminho canônico de /a/b/cseria algo completamente diferente. Em particular, a ..entrada /a/b/cnão é necessariamente /a/b.

No shell Bourne, se você fizer:

cd /a/b/c
cd ..

Ou até:

cd /a/b/c/..

Não há garantia de que você acabará /a/b.

Assim como:

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

não é necessariamente o mesmo que:

vi /a/b/d

kshintroduziu o conceito de um diretório de trabalho atual lógico para, de alguma forma, solucionar esse problema. As pessoas se acostumaram e o POSIX acabou especificando esse comportamento, o que significa que a maioria dos shells atualmente também o faz:

Para os cde pwdbuiltin comandos ( e só para eles (embora também para popd/ pushdde conchas que eles) têm), o shell mantém a sua própria ideia do diretório de trabalho atual. É armazenado na $PWDvariável especial.

Quando você faz:

cd c/d

mesmo que cou c/dsejam links simbólicos, embora $PWDcontenha /a/b, ele se anexa c/dao final que $PWDse torna /a/b/c/d. E quando você faz:

cd ../e

Em vez de fazer chdir("../e"), faz chdir("/a/b/c/e").

E o pwdcomando retorna apenas o conteúdo da $PWDvariável.

Isso é útil em conchas interativos porque pwdgera um caminho para o diretório atual que dá informações sobre como você chegou lá e, enquanto você usar somente ..em argumentos para cde não outros comandos, é menos provável que surpreendê-lo, porque cd a; cd ..ou cd a/..faria geralmente ter você de volta para onde você estava.

Agora, $PWDnão é modificado, a menos que você faça um cd. Até a próxima vez que você ligar cdou pwd, muitas coisas acontecerem, qualquer um dos componentes $PWDpoderá ser renomeado. O diretório atual nunca muda (é sempre o mesmo inode, embora possa ser excluído), mas seu caminho na árvore de diretórios pode mudar completamente. getcwd()calcula o diretório atual cada vez que é chamado, percorrendo a árvore de diretórios para que suas informações sejam sempre precisas, mas para o diretório lógico implementado pelos shells POSIX, as informações $PWDpodem ficar obsoletas. Assim, ao executar cdou pwd, algumas conchas pode querer proteger contra isso.

Nesse caso em particular, você vê comportamentos diferentes com conchas diferentes.

Algumas pessoas ksh93ignoram completamente o problema e retornam informações incorretas mesmo depois de ligar cd(e você não vê o comportamento que está vendo bashlá).

Alguns gostam bashou zshverificam que $PWDainda é um caminho para o diretório atual cd, mas não para o diretório atual pwd.

O pdksh verifica ambos pwde cd(mas pwdnão atualiza $PWD)

ash(pelo menos o encontrado no Debian) não verifica e, quando o faz cd a, ele realmente faz cd "$PWD/a"; portanto, se o diretório atual mudou e $PWDnão aponta mais para o diretório atual, ele não será alterado para o adiretório no diretório atual , mas o que está dentro $PWD(e retorne um erro se ele não existir).

Se você quiser brincar com ele, pode:

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)

em várias conchas.

No seu caso, desde que você esteja usando bash, após a cd a, bashverifica que $PWDainda aponta para o diretório atual. Para fazer isso, ele chama stat()o valor de $PWDpara verificar seu número de inode e compará-lo com o de ..

Porém, quando a busca do $PWDcaminho envolve a solução de muitos links simbólicos, isso stat()retorna com um erro, de modo que o shell não pode verificar se $PWDainda corresponde ao diretório atual, portanto o calcula novamente getcwd()e atualiza de $PWDacordo.

Agora, para esclarecer a resposta de Patrice, essa verificação do número de links simbólicos encontrados ao procurar um caminho é para evitar loops de links simbólicos. O loop mais simples pode ser feito com

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

Sem essa proteção, cd a/xo sistema teria que encontrar para onde os alinks estão, encontra-os be é um link simbólico ao qual os links estão a, e isso continuaria indefinidamente. A maneira mais simples de evitar isso é desistir depois de resolver mais do que um número arbitrário de links simbólicos.

Agora, de volta ao diretório de trabalho atual lógico e por que esse recurso não é tão bom. É importante perceber que é apenas para cdo shell e não para outros comandos.

Por exemplo:

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

nem sempre é o mesmo que:

vi -- "$dir/$file"

É por isso que às vezes você acha que as pessoas recomendam o uso sempre de cd -Pscripts para evitar confusão (você não quer que o seu software lide com argumentos de maneira ../xdiferente dos outros comandos, apenas porque está escrito em shell e não em outro idioma).

A -Popção é desativar o manuseio do diretório lógico, para que cd -P -- "$var"realmente chame chdir()o conteúdo de $var(exceto quando $varé, -mas isso é outra história). E depois de a cd -P, $PWDconterá um caminho canônico.

Stéphane Chazelas
fonte
7
Doce Jesus! Graças a uma resposta tão abrangente, é realmente bastante interessante :)
Lucas
Resposta incrível, muito obrigado! Eu sinto que eu meio que sabia todas essas coisas, mas eu nunca tinha entendido ou pensou em como todos eles vieram juntos. Ótima explicação.
dimo414
42

Este é o resultado de um limite codificado na fonte do kernel do Linux; para impedir a negação de serviço, o limite para o número de links simbólicos aninhados é 40 (encontrado na follow_link()função interna fs/namei.c, chamada pela nested_symlink()fonte do kernel).

Você provavelmente obteria um comportamento semelhante (e possivelmente outro limite que 40) com outros kernels que suportam links simbólicos.

Patrice Levesque
fonte
11
Existe uma razão para "redefinir", em vez de apenas parar. ou seja, em x%40vez de max(x,40). Eu acho que você ainda pode ver que mudou de diretório.
Lucas
4
Um link para a fonte, para qualquer pessoa curiosa: lxr.linux.no/linux+v3.9.6/fs/namei.c#L818
Ben