Wie wird die Prozessersetzung in bash implementiert?

12

Ich habe die andere Frage recherchiert , als mir klar wurde, dass ich nicht verstehe, was unter der Haube passiert, was diese /dev/fd/*Dateien sind und warum untergeordnete Prozesse sie öffnen können.

x-yuri
quelle
Ist diese Frage nicht beantwortet?
phk

Antworten:

21

Nun, es gibt viele Aspekte.

Dateideskriptoren

Für jeden Prozess verwaltet der Kernel eine Tabelle mit geöffneten Dateien (möglicherweise ist diese Tabelle anders implementiert, aber da Sie sie sowieso nicht sehen können, können Sie davon ausgehen, dass es sich um eine einfache Tabelle handelt). Diese Tabelle enthält Informationen darüber, um welche Datei es sich handelt / wo sie sich befindet, in welchem ​​Modus Sie sie geöffnet haben, an welcher Position Sie gerade lesen / schreiben und was sonst noch benötigt wird, um tatsächlich E / A-Operationen an dieser Datei durchzuführen. Jetzt kann der Prozess diese Tabelle nie mehr lesen (oder sogar schreiben). Wenn der Prozess eine Datei öffnet, erhält er einen sogenannten Dateideskriptor zurück. Welches ist einfach ein Index in die Tabelle.

Das Verzeichnis /dev/fdund sein Inhalt

Unter Linux dev/fdist das eigentlich eine symbolische Verknüpfung zu /proc/self/fd. /procist ein Pseudodateisystem, in dem der Kernel mehrere interne Datenstrukturen abbildet, auf die mit der Datei-API zugegriffen werden kann (sie sehen also wie normale Dateien / Verzeichnisse / Symlinks zu den Programmen aus). Insbesondere gibt es Informationen zu allen Prozessen (was ihm den Namen gab). Die symbolische Verknüpfung /proc/selfbezieht sich immer auf das Verzeichnis, das dem aktuell ausgeführten Prozess zugeordnet ist (dh auf den Prozess, der ihn anfordert; verschiedene Prozesse sehen daher unterschiedliche Werte). Im Verzeichnis des Prozesses befindet sich ein Unterverzeichnisfd Die Datei enthält für jede geöffnete Datei eine symbolische Verknüpfung, deren Name nur die dezimale Darstellung des Dateideskriptors ist (der Index in der Dateitabelle des Prozesses, siehe vorherigen Abschnitt) und deren Ziel die Datei ist, der sie entspricht.

Dateideskriptoren beim Erstellen von untergeordneten Prozessen

Ein untergeordneter Prozess wird von a erstellt fork. A erstellt forkeine Kopie der Dateideskriptoren. Dies bedeutet, dass der erstellte untergeordnete Prozess dieselbe Liste offener Dateien enthält wie der übergeordnete Prozess. Wenn eine der geöffneten Dateien nicht vom untergeordneten Element geschlossen wird, greift der Zugriff auf einen geerbten Dateideskriptor im untergeordneten Element auf dieselbe Datei zu wie der Zugriff auf den ursprünglichen Dateideskriptor im übergeordneten Prozess.

Beachten Sie, dass Sie nach einem Fork zunächst zwei Kopien desselben Prozesses haben, die sich nur im Rückgabewert vom Fork-Aufruf unterscheiden (das Elternteil erhält die PID des Kindes, das Kind erhält 0). Normalerweise folgt einem Fork ein exec, um eine der Kopien durch eine andere ausführbare Datei zu ersetzen. Die offenen Dateideskriptoren überleben diesen exec. Beachten Sie auch, dass der Prozess vor der Ausführung andere Manipulationen ausführen kann (z. B. das Schließen von Dateien, die der neue Prozess nicht erhalten sollte, oder das Öffnen anderer Dateien).

Unbenannte Rohre

Eine unbenannte Pipe ist nur ein Paar von Dateideskriptoren, die auf Anforderung des Kernels erstellt wurden, sodass alles, was in den ersten Dateideskriptor geschrieben wurde, an den zweiten übergeben wird. Am häufigsten wird das Piping-Konstrukt foo | barvon verwendet bash, bei dem die Standardausgabe von foodurch den Schreibteil der Pipe und die Standardeingabe durch den Leseteil ersetzt wird. Standardeingabe und Standardausgabe sind nur die ersten beiden Einträge in der Dateitabelle (Eintrag 0 und 1; 2 ist Standardfehler), und daher bedeutet das Ersetzen dieses Eintrags nur das Umschreiben dieses Tabelleneintrags mit den Daten, die dem anderen Dateideskriptor entsprechen (wieder der Die tatsächliche Implementierung kann davon abweichen. Da der Prozess nicht direkt auf die Tabelle zugreifen kann, gibt es dafür eine Kernelfunktion.

Prozessersetzung

Jetzt haben wir alles zusammen, um zu verstehen, wie die Prozessersetzung funktioniert:

  1. Der Bash-Prozess erstellt eine unbenannte Pipe für die Kommunikation zwischen den beiden später erstellten Prozessen.
  2. Bash Gabeln für den echoProzess. Der untergeordnete Prozess (der eine exakte Kopie des ursprünglichen bashProzesses ist) schließt das Leseende der Pipe und ersetzt die eigene Standardausgabe durch das Schreibende der Pipe. echoVorausgesetzt, dass es sich um eine eingebaute Shell handelt, erspart sich bashmöglicherweise den execAufruf, spielt jedoch keine Rolle (die eingebaute Shell ist möglicherweise ebenfalls deaktiviert und wird in diesem Fall ausgeführt /bin/echo).
  3. Bash (das ursprüngliche, übergeordnete Element) ersetzt den Ausdruck <(echo 1)durch die Pseudodateiverknüpfung in /dev/fdBezug auf das Leseende der unbenannten Pipe.
  4. Bash-Execs für den PHP-Prozess (beachten Sie, dass wir uns nach dem Fork immer noch in [einer Kopie von] bash befinden). Der neue Prozess schließt das geerbte Schreibende der unbenannten Pipe (und führt einige andere vorbereitende Schritte aus), lässt jedoch das Leseende offen. Dann wurde PHP ausgeführt.
  5. Das PHP-Programm erhält den Namen in /dev/fd/. Da der entsprechende Dateideskriptor noch offen ist, entspricht er immer noch dem Leseende der Pipe. Wenn das PHP-Programm die angegebene Datei zum Lesen öffnet, erstellt es tatsächlich einen secondDateideskriptor für das Leseende der unbenannten Pipe. Aber das ist kein Problem, das könnte man auch lesen.
  6. Jetzt kann das PHP-Programm das Leseende der Pipe durch den neuen Dateideskriptor lesen und erhält so die Standardausgabe des echoBefehls, der an das Schreibende derselben Pipe geht.
Celtschk
quelle
Klar, ich weiß deine Mühe zu schätzen. Aber ich wollte auf einige Punkte hinweisen. Zunächst sprechen Sie über ein phpSzenario, aber es phpgeht nicht gut mit Rohren um . In Anbetracht des Kommandos cat <(echo test)ist das Seltsame hier, dass sich die bashGabeln einmal cat, aber zweimal teilen echo test.
X-Yuri
13

Das Ausleihen aus celtschkder Antwort /dev/fdist eine symbolische Verknüpfung zu /proc/self/fd. Und /procist ein Pseudo-Dateisystem, das Informationen über Prozesse und andere Systeminformationen in einer hierarchischen dateiähnlichen Struktur darstellt. Dateien in /dev/fdentsprechen Dateien, die von einem Prozess geöffnet wurden, und haben einen Dateideskriptor als Namen und Dateien selbst als Ziele. Das Öffnen der Datei /dev/fd/Nentspricht dem Duplizieren des Deskriptors N(vorausgesetzt, der Deskriptor Nist geöffnet).

Und hier sind die Ergebnisse meiner Untersuchung, wie es funktioniert (die straceAusgabe ist frei von unnötigen Details und wurde modifiziert, um besser auszudrücken, was passiert):

$ 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)

Erstellt im bashAllgemeinen eine Pipe und übergibt ihre Enden als Dateideskriptoren an ihre untergeordneten Elemente (Leseende an 1.outund Schreibende an 2.out). Und übergibt read end als Befehlszeilenparameter an 1.out( /dev/fd/63). Dieser Weg 1.outkann sich öffnen /dev/fd/63.

x-yuri
quelle