Jak zaimplementowano podstawianie procesów w bash?

12

Badałem drugie pytanie , kiedy zdałem sobie sprawę, że nie rozumiem, co dzieje się pod maską, jakie są te /dev/fd/*pliki i jak mogą je otwierać procesy potomne.

x-yuri
źródło
Czy na to pytanie nie ma odpowiedzi?
phk

Odpowiedzi:

21

Jest wiele aspektów.

Deskryptory plików

Dla każdego procesu jądro utrzymuje tabelę otwartych plików (cóż, może być zaimplementowane inaczej, ale ponieważ i tak nie możesz go zobaczyć, możesz po prostu założyć, że jest to prosta tabela). Ta tabela zawiera informacje o tym, który plik / gdzie można go znaleźć, w jakim trybie go otworzyłeś, w jakiej pozycji aktualnie czytasz / piszesz i cokolwiek innego, co jest potrzebne do faktycznego wykonania operacji we / wy na tym pliku. Teraz proces nigdy nie może odczytać (ani nawet napisać) tej tabeli. Gdy proces otwiera plik, otrzymuje tak zwany deskryptor pliku. Który jest po prostu indeksem w tabeli.

Katalog /dev/fdi jego zawartość

W systemie Linux dev/fdjest faktycznie dowiązaniem symbolicznym /proc/self/fd. /procto pseudo system plików, w którym jądro mapuje kilka wewnętrznych struktur danych, do których można uzyskać dostęp za pomocą interfejsu API plików (więc wyglądają jak zwykłe pliki / katalogi / dowiązania symboliczne do programów). Zwłaszcza są informacje o wszystkich procesach (co nadało mu nazwę). Łącze symboliczne /proc/selfzawsze odnosi się do katalogu związanego z aktualnie uruchomionym procesem (czyli procesem go żądającym; dlatego różne procesy będą widzieć różne wartości). W katalogu procesu znajduje się podkatalogfd który dla każdego otwartego pliku zawiera dowiązanie symboliczne, którego nazwa jest po prostu dziesiętną reprezentacją deskryptora pliku (indeks do tabeli plików procesu, patrz poprzednia sekcja), a którego celem jest plik, do którego odpowiada.

Deskryptory plików podczas tworzenia procesów potomnych

Proces potomny jest tworzony przez fork. A forktworzy kopię deskryptorów plików, co oznacza, że ​​utworzony proces potomny ma tę samą listę otwartych plików, co proces macierzysty. Tak więc, dopóki jeden z otwartych plików nie zostanie zamknięty przez dziecko, dostęp do odziedziczonego deskryptora pliku w dziecku uzyska dostęp do tego samego pliku, co dostęp do oryginalnego deskryptora pliku w procesie nadrzędnym.

Zauważ, że po rozwidleniu początkowo masz dwie kopie tego samego procesu, które różnią się tylko wartością zwracaną z wywołania rozwidlenia (rodzic otrzymuje PID dziecka, dziecko dostaje 0). Zwykle po rozwidleniu następuje execzamiana jednej z kopii na inny plik wykonywalny. Otwarte deskryptory plików przetrwają to exec. Zauważ też, że przed wykonaniem proces może wykonywać inne manipulacje (takie jak zamykanie plików, których nowy proces nie powinien otrzymać, lub otwieranie innych plików).

Nienazwane rury

Potok bez nazwy to tylko para deskryptorów plików utworzonych na żądanie przez jądro, dzięki czemu wszystko, co zapisano w pierwszym deskryptorze pliku, jest przekazywane do drugiego. Najczęstszym zastosowaniem jest konstruktem rurociągów foo | barz bash, których poziom wyjściowy foojest zastąpiona przez część zapisu rury, a standardowe wejście jest zastąpione przez odczytu strony. Standardowe wejście i standardowe wyjście to tylko dwa pierwsze wpisy w tabeli plików (wpisy 0 i 1; 2 to błąd standardowy), dlatego zastąpienie go oznacza po prostu przepisanie tego wpisu tabeli danymi odpowiadającymi drugiemu deskryptorowi pliku (ponownie faktyczna implementacja może się różnić). Ponieważ proces nie może uzyskać bezpośredniego dostępu do tabeli, dostępna jest funkcja jądra.

Zastąpienie procesu

Teraz mamy wszystko razem, aby zrozumieć, jak działa podstawienie procesu:

  1. Proces bash tworzy nienazwany potok do komunikacji między dwoma procesami utworzonymi później.
  2. Bash rozwidla echoproces. Proces potomny (który jest dokładną kopią oryginalnego bashprocesu) zamyka koniec odczytu potoku i zamienia własne standardowe wyjście na koniec zapisu potoku. Biorąc pod uwagę, że echojest to wbudowana powłoka, bashmoże oszczędzić sobie execwywołania, ale i tak nie ma to znaczenia (wbudowana powłoka może być również wyłączona, w takim przypadku wykonuje się /bin/echo).
  3. Bash (oryginalny, nadrzędny) zastępuje wyrażenie <(echo 1)pseudo linkiem pliku w /dev/fdodniesieniu do końca odczytu nienazwanego potoku.
  4. Bash wykonuje dla procesu PHP (zwróć uwagę, że po rozwidleniu nadal jesteśmy w [kopii] bash). Nowy proces zamyka odziedziczony koniec zapisu nienazwanego potoku (i wykonuje inne kroki przygotowawcze), ale pozostawia otwarty koniec odczytu. Następnie wykonał PHP.
  5. Program PHP otrzymuje nazwę w /dev/fd/. Ponieważ odpowiedni deskryptor pliku jest nadal otwarty, nadal odpowiada końcowi odczytu potoku. Dlatego jeśli program PHP otworzy dany plik do odczytu, to w rzeczywistości tworzy seconddeskryptor pliku dla końca odczytu nienazwanego potoku. Ale to nie problem, można odczytać z obu.
  6. Teraz program PHP może odczytać koniec odczytu potoku przez nowy deskryptor pliku, a tym samym otrzymać standardowe wyjście echopolecenia, które przechodzi do końca zapisu tego samego potoku.
celtschk
źródło
Jasne, doceniam twój wysiłek. Ale chciałem zwrócić uwagę na kilka kwestii. Po pierwsze, mówisz o phpscenariuszu, ale phpnie radzi sobie dobrze z rurami . Ponadto, biorąc pod uwagę polecenie cat <(echo test), dziwną rzeczą jest to, że bashrozwidla się raz cat, ale dwa razy echo test.
x-yuri
13

Pożyczanie od celtschkodpowiedzi /dev/fdto symboliczny link do /proc/self/fd. I /procto pseudo system plików, który prezentuje informacje o procesach i inne informacje systemowe w hierarchicznej plikopodobny struktury. Pliki w plikach /dev/fdodpowiadają plikom otwieranym przez proces i mają deskryptor plików jako swoje nazwy i same pliki jako cele. Otwarcie pliku /dev/fd/Njest równoważne z powieleniem deskryptora N(przy założeniu, że deskryptor Njest otwarty).

A oto wyniki mojego badania tego, jak to działa ( stracedane wyjściowe pozbywają się niepotrzebnych szczegółów i zmodyfikowane, aby lepiej wyrazić, co się dzieje):

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

Zasadniczo bashtworzy potok i przekazuje swoje końce swoim potomkom jako deskryptory plików (odczytaj koniec do 1.outi zapisz koniec do 2.out). I przekazuje koniec odczytu jako parametr wiersza poleceń do 1.out( /dev/fd/63). W ten sposób 1.outmożna się otworzyć /dev/fd/63.

x-yuri
źródło