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.
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.
Respuestas:
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/fd
y su contenido.
En Linux dev/fd
es en realidad un enlace simbólico a /proc/self/fd
. /proc
es 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/self
siempre 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 fork
hace 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 exec
para 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 | bar
de tuberías de bash
, donde la salida estándar de foo
se 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:
echo
proceso. El proceso secundario (que es una copia exacta del bash
proceso 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 echo
es un shell integrado, bash
puede ahorrarse la exec
llamada, pero no importa de todos modos (el shell incorporado también puede estar deshabilitado, en cuyo caso se ejecuta /bin/echo
).<(echo 1)
por el enlace del pseudo archivo al /dev/fd
referirse al final de la lectura de la tubería sin nombre./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 second
descriptor de archivo para el final de la lectura de la tubería sin nombre. Pero eso no es problema, podría leer de cualquiera.echo
comando que va al final de la escritura de la misma tubería.php
escenario, pero php
no maneja bien las tuberías . Además, considerando el comando cat <(echo test)
, lo extraño aquí es que se bash
bifurca una vez cat
, pero dos veces echo test
.
Tomando prestado de celtschk
la respuesta de, /dev/fd
es un enlace simbólico a /proc/self/fd
. Y /proc
es 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/fd
corresponden 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/N
es equivalente a duplicar el descriptor N
(suponiendo que el descriptor N
esté abierto).
Y aquí están los resultados de mi investigación de cómo funciona (la strace
salida 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, bash
crea una tubería y pasa sus extremos a sus elementos secundarios como descriptores de archivo (lectura de fin 1.out
y 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.out
se puede abrir /dev/fd/63
.