Dlaczego musimy rozwidlać się, aby tworzyć nowe procesy?


95

W Uniksie, ilekroć chcemy utworzyć nowy proces, rozwidlamy bieżący proces, tworząc nowy proces potomny, który jest dokładnie taki sam jak proces macierzysty; następnie wykonujemy wywołanie systemowe exec, aby zastąpić wszystkie dane z procesu nadrzędnego danymi z nowego procesu.

Dlaczego w pierwszej kolejności tworzymy kopię procesu nadrzędnego, a nie bezpośrednio nowego?


Odpowiedzi:


61

Krótka odpowiedź brzmi: forkjest w Uniksie, ponieważ w tamtym czasie łatwo było go dopasować do istniejącego systemu, a ponieważ poprzedni system w Berkeley zastosował koncepcję wideł.

From the Evolution of the Unix Time-Sharing System ( zaznaczono odpowiedni tekst ):

Kontrola procesu w nowoczesnej formie została zaprojektowana i wdrożona w ciągu kilku dni. To zadziwiające, jak łatwo wpasowuje się w istniejący system; jednocześnie łatwo jest zobaczyć, jak niektóre z nieco nietypowych cech projektu są obecne właśnie dlatego, że reprezentowały małe, łatwe do zakodowania zmiany w tym, co istniało . Dobrym przykładem jest oddzielenie funkcji fork i exec. Najczęstszy model tworzenia nowych procesów obejmuje określenie programu do wykonania; w Uniksie proces rozwidlony uruchamia ten sam program co jego rodzic, dopóki nie wykona jawnego exec. Rozdzielenie funkcji z pewnością nie jest unikalne dla Uniksa i faktycznie było obecne w systemie podziału czasu Berkeley, który był dobrze znany Thompsonowi. Jednak rozsądne wydaje się założenie, że istnieje on w Uniksie, głównie ze względu na łatwość implementacji rozwidlenia bez zmiany wielu innych elementów . System obsługiwał już wiele (tj. Dwa) procesów; istniała tabela procesów, a procesy były zamieniane między pamięcią główną a dyskiem. Wstępna implementacja widelca była wymagana

1) Rozszerzenie tabeli procesów

2) Dodanie wywołania rozwidlenia, które skopiowało bieżący proces do obszaru wymiany dysku, używając już istniejących operacji podstawowych wymiany IO i dokonało pewnych zmian w tabeli procesów.

W rzeczywistości wywołanie wideł PDP-7 wymagało dokładnie 27 wierszy kodu asemblera. Oczywiście wymagane były inne zmiany w systemie operacyjnym i programach użytkownika, a niektóre z nich były dość interesujące i nieoczekiwane. Ale połączenie fork-exec byłoby znacznie bardziej skomplikowane , choćby dlatego, że exec jako taki nie istniał; jego funkcja została już wykonana, przy użyciu jawnego IO, przez powłokę.

Od tego czasu Unix ewoluował. forknastępnie execnie jest już jedynym sposobem na uruchomienie programu.

  • vfork został stworzony, aby być bardziej wydajnym rozwidleniem w przypadku, gdy nowy proces zamierza wykonać exec zaraz po rozwidleniu. Po wykonaniu vfork procesy nadrzędne i podrzędne współużytkują tę samą przestrzeń danych, a proces nadrzędny jest zawieszany do czasu, aż proces podrzędny wykona program lub zakończy działanie.

  • posix_spawn tworzy nowy proces i wykonuje plik w jednym wywołaniu systemowym. Wymaga zestawu parametrów, które umożliwiają selektywne udostępnianie otwartych plików dzwoniącego i kopiowanie jego dyspozycji sygnału i innych atrybutów do nowego procesu.


5
Dobra odpowiedź, ale dodam, że vfork nie powinien być już używany. Różnica w wydajności jest teraz marginalna, a jej użycie może być niebezpieczne. Zobacz to pytanie SO stackoverflow.com/questions/4856255/…, tę stronę ewontfix.com/7 oraz „Advanced Unix Programming” strona 299 na temat vfork
Raphael Ahrens

4
Machinacje (konfiguracja struktury danych) wymagane do posix_spawn()wykonania tych samych zadań związanych z ponownym numerowaniem po rozwidleniu, które można łatwo wykonać za pomocą fork()wbudowanego kodu, stanowią przekonujący argument za tym, fork()że jest o wiele prostszy w użyciu.
Jonathan Leffler

34

[Będę powtarzać część mojej odpowiedzi od tutaj .]

Dlaczego po prostu nie ma polecenia, które tworzy nowy proces od zera? Czy nie jest absurdalne i nieefektywne kopiowanie takiego, który zostanie zastąpiony od razu?

W rzeczywistości prawdopodobnie nie byłoby to tak skuteczne z kilku powodów:

  1. „Kopiuj”, wyprodukowany przez fork()to trochę abstrakcji, ponieważ jądro wykorzystuje kopiowanie przy zapisie systemu ; wszystko, co naprawdę trzeba stworzyć, to wirtualna mapa pamięci. Jeśli kopia następnie natychmiast wywołuje exec(), większość danych, które zostałyby skopiowane, gdyby zostały zmodyfikowane przez aktywność procesu, nigdy tak naprawdę nie musi być kopiowana / tworzona, ponieważ proces nie robi nic wymagającego jego użycia.

  2. Różne znaczące aspekty procesu potomnego (np. Jego środowisko) nie muszą być indywidualnie duplikowane ani ustawiane w oparciu o złożoną analizę kontekstu itp. Po prostu zakłada się, że są takie same jak proces wywołujący, i jest to dość intuicyjny system, który znamy.

Aby wyjaśnić # 1 nieco dalej, pamięć, która jest „kopiowana”, ale nigdy później dostępna, nigdy nie jest tak naprawdę kopiowana, przynajmniej w większości przypadków. Wyjątkiem w tym kontekście może być rozwidlenie procesu, a następnie zakończenie procesu nadrzędnego przed zastąpieniem go przez dziecko exec(). Mówię, że może, ponieważ duża część rodzica mogłaby być buforowana, jeśli jest wystarczająca ilość wolnej pamięci, i nie jestem pewien, w jakim stopniu zostanie to wykorzystane (co zależy od implementacji systemu operacyjnego).

Oczywiście, to nie sprawia, że ​​używanie kopii jest bardziej wydajne niż używanie pustej tabliczki - z wyjątkiem tego, że „pusta tablica” nie jest dosłownie niczym i musi obejmować przydział. System może mieć ogólny pusty / nowy szablon procesu, który kopiuje w ten sam sposób 1, ale tak naprawdę nie zachowałby niczego w porównaniu z widelcem kopiowania przy zapisie. Więc # 1 pokazuje tylko, że użycie „nowego” pustego procesu nie byłoby bardziej wydajne.

Punkt 2 wyjaśnia, dlaczego korzystanie z widelca jest prawdopodobnie bardziej wydajne. Środowisko dziecka jest dziedziczone od jego rodzica, nawet jeśli jest to zupełnie inny plik wykonywalny. Na przykład, jeśli proces nadrzędny jest powłoką, a podrzędna przeglądarka internetowa, $HOMEjest nadal taka sama dla obu z nich, ale ponieważ oba mogą później to zmienić, muszą to być dwie osobne kopie. Ten w dziecku jest produkowany przez oryginał fork().

1. Strategia, która może nie ma większego sensu, ale chodzi mi o to, że tworzenie procesu wymaga więcej niż kopiowania jego obrazu do pamięci z dysku.


3
Chociaż oba punkty są prawdziwe, żadne nie obsługuje, dlaczego wybrano metodę rozwidlania zamiast tworzenia nowego procesu z danego pliku wykonywalnego.
SkyDan

3
Myślę, że to odpowiada na pytanie. Widelec jest używany, ponieważ w przypadkach, w których tworzenie nowego procesu jest najbardziej wydajnym sposobem, koszt użycia widelca jest trywialny (prawdopodobnie mniej niż 1% kosztów tworzenia procesu). Z drugiej strony istnieje wiele miejsc, w których fork jest znacznie wydajniejszy lub znacznie prostszy w stosunku do API (np. Obsługa uchwytów plików). Podjęta przez Unix decyzja dotyczyła obsługi tylko jednego interfejsu API, co upraszcza specyfikację.
Cort Ammon

1
@SkyDan Masz rację, to raczej odpowiedź na pytanie dlaczego, a nie dlaczego , na którą Mark Plotnick odpowiada bardziej bezpośrednio - co interpretowałbym jako nie tylko to, że był to najłatwiejszy wybór, ale także, że był to prawdopodobnie najbardziej wydajny wybór (zgodnie z cytatem Dennisa Richiego: „wywołanie widełkowe PDP-7 wymagało dokładnie 27 linii montażowych ... exec jako taki nie istniał; jego funkcja została już wykonana”). Tak więc to „dlaczego nie” naprawdę zastanawia się nad dwiema strategiami, w których jedna z pozorów wydaje się prostsza i bardziej wydajna, a być może nie jest (świadkiem wątpliwego losu ...
goldilocks

1
Złotowłosa ma rację. Są sytuacje, w których rozwidlanie i modyfikowanie jest tańsze niż tworzenie nowego od zera. Najbardziej ekstremalnym przykładem jest oczywiście sytuacja, w której chcesz zachować rozwidlenie. fork()może to bardzo szybko (jak wspomniano GL, rzędu 27 linii montażowych). Patrząc w drugą stronę, jeśli chcesz „utworzyć proces od zera”, fork()kosztuje tylko odrobinę więcej niż rozpoczęcie od pustego procesu (27 linii montażu + koszt zamykania uchwytów plików). Więc dobrze forkradzi sobie zarówno z widelcem, jak i tworzeniem, a createtylko z tworzeniem dobrze.
Cort Ammon

2
Twoja odpowiedź dotyczyła ulepszeń sprzętowych: pamięć wirtualna, kopiowanie przy zapisie. Wcześniej forkkopiował całą pamięć procesu i była bardzo droga.
Barmar

6

Myślę, że powodem, dla którego Unix miał tylko forkfunkcję tworzenia nowych procesów, jest wynik filozofii Uniksa

Budują jedną funkcję, która dobrze robi jedną rzecz. Tworzy proces potomny.

To, co zrobisz z nowym procesem, zależy od programisty. Może użyć jednej z exec*funkcji i uruchomić inny program, lub nie może użyć exec i użyć dwóch instancji tego samego programu, co może być przydatne.

Dzięki temu zyskujesz większą swobodę, ponieważ możesz z niego korzystać

  1. widelec bez exec *
  2. fork with exec * lub
  3. po prostu exec * bez widelca

a ponadto trzeba tylko zapamiętać fork, a exec*wywołania funkcji, które w 1970 roku trzeba było zrobić.


3
Rozumiem, jak działają widelce i jak z nich korzystać. Ale dlaczego miałbym chcieć stworzyć nowy proces, skoro mogę zrobić to samo, ale przy mniejszym wysiłku? Na przykład mój nauczyciel dał mi zadanie, w którym musiałem utworzyć proces dla każdej liczby przekazywanej do argv, aby sprawdzić, czy liczba jest liczbą pierwszą. Ale czy nie jest to tylko objazd ostatecznego zrobienia tego samego? Mógłbym po prostu użyć tablicy i funkcji dla każdej liczby ... Dlaczego więc tworzymy procesy potomne, zamiast wykonywać wszystkie procesy w procesie głównym?
user1534664,

2
Zaryzykuję stwierdzenie, że rozumiesz, jak działają widelce i jak z nich korzystać, ponieważ kiedyś miałeś nauczyciela, który zlecił ci zadanie polegające na utworzeniu szeregu procesów (z liczbą określoną w czasie wykonywania), kontroluj ich, koordynuj i komunikuj się między nimi. Oczywiście nikt nie zrobiłby czegoś takiego trywialnego w prawdziwym życiu. Ale jeśli masz duży problem, który można łatwo rozłożyć na części, które mogą być obsługiwane równolegle (np. Wykrywanie krawędzi na obrazie), rozwidlenie pozwala na użycie wielu rdzeni procesora jednocześnie.
Scott,

5

Istnieją dwie filozofie tworzenia procesów: rozwidlaj z dziedziczeniem i twórz z argumentami. Oczywiście Unix używa widelca. (Na przykład OSE i VMS używają metody create.) Unix ma WIELE odziedziczonych cech, a kolejne są dodawane okresowo. Poprzez dziedziczenie te nowe cechy można dodawać BEZ ZMIANY ISTNIEJĄCYCH PROGRAMÓW! Używając modelu tworzenia z argumentami, dodanie nowych cech oznaczałoby dodanie nowych argumentów do wywołania create. Model uniksowy jest prostszy.

Daje to również bardzo użyteczny model typu fork-without-exec, w którym proces można podzielić na wiele części. Było to niezbędne, gdy nie było żadnej formy asynchronicznych operacji we / wy, i jest przydatne, gdy wykorzystuje się wiele procesorów w systemie. (Wątki wstępne). Robiłem to wiele lat, nawet ostatnio. Zasadniczo pozwala on na umieszczenie w kontenerach wielu „programów” w jeden program, więc absolutnie nie ma miejsca na uszkodzenia lub niedopasowania wersji itp.

Model rozwidlenia / egzekucji umożliwia także dziecku odziedziczenie radykalnie dziwnego środowiska, ustawionego między rozwidleniem a exec. Szczególnie takie jak odziedziczone deskryptory plików. (Rozszerzenie stdio fd.) Model tworzenia nie oferuje możliwości dziedziczenia niczego, co nie zostało przewidziane przez twórców wywołania create.

Niektóre systemy mogą również obsługiwać dynamiczną kompilację kodu natywnego, w którym proces faktycznie pisze własny program natywny. Innymi słowy, chce nowego programu, który sam pisze w locie, BEZ konieczności przechodzenia przez cykl kodu źródłowego / kompilatora / linkera i zajmowania miejsca na dysku. (Wierzę, że istnieje system językowy Verilog, który to robi.) Model rozwidlenia to obsługuje, model tworzenia zwykle by tego nie robił.


Deskryptory plików nie są „rozszerzeniem stdio”; Wskaźniki plików stdio są opakowaniem wokół deskryptorów plików. Deskryptory plików były na pierwszym miejscu i są podstawowymi uchwytami we / wy systemu Unix. Ale w przeciwnym razie jest to dobra uwaga.
Scott,

2

Funkcja fork () nie służy tylko do kopiowania procesu ojca, zwraca wartość wskazującą, że proces jest procesem ojca lub syna, poniższy rysunek wyjaśnia, w jaki sposób można korzystać z fork () jako ojca i syn:

wprowadź opis zdjęcia tutaj

jak pokazano, gdy proces jest ojcem fork () zwraca identyfikator procesu son PID , w przeciwnym razie zwraca0

na przykład możesz z niego skorzystać, jeśli masz proces (serwer WWW), który odbiera żądania, i na każde żądanie tworzy a, son processaby przetworzyć to żądanie, tutaj ojciec i jego synowie mają różne zadania.

SO, brak uruchamiania kopii procesu nie jest dokładnie taki, jak fork ().


5
Chociaż to prawda, to nie odpowiada na pytanie. Dlaczego rozwidlenie jest konieczne do utworzenia procesu, jeśli chcę uruchomić inny plik wykonywalny?
SkyDan

1
Zgadzam się z SkyDan - to nie odpowiada na pytanie. posix_spawn jest nieco bardziej wymyślną wersją tego, co można sobie wyobrazić 30 lat temu (zanim istniał Posix) jako funkcja fork_execve ; taki, który tworzy nowy proces, inicjując jego obraz z pliku wykonywalnego, nawet nie sugerując, że kopiuje obraz procesu nadrzędnego (z wyjątkiem listy argumentów, środowiska i atrybutów procesu (np. katalogu roboczego)) i zwraca PID nowego procesu dla dzwoniącego (proces nadrzędny) .
Scott,

1
Istnieją inne sposoby przekazywania dziecku informacji „rodzica”. Technika ta zwracana wartość okazuje się być najbardziej skutecznym sposobem robi to od fork jeśli zakładać, że chcesz forkw pierwszej kolejności
Cort Ammon

0

Przekierowanie we / wy najłatwiej jest zaimplementować po rozwidleniu i przed wykonaniem. Dziecko, wiedząc, że jest dzieckiem, może zamykać deskryptory plików, otwierać nowe, dup () lub dup2 (), aby uzyskać właściwy numer fd itp., Wszystko bez wpływu na rodzica. Po wykonaniu tej czynności i być może dowolnych zmianach zmiennych środowiskowych (również nie wpływających na element nadrzędny), może uruchomić nowy program w dostosowanym środowisku.


Wszystko, co tu robisz, to powtarzanie trzeciego akapitu odpowiedzi Jima Catheya z odrobinę bardziej szczegółowymi szczegółami.
Scott,

-2

Myślę, że wszyscy tutaj wiedzą, jak działa rozwidlenie, ale pytanie brzmi: dlaczego musimy stworzyć dokładną kopię rodzica za pomocą rozwidlenia? Odpowiedź ==> Weź przykład serwera (bez rozwidlenia), gdy klient-1 uzyskuje dostęp do serwera, jeśli w tym samym czasie przybywa drugi klient-2 i chce uzyskać dostęp do serwera, ale serwer nie daje pozwolenia nowo przybyłemu klient-2, ponieważ serwer jest zajęty obsługą klienta-1, więc klient-2 musi czekać. po zakończeniu wszystkich usług dla klienta-1 klient-2 może teraz uzyskać dostęp do serwera. Teraz zastanów się, czy w tym samym czasie przybywa klient-3, więc klient-3 musi poczekać, aż wszystkie usługi dla klienta-2 zostaną zakończone. Weźmy scenariusz, w którym tysiące klientów muszą uzyskać dostęp do serwera w tym samym czasie ... wtedy wszyscy klienci muszą czekaj (serwer jest zajęty !!).

Można tego uniknąć, tworząc (za pomocą rozwidlenia) dokładną duplikat serwera (tj. Dziecko) serwera, przy czym każde dziecko (które jest dokładnie duplikatem kopii jego rodzica, czyli serwera) jest dedykowane nowo przybyłemu klientowi, a tym samym wszyscy klienci uzyskują dostęp do tego samego serwer.


Dlatego procesy serwera nie powinny być jednowątkowe, obsługujące żądania klientów kolejno, gdy można je obsługiwać jednocześnie - np. W osobnych procesach. Ale wielowątkowy model serwera można łatwo zaimplementować za pomocą procesu nasłuchiwania, który przyjmuje żądania od klientów i tworzy zupełnie nowy proces uruchamiania programu obsługi klienta. Jedyną zaletą forkwywołania, które kopiuje proces nadrzędny, jest to, że nie trzeba mieć dwóch osobnych programów - ale posiadanie osobnych programów (np. inetd) Może uczynić system bardziej modułowym.
Scott,
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.