¿Por qué cat x >> x loop?

17

Los siguientes comandos bash entran en un bucle infinito:

$ echo hi > x
$ cat x >> x

Puedo adivinar que catcontinúa leyendo xdespués de que comenzó a escribir en stdout. Sin embargo, lo que es confuso es que mi propia implementación de prueba de cat exhibe un comportamiento diferente:

// 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 corro:

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

Lo hace no lazo. Dado el comportamiento caty el hecho de que me estoy volviendo loco stdoutantes fread, se espera que este código C continúe leyendo y escribiendo en un ciclo.

¿Cómo son consistentes estos dos comportamientos? ¿Qué mecanismo explica por qué se catrepite mientras que el código anterior no lo hace?

Tyler
fuente
Lo hace para mí. ¿Has intentado ejecutarlo bajo strace / truss? ¿En qué sistema estás?
Stéphane Chazelas
Parece que BSD cat tiene este comportamiento y GNU cat informa un error cuando intentamos algo como esto. Esta respuesta discute lo mismo y creo que está usando BSD cat ya que tengo GNU cat y cuando lo probé obtuvo el error.
Ramesh
Estoy usando a Darwin. Me gusta la idea que cat x >> xcausa un error; sin embargo, este comando se sugiere en Kernighan y el libro de Unike de Pike como ejercicio.
Tyler
3
catLo más probable es que use llamadas del sistema en lugar de stdio. Con stdio, su programa puede estar almacenando en caché EOFness. Si comienza con un archivo de más de 4096 bytes, ¿obtiene un bucle infinito?
Mark Plotnick
@ MarkPlotnick, ¡sí! El código C se repite cuando el archivo supera los 4k. Gracias, tal vez esa es toda la diferencia allí mismo.
Tyler

Respuestas:

12

En un sistema más antiguo RHEL lo que tengo, /bin/catlo hace no lazo para cat x >> x. catda el mensaje de error "cat: x: el archivo de entrada es el archivo de salida". Puedo engañar /bin/catal hacer esto: cat < x >> x. Cuando pruebo su código anterior, obtengo el "bucle" que describe. También escribí una llamada al sistema basada en "cat":

#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;
}

Esto también se repite. El único búfer aquí (a diferencia de "mycat" basado en stdio) es lo que sucede en el núcleo.

Creo que lo que está sucediendo es que el descriptor de archivo 3 (el resultado de open(av[1])) tiene un desplazamiento dentro del fichero de 0. 1 Filed descriptor (stdout) cuenta con un desplazamiento de 3, debido a que el ">>" hace que el shell que invoca a hacer una lseek()en el descriptor de archivo antes de entregarlo al catproceso secundario.

Hacer una read()de cualquier tipo, ya sea en un búfer stdio o en un plano, char buf[]avanza la posición del descriptor de archivo 3. Hacer un write()avance de la posición del descriptor de archivo 1. Esos dos desplazamientos son números diferentes. Debido al ">>", el descriptor de archivo 1 siempre tiene un desplazamiento mayor o igual que el desplazamiento del descriptor de archivo 3. Por lo tanto, cualquier programa "similar a un gato" se repetirá, a menos que haga un búfer interno. Es posible, incluso probable, que una implementación estándar de un FILE *(que es el tipo de símbolos stdouty fen su código) que incluya su propio búfer. fread()en realidad puede hacer una llamada read()al sistema para llenar el búfer interno fo f. Esto puede o no cambiar nada en el interior de stdout. llamando fwrite()enstdoutpuede o no cambiar nada en el interior de f. Por lo tanto, un "gato" basado en stdio podría no repetirse. O que podría hacerlo. Difícil de decir sin leer un montón de código libc feo y feo.

Hice una straceen la RHEL cat- sólo se hace una sucesión de read()y write()llamadas al sistema. Pero a catno tiene que funcionar de esta manera. Sería posible mmap()ingresar el archivo, luego hacer write(1, mapped_address, input_file_size). El núcleo haría todo el trabajo. O puede hacer una sendfile()llamada al sistema entre los descriptores de los archivos de entrada y salida en los sistemas Linux. Se rumoreaba que los viejos sistemas SunOS 4.x hacían el truco de mapeo de memoria, pero no sé si alguien había hecho un gato basado en sendfile. En cualquier caso, el "bucle" sería no ocurrir, ya que tanto write()y sendfile()requerir un parámetro de longitud a transferencia.

Bruce Ediger
fuente
Gracias. En Darwin, parece que la freadllamada almacenó en caché una bandera EOF como sugirió Mark Plotnick. Evidencia: [1] El gato Darwin usa lectura, no fread; y [2] el fread de Darwin llama __srefill que se establece fp->_flags |= __SEOF;en algunos casos. [1] src.gnu-darwin.org/src/bin/cat/cat.c [2] opensource.apple.com/source/Libc/Libc-167/stdio.subproj/…
Tyler
1
Esto es increíble: ayer fui el primero en votarlo. Se podría valer la pena mencionar que la única interruptor definido POSIX para cates cat -u- U para no tamponada .
mikeserv
En realidad, >>debe implementarse llamando a open () con el O_APPENDindicador, lo que hace que cada operación de escritura escriba (atómicamente) en el final actual del archivo, sin importar la posición del descriptor del archivo antes de la lectura. Este comportamiento es necesario para foo >> logfile & bar >> logfileque funcione correctamente, por ejemplo: no puede darse el lujo de asumir que la posición después del final de su última escritura sigue siendo el final del archivo.
hmakholm dejó a Mónica el
1

Una implementación cat moderna (sunos-4.0 1988) usa mmap () para mapear todo el archivo y luego llama a 1x write () para este espacio. Dicha implementación no se repetirá mientras la memoria virtual permita mapear todo el archivo.

Para otras implementaciones, depende de si el archivo es más grande que el búfer de E / S.

astuto
fuente
Muchas catimplementaciones no amortiguan su salida ( -uimplícita). Esos siempre se repetirán.
Stéphane Chazelas
Solaris 11 (SunOS-5.11) no parece estar usando mmap () para archivos pequeños (parece recurrir a él solo para archivos de 32769 bytes o más).
Stéphane Chazelas
Correcto -u suele ser el valor predeterminado. Esto no implica un bucle, ya que una implementación puede leer todo el tamaño del archivo y solo escribir con ese buf.
schily
Solaris cat solo se repite si el tamaño del archivo es> max mapize o si el desplazamiento inicial del archivo es! = 0.
schily
Lo que observo con Solaris 11. Hace un ciclo de lectura () si el desplazamiento inicial es! = 0 o si el tamaño del archivo está entre 0 y 32768. Por encima de eso, mapea () 8MiB grandes regiones del archivo a la vez y nunca parece volver a leer () bucles incluso para archivos PiB (probado en archivos dispersos).
Stéphane Chazelas
0

Como está escrito en las trampas de Bash , no puede leer un archivo y escribir en él en la misma tubería.

Dependiendo de lo que haga su canalización, el archivo puede estar bloqueado (a 0 bytes, o posiblemente a un número de bytes igual al tamaño del búfer de canalización de su sistema operativo), o puede crecer hasta que llene el espacio disponible en disco, o alcance la limitación de tamaño de archivo de su sistema operativo, o su cuota, etc.

La solución es usar un editor de texto o una variable temporal.

MatthewRock
fuente
-1

Tienes algún tipo de condición de carrera entre ambos x. Algunas implementaciones de cat(por ejemplo, coreutils 8.23) prohíben que:

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

Si esto no se detecta, el comportamiento obviamente dependerá de la implementación (tamaño del búfer, etc.).

En su código, puede intentar agregar un clearerr(f);después de fflush, en caso de que el siguiente freaddevuelva un error si se establece el indicador de fin de archivo.

vinc17
fuente
Parece que un buen sistema operativo tendrá un comportamiento determinista para un solo proceso con un solo hilo que ejecuta los mismos comandos de lectura / escritura. En cualquier caso, el comportamiento es determinista para mí, y principalmente pregunto por la discrepancia.
Tyler
@Tyler En mi humilde opinión, sin una especificación clara en este caso, el comando anterior no tiene sentido, y el determinismo no es realmente importante (excepto un error como este, que es el mejor comportamiento). Esto es un poco como el i = i++;comportamiento indefinido de C , de ahí la discrepancia.
vinc17
1
No, no hay condición de carrera aquí, el comportamiento está bien definido. Sin embargo, está definida por la implementación, según el tamaño relativo del archivo y el búfer utilizado por cat.
Gilles 'SO- deja de ser malvado'
@Gilles ¿Dónde ve que el comportamiento está bien definido / definido por la implementación? ¿Puedes dar alguna referencia? La especificación POSIX cat solo dice: "Está definido por la implementación si la utilidad cat almacena la salida si la opción -u no está especificada". Sin embargo, cuando se usa un búfer, la implementación no tiene que definir cómo se usa; puede ser no determinista, por ejemplo, con un búfer enjuagado al azar.
vinc17
@ vinc17 Por favor inserte "en la práctica" en mi comentario anterior. Sí, eso es teóricamente posible y compatible con POSIX, pero nadie lo hace.
Gilles 'SO- deja de ser malvado'