Stavo facendo ricerche sull'altra domanda , quando mi sono reso conto che non capisco cosa sta succedendo sotto il cofano, quali sono questi /dev/fd/*
file e in che modo i processi figlio possono aprirli.
Stavo facendo ricerche sull'altra domanda , quando mi sono reso conto che non capisco cosa sta succedendo sotto il cofano, quali sono questi /dev/fd/*
file e in che modo i processi figlio possono aprirli.
Risposte:
Bene, ci sono molti aspetti.
Descrittori di file
Per ogni processo, il kernel mantiene una tabella di file aperti (beh, potrebbe essere implementato in modo diverso, ma dal momento che non sei in grado di vederlo comunque, puoi semplicemente supporre che sia una tabella semplice). Quella tabella contiene informazioni su quale file è / dove può essere trovato, in quale modalità lo hai aperto, in quale posizione stai attualmente leggendo / scrivendo e qualsiasi altra cosa sia necessaria per eseguire effettivamente operazioni di I / O su quel file. Ora il processo non riesce mai a leggere (o persino a scrivere) quella tabella. Quando il processo apre un file, viene restituito un cosiddetto descrittore di file. Che è semplicemente un indice nella tabella.
La directory /dev/fd
e il suo contenuto
Su Linux dev/fd
è in realtà un collegamento simbolico a /proc/self/fd
. /proc
è uno pseudo file system in cui il kernel mappa diverse strutture di dati interne a cui è possibile accedere con l'API di file (in modo che appaiano semplicemente come file / directory / collegamenti simbolici ai programmi). Soprattutto ci sono informazioni su tutti i processi (che è ciò che gli ha dato il nome). Il collegamento simbolico /proc/self
si riferisce sempre alla directory associata al processo attualmente in esecuzione (ovvero, il processo che lo richiede; processi diversi vedranno quindi valori diversi). Nella directory del processo, c'è una sottodirectoryfd
quale per ogni file aperto contiene un collegamento simbolico il cui nome è solo la rappresentazione decimale del descrittore di file (l'indice nella tabella dei file del processo, vedere la sezione precedente) e la cui destinazione è il file a cui corrisponde.
Descrittori di file durante la creazione di processi figlio
Un processo figlio viene creato da a fork
. A fork
crea una copia dei descrittori di file, il che significa che il processo figlio creato ha lo stesso elenco di file aperti del processo principale. Pertanto, a meno che uno dei file aperti non venga chiuso dal figlio, l'accesso a un descrittore di file ereditato nel figlio accederà allo stesso file dell'accesso al descrittore di file originale nel processo padre.
Si noti che dopo un fork, inizialmente si hanno due copie dello stesso processo che differiscono solo per il valore di ritorno dalla chiamata fork (il genitore ottiene il PID del figlio, il figlio ottiene 0). Normalmente, un fork è seguito da un exec
per sostituire una delle copie con un altro eseguibile. I descrittori di file aperti sopravvivono a quello exec. Si noti inoltre che prima dell'esecut, il processo può eseguire altre manipolazioni (come la chiusura di file che il nuovo processo non dovrebbe ottenere o l'apertura di altri file).
Tubi senza nome
Una pipe senza nome è solo una coppia di descrittori di file creati su richiesta dal kernel, in modo che tutto ciò che è scritto nel primo descrittore di file venga passato al secondo. L'uso più comune è per il costrutto tubazioni foo | bar
di bash
, dove l'uscita standard foo
è sostituita dalla parte di scrittura del tubo, e l'ingresso standard è sostituito con il parte lettura. L'input standard e l'output standard sono solo le prime due voci nella tabella dei file (le voci 0 e 1; 2 è un errore standard) e quindi sostituirla significa semplicemente riscrivere quella voce della tabella con i dati corrispondenti all'altro descrittore di file (di nuovo, il l'implementazione effettiva può essere diversa). Poiché il processo non può accedere direttamente alla tabella, esiste una funzione del kernel per farlo.
Sostituzione del processo
Ora abbiamo tutto insieme per capire come funziona la sostituzione del processo:
echo
processo. Il processo figlio (che è una copia esatta dell'originale bash
processo) chiude l'estremità del tubo lettura e sostituisce la propria uscita di serie con la fine scrittura del tubo. Dato che echo
è un built-in della shell, bash
potrebbe risparmiarsi la exec
chiamata, ma non importa comunque (anche il built-in della shell potrebbe essere disabilitato, nel qual caso viene eseguito /bin/echo
).<(echo 1)
con il collegamento al pseudo file /dev/fd
facendo riferimento all'estremità di lettura della pipe senza nome./dev/fd/
. Poiché il descrittore di file corrispondente è ancora aperto, corrisponde comunque all'estremità di lettura della pipe. Pertanto, se il programma PHP apre il file dato per la lettura, ciò che effettivamente fa è creare un second
descrittore di file per la fine della lettura della pipe senza nome. Ma questo non è un problema, potrebbe leggere da entrambi.echo
comando che va alla fine della scrittura della stessa pipe.php
scenari, ma php
non gestisce bene le pipe . Inoltre, considerando il comando cat <(echo test)
, la cosa strana qui è che bash
forcelle una volta per cat
, ma due volte per echo test
.
Prendere in prestito dalla celtschk
risposta, /dev/fd
è un collegamento simbolico a /proc/self/fd
. Ed /proc
è uno pseudo filesystem, che presenta informazioni sui processi e altre informazioni di sistema in una struttura gerarchica simile a un file. I file in /dev/fd
corrispondono a file, aperti da un processo e hanno il descrittore di file come nomi e i file stessi come obiettivi. L'apertura del file /dev/fd/N
equivale alla duplicazione del descrittore N
(presupponendo che il descrittore N
sia aperto).
E qui ci sono i risultati della mia indagine su come funziona (l' strace
output è privo di dettagli non necessari e modificato per esprimere meglio ciò che sta accadendo):
$ 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)
Fondamentalmente, bash
crea una pipe e passa le sue estremità ai suoi figli come descrittori di file (leggi end 1.out
e scrivi end to 2.out
). E passa read end come parametro della riga di comando a 1.out
( /dev/fd/63
). In questo modo 1.out
è in grado di aprire /dev/fd/63
.