Sorpreso dal comportamento di cp con hardlink


20

Capisco molto bene la nozione di hardlink e ho letto le pagine man per strumenti di base come cp--- e persino le recenti specifiche POSIX --- un numero di volte. Sono rimasto sorpreso di osservare il seguente comportamento:

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

A questo punto johne paulavrà lo stesso inode (e contenuto) e georgedifferirà per entrambi gli aspetti. Adesso facciamo:

$ cp george paul

A questo punto ho aspettato georgee paulavere diversi numeri di inode, ma lo stesso contenuto --- questa aspettativa era rispettato --- ma ho anche aspettato pauldi avere ora un numero di inode diverso da john, e per johnavere ancora il contenuto john. Questo è dove sono stato sorpreso. Si scopre che la copia di un file nel percorso di destinazione paulha anche il risultato dell'installazione dello stesso file (stesso inode) in tutti gli altri percorsi di destinazione che condividono paull'inode. Stavo pensando che cpcrea un nuovo file e lo sposta nel posto precedentemente occupato dal vecchio file paul. Invece quello che sembra fare è aprire il file esistente paul, troncandolo e scriveregeorgecontenuto in quel file esistente. Quindi qualsiasi "altro" file con lo stesso inode ottiene "contenuto" aggiornato allo stesso tempo.

Ok, questo è un comportamento sistematico e ora che so di aspettarmelo, posso capire come aggirarlo o sfruttarlo, a seconda dei casi. Cosa mi confonde dove avrei dovuto vedere questo comportamento documentato? Sarei sorpreso se non fosse documentato da qualche parte nei documenti che ho già visto. Ma a quanto pare l'ho perso, e ora non riesco a trovare una fonte che discute di questo comportamento.

Risposte:


4

Innanzitutto, perché è fatto in questo modo? Uno dei motivi è storico: è così che è stato fatto in Unix First Edition .

I file sono presi in coppia; il primo viene aperto per la lettura, il secondo viene creato in modalità 17. Quindi il primo viene copiato nel secondo.

"Creato" si riferisce alla creatchiamata di sistema (quella a cui manca notoriamente una e ), che tronca il file esistente con il nome dato se ce n'è uno.

Ed ecco il codice sorgente di cpin Unix Second Edition (non riesco a trovare il codice sorgente di First Edition). Puoi vedere le chiamate a openper il file sorgente e creatper il secondo file; e, come miglioramento alla Prima Edizione, se il secondo file è una directory esistente, cpcrea un file in quella directory.

Ma, potresti chiedere, perché è stato fatto in quel modo in quel momento? La risposta a "perché Unix inizialmente lo faceva in quel modo" è quasi sempre semplicità. cpapre la sua fonte per la lettura e crea la sua destinazione - e la chiamata di sistema per creare un file sovrascrive un file esistente aprendolo per la scrittura, perché ciò consente al chiamante di imporre il contenuto di un file con il nome dato se il file esiste già o non.

Ora, dove è documentato: nella pagina man di FreeBSD .

Per ogni file di destinazione già esistente, il suo contenuto viene sovrascritto se le autorizzazioni lo consentono. Modalità, ID utente e ID gruppo rimangono invariati a meno che non sia stata specificata l'opzione -p.

Tale formulazione era presente almeno fino al 1990 (quando BSD era 4.3BSD). Esiste una formulazione simile su Solaris 10 :

Se esiste target_file, cp sovrascrive il suo contenuto, ma la modalità (e ACL se applicabile), il proprietario e il gruppo ad esso associati non vengono modificati.

La tua custodia è anche spiegata nel manuale di HP-UX 10 :

Se new_file è un collegamento a un file esistente con altri collegamenti, sovrascrive il file esistente e conserva tutti i collegamenti.

POSIX lo mette in standardese. Citando da Single UNIX v2 :

Se esiste dest_file, vengono eseguite le seguenti operazioni: (...) Un descrittore di file per dest_file verrà ottenuto eseguendo azioni equivalenti alla funzione open () della specifica XSH chiamata utilizzando dest_file come argomento path e l'OR inclusivo bit a bit di O_WRONLY e O_TRUNC come argomento oflag.

Le pagine man e le specifiche che ho citato specificano inoltre che se l' -fopzione viene passata e il tentativo di aprire / creare il file di destinazione fallisce (in genere a causa del mancato permesso di scrivere il file), cptenta di rimuovere la destinazione e creare nuovamente un file . Ciò spezzerebbe il collegamento reale nel tuo scenario.

Potresti voler segnalare un bug di documentazione sul manuale GNU coreutils , poiché non documenta questo comportamento. Anche la descrizione di --preserve=links, che nel tuo scenario porterebbe alla paulrimozione del link e alla creazione di un nuovo file, non chiarisce cosa succede senza --preserve=links. La descrizione del -ftipo di cosa implica ciò che accade senza di essa ma non lo spiega ("Quando si copia senza questa opzione e un file di destinazione esistente non può essere aperto per la scrittura, la copia fallisce. Tuttavia, con --force, ...").


perché dici "perché ciò consente al chiamante di prendere la proprietà di un nome di file se il file esiste già o no"? Cp non diventa proprietario di un file preesistente.
jrw32982 supporta Monica l'

@ jrw32982 Intendevo la proprietà nel senso di decidere cosa va inserito nel file, non la proprietà nel senso di metadati del file. Ho riscritto quella frase.
Gilles 'SO- smetti di essere malvagio' l'

20

cpdocumenti che sovrascrive il file di destinazione se il file di destinazione è già presente. Hai ragione nel dire che non specifica in dettaglio cosa significhi "sovrascrivere", ma dice sicuramente "sovrascrivi", non "sostituisci". Se vuoi essere pedante, puoi sostenere che "sovrascrivere" è esattamente ciò che cpfa, e il comportamento che ti aspettavi sarebbe correttamente chiamato "sostituisci".

Si noti inoltre che se cpsi dovesse "sostituire" i file di destinazione preesistenti, ciò potrebbe ragionevolmente essere considerato sorprendente o errato, probabilmente più della "sovrascrittura". Per esempio:

  • Se cpprima cancellassi il vecchio file e poi ne creassi uno nuovo, ci sarebbe un intervallo di tempo durante il quale il file sarebbe assente, il che sarebbe sorprendente.
  • Se cpprima creava un file temporaneo e poi lo spostava in posizione, probabilmente dovrebbe documentarlo, a causa del fatto che tali file temporanei con nomi strani potrebbero essere notati di tanto in tanto ... ma non lo fa.
  • Se cpnon fosse possibile creare un nuovo file nella stessa directory del vecchio file a causa delle autorizzazioni, ciò sarebbe sfortunato (soprattutto se avesse già eliminato quello vecchio).
  • Se il file non era di proprietà dell'utente in esecuzione cpe l'utente in esecuzione cpnon lo era root, sarebbe impossibile far corrispondere il proprietario e le autorizzazioni del nuovo file a quelle del nuovo file.
  • Se il file ha attributi speciali fantasiosi di cui cpnon è a conoscenza, questi andrebbero persi nella copia. Oggi le implementazioni di cpdovrebbero comprendere in modo affidabile cose come attributi estesi, ma non è sempre stato così. E ci sono altre cose, come i fork di risorse MacOS o, per i filesystem remoti, praticamente qualsiasi cosa.

Quindi in conclusione: ora sai cosa cpfa davvero. Non ne sarai mai più sorpreso! Onestamente, penso che la stessa cosa potrebbe essere successa anche a me, molti anni fa.


Devono controllare il riferimento POSIX, ma in realtà le manpagine per le cpversioni di BSD (almeno OSX) e Gnu cpnon sono così esplicite sulla "sovrascrittura". Quella parola è usata solo nei commenti su opzioni -ie -n. La manpage di Gnu è particolarmente poco informativa, Copy SOURCE to DEST, or multiple SOURCE(s) to DIRECTORY.almeno all'inizio della manpage di BSD / MacIn the first synopsis form, the cp utility copies the contents of the source_file to the target_file.
dubiousjim

La pagina di informazioni su coreutils di Gnu inizia:‘cp’ copies files (or, optionally, directories). The copy is completely independent of the original.
dubiousjim,

2
Vedo che lo standard POSIX 2008 specifica il comportamento osservato; Aggiungerò una risposta.
dubiousjim,

16

Vedo che lo standard POSIX 2013 specifica il comportamento osservato . Dice:

  1. Se source_file è di tipo file normale, devono essere eseguite le seguenti operazioni:

    un. ... se esiste dest_file , devono essere prese le seguenti misure:

    io. Se l' -iopzione è attiva, l' cputilità deve scrivere una richiesta per l'errore standard e leggere una riga dall'input standard. Se la risposta non è affermativa, cpnon deve fare altro con source_file e passare a tutti i file rimanenti.

    ii. Un descrittore di file per dest_file deve essere ottenuto eseguendo azioni equivalenti alla open()funzione definita nel volume Interfacce di sistema di POSIX.1-2008 chiamato usando dest_file come argomento path e inclusivo bit a bit ORdi O_WRONLYe O_TRUNCcome argomento oflag .

    iii. Se il tentativo di ottenere un descrittore di file fallisce e l' -fopzione è attiva, cptenterà di rimuovere il file eseguendo azioni equivalenti alla unlink()funzione definita nel volume Interfacce di sistema di POSIX.1-2008 chiamata usando dest_file come argomento path. Se questo tentativo ha esito positivo, cpcontinua con il passaggio 3b.

    ...

    d. Il contenuto di source_file deve essere scritto nel descrittore di file. Eventuali errori di scrittura devono causare la cpscrittura di un messaggio diagnostico nell'errore standard e continuare con il passaggio 3e.

    e. Il descrittore di file deve essere chiuso.


1
Interessante. Come te, immaginavo che cpavrebbe dato risultati simili e che avrebbe mvrotto ogni hardlink di cui la dest faceva parte. Ma ora che ci penso, ciò significherebbe che dovrebbe specificamente unlink(2)il target ( cp -f), o creare un temporaneo con un nome diverso e poi rename(2). L'implementazione semplice è semplicemente aprire il file per la sovrascrittura, che è ciò che richiede POSIX. È equivalente acat src > dest
Peter Cordes,

2

Se si può dire, "la copia di un file nel percorso di destinazione paul copia anche lo stesso file (stesso inode) in tutti gli altri percorsi di destinazione che condividono paull'inode". hard link molto bene. Se do una mela a Sir McCartney, ho dato una mela a Paul e ho dato una mela al compagno di cantautori di John Lennon. Ma non ho distribuito tre mele; Ho dato una mela a una persona che ha più nomi / titoli / descrittori.

Allo stesso modo, quando si copia georgeper paul, non siete anche copiarlo john. Piuttosto, stai copiando i georgedati nel file il cui inode è indicato dalla paulvoce della directory.

Step by Step:   quando lo fai

echo john > john

hai creato un nuovo file (supponendo che non ci fosse già un file chiamato johnin quella directory). Oppure, per dirla più rigorosamente, ciò presuppone che non vi fosse già una voce di directory con il nome johnin quella directory (perché, a rigor di termini, non ci sono file nelle directory; solo voci di directory, che indicano inode). Dopo averlo fatto

cp -l john paul

o

ln john paul

non hai creato un nuovo file; piuttosto, hai dato al tuo file esistente un nuovo nome. Ora hai un file con due nomi: johne paul. E quando dici

cp george paul

stai sovrascrivendo quel file . Il fatto che abbia due nomi è irrilevante; potrebbe avere 42 nomi, possibilmente in luoghi ai quali non è nemmeno possibile accedere, e questo comando non copierebbe i george\ndati su tutti quei nomi (percorsi); sta solo copiando i dati in un unico file che ha più nomi.


1
Grazie. Bene, ero consapevole del carattere necessario per le citazioni di paura di ciò che stavo scrivendo mentre lo scrivevo: johne paulinizio come due nomi di percorso per lo stesso file. Ma era il modo più semplice a cui potevo pensare di esprimermi. Non penso che la semplice nozione di un legame duro, compresa correttamente, imponga uno dei due comportamenti per cp(senza -l).
dubiousjim,

Ma grazie per il pungolo; Ho cercato di chiarire la formulazione.
dubiousjim,
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.