Pourquoi le chat x >> x fait-il une boucle?

17

Les commandes bash suivantes entrent dans une boucle infinie:

$ echo hi > x
$ cat x >> x

Je peux deviner que la catlecture continue xaprès avoir commencé à écrire sur stdout. Ce qui est déroutant, cependant, c'est que ma propre implémentation de test de cat présente un comportement différent:

// mycat.c
#include <stdio.h>

int main(int argc, char **argv) {
  FILE *f = fopen(argv[1], "rb");
  char buf[4096];
  int num_read;
  while ((num_read = fread(buf, 1, 4096, f))) {
    fwrite(buf, 1, num_read, stdout);
    fflush(stdout);
  }

  return 0;
}

Si je cours:

$ make mycat
$ echo hi > x
$ ./mycat x >> x

Il ne boucle pas . Étant donné le comportement de catet le fait que je vidais stdoutavant freadest appelé à nouveau, je m'attendrais à ce que ce code C continue à lire et à écrire dans un cycle.

Comment ces deux comportements sont-ils cohérents? Quel mécanisme explique pourquoi les catboucles alors que le code ci-dessus ne fonctionne pas?

Tyler
la source
Cela fait une boucle pour moi. Avez-vous essayé de l'exécuter sous strace / truss? Sur quel système êtes-vous?
Stéphane Chazelas
Il semble que BSD cat ait ce comportement et GNU cat signale une erreur lorsque nous essayons quelque chose comme ça. Cette réponse traite de la même chose et je crois que vous utilisez BSD cat depuis que j'ai GNU cat et lors du test a obtenu l'erreur.
Ramesh
J'utilise Darwin. J'aime l'idée qui cat x >> xcause une erreur; cependant, cette commande est suggérée dans le livre Unix de Kernighan et Pike comme exercice.
Tyler
3
catutilise très probablement des appels système au lieu de stdio. Avec stdio, votre programme peut mettre en cache EOFness. Si vous commencez avec un fichier de plus de 4096 octets, obtenez-vous une boucle infinie?
Mark Plotnick
@MarkPlotnick, oui! Le code C boucle lorsque le fichier dépasse 4k. Merci, c'est peut-être là toute la différence.
Tyler

Réponses:

12

Sur un ancien système RHEL que j'ai, /bin/catne fait pas de boucle cat x >> x. catdonne le message d'erreur "cat: x: le fichier d'entrée est le fichier de sortie". Je peux tromper /bin/caten faisant ceci: cat < x >> x. Lorsque j'essaie votre code ci-dessus, j'obtiens le "bouclage" que vous décrivez. J'ai également écrit un "chat" basé sur les appels système:

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
int
main(int ac, char **av)
{
        char buf[4906];
        int fd, cc;
        fd = open(av[1], O_RDONLY);
        while ((cc = read(fd, buf, sizeof(buf))) > 0)
                if (cc > 0) write(1, buf, cc);
        close(fd);
        return 0;
}

Cela boucle aussi. La seule mise en mémoire tampon ici (contrairement à "mycat" basé sur stdio) est ce qui se passe dans le noyau.

Je pense que ce qui se passe est que le descripteur de fichier 3 (le résultat de open(av[1])) a un décalage dans le fichier de 0. Le descripteur de fichier 1 (stdout) a un décalage de 3, car le ">>" fait que le shell appelant fait un lseek()sur le descripteur de fichier avant de le transmettre au catprocessus enfant.

Faire un read()de n'importe quelle sorte, que ce soit dans un tampon stdio ou un simple, char buf[]avance la position du descripteur de fichier 3. Faire un write()avance la position du descripteur de fichier 1. Ces deux décalages sont des nombres différents. En raison du ">>", le descripteur de fichier 1 a toujours un décalage supérieur ou égal à l'offset du descripteur de fichier 3. Ainsi, tout programme "semblable à un chat" bouclera, à moins qu'il ne fasse un tampon interne. Il est possible, voire probable, qu'une implémentation stdio d'un FILE *(qui est le type des symboles stdoutet fdans votre code) qui inclut son propre tampon. fread()peut en fait faire un appel système read()pour remplir le tampon interne fo f. Cela peut ou ne peut rien changer à l'intérieur de stdout. Appel fwrite()surstdoutpeut ou ne peut rien changer à l'intérieur de f. Un "chat" basé sur stdio peut donc ne pas boucler. Ou peut-être. Difficile à dire sans lire beaucoup de code libc laid et laid.

Je l' ai fait un stracesur la RHEL cat- il fait juste une succession de read()et write()appels système. Mais un catne doit pas fonctionner de cette façon. Il serait possible pour mmap()le fichier d'entrée, alors faites write(1, mapped_address, input_file_size). Le noyau ferait tout le travail. Ou vous pouvez faire un sendfile()appel système entre les descripteurs de fichiers d'entrée et de sortie sur les systèmes Linux. Les vieux systèmes SunOS 4.x étaient censés faire l'affaire de mappage de la mémoire, mais je ne sais pas si quelqu'un a déjà fait un chat basé sur sendfile. Dans les deux cas, le "bouclage" ne se produirait pas, car les deux write()et sendfile()nécessitent un paramètre de longueur à transférer.

Bruce Ediger
la source
Merci. Sur Darwin, il semble que l' freadappel ait mis en cache un indicateur EOF comme l'a suggéré Mark Plotnick. Preuve: [1] Le chat Darwin utilise la lecture, pas la peur; et [2] la frayeur de Darwin appelle __srefill qui définit fp->_flags |= __SEOF;dans certains cas. [1] src.gnu-darwin.org/src/bin/cat/cat.c [2] opensource.apple.com/source/Libc/Libc-167/stdio.subproj/…
Tyler
1
C'est génial - j'ai été le premier à le voter hier. Il peut être utile de mentionner que le seul commutateur défini par POSIX pour catest cat -u- u pour non tamponné .
mikeserv le
En fait, >>doit être implémenté en appelant open () avec l' O_APPENDindicateur, ce qui fait que chaque opération d'écriture écrit (atomiquement) à la fin actuelle du fichier, quelle que soit la position du descripteur de fichier avant la lecture. Ce comportement est nécessaire pour foo >> logfile & bar >> logfilefonctionner correctement, par exemple - vous ne pouvez pas vous permettre de supposer que la position après la fin de votre dernière écriture est toujours la fin du fichier.
hmakholm a quitté Monica le
1

Une implémentation de chat moderne (sunos-4.0 1988) utilise mmap () pour mapper le fichier entier, puis appelle 1x write () pour cet espace. Une telle implémentation ne boucle pas tant que la mémoire virtuelle permet de mapper le fichier entier.

Pour les autres implémentations, cela dépend si le fichier est plus grand que le tampon d'E / S.

schily
la source
De nombreuses catimplémentations ne tamponnent pas leur sortie ( -uimplicite). Ceux-ci seront toujours en boucle.
Stéphane Chazelas
Solaris 11 (SunOS-5.11) ne semble pas utiliser mmap () pour les petits fichiers (semble y avoir recours uniquement pour les fichiers de 32 769 octets ou plus).
Stéphane Chazelas
Correct -u est généralement la valeur par défaut. Cela n'implique pas une boucle car une implémentation peut lire toute la taille du fichier et faire une seule écriture avec ce buf.
schily
Solaris cat ne boucle que si la taille du fichier est> max mapize ou si le décalage de fichier initial est! = 0.
schily
Ce que j'observe avec Solaris 11. Il fait une boucle read () si le décalage initial est! = 0 ou si la taille du fichier est comprise entre 0 et 32768. Au-dessus de cela, il mmaps () 8MiB de grandes régions du fichier à la fois et jamais semblent revenir aux boucles read () même pour les fichiers PiB (testés sur des fichiers clairsemés).
Stéphane Chazelas
0

Comme écrit dans les pièges de Bash , vous ne pouvez pas lire à partir d'un fichier et y écrire dans le même pipeline.

Selon ce que fait votre pipeline, le fichier peut être clobé (à 0 octet, ou éventuellement à un nombre d'octets égal à la taille de la mémoire tampon du pipeline de votre système d'exploitation), ou il peut augmenter jusqu'à ce qu'il remplisse l'espace disque disponible ou atteigne la limitation de la taille des fichiers de votre système d'exploitation, ou votre quota, etc.

La solution consiste à utiliser l'éditeur de texte ou une variable temporaire.

MatthewRock
la source
-1

Vous avez une sorte de condition de concurrence entre les deux x. Certaines implémentations de cat(par exemple coreutils 8.23) interdisent que:

$ cat x >> x
cat: x: input file is output file

Si cela n'est pas détecté, le comportement dépendra évidemment de l'implémentation (taille du buffer, etc.).

Dans votre code, vous pouvez essayer d'ajouter un clearerr(f);après le fflush, au cas où le suivant freadretournerait une erreur si l'indicateur de fin de fichier est défini.

vinc17
la source
Il semble qu'un bon système d'exploitation aura un comportement déterministe pour un seul processus avec un seul thread exécutant les mêmes commandes de lecture / écriture. En tout cas, le comportement est déterministe pour moi, et je pose principalement des questions sur l'écart.
Tyler
@Tyler IMHO, sans spécification claire sur ce cas, la commande ci-dessus n'a aucun sens, et le déterminisme n'est pas vraiment important (sauf une erreur comme ici, qui est le meilleur comportement). C'est un peu comme le i = i++;comportement indéfini de C , d'où la divergence.
vinc17
1
Non, il n'y a pas de condition de course ici, le comportement est bien défini. Il est cependant défini par l'implémentation, en fonction de la taille relative du fichier et du tampon utilisé par cat.
Gilles 'SO- arrête d'être méchant'
@Gilles Où voyez-vous que le comportement est bien défini / défini par l'implémentation? Pouvez-vous donner une référence? La spécification POSIX cat dit simplement: "Il est défini par l'implémentation si l'utilitaire cat tamponne la sortie si l'option -u n'est pas spécifiée." Cependant, lorsqu'un tampon est utilisé, l'implémentation n'a pas à définir comment il est utilisé; il peut être non déterministe, par exemple avec un tampon vidé à un moment aléatoire.
vinc17
@ vinc17 Veuillez insérer «en pratique» dans mon commentaire précédent. Oui, c'est théoriquement possible et conforme à POSIX, mais personne ne le fait.
Gilles 'SO- arrête d'être méchant'