¿Cómo se implementa la sustitución de procesos en bash?

12

Estaba investigando la otra pregunta , cuando me di cuenta de que no entiendo lo que sucede debajo del capó, qué son esos /dev/fd/*archivos y cómo es que los procesos secundarios pueden abrirlos.

x-yuri
fuente
¿No se responde esa pregunta?
phk

Respuestas:

21

Bueno, hay muchos aspectos.

Descriptores de archivo

Para cada proceso, el kernel mantiene una tabla de archivos abiertos (bueno, podría implementarse de manera diferente, pero como no puede verlo de todos modos, puede asumir que es una tabla simple). Esa tabla contiene información sobre qué archivo es / dónde se puede encontrar, en qué modo lo abrió, en qué posición está leyendo / escribiendo actualmente, y cualquier otra cosa necesaria para realizar operaciones de E / S en ese archivo. Ahora el proceso nunca llega a leer (o incluso escribir) esa tabla. Cuando el proceso abre un archivo, recupera el llamado descriptor de archivo. Que es simplemente un índice en la tabla.

El directorio /dev/fdy su contenido.

En Linux dev/fdes en realidad un enlace simbólico a /proc/self/fd. /proces un pseudo sistema de archivos en el que el kernel mapea varias estructuras de datos internas para acceder con la API de archivos (por lo que solo parecen archivos / directorios / enlaces simbólicos normales a los programas). Especialmente hay información sobre todos los procesos (que es lo que le dio el nombre). El enlace simbólico /proc/selfsiempre se refiere al directorio asociado con el proceso que se está ejecutando actualmente (es decir, el proceso que lo solicita; por lo tanto, diferentes procesos verán diferentes valores). En el directorio del proceso, hay un subdirectoriofd que para cada archivo abierto contiene un enlace simbólico cuyo nombre es solo la representación decimal del descriptor de archivo (el índice en la tabla de archivos del proceso, consulte la sección anterior), y cuyo objetivo es el archivo al que corresponde.

Descriptores de archivo al crear procesos secundarios

Un proceso hijo es creado por a fork. A forkhace una copia de los descriptores de archivo, lo que significa que el proceso hijo creado tiene la misma lista de archivos abiertos que el proceso padre. Por lo tanto, a menos que el niño cierre uno de los archivos abiertos, el acceso a un descriptor de archivo heredado en el niño accederá al mismo archivo que el acceso al descriptor de archivo original en el proceso padre.

Tenga en cuenta que después de una bifurcación, inicialmente tiene dos copias del mismo proceso que difieren solo en el valor de retorno de la llamada de la bifurcación (el padre obtiene el PID del niño, el niño obtiene 0). Normalmente, un tenedor es seguido por un execpara reemplazar una de las copias por otro ejecutable. Los descriptores de archivos abiertos sobreviven a ese ejecutivo. Tenga en cuenta también que antes del ejecutivo, el proceso puede hacer otras manipulaciones (como cerrar archivos que el nuevo proceso no debería obtener o abrir otros archivos).

Tubos sin nombre

Una tubería sin nombre es solo un par de descriptores de archivo creados a pedido por el núcleo, de modo que todo lo escrito en el primer descriptor de archivo se pasa al segundo. El uso más común es para la construcción foo | barde tuberías de bash, donde la salida estándar de foose reemplaza por la parte de escritura de la tubería, y la entrada estándar se reemplaza por la parte de lectura. La entrada estándar y la salida estándar son solo las dos primeras entradas en la tabla de archivos (la entrada 0 y 1; 2 es un error estándar), y por lo tanto reemplazarla significa simplemente reescribir esa entrada de la tabla con los datos correspondientes al otro descriptor de archivo (nuevamente, el La implementación real puede diferir). Como el proceso no puede acceder a la tabla directamente, hay una función del núcleo para hacerlo.

Proceso de sustitución

Ahora tenemos todo junto para comprender cómo funciona la sustitución del proceso:

  1. El proceso bash crea una tubería sin nombre para la comunicación entre los dos procesos creados más tarde.
  2. Bash se bifurca para el echoproceso. El proceso secundario (que es una copia exacta del bashproceso original ) cierra el extremo de lectura de la tubería y reemplaza su propia salida estándar con el final de escritura de la tubería. Dado que echoes un shell integrado, bashpuede ahorrarse la execllamada, pero no importa de todos modos (el shell incorporado también puede estar deshabilitado, en cuyo caso se ejecuta /bin/echo).
  3. Bash (el original, principal) reemplaza la expresión <(echo 1)por el enlace del pseudo archivo al /dev/fdreferirse al final de la lectura de la tubería sin nombre.
  4. Bash execs para el proceso PHP (tenga en cuenta que después de la bifurcación, todavía estamos dentro de [una copia de] bash). El nuevo proceso cierra el final de escritura heredado de la tubería sin nombre (y realiza algunos otros pasos preparatorios), pero deja abierto el final de lectura. Luego ejecutó PHP.
  5. El programa PHP recibe el nombre en /dev/fd/. Dado que el descriptor de archivo correspondiente todavía está abierto, todavía corresponde al final de la lectura de la tubería. Por lo tanto, si el programa PHP abre el archivo dado para leer, lo que realmente hace es crear un seconddescriptor de archivo para el final de la lectura de la tubería sin nombre. Pero eso no es problema, podría leer de cualquiera.
  6. Ahora el programa PHP puede leer el extremo de lectura de la tubería a través del nuevo descriptor de archivo y, por lo tanto, recibir la salida estándar del echocomando que va al final de la escritura de la misma tubería.
celtschk
fuente
Claro, aprecio tu esfuerzo. Pero quería señalar varios problemas. Primero, estás hablando del phpescenario, pero phpno maneja bien las tuberías . Además, considerando el comando cat <(echo test), lo extraño aquí es que se bashbifurca una vez cat, pero dos veces echo test.
x-yuri
13

Tomando prestado de celtschkla respuesta de, /dev/fdes un enlace simbólico a /proc/self/fd. Y /proces un pseudo sistema de archivos, que presenta información sobre procesos y otra información del sistema en una estructura jerárquica similar a un archivo. Los archivos en /dev/fdcorresponden a archivos, abiertos por un proceso y tienen un descriptor de archivo como sus nombres y los archivos mismos como sus objetivos. Abrir el archivo /dev/fd/Nes equivalente a duplicar el descriptor N(suponiendo que el descriptor Nesté abierto).

Y aquí están los resultados de mi investigación de cómo funciona (la stracesalida se deshace de detalles innecesarios y se modifica para expresar mejor lo que está sucediendo):

$ cat 1.c
#include <unistd.h>
#include <fcntl.h>

int main(int argc, char *argv[])
{
    char buf[100];
    int fd;
    fd = open(argv[1], O_RDONLY);
    read(fd, buf, 100);
    write(STDOUT_FILENO, buf, n_read);
    return 0;
}
$ gcc 1.c -o 1.out
$ cat 2.c
#include <unistd.h>
#include <string.h>

int main(void)
{
    char *p = "hello, world\n";
    write(STDOUT_FILENO, p, strlen(p));
    return 0;
}
$ gcc 2.c -o 2.out
$ strace -f -e pipe,fcntl,dup2,close,clone,close,execve,wait4,read,open,write bash -c './1.out <(./2.out)'
[bash] pipe([3, 4]) = 0
[bash] dup2(3, 63) = 63
[bash] close(3) = 0
[bash] clone(child_stack=0, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0x7f7c211fb9d0) = p2
Process p2 attached
[bash] close(4) = 0
[bash] clone(child_stack=0, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0x7f7c211fb9d0) = p1
Process p1 attached
[bash] close(63) = 0
[p2] dup2(4, 1) = 1
[p2] close(4) = 0
[p2] close(63) = 0
[bash] wait4(-1, <unfinished ...>
Process bash suspended
[p1] execve("/home/yuri/_/1.out", ["/home/yuri/_/1.out", "/dev/fd/63"], [/* 31 vars */]) = 0
[p2] clone(child_stack=0, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0x7f7c211fb9d0) = p22
Process p22 attached
[p22] execve("/home/yuri/_/2.out", ["/home/yuri/_/2.out"], [/* 31 vars */]) = 0
[p2] wait4(-1, <unfinished ...>
Process p2 suspended
[p1] open("/dev/fd/63", O_RDONLY) = 3
[p1] read(3,  <unfinished ...>
[p22] write(1, "hello, world\n", 13) = 13
[p1] <... read resumed> "hello, world\n", 100) = 13
Process p2 resumed
Process p22 detached
[p1] write(1, "hello, world\n", 13) = 13
hello, world
[p2] <... wait4 resumed> [{WIFEXITED(s) && WEXITSTATUS(s) == 0}], 0, NULL) = p22
[p2] --- SIGCHLD (Child exited) @ 0 (0) ---
[p2] wait4(-1, 0x7fff190f289c, WNOHANG, NULL) = -1 ECHILD (No child processes)
Process bash resumed
Process p1 detached
[bash] <... wait4 resumed> [{WIFEXITED(s) && WEXITSTATUS(s) == 0}], 0, NULL) = p1
[bash] --- SIGCHLD (Child exited) @ 0 (0) ---
Process p2 detached
[bash] wait4(-1, 0x7fff190f2bdc, WNOHANG, NULL) = 0
--- SIGCHLD (Child exited) @ 0 (0) ---
[bash] wait4(-1, [{WIFEXITED(s) && WEXITSTATUS(s) == 0}], WNOHANG, NULL) = p2
[bash] wait4(-1, 0x7fff190f299c, WNOHANG, NULL) = -1 ECHILD (No child processes)

Básicamente, bashcrea una tubería y pasa sus extremos a sus elementos secundarios como descriptores de archivo (lectura de fin 1.outy escritura de fin 2.out). Y pasa el fin de lectura como un parámetro de línea de comando a 1.out( /dev/fd/63). De esta manera 1.outse puede abrir /dev/fd/63.

x-yuri
fuente