Surpreendido pelo comportamento do cp com hardlinks

20

Entendo muito bem a noção de hardlinks e li as páginas de manual para obter ferramentas básicas como cp--- e até as especificações recentes do POSIX --- várias vezes. Ainda fiquei surpreso ao observar o seguinte comportamento:

$ echo john > john
$ cp -l john paul
$ echo george > george

Neste ponto johne paulterá o mesmo nodo (e conteúdo), e georgeserão diferentes em ambos os aspectos. Agora fazemos:

$ cp george paul

Nesse ponto, eu esperava georgee paulter números de inode diferentes, mas o mesmo conteúdo - essa expectativa foi atendida -, mas também esperava paulagora ter um número de inode diferente johne johnainda ter o conteúdo john. Foi aqui que fiquei surpresa. Acontece que copiar um arquivo para o caminho de destino paultambém resulta na instalação desse mesmo arquivo (mesmo inode) em todos os outros caminhos de destino que compartilham paulo inode. Eu estava pensando que cpcria um novo arquivo e o move para o local anteriormente ocupado pelo arquivo antigo paul. Em vez disso, o que parece fazer é abrir o arquivo existente paul, truncá-lo e gravargeorgeo conteúdo desse arquivo existente. Portanto, qualquer "outro" arquivo com o mesmo inode recebe "seu" conteúdo atualizado ao mesmo tempo.

Ok, esse é um comportamento sistemático e, agora que sei esperar, posso descobrir como contornar ou tirar proveito disso, conforme apropriado. O que me intriga é onde eu deveria ver esse comportamento documentado? Eu ficaria surpreso se não estiver documentado em algum lugar nos documentos que eu já olhei. Mas, aparentemente, eu senti falta disso, e agora não consigo encontrar uma fonte que discuta esse comportamento.

dubiousjim
fonte

Respostas:

4

Primeiro, por que é feito dessa maneira? Um motivo é histórico: foi assim que foi feito no Unix First Edition .

Arquivos são tirados em pares; o primeiro é aberto para leitura, o segundo modo criado 17. Em seguida, o primeiro é copiado para o segundo.

"Criado" refere-se à creatchamada do sistema (a que está faltando um e ), que trunca o arquivo existente pelo nome fornecido, se houver um.

E aqui está o código fonte do cpUnix Second Edition (não consigo encontrar o código fonte da Primeira Edição). Você pode ver as chamadas openpara o arquivo de origem e creatpara o segundo arquivo; e, como uma melhoria na Primeira Edição, se o segundo arquivo for um diretório existente, cpcria um arquivo nesse diretório.

Mas, você pode perguntar, por que foi feito dessa maneira na época? A resposta para "por que o Unix originalmente fez dessa maneira" é quase sempre simplicidade. cpabre sua fonte de leitura e cria seu destino - e a chamada do sistema para criar um arquivo substitui um arquivo existente, abrindo-o para gravação, porque isso permite que o chamador imponha o conteúdo de um arquivo pelo nome fornecido, se o arquivo já existia ou não. não.

Agora, onde está documentado: na página de manual do FreeBSD .

Para cada arquivo de destino que já existe, seu conteúdo é substituído, se as permissões permitirem. Seu modo, ID do usuário e ID do grupo permanecem inalterados, a menos que a opção -p tenha sido especificada.

Essa redação estava presente pelo menos desde 1990 (quando BSD era 4,3BSD). Há palavras semelhantes no Solaris 10 :

Se o target_file existir, o cp substituirá seu conteúdo, mas o modo (e ACL, se aplicável), proprietário e grupo associado a ele não serão alterados.

Seu caso é explicado no manual do HP-UX 10 :

Se new_file for um link para um arquivo existente com outros links, substituirá o arquivo existente e manterá todos os links.

O POSIX coloca isso em padrão. Citando no Single UNIX v2 :

Se existir dest_file, serão executadas as seguintes etapas: (…) Um descritor de arquivo para dest_file será obtido executando ações equivalentes à função open () da especificação XSH chamada usando dest_file como argumento do caminho e o OR bit a bit inclusivo de O_WRONLY e O_TRUNC como o argumento oflag.

As páginas de manual e a especificação que citei especificam ainda que, se a -fopção for aprovada e a tentativa de abrir / criar o arquivo de destino falhar (geralmente devido à falta de permissão para gravar o arquivo), cptente remover o destino e criar um arquivo novamente . Isso quebraria o vínculo físico no seu cenário.

Você pode relatar um erro de documentação no manual do coreUtil GNU , pois ele não documenta esse comportamento. Mesmo a descrição de --preserve=links, que no seu cenário levaria à paulremoção do link e à criação de um novo arquivo, não deixa claro o que acontece sem ele --preserve=links. A descrição de um -ftipo de implica o que acontece sem ele, mas não o explica ("Ao copiar sem essa opção e um arquivo de destino existente não pode ser aberto para gravação, a cópia falha. No entanto, com --force, ...").

Gilles 'SO- parar de ser mau'
fonte
por que você diz "porque isso permite que o chamador aproprie-se de um nome de arquivo, se o arquivo já existe ou não"? O Cp não se apropria de um arquivo preexistente.
Jrw32982 suporta Monica
@ jrw32982 Eu quis dizer propriedade no sentido de decidir o que entra no arquivo, não propriedade no sentido de metadados do arquivo. Eu reescrevi essa frase.
Gilles 'SO- stop be evil'
20

cpdocumenta que substitui o arquivo de destino se o arquivo de destino já estiver presente. Você está certo que não especifica em detalhes o que "substituir" significa, mas definitivamente diz "substituir", não "substituir". Se você quer ser pedante, pode argumentar que "substituir" é exatamente o que cpfaz, e o comportamento que você esperava seria chamado apropriadamente de "substituir".

Observe também que, se cp"substituir" os arquivos de destino preexistentes, isso pode ser considerado surpreendente ou incorreto, provavelmente mais do que "sobrescrever". Por exemplo:

  • Se cpprimeiro excluísse o arquivo antigo e depois criasse um novo, haveria um intervalo de tempo durante o qual o arquivo estaria ausente, o que seria surpreendente.
  • Se cpprimeiro criou um arquivo temporário e depois o moveu no lugar, provavelmente deveria documentar isso, devido ao fato de que esses arquivos temporários com nomes estranhos seriam ocasionalmente notados ... mas não é.
  • Se cpnão foi possível criar um novo arquivo no mesmo diretório do arquivo antigo devido a permissões, isso seria lamentável (especialmente se ele já havia excluído o antigo).
  • Se o arquivo não pertencesse ao usuário em execução cpe o usuário em questão cpnão era root, seria impossível corresponder o proprietário e as permissões do novo arquivo com os do novo arquivo.
  • Se o arquivo tiver atributos especiais sofisticados que cpnão conhecem, eles serão perdidos na cópia. Atualmente, as implementações de cpdevem entender coisas como atributos estendidos, mas nem sempre foi assim. E há outras coisas, como garfos de recursos do MacOS ou, para sistemas de arquivos remotos, basicamente qualquer coisa.

Então, em conclusão: agora você sabe o que cprealmente faz. Você nunca ficará surpreso com isso de novo! Honestamente, acho que a mesma coisa pode ter acontecido comigo também, há muitos anos.

Celada
fonte
É necessário verificar a referência do POSIX, mas, de fato, as manpáginas para cpversões do BSD (pelo menos OSX) e do Gnu cpnão são tão explícitas sobre "substituição". Essa palavra é usada apenas nos comentários sobre as opções -ie -n. A manpage Gnu é especialmente informativo, começando Copy SOURCE to DEST, or multiple SOURCE(s) to DIRECTORY.a BSD / manpage Mac, pelo menos dizIn the first synopsis form, the cp utility copies the contents of the source_file to the target_file.
dubiousjim
A página de informações do ‘cp’ copies files (or, optionally, directories). The copy is completely independent of the original.
corenils do
2
Vejo que o padrão POSIX 2008 especifica o comportamento observado; Vou adicionar uma resposta.
dubiousjim
16

Vejo que o padrão POSIX 2013 especifica o comportamento observado . Diz:

  1. Se source_file for do tipo regular file, as seguintes etapas deverão ser seguidas:

    uma. ... se dest_file existir, as seguintes etapas devem ser tomadas:

    Eu. Se a -iopção estiver em vigor, o cputilitário deve escrever um aviso para o erro padrão e ler uma linha da entrada padrão. Se a resposta não for afirmativa, cpnão fará mais nada com o source_file e continuará com os arquivos restantes.

    ii. Um descritor de arquivo para dest_file deve ser obtido executando ações equivalentes à open()função definida no volume System Interfaces do POSIX.1-2008 chamado usando dest_file como argumento do caminho, e incluindo bit a bit ORde O_WRONLYe O_TRUNCcomo argumento oflag .

    iii. Se a tentativa de obter um descritor de arquivo falhar e a -fopção estiver em vigor, cptentará remover o arquivo executando ações equivalentes à unlink()função definida no volume de interfaces do sistema do POSIX.1-2008 chamado usando dest_file como argumento do caminho. Se esta tentativa for bem-sucedida, cpcontinue com a etapa 3b.

    ...

    d. O conteúdo do source_file serão escritos para o descritor de arquivo. Quaisquer erros de gravação devem causar cpuma mensagem de diagnóstico no erro padrão e continuar na etapa 3e.

    e O descritor de arquivo deve ser fechado.

dubiousjim
fonte
11
Interessante. Como você, eu supus cpque daria resultados semelhantes mve quebre todos os hardlinks dos quais o dest fazia parte. Mas agora que penso nisso, isso significaria que teria que ser especificamente unlink(2)o target ( cp -f) ou criar um temporário de nome diferente e depois rename(2). A implementação direta é apenas abrir o arquivo para substituição, que é o que o POSIX exige. É equivalente acat src > dest
Peter Cordes
2

Se você pode dizer, “copiar um arquivo para o caminho de destino paul também copia o mesmo arquivo (mesmo inode) para todos os outros caminhos de destino que compartilham paulo inode.”, Lamento dizer que você não entende a noção de links físicos muito bem. Se eu der uma maçã a Sir McCartney, eu dou uma maçã a Paul e uma maçã ao parceiro de composição de John Lennon. Mas não dei três maçãs; Eu dei uma maçã para uma pessoa que tem vários nomes / títulos / descritores.

Da mesma forma, quando você copia georgepara paul, também não está copiando john. Em vez disso, você está copiando os georgedados para o arquivo cujo inode é apontado pela paulentrada do diretório.

Passo a passo:   quando você faz

echo john > john

você criou um novo arquivo (assumindo que ainda não havia um arquivo nomeado johnnesse diretório). Ou, para falar mais estritamente, isso pressupõe que já não havia uma entrada de diretório com o nome johnnesse diretório (porque, falando estritamente, não há arquivos nos diretórios; somente entradas de diretório que apontam para inodes). Depois que você faz

cp -l john paul

ou

ln john paul

você não criou um novo arquivo; em vez disso, você deu um novo nome ao arquivo existente. Agora você tem um arquivo com dois nomes: johne paul. E quando você diz

cp george paul

você está substituindo esse arquivo . O fato de ter dois nomes é irrelevante; poderia ter 42 nomes, possivelmente em lugares que você não pode acessar, e esse comando não estaria copiando os george\ndados para todos esses nomes (caminhos); está apenas copiando os dados para o arquivo que possui vários nomes.

Scott
fonte
11
Obrigado. Certo, eu estava ciente do caráter necessário para assustar as aspas do que estava escrevendo quando o escrevi: johne paulcomecei como dois nomes de caminho para o mesmo arquivo. Mas era a maneira mais fácil de pensar em me expressar. Não acho que a mera noção de vínculo físico, entendida corretamente, dite um dos dois comportamentos para cp(sem -l).
dubiousjim
Mas obrigado pela insistência; Eu tentei esclarecer a redação.
dubiousjim