Überrascht vom Verhalten von cp mit Hardlinks

20

Ich verstehe den Begriff der Hardlinks sehr gut und habe die Manpages für grundlegende Tools wie cp--- und sogar die neuesten POSIX-Spezifikationen --- einige Male gelesen . Trotzdem war ich überrascht, folgendes Verhalten zu beobachten:

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

Zu diesem Zeitpunkt hat johnund paulden gleichen Inode (und Inhalt) und georgeunterscheidet sich in beiden Punkten. Jetzt machen wir:

$ cp george paul

Zu diesem Zeitpunkt erwartete georgeund erwartete paulich unterschiedliche Inode-Nummern, aber den gleichen Inhalt - diese Erwartung wurde erfüllt -, aber ich erwartete auch, dass pauljetzt eine andere Inode-Nummer als johnund johnder Inhalt noch vorhanden sein würde john. Hier war ich überrascht. Es stellt sich heraus, dass das Kopieren einer Datei in den Zielpfad paulauch dazu führt, dass dieselbe Datei (derselbe Inode) auf allen anderen Zielpfaden installiert wird, die den paulInode gemeinsam nutzen. Ich dachte, dass cperstellt eine neue Datei und verschiebt es an die Stelle, die zuvor von der alten Datei besetzt war paul. Stattdessen scheint es so zu sein, dass die vorhandene Datei geöffnet paul, abgeschnitten und geschrieben wirdgeorge's Inhalt in die vorhandene Datei. Daher werden alle "anderen" Dateien mit demselben Inode "ihren" Inhalt gleichzeitig aktualisiert.

Ok, dies ist ein systematisches Verhalten, und jetzt, da ich weiß, dass ich es erwarten kann, kann ich herausfinden, wie ich es umgehen oder wie es angebracht ist, es auszunutzen. Was mich verwirrt, ist, wo ich dieses Verhalten dokumentiert sehen sollte? Es würde mich wundern, wenn es nicht irgendwo in Dokumenten dokumentiert ist, die ich mir bereits angesehen habe. Aber anscheinend habe ich es verpasst und kann jetzt keine Quelle finden, die dieses Verhalten beschreibt.

zweifelhafter jim
quelle

Antworten:

4

Erstens, warum wird das so gemacht? Ein Grund ist historisch: So wurde es in Unix First Edition gemacht .

Dateien werden paarweise aufgenommen. Der erste wird zum Lesen geöffnet, der zweite zum Erstellen. 17. Dann wird der erste in den zweiten kopiert.

"Erstellt" bezieht sich auf den creatSystemaufruf (der bekanntermaßen ein e fehlt ), der die vorhandene Datei durch den angegebenen Namen abschneidet, sofern einer vorhanden ist.

Und hier ist der Quellcode von cpUnix Second Edition (ich kann den Quellcode von First Edition nicht finden). Sie können die Aufrufe openfür die Quelldatei und creatfür die zweite Datei anzeigen. Wenn es sich bei der zweiten Datei um ein vorhandenes Verzeichnis handelt, cpwird als Verbesserung zu First Edition eine Datei in diesem Verzeichnis erstellt.

Sie fragen sich vielleicht, warum das damals so war? Die Antwort auf "Warum hat Unix das ursprünglich so gemacht?" Ist fast immer Einfachheit. cpöffnet seine Quelle zum Lesen und erstellt sein Ziel - und der Systemaufruf zum Erstellen einer Datei überschreibt eine vorhandene Datei, indem er sie zum Schreiben öffnet, da der Aufrufer den Inhalt einer Datei mit dem angegebenen Namen auferlegen kann, ob die Datei bereits vorhanden ist oder nicht nicht.

Nun, wo es dokumentiert ist: in der FreeBSD-Manpage .

Für jede bereits vorhandene Zieldatei wird der Inhalt überschrieben, sofern die Berechtigungen dies zulassen. Der Modus, die Benutzer-ID und die Gruppen-ID bleiben unverändert, sofern nicht die Option -p angegeben wurde.

Dieser Wortlaut war mindestens 1990 (damals BSD 4.3BSD) vorhanden. Unter Solaris 10 gibt es einen ähnlichen Wortlaut :

Wenn target_file vorhanden ist, überschreibt cp seinen Inhalt, aber der Modus (und gegebenenfalls die Zugriffssteuerungsliste), der Eigentümer und die damit verbundene Gruppe werden nicht geändert.

Ihr Fall ist sogar im HP-UX 10- Handbuch aufgeführt:

Wenn new_file eine Verknüpfung zu einer vorhandenen Datei mit anderen Verknüpfungen ist, überschreibt die vorhandene Datei und behält alle Verknüpfungen bei.

POSIX bringt es auf Standard. Zitieren von Single UNIX v2 :

Wenn dest_file vorhanden ist, werden die folgenden Schritte ausgeführt: (…) Ein Dateideskriptor für dest_file wird ermittelt, indem Aktionen ausgeführt werden, die der aufgerufenen open () - Funktion der XSH-Spezifikation entsprechen, wobei dest_file als Pfadargument und das bitweise inklusive OR von O_WRONLY und O_TRUNC verwendet werden als oflag Argument.

Die Manpages und Spezifikationen, die ich zitiert habe, geben an, dass, wenn die -fOption übergeben wird und der Versuch, die Zieldatei zu öffnen / erstellen, fehlschlägt (normalerweise, weil keine Berechtigung zum Schreiben der Datei vorliegt), cpversucht wird, das Ziel zu entfernen und eine Datei erneut zu erstellen . Dies würde die harte Verbindung in Ihrem Szenario unterbrechen.

Möglicherweise möchten Sie einen Dokumentationsfehler im Handbuch zu GNU coreutils melden , da dieses Verhalten nicht dokumentiert wird. Selbst die Beschreibung von --preserve=links, die in Ihrem Szenario dazu führen würde, dass der paulLink entfernt und eine neue Datei erstellt wird, macht nicht klar, was ohne passiert --preserve=links. Die Beschreibung der -fArt impliziert, was ohne diese Option passiert, ohne sie jedoch zu formulieren ("Wenn das Kopieren ohne diese Option und eine vorhandene Zieldatei nicht zum Schreiben geöffnet werden kann, schlägt die Kopie fehl. Mit --force, ...").

Gilles 'SO - hör auf böse zu sein'
quelle
Warum sagen Sie "weil dies dem Aufrufer erlaubt, den Besitz eines Dateinamens zu übernehmen, unabhängig davon, ob die Datei bereits existiert oder nicht"? Cp übernimmt kein Eigentum an einer bereits vorhandenen Datei.
jrw32982 unterstützt Monica
@ jrw32982 Ich meinte Eigentum im Sinne der Entscheidung, was in die Datei geht, nicht Eigentum im Sinne von Dateimetadaten. Ich habe diesen Satz umgeschrieben.
Gilles 'SO- hör auf böse zu sein'
20

cpdokumentiert, dass es die Zieldatei überschreibt, wenn die Zieldatei bereits vorhanden ist. Sie haben Recht, dass nicht im Detail angegeben wird, was "Überschreiben" bedeutet, aber definitiv "Überschreiben", nicht "Ersetzen". Wenn Sie pedantisch sein wollen, können Sie argumentieren, dass "Überschreiben" genau das ist, was cptut, und das erwartete Verhalten würde zu Recht als "Ersetzen" bezeichnet.

Beachten Sie auch, dass, wenn cpbereits vorhandene Zieldateien "ersetzt" würden, dies vernünftigerweise als überraschend oder falsch angesehen werden könnte, wahrscheinlich mehr als "überschreiben". Beispielsweise:

  • Wenn cpzuerst die alte Datei gelöscht und dann eine neue erstellt würde, gäbe es ein Zeitintervall, in dem die Datei nicht vorhanden wäre, was überraschend wäre.
  • Wenn cpzuerst eine temporäre Datei erstellt und dann verschoben wird, sollte dies wahrscheinlich dokumentiert werden, da solche temporären Dateien mit seltsamen Namen gelegentlich bemerkt werden ... aber nicht.
  • Wenn cpaufgrund von Berechtigungen keine neue Datei im selben Verzeichnis wie die alte Datei erstellt werden konnte, ist dies unglücklich (insbesondere, wenn die alte Datei bereits gelöscht wurde).
  • Wenn die Datei nicht dem Benutzer gehört, der sie ausführt, cpund der Benutzer, der sie ausführt, cpnicht, rootist es unmöglich, den Eigentümer und die Berechtigungen der neuen Datei mit denen der neuen Datei abzugleichen.
  • Wenn die Datei ausgefallene Spezialattribute aufweist, cpdie nicht bekannt sind, gehen diese in der Kopie verloren. Heutzutage sollten Implementierungen von cpzuverlässig Dinge wie erweiterte Attribute verstehen, aber das war nicht immer so. Und es gibt noch andere Dinge, wie MacOS Resource Forks oder für entfernte Dateisysteme im Grunde alles.

Fazit: Jetzt wissen Sie, was cpwirklich funktioniert. Sie werden nie wieder überrascht sein! Ehrlich gesagt denke ich, dass mir vor vielen Jahren das gleiche passiert sein könnte.

Celada
quelle
Müssen Sie die POSIX-Referenz überprüfen, aber in der Tat sind die manSeiten für cpBSD (zumindest OSX) und Gnu-Versionen cpnicht so explizit über "Überschreiben". Dieses Wort wird nur in den Kommentaren zu Optionen -iund verwendet -n. Die Gnu-Hilfeseite ist besonders wenig Copy SOURCE to DEST, or multiple SOURCE(s) to DIRECTORY.In the first synopsis form, the cp utility copies the contents of the source_file to the target_file.
aussagekräftig
Die Informationsseite zu Gnu coreutils beginnt:‘cp’ copies files (or, optionally, directories). The copy is completely independent of the original.
dubiousjim
2
Ich sehe, dass der POSIX 2008-Standard das beobachtete Verhalten spezifiziert. Ich werde eine Antwort hinzufügen.
dubiousjim
16

Ich sehe, dass der POSIX 2013-Standard das beobachtete Verhalten spezifiziert . Es sagt:

  1. Wenn source_file vom Typ regular file ist, müssen die folgenden Schritte ausgeführt werden:

    ein. ... falls dest_file existiert, sind folgende Schritte durchzuführen :

    ich. Wenn die -iOption cpaktiviert ist, schreibt das Dienstprogramm eine Aufforderung zum Standardfehler und liest eine Zeile von der Standardeingabe. Wenn die Antwort nicht positiv ist, cpdürfen Sie mit source_file nichts weiter tun und mit den verbleibenden Dateien fortfahren .

    ii. Ein Dateideskriptor für dest_file wird erhalten, indem Aktionen ausgeführt werden, die der open()Funktion entsprechen, die im System Interfaces-Volume von POSIX.1-2008 definiert ist, das unter Verwendung von dest_file als Pfadargument und der bitweisen Einbeziehung ORvon O_WRONLYund O_TRUNCals oflag- Argument aufgerufen wird .

    iii. Wenn der Versuch, einen Dateideskriptor abzurufen, fehlschlägt und die -fOption aktiviert ist, cpsoll versucht werden, die Datei zu entfernen, indem Aktionen ausgeführt werden, die unlink()der im POSIX.1-2008-Volume System Interfaces definierten Funktion entsprechen, die mit dest_file als Pfadargument aufgerufen wird . Wenn dieser Versuch erfolgreich ist, cpfahren Sie mit Schritt 3b fort.

    ...

    d. Der Inhalt von source_file wird in den Dateideskriptor geschrieben. Schreibfehler führen cpdazu, dass eine Diagnosemeldung in den Standardfehler geschrieben wird, und fahren Sie mit Schritt 3e fort.

    e. Der Dateideskriptor wird geschlossen.

zweifelhafter jim
quelle
1
Interessant. Wie Sie nahm ich an, cpdass das Ziel ähnliche Ergebnisse mvliefert und alle Hardlinks unterbricht, zu denen es gehört. Aber jetzt, wo ich darüber nachdenke, würde das bedeuten, dass es spezifisch unlink(2)das Ziel ( cp -f) sein müsste oder ein anders benanntes temporäres Objekt erstellen müsste und dann rename(2). Die einfache Implementierung besteht darin, die Datei nur zum Überschreiben zu öffnen, was für POSIX erforderlich ist. Es ist gleichbedeutend mitcat src > dest
Peter Cordes
2

Wenn Sie sagen können: "Wenn Sie eine Datei in den Zielpfad kopieren, wird paul dieselbe Datei (derselbe Inode) auch in alle anderen Zielpfade kopiert, die den paulInode gemeinsam haben." harte links sehr gut. Wenn ich Sir McCartney einen Apfel gebe, habe ich Paul einen Apfel gegeben, und ich habe John Lennons Songwriting-Partner einen Apfel gegeben. Aber ich habe nicht drei Äpfel ausgegeben; Ich habe einer Person, die mehrere Namen / Titel / Deskriptoren hat, einen Apfel gegeben.

Ebenso, wenn Sie kopieren georgezu paul, sind Sie nicht auch kopieren es john. Sie kopieren die georgeDaten vielmehr in die Datei, auf deren Inode der paulVerzeichniseintrag verweist.

Schritt für Schritt:   Wenn Sie es tun

echo john > john

Sie haben eine neue Datei erstellt (vorausgesetzt, johnin diesem Verzeichnis wurde noch keine Datei benannt ). Genauer johngesagt wird davon ausgegangen, dass sich noch kein Verzeichniseintrag mit dem Namen in diesem Verzeichnis befindet (da sich streng genommen keine Dateien in Verzeichnissen befinden, sondern nur Verzeichniseinträge, die auf Inodes verweisen). Nachdem du es getan hast

cp -l john paul

oder

ln john paul

Sie haben keine neue Datei erstellt. Stattdessen haben Sie Ihrer vorhandenen Datei einen neuen Namen gegeben. Sie haben jetzt eine Datei mit zwei Namen: johnund paul. Und wenn du sagst

cp george paul

Sie überschreiben diese Datei . Die Tatsache, dass es zwei Namen hat, ist irrelevant; es könnte 42 Namen haben, möglicherweise an Stellen, auf die Sie nicht einmal zugreifen können, und dieser Befehl würde die george\nDaten nicht in alle diese Namen (Pfade) kopieren ; Es werden lediglich die Daten in eine Datei mit mehreren Namen kopiert .

Scott
quelle
1
Vielen Dank. Richtig, ich war mir des Charakters bewusst, den ich geschrieben habe, als ich es geschrieben habe: johnund paulbeginne als zwei Pfadnamen für dieselbe Datei. Aber es war der einfachste Weg, mich auszudrücken. Ich denke nicht, dass die bloße Vorstellung einer festen Verbindung, richtig verstanden, eines der beiden Verhaltensweisen für (ohne ) diktiert . cp-l
dubiousjim
Aber danke für das Stupsen; Ich habe versucht, den Wortlaut zu klären.
dubiousjim