Surpreendido pelo comportamento do cp com hardlinks

20

Eu entendo muito bem a noção de hardlinks, e li as man pages para ferramentas básicas como cp --- e até mesmo as recentes especificações POSIX --- várias vezes. Ainda fiquei surpreso ao observar o seguinte comportamento:

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

Neste ponto, john e paul terão o mesmo inode (e conteúdo) e george será diferente em ambos os aspectos. Agora fazemos:

$ cp george paul

Neste ponto, esperava que george e paul tivessem números de inode diferentes, mas o mesmo conteúdo --- essa expectativa foi atendida --- mas também esperavam paul agora tem um número de inode diferente de john e, para john , ainda tem o conteúdo john . É aqui que fiquei surpreso. Acontece que copiar um arquivo para o caminho de destino paul também tem o resultado de instalar o mesmo arquivo (mesmo inode) em todos os outros caminhos de destino que compartilham o inode de paul . Eu estava pensando que cp cria um novo arquivo e o move para o lugar anteriormente ocupado pelo antigo arquivo paul . Em vez disso, o que parece fazer é abrir o arquivo existente paul , truncá-lo e gravar o conteúdo de george nesse arquivo existente. Portanto, quaisquer "outros" arquivos com o mesmo inode obtêm "seu" conteúdo atualizado ao mesmo tempo.

Ok, isso é um comportamento sistemático e agora que sei que posso descobrir como lidar com isso ou aproveitá-lo, conforme apropriado. O que me intriga é onde eu deveria ver esse comportamento documentado? Eu ficaria surpreso se não fosse documentado em algum lugar em documentos que eu já vi. Mas aparentemente eu perdi isso e agora não consigo encontrar uma fonte que discuta esse comportamento.

    
por dubiousjim 02.09.2015 / 04:28

4 respostas

4

Primeiro, por que isso é feito dessa maneira? Um dos motivos é histórico: foi assim que foi feito na primeira edição do Unix .

Files are taken in pairs; the first is opened for reading,the second created mode 17. Then the first is copied into the second.

"Criado" refere-se à creat chamada do sistema (aquela que é famosa falta de um e ), que trunca o arquivo existente pelo nome dado, se houver um.

E aqui é o código-fonte de cp no Unix Second Edition (não consigo encontrar o código-fonte da First Edition). Você pode ver as chamadas para open para o arquivo de origem e creat para o segundo arquivo; e, como uma melhoria para a Primeira Edição, se o segundo arquivo for um diretório existente, cp criará um arquivo nesse diretório.

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

Agora, sobre onde está documentado: no Página de manual do FreeBSD .

For each destination file that already exists, its contents are overwritten if permissions allow. Its mode, user ID, and group ID are unchanged unless the -p option was specified.

Esse texto estava presente pelo menos desde então 1990 (quando o BSD era 4.3BSD). Há uma redação semelhante no Solaris 10 :

If target_file exists, cp overwrites its contents, but the mode (and ACL if applicable), owner, and group associated with it are not changed.

O seu caso está descrito na HP -UX 10 manual:

If new_file is a link to an existing file with other links, overwrites the existing file and retains all links.

POSIX coloca em standardese. Citando Single UNIX v2 :

If dest_file exists, the following steps are taken: (…) A file descriptor for dest_file will be obtained by performing actions equivalent to the XSH specification open() function called using dest_file as the path argument, and the bitwise inclusive OR of O_WRONLY and O_TRUNC as the oflag argument.

As páginas man e as especificações que eu citei ainda especificam que se a opção -f for passada e a tentativa de abrir / criar o arquivo de destino falhar (normalmente devido a não ter permissão para gravar o arquivo), cp tenta para remover o alvo e criar um arquivo novamente. Isso quebraria o link físico em seu cenário.

Você pode relatar um bug de documentação contra o manual do GNU coreutils , já que não documenta esse comportamento. Mesmo a descrição de --preserve=links , que no seu cenário levaria à remoção do link paul e a criação de um novo arquivo, não deixa claro o que acontece sem --preserve=links . A descrição de -f kind implica o que acontece sem ela, mas não a soletra (“Quando 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,… ").

    
por 03.09.2015 / 02:50
20

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

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

  • Se cp primeiro excluísse o arquivo antigo e criasse um novo, haveria um intervalo de tempo durante o qual o arquivo estaria ausente, o que seria surpreendente.
  • Se cp primeiro criou um arquivo temporário e o moveu para o lugar, provavelmente ele deve documentar isso, devido ao fato de que esses arquivos temporários com nomes estranhos ocasionalmente seriam notados ... mas isso não acontece. / li>
  • Se cp não pudesse criar um novo arquivo no mesmo diretório que o arquivo antigo devido a permissões, isso seria lamentável (especialmente se já tivesse sido excluído o antigo).
  • Se o arquivo não pertencesse ao usuário que estava executando cp e o usuário que estava executando cp não fosse root , seria impossível corresponder ao proprietário & permissões do novo arquivo para as do novo arquivo.
  • Se o arquivo tiver atributos especiais especiais que cp não conhece, eles serão perdidos na cópia. Hoje em dia, as implementações de cp devem entender de forma confiável coisas como atributos estendidos, mas nem sempre foi assim. E há outras coisas, como os forks de recursos do MacOS, ou, para sistemas de arquivos remotos, basicamente qualquer coisa.

Então, em conclusão: agora você sabe o que o cp realmente faz. Você nunca mais ficará surpreso com isso! Honestamente, acho que a mesma coisa poderia ter acontecido comigo também, muitos anos atrás.

    
por 02.09.2015 / 04:51
16

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

  1. If source_file is of type regular file, the following steps shall be taken:

    a. ... if dest_file exists, the following steps shall be taken:

    i. If the -i option is in effect, the cp utility shall write a prompt to the standard error and read a line from the standard input. If the response is not affirmative, cp shall do nothing more with source_file and go on to any remaining files.

    ii. A file descriptor for dest_file shall be obtained by performing actions equivalent to the open() function defined in the System Interfaces volume of POSIX.1-2008 called using dest_file as the path argument, and the bitwise-inclusive OR of O_WRONLY and O_TRUNC as the oflag argument.

    iii. If the attempt to obtain a file descriptor fails and the -f option is in effect, cp shall attempt to remove the file by performing actions equivalent to the unlink() function defined in the System Interfaces volume of POSIX.1-2008 called using dest_file as the path argument. If this attempt succeeds, cp shall continue with step 3b.

    ...

    d. The contents of source_file shall be written to the file descriptor. Any write errors shall cause cp to write a diagnostic message to standard error and continue to step 3e.

    e. The file descriptor shall be closed.

    
por 02.09.2015 / 09:04
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 o inode do paul . ", lamento dizer que você não entende muito bem a noção de links físicos. Se eu der uma maçã a Sir McCartney, eu dei uma maçã a Paul, e eu dei uma maçã ao parceiro de composição de John Lennon. Mas eu 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 george para paul , você não está também copiando-o para john . Em vez disso, você está copiando os dados george para o arquivo cujo inode é apontado pela entrada de diretório paul .

Passo a passo: Quando você faz

echo john > john

você criou um novo arquivo (supondo que já não houvesse um arquivo chamado john nesse diretório). Ou, para falar mais estritamente, isso está assumindo que já não havia uma entrada de diretório com o nome john nesse diretório (porque, estritamente falando, não há arquivos nos diretórios; somente entradas de diretório, que apontam para inodes). Depois você faz

cp -l john paul

ou

ln john paul

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

cp george paul

você está sobrescrevendo esse arquivo . O fato de ter dois nomes é irrelevante; pode ter 42 nomes, possivelmente em lugares que você nem acessa, e este comando não estaria copiando os dados george\n para todos esses nomes (caminhos); é apenas copiar os dados para o único arquivo que tem vários nomes.

    
por 02.09.2015 / 07:50

Tags