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.
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.
Odpowiedzi:
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/fd
i jego zawartość
W systemie Linux dev/fd
jest faktycznie dowiązaniem symbolicznym /proc/self/fd
. /proc
to 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/self
zawsze 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 fork
tworzy 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 exec
zamiana 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 | bar
z bash
, których poziom wyjściowy foo
jest 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:
echo
proces. Proces potomny (który jest dokładną kopią oryginalnego bash
procesu) zamyka koniec odczytu potoku i zamienia własne standardowe wyjście na koniec zapisu potoku. Biorąc pod uwagę, że echo
jest to wbudowana powłoka, bash
może oszczędzić sobie exec
wywoł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
).<(echo 1)
pseudo linkiem pliku w /dev/fd
odniesieniu do końca odczytu nienazwanego potoku./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 second
deskryptor pliku dla końca odczytu nienazwanego potoku. Ale to nie problem, można odczytać z obu.echo
polecenia, które przechodzi do końca zapisu tego samego potoku.php
scenariuszu, ale php
nie radzi sobie dobrze z rurami . Ponadto, biorąc pod uwagę polecenie cat <(echo test)
, dziwną rzeczą jest to, że bash
rozwidla się raz cat
, ale dwa razy echo test
.
Pożyczanie od celtschk
odpowiedzi /dev/fd
to symboliczny link do /proc/self/fd
. I /proc
to pseudo system plików, który prezentuje informacje o procesach i inne informacje systemowe w hierarchicznej plikopodobny struktury. Pliki w plikach /dev/fd
odpowiadają plikom otwieranym przez proces i mają deskryptor plików jako swoje nazwy i same pliki jako cele. Otwarcie pliku /dev/fd/N
jest równoważne z powieleniem deskryptora N
(przy założeniu, że deskryptor N
jest otwarty).
A oto wyniki mojego badania tego, jak to działa ( strace
dane 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 bash
tworzy potok i przekazuje swoje końce swoim potomkom jako deskryptory plików (odczytaj koniec do 1.out
i zapisz koniec do 2.out
). I przekazuje koniec odczytu jako parametr wiersza poleceń do 1.out
( /dev/fd/63
). W ten sposób 1.out
można się otworzyć /dev/fd/63
.