Surpris par le comportement de cp avec des hardlinks

20

Je comprends très bien la notion de liens physiques et j'ai lu plusieurs fois les pages de manuel des outils de base comme cp--- et même les spécifications POSIX récentes ---. J'ai quand même été surpris d'observer le comportement suivant:

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

À ce stade, johnil paulaura le même inode (et le même contenu) et georgedifférera sur les deux aspects. Maintenant, nous faisons:

$ cp george paul

A ce stade , je me attendais georgeet pauld'avoir un nombre différent de inodes , mais le même contenu --- cette attente est satisfaite --- mais je également prévu pauld'avoir maintenant un inode différent de john, et johnd'avoir encore le contenu john. C'est là que j'ai été surpris. Il s'avère que la copie d'un fichier vers le chemin de destination paula également pour résultat d'installer ce même fichier (même inode) sur tous les autres chemins de destination qui partagent paull'inode. Je pensais que cela cpcrée un nouveau fichier et le déplace à la place autrefois occupée par l'ancien fichier paul. Au lieu de cela, ce qu'il semble faire est d'ouvrir le fichier existant paul, de le tronquer et d'écriregeorgecontenu dans ce fichier existant. Par conséquent, tous les "autres" fichiers avec le même inode obtiennent "leur" contenu mis à jour en même temps.

Ok, c'est un comportement systématique et maintenant que je sais m'y attendre, je peux trouver un moyen de contourner ce problème ou d'en tirer parti, le cas échéant. Ce qui me laisse perplexe, c'est où j'étais censé voir ce comportement documenté? Je serais surpris si ce n'est pas documenté quelque part dans des documents que j'ai déjà consultés. Mais apparemment, je l'ai manqué, et je ne trouve pas maintenant une source qui discute de ce comportement.

dubiousjim
la source

Réponses:

4

D'abord, pourquoi est-ce fait de cette façon? Une raison est historique: c'est ainsi que cela a été fait dans Unix First Edition .

Les fichiers sont pris par paires; le premier est ouvert en lecture, le second mode créé 17. Puis le premier est copié dans le second.

«Créé» fait référence à l' creatappel système (celui auquel il manque un e ), qui tronque le fichier existant par le nom donné s'il en existe un.

Et voici le code source de cpdans Unix Second Edition (je ne trouve pas le code source de First Edition). Vous pouvez voir les appels à openpour le fichier source et creatpour le deuxième fichier; et, pour améliorer First Edition, si le deuxième fichier est un répertoire existant, cpcrée un fichier dans ce répertoire.

Mais, vous pouvez vous demander, pourquoi a-t-il été fait de cette façon à l'époque? La réponse à «pourquoi Unix l'a-t-il fait à l'origine de cette façon» est presque toujours la simplicité. cpouvre sa source pour la lecture et crée sa destination - et l'appel système pour créer un fichier écrase un fichier existant en l'ouvrant pour l'écriture, car cela permet à l'appelant d'imposer le contenu d'un fichier par le nom donné, que le fichier existe déjà ou ne pas.

Maintenant, pour savoir où c'est documenté: dans la page de manuel de FreeBSD .

Pour chaque fichier de destination qui existe déjà, son contenu est écrasé si les autorisations le permettent. Son mode, son ID utilisateur et son ID de groupe sont inchangés sauf si l'option -p a été spécifiée.

Cette formulation était présente au moins dès 1990 (à l'époque où BSD était de 4,3 BSD). La formulation est similaire sur Solaris 10 :

Si target_file existe, cp écrase son contenu, mais le mode (et ACL le cas échéant), le propriétaire et le groupe qui lui sont associés ne sont pas modifiés.

Votre cas est même expliqué dans le manuel HP-UX 10 :

Si new_file est un lien vers un fichier existant avec d'autres liens, écrase le fichier existant et conserve tous les liens.

POSIX le met en standardese. Citant de Single UNIX v2 :

Si dest_file existe, les étapes suivantes sont suivies: (…) Un descripteur de fichier pour dest_file sera obtenu en effectuant des actions équivalentes à la fonction open () de spécification XSH appelée en utilisant dest_file comme argument de chemin, et le OU inclus au niveau du bit de O_WRONLY et O_TRUNC comme argument oflag.

Les pages de manuel et la spécification que j'ai citées spécifient en outre que si l' -foption est passée et que la tentative d'ouverture / création du fichier cible échoue (généralement en raison de la non autorisation d'écrire le fichier), cpessaie de supprimer la cible et de recréer un fichier . Cela romprait le lien dur dans votre scénario.

Vous voudrez peut-être signaler un bogue de documentation par rapport au manuel GNU coreutils , car il ne documente pas ce comportement. Même la description de --preserve=linksce qui, dans votre scénario, entraînerait la paulsuppression du lien et la création d'un nouveau fichier, ne précise pas ce qui se passe sans --preserve=links. La description du -ftype implique ce qui se passe sans elle mais ne l'explique pas («Lorsque la copie sans cette option et qu'un fichier de destination existant ne peut pas être ouvert pour l'écriture, la copie échoue. Cependant, avec --force,…»).

Gilles 'SO- arrête d'être méchant'
la source
pourquoi dites-vous "parce que cela permet à l'appelant de s'approprier un nom de fichier, que le fichier existe déjà ou non"? Cp ne prend pas possession d'un fichier préexistant.
jrw32982 prend en charge Monica
@ jrw32982 Je voulais dire la propriété dans le sens de décider ce qui va dans le fichier, pas la propriété dans le sens des métadonnées du fichier. J'ai réécrit cette phrase.
Gilles 'SO- arrête d'être méchant'
20

cpdocuments qu'il écrase le fichier de destination si le fichier de destination est déjà présent. Vous avez raison, il ne spécifie pas en détail ce que signifie "écraser", mais il dit définitivement "écraser", pas "remplacer". Si vous voulez être pédant, vous pouvez affirmer que «remplacer» est exactement ce qui cpfait, et le comportement que vous attendiez serait correctement appelé «remplacer».

Notez également que si cp"remplacer" des fichiers de destination préexistants, cela pourrait raisonnablement être considéré comme surprenant ou incorrect, probablement plus que "l'écrasement". Par exemple:

  • Si d' cpabord supprimé l'ancien fichier puis créé un nouveau, il y aurait un intervalle de temps pendant lequel le fichier serait absent, ce qui serait surprenant.
  • Si d' cpabord créé un fichier temporaire puis déplacé en place, il devrait probablement documenter cela, en raison du fait que de tels fichiers temporaires avec des noms étranges seraient parfois remarqués ... mais ce n'est pas le cas.
  • Si vous cpne pouviez pas créer un nouveau fichier dans le même répertoire que l'ancien fichier en raison d'autorisations, ce serait regrettable (surtout s'il avait déjà supprimé l'ancien).
  • Si le fichier n'appartenait pas à l'utilisateur en cours d'exécution cpet que l'utilisateur en cours d'exécution cpne l'était pas, rootil serait impossible de faire correspondre le propriétaire et les autorisations du nouveau fichier à ceux du nouveau fichier.
  • Si le fichier a des attributs spéciaux fantaisistes qui cpne sont pas connus, ils seront perdus dans la copie. De nos jours, les implémentations de cpdevraient comprendre de manière fiable des choses comme les attributs étendus, mais il n'en a pas toujours été ainsi. Et il y a d'autres choses, comme les fourchettes de ressources MacOS, ou, pour les systèmes de fichiers distants, pratiquement n'importe quoi.

Donc en conclusion: maintenant vous savez ce cpqui fait vraiment. Vous n'en serez plus jamais surpris! Honnêtement, je pense que la même chose aurait pu m'arriver aussi, il y a de nombreuses années.

Celada
la source
Il faut vérifier la référence POSIX, mais en fait les manpages cpsur BSD (au moins, OSX) et Gnu cpne sont pas aussi explicites sur la "réécriture". Ce mot n'est utilisé que dans les commentaires sur les options -iet -n. La page de manuel Gnu est particulièrement peu informative, commençant Copy SOURCE to DEST, or multiple SOURCE(s) to DIRECTORY.La page de In the first synopsis form, the cp utility copies the contents of the source_file to the target_file.
manuel
La page d'informations de Gnu coreutils commence:‘cp’ copies files (or, optionally, directories). The copy is completely independent of the original.
dubiousjim
2
Je vois que la norme POSIX 2008 spécifie le comportement observé; J'ajouterai une réponse.
dubiousjim
16

Je vois que la norme POSIX 2013 spécifie le comportement observé . Ça dit:

  1. Si le fichier source est de type fichier normal, les étapes suivantes doivent être suivies:

    une. ... si dest_file existe, les étapes suivantes doivent être suivies :

    je. Si l' -ioption est en vigueur, l' cputilitaire doit écrire une invite à l'erreur standard et lire une ligne à partir de l'entrée standard. Si la réponse n'est pas affirmative, cpne faites rien de plus avec source_file et passez aux fichiers restants.

    ii. Un descripteur de fichier pour DEST_FILE doit être obtenu en effectuant des actions équivalent à la open()fonction définie dans le volume Interfaces système de POSIX.1-2008 appelé à l' aide DEST_FILE comme argument de chemin, et le bit compris ORde O_WRONLYet O_TRUNCcomme l' Oflag argument.

    iii. Si la tentative d'obtention d'un descripteur de fichier échoue et que l' -foption est en vigueur, cpdoit tenter de supprimer le fichier en effectuant des actions équivalentes à la unlink()fonction définie dans le volume System Interfaces de POSIX.1-2008 appelé en utilisant dest_file comme argument de chemin. Si cette tentative réussit, cppassez à l'étape 3b.

    ...

    ré. Le contenu du fichier source doit être écrit dans le descripteur de fichier. Toute erreur d'écriture doit entraîner l' cpécriture d'un message de diagnostic sur l'erreur standard et passer à l'étape 3e.

    e. Le descripteur de fichier doit être fermé.

dubiousjim
la source
1
Intéressant. Comme vous, je supposais que cpcela donnerait des résultats similaires mvet casserait tous les liens durs dont le dest faisait partie. Mais maintenant que j'y pense, cela signifierait qu'il faudrait spécifiquement unlink(2)la cible ( cp -f), ou créer un temporaire différemment nommé et ensuite rename(2). L'implémentation simple consiste à simplement ouvrir le fichier pour l'écraser, ce qui est requis par POSIX. Cela équivaut àcat src > dest
Peter Cordes
2

Si vous pouvez dire: «la copie d'un fichier vers le chemin de destination paul copie également le même fichier (même inode) vers tous les autres chemins de destination qui partagent paull'inode»., Je suis désolé de dire que vous ne comprenez pas la notion de liens durs très bien. Si je donne une pomme à Sir McCartney, j'ai donné une pomme à Paul et j'ai donné une pomme au partenaire auteur-compositeur de John Lennon. Mais je n'ai pas distribué trois pommes; J'ai donné une pomme à une personne qui a plusieurs noms / titres / descripteurs.

De même, lorsque vous copiez georgevers paul, vous ne le copiez pas égalementjohn . Vous copiez plutôt les georgedonnées dans le fichier dont l'inode est pointé par l' paulentrée de répertoire.

Étape par étape:   quand vous le faites

echo john > john

vous avez créé un nouveau fichier (en supposant qu'il n'y avait pas déjà un fichier nommé johndans ce répertoire). Ou, pour parler plus strictement, cela suppose qu'il n'y avait pas déjà une entrée de répertoire avec le nom johndans ce répertoire (car, à proprement parler, il n'y a pas de fichiers dans les répertoires; seulement des entrées de répertoire, qui pointent vers des inodes). Après avoir fait

cp -l john paul

ou

ln john paul

vous n'avez pas créé de nouveau fichier; vous avez plutôt donné un nouveau nom à votre fichier existant. Vous avez maintenant un fichier avec deux noms: johnet paul. Et quand tu dis

cp george paul

vous écrasez ce fichier . Le fait qu'il porte deux noms est sans importance; elle pourrait avoir 42 noms, peut-être dans des endroits auxquels vous ne pouvez même pas accéder, et cette commande ne copierait pas les george\ndonnées dans tous ces noms (chemins); il s'agit simplement de copier les données dans un seul fichier qui a plusieurs noms.

Scott
la source
1
Merci. À droite, j'étais conscient du caractère nécessaire de guillemets effrayants de ce que j'écrivais comme je l'ai écrit: johnet paulcommencez comme deux chemins pour le même fichier. Mais c'était la manière la plus simple à laquelle je pouvais penser pour m'exprimer. Je ne pense pas que la simple notion de lien dur, correctement comprise, dicte l'un ou l'autre des deux comportements pour cp(sans -l).
dubiousjim
Mais merci pour les incitations; J'ai essayé de clarifier le libellé.
dubiousjim