TL; DR : Ponieważ jest to optymalna metoda tworzenia nowych procesów i utrzymywania kontroli w interaktywnej powłoce
fork () jest niezbędny dla procesów i rur
Aby odpowiedzieć na konkretną część tego pytania, gdyby grep blabla foo
zadzwonić exec()
bezpośrednio przez rodzica, rodzic wykorzystałby istnienie, a jego PID ze wszystkimi zasobami zostałby przejęty grep blabla foo
.
Porozmawiajmy jednak ogólnie o exec()
i fork()
. Głównym powodem takiego zachowania jest to, że fork()/exec()
jest to standardowa metoda tworzenia nowego procesu w systemach Unix / Linux, a nie jest to rzecz specyficzna dla basha; metoda ta istnieje od samego początku i jest pod wpływem tej samej metody z już istniejących systemów operacyjnych tamtych czasów. Aby nieco sparafrazować odpowiedź goldilocks na pokrewne pytanie, fork()
tworzenie nowego procesu jest łatwiejsze, ponieważ jądro ma mniej pracy, jeśli chodzi o przydzielanie zasobów, i wiele właściwości (takich jak deskryptory plików, środowisko itp.) - wszystko może być dziedziczone z procesu nadrzędnego (w tym przypadku z bash
).
Po drugie, jeśli chodzi o interaktywne powłoki, nie można uruchomić zewnętrznego polecenia bez rozwidlenia. Aby uruchomić plik wykonywalny znajdujący się na dysku (na przykład /bin/df -h
), musisz wywołać jedną z exec()
funkcji rodziny, na przykład execve()
, która zastąpi element nadrzędny nowym procesem, przejmie jego identyfikator PID i istniejące deskryptory plików itp. W przypadku powłoki interaktywnej chcesz, aby formant zwrócił się do użytkownika i pozwolił na kontynuację nadrzędnej powłoki interaktywnej. Zatem najlepszym sposobem jest utworzenie podprocesu przez fork()
i pozwolenie na przejęcie tego procesu execve()
. Tak więc interaktywna powłoka PID 1156 odradza dziecko za fork()
pomocą PID 1157, a następnie wywołuje execve("/bin/df",["df","-h"],&environment)
, co /bin/df -h
uruchamia się z PID 1157. Teraz powłoka musi tylko czekać na zakończenie procesu i przywrócenie mu kontroli.
W przypadku, gdy musisz utworzyć potok między dwoma lub więcej poleceniami, powiedzmy df | grep
, że potrzebujesz sposobu na utworzenie dwóch deskryptorów plików (czyli odczyt i zapis końca potoku, który pochodzi z pipe()
syscall), a następnie pozwól, aby odziedziczyły je dwa nowe procesy. Odbywa się to przy rozwiązywaniu nowego procesu, a następnie poprzez kopiowanie końca zapisu potoku poprzez dup2()
wywołanie na jego stdout
aka fd 1 (więc jeśli koniec zapisu to fd 4, to robimy dup2(4,1)
). Kiedy exec()
się odrodzi df
, proces potomny nic o nim nie pomyśli stdout
i napisze do niego, nie będąc świadomym (chyba że aktywnie sprawdza), że jego dane wyjściowe idą potokiem. Ten sam proces zdarza się grep
, z wyjątkiem tego fork()
, że odczytujemy koniec potoku z fd 3 i dup(3,0)
przed spawnem za grep
pomocąexec()
. Cały czas trwa proces nadrzędny, który czeka na odzyskanie kontroli po ukończeniu potoku.
W przypadku wbudowanych poleceń, ogólnie powłoka nie fork()
, z wyjątkiem source
polecenia. Wymagane są podpowłoki fork()
.
Krótko mówiąc, jest to niezbędny i użyteczny mechanizm.
Wady rozwidlania i optymalizacji
Teraz jest inaczej w przypadku nieinteraktywnych powłok , takich jak bash -c '<simple command>'
. Pomimo fork()/exec()
tego, że jest to optymalna metoda, w której musisz przetwarzać wiele poleceń, marnowanie zasobów jest możliwe, gdy masz tylko jedno polecenie. Cytując Stéphane'a Chazelasa z tego postu :
Rozwidlenie jest drogie, czas procesora, pamięć, przydzielone deskryptory plików ... Posiadanie procesu powłoki polegającego na czekaniu na inny proces przed wyjściem jest marnowaniem zasobów. Utrudnia to także prawidłowe zgłaszanie statusu wyjścia z osobnego procesu, który wykonałby polecenie (na przykład, gdy proces został zabity).
Dlatego wiele powłok (nie tylko bash
) używa, exec()
aby umożliwić bash -c ''
przejęcie tego pojedynczego prostego polecenia. I właśnie z wyżej wymienionych powodów, minimalizacja potoków w skryptach powłoki jest lepsza. Często widać, że początkujący robią coś takiego:
cat /etc/passwd | cut -d ':' -f 6 | grep '/home'
Oczywiście będą to fork()
3 procesy. Jest to prosty przykład, ale rozważ duży plik z zakresu gigabajtów. Byłby o wiele bardziej wydajny z jednym procesem:
awk -F':' '$6~"/home"{print $6}' /etc/passwd
Marnowanie zasobów może w rzeczywistości być formą ataku Denial of Service, a w szczególności bomby rozwidlone są tworzone za pomocą funkcji powłoki, które wywołują się w rurociągu, który wykopuje wiele kopii. Obecnie ogranicza się to poprzez ograniczenie maksymalnej liczby procesów w grupach systemd na systemied , z których korzysta także Ubuntu od wersji 15.04.
Oczywiście nie oznacza to, że rozwidlenie jest po prostu złe. Jest to nadal przydatny mechanizm, jak omówiono wcześniej, ale w przypadku, gdy można uniknąć mniejszych procesów, a co za tym idzie mniej zasobów, a tym samym lepszej wydajności, należy unikać, fork()
jeśli to możliwe.
Zobacz też