Come viene implementata la sostituzione del processo in bash?


12

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.


Non è una risposta a questa domanda?
phk,

Risposte:


21

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/fde 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/selfsi 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 forkcrea 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 execper 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 | bardi 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:

  1. Il processo bash crea una pipe senza nome per la comunicazione tra i due processi creati in seguito.
  2. Forchette Bash per il echoprocesso. Il processo figlio (che è una copia esatta dell'originale bashprocesso) 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, bashpotrebbe risparmiarsi la execchiamata, ma non importa comunque (anche il built-in della shell potrebbe essere disabilitato, nel qual caso viene eseguito /bin/echo).
  3. Bash (l'originale, quello principale) sostituisce l'espressione <(echo 1)con il collegamento al pseudo file /dev/fdfacendo riferimento all'estremità di lettura della pipe senza nome.
  4. Bash execs per il processo PHP (nota che dopo il fork, siamo ancora dentro [una copia di] bash). Il nuovo processo chiude l'estremità di scrittura ereditata della pipe senza nome (e esegue alcune altre fasi preparatorie), ma lascia aperta la fine di lettura. Quindi ha eseguito PHP.
  5. Il programma PHP riceve il nome in /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 seconddescrittore di file per la fine della lettura della pipe senza nome. Ma questo non è un problema, potrebbe leggere da entrambi.
  6. Ora il programma PHP può leggere la fine della lettura della pipe attraverso il nuovo descrittore di file, e quindi ricevere l'output standard del echocomando che va alla fine della scrittura della stessa pipe.

Certo, apprezzo il tuo sforzo. Ma volevo sottolineare diversi problemi. Innanzitutto, stai parlando di phpscenari, ma phpnon gestisce bene le pipe . Inoltre, considerando il comando cat <(echo test), la cosa strana qui è che bashforcelle una volta per cat, ma due volte per echo test.
x-yuri,

13

Prendere in prestito dalla celtschkrisposta, /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/fdcorrispondono 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/Nequivale alla duplicazione del descrittore N(presupponendo che il descrittore Nsia aperto).

E qui ci sono i risultati della mia indagine su come funziona (l' straceoutput è 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, bashcrea una pipe e passa le sue estremità ai suoi figli come descrittori di file (leggi end 1.oute 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.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.