Różnica między fork (), vfork (), exec () i clone ()

198

Chciałem znaleźć różnicę między tymi czterema w Google i spodziewałem się, że będzie wiele informacji na ten temat, ale tak naprawdę nie było żadnego solidnego porównania między czterema połączeniami.

Próbowałem skompilować rodzaj podstawowego spojrzenia na różnice między tymi wywołaniami systemowymi i oto, co otrzymałem. Czy wszystkie te informacje są prawidłowe / czy brakuje mi czegoś ważnego?

Fork : Wywołanie rozwidlenia zasadniczo tworzy duplikat bieżącego procesu, identyczny pod każdym względem (nie wszystko jest kopiowane, na przykład limity zasobów w niektórych implementacjach, ale pomysł polega na stworzeniu jak najbliższej kopii).

Nowy proces (podrzędny) otrzymuje inny identyfikator procesu (PID) i ma PID starego procesu (nadrzędnego) jako nadrzędny PID (PPID). Ponieważ oba procesy działają teraz dokładnie w tym samym kodzie, mogą stwierdzić, który jest kod powrotu rozwidlenia - dziecko otrzymuje 0, rodzic otrzymuje PID dziecka. To wszystko oczywiście przy założeniu, że wywołanie fork działa - jeśli nie, nie zostanie utworzone żadne dziecko, a rodzic otrzyma kod błędu.

Vfork: Podstawową różnicą między vfork a fork jest to, że gdy nowy proces jest tworzony za pomocą vfork (), proces nadrzędny jest tymczasowo zawieszony, a proces potomny może pożyczyć przestrzeń adresową rodzica. Ten dziwny stan rzeczy trwa do momentu, gdy proces potomny albo wyjdzie, albo wywoła execve (), w którym to momencie proces macierzysty jest kontynuowany.

Oznacza to, że proces potomny vfork () musi zachować ostrożność, aby uniknąć nieoczekiwanej modyfikacji zmiennych procesu macierzystego. W szczególności proces potomny nie może powracać z funkcji zawierającej wywołanie vfork () i nie może wywoływać exit () (jeśli musi wyjść, powinien użyć _exit (); w rzeczywistości dotyczy to również dziecka normalnego widelca ()).

Exec :Wywołanie exec jest sposobem na zastąpienie całego bieżącego procesu nowym programem. Ładuje program do bieżącej przestrzeni procesu i uruchamia go od punktu wejścia. exec () zastępuje bieżący proces plikiem wykonywalnym wskazanym przez funkcję. Kontrola nigdy nie wraca do oryginalnego programu, chyba że wystąpi błąd exec ().

Clone :Klon, jako widelec, tworzy nowy proces. W przeciwieństwie do fork, wywołania te pozwalają procesowi potomnemu na dzielenie się częściami jego kontekstu wykonawczego z procesem wywoływania, takim jak przestrzeń pamięci, tabela deskryptorów plików i tabela procedur obsługi sygnałów.

Gdy proces potomny jest tworzony za pomocą klonowania, wykonuje aplikację funkcji fn (arg). (Różni się to od fork, gdzie wykonywanie jest kontynuowane w potomku od momentu pierwotnego wywołania fork). Argument fn jest wskaźnikiem funkcji, która jest wywoływana przez proces potomny na początku jego wykonywania. Argument arg jest przekazywany do funkcji fn.

Gdy aplikacja funkcji fn (arg) powraca, proces potomny zostaje zakończony. Liczba całkowita zwracana przez fn jest kodem wyjścia dla procesu potomnego. Proces potomny może także zostać jawnie zakończony przez wywołanie exit (2) lub po odebraniu krytycznego sygnału.

Otrzymane informacje:

Dziękuję za czas poświęcony na przeczytanie tego ! :)

użytkownik476033
źródło
2
Dlaczego vfork nie może wywoływać exit ()? A może nie wrócić? Czy exit () nie używa po prostu _exit ()? Próbuję też zrozumieć :)
LazerSharks,
2
@Gnuey: ponieważ potencjalnie (jeśli jest zaimplementowany inaczej niż fork()w Linuksie i prawdopodobnie we wszystkich BSD) pożycza przestrzeń adresową swojego rodzica. Wszystko, co robi, oprócz dzwonienia execve()lub _exit(), ma ogromny potencjał, aby zepsuć rodzica. W szczególności exit()programy atexit()obsługi połączeń i inne „finalizatory”, np .: opróżniają strumienie standardowego przekazu. Powrót od vfork()dziecka może (podobnie jak poprzednio) zepsuć stos rodzica.
ninjalj
Zastanawiałem się, co dzieje się z wątkami procesu macierzystego; Czy wszystkie są sklonowane, czy tylko wątek, który wywołuje forksyscall?
Mohammad Jafar Mashhadi
@LazerSharks vfork tworzy proces podobny do wątku, w którym pamięć jest współużytkowana bez zabezpieczeń przed kopiowaniem przy zapisie, więc robienie stosów może zniszczyć proces nadrzędny.
Jasen

Odpowiedzi:

160
  • vfork()to przestarzała optymalizacja. Przed dobrym zarządzaniem pamięcią wykonałem fork()pełną kopię pamięci rodzica, więc była dość droga. ponieważ w wielu przypadkach fork()następowało po niej exec(), które odrzuca obecną mapę pamięci i tworzy nową, był to niepotrzebny wydatek. W dzisiejszych czasach fork()nie kopiuje pamięci; jest po prostu ustawiony jako „kopiuj przy zapisie”, więc fork()+ exec()jest tak samo wydajny jak vfork()+ exec().

  • clone()to wywołanie systemowe używane przez fork(). z niektórymi parametrami tworzy nowy proces, z innymi tworzy wątek. różnica między nimi polega tylko na tym, które struktury danych (przestrzeń pamięci, stan procesora, stos, PID, otwarte pliki itp.) są wspólne, czy nie.

Javier
źródło
22
vforkunika się potrzeby tymczasowego angażowania znacznie większej ilości pamięci tylko po to, aby można było ją wykonać exec, i jest to nadal bardziej wydajne niż fork, nawet jeśli nie prawie tak wysokie. W ten sposób można uniknąć nadpisywania pamięci, tak aby duży program mógł zrodzić proces potomny. Tak więc nie tylko zwiększenie wydajności, ale może w ogóle uczynić to wykonalnym.
Deduplicator
5
W rzeczywistości widziałem z pierwszej ręki, jak fork () jest daleki od taniego, gdy twoje RSS jest duże. Zakładam, że dzieje się tak, ponieważ jądro wciąż musi skopiować wszystkie tabele stron.
Martina Ferrari,
4
Musi skopiować wszystkie tabele stron, ustawić całą zapisywalną pamięć kopiowania przy zapisie w obu procesach , opróżnić TLB, a następnie musi przywrócić wszystkie zmiany do elementu nadrzędnego (i ponownie opróżnić TLB) exec.
zwol
3
vfork jest nadal przydatny w cygwin (emulujący dll jądro, który działa na systemie Windows Microsoftu). cygwin nie może zaimplementować wydajnego rozwidlenia, ponieważ podstawowy system operacyjny go nie ma.
ctrl-alt-delor
81
  • execve() zastępuje bieżący obraz wykonywalny innym obrazem załadowanym z pliku wykonywalnego.
  • fork() tworzy proces potomny.
  • vfork()jest zoptymalizowaną historycznie wersją fork(), przeznaczoną do użycia, gdy execve()wywoływana jest bezpośrednio po fork(). Okazało się, że działa dobrze w systemach innych niż MMU (gdzie fork()nie może działać wydajnie) i podczas fork()przetwarzania procesów o dużej powierzchni pamięci w celu uruchomienia małego programu (pomyśl o Javie Runtime.exec()). POSIX ustandaryzował, posix_spawn()aby zastąpić te ostatnie dwa bardziej nowoczesne zastosowania vfork().
  • posix_spawn()robi ekwiwalent a fork()/execve(), a także pozwala żonglerce fd pomiędzy. Ma to zastąpić fork()/execve(), głównie na platformach innych niż MMU.
  • pthread_create() tworzy nowy wątek.
  • clone()to wywołanie specyficzne dla Linuksa, którego można użyć do zaimplementowania czegokolwiek od fork()do pthread_create(). Daje dużo kontroli. Inspirowany rfork().
  • rfork()to połączenie specyficzne dla planu 9. To ma być ogólne wywołanie, umożliwiające kilka stopni udostępniania między pełnymi procesami i wątkami.
ninjalj
źródło
2
Dzięki, że dodałem więcej informacji, niż było w rzeczywistości, pomogło mi to zaoszczędzić czas
Neeraj
5
Plan 9 to taka złośliwość.
JJ
1
Dla tych, którzy nie pamiętają, co oznacza MMU: „Jednostka zarządzania pamięcią” - dalsze czytanie w Wikipedii
mgarey
43
  1. fork()- tworzy nowy proces potomny, który jest kompletną kopią procesu macierzystego. Procesy potomne i macierzyste używają różnych wirtualnych przestrzeni adresowych, które początkowo są zapełniane przez te same strony pamięci. Następnie, gdy oba procesy są wykonywane, wirtualne przestrzenie adresowe zaczynają się coraz bardziej różnić, ponieważ system operacyjny wykonuje leniwe kopiowanie stron pamięci zapisywanych przez jeden z tych dwóch procesów i przypisuje niezależne kopie zmodyfikowanych stron pamięć dla każdego procesu. Ta technika nazywa się Copy-On-Write (COW).
  2. vfork()- tworzy nowy proces potomny, który jest „szybką” kopią procesu nadrzędnego. W przeciwieństwie do wywołania systemowego fork()procesy potomne i macierzyste korzystają z tej samej wirtualnej przestrzeni adresowej. UWAGA! Korzystając z tej samej wirtualnej przestrzeni adresowej, zarówno rodzic, jak i dziecko używają tego samego stosu, wskaźnika stosu i wskaźnika instrukcji, jak w przypadku klasycznego fork()! Aby zapobiec niepożądanej ingerencji między rodzicem a dzieckiem, które używają tego samego stosu, wykonywanie procesu nadrzędnego jest zawieszane, dopóki dziecko nie wywoła albo exec()(utworzy nową wirtualną przestrzeń adresową i przejście do innego stosu), albo _exit()(zakończenie wykonywania procesu ). vfork()to optymalizacja fork()modelu „fork-and-exec”. Można to wykonać 4-5 razy szybciej niż fork(), ponieważ w przeciwieństwie dofork()(nawet mając na uwadze COW), implementacja vfork()wywołania systemowego nie obejmuje utworzenia nowej przestrzeni adresowej (przydzielanie i konfigurowanie nowych katalogów stron).
  3. clone()- tworzy nowy proces potomny. Różne parametry tego wywołania systemowego określają, które części procesu nadrzędnego muszą zostać skopiowane do procesu podrzędnego i które części będą między nimi współużytkowane. W rezultacie tego wywołania systemowego można używać do tworzenia wszelkiego rodzaju jednostek wykonawczych, poczynając od wątków, a kończąc na całkowicie niezależnych procesach. W rzeczywistości clone()wywołanie systemowe jest bazą używaną do realizacji pthread_create()i całej rodziny fork()wywołań systemowych.
  4. exec()- resetuje całą pamięć procesu, ładuje i analizuje określony plik binarny wykonywalny, konfiguruje nowy stos i przekazuje kontrolę do punktu wejścia załadowanego pliku wykonywalnego. To wywołanie systemowe nigdy nie zwraca kontroli dzwoniącemu i służy do ładowania nowego programu do już istniejącego procesu. To wywołanie fork()systemowe wraz z wywołaniem systemowym tworzą razem klasyczny model zarządzania procesami UNIX o nazwie „fork-and-exec”.
ZarathustrA
źródło
2
Zauważ, że wymagania BSD i POSIX vforksą tak słabe, że legalne byłoby utworzenie vforksynonimu fork(a POSIX.1-2008 vforkcałkowicie usuwa ze specyfikacji). Jeśli zdarzy ci się przetestować kod w systemie, który je synonimizuje (np. Większość BSD po wersji 4.4 oprócz NetBSD, jądra Linuksa w wersjach wcześniejszych niż 2.2.0-pre6 itp.), Może działać nawet w przypadku naruszenia vforkumowy, a następnie wybuchu jeśli uruchomisz go gdzie indziej. Niektóre z tych, które go symulują fork(np. OpenBSD) nadal gwarantują, że rodzic nie wznowi działania, dopóki dziecko execlub _exits. Jest absurdalnie nieprzenośny.
ShadowRanger,
2
odnośnie ostatniego zdania twojego trzeciego punktu: Zauważyłem w Linuksie za pomocą strace, że chociaż otoki glibc dla fork () wywołują klonowanie syscall, otoki dla vfork () wywołują syscall
vfork
7

Fork (), vfork () i clone () wywołują do_fork (), aby wykonać prawdziwą pracę, ale z różnymi parametrami.

asmlinkage int sys_fork(struct pt_regs regs)
{
    return do_fork(SIGCHLD, regs.esp, &regs, 0);
}

asmlinkage int sys_clone(struct pt_regs regs)
{
    unsigned long clone_flags;
    unsigned long newsp;

    clone_flags = regs.ebx;
    newsp = regs.ecx;
    if (!newsp)
        newsp = regs.esp;
    return do_fork(clone_flags, newsp, &regs, 0);
}
asmlinkage int sys_vfork(struct pt_regs regs)
{
    return do_fork(CLONE_VFORK | CLONE_VM | SIGCHLD, regs.esp, &regs, 0);
}
#define CLONE_VFORK 0x00004000  /* set if the parent wants the child to wake it up on mm_release */
#define CLONE_VM    0x00000100  /* set if VM shared between processes */

SIGCHLD means the child should send this signal to its father when exit.

W przypadku rozwidlenia dziecko i ojciec mają niezależną tabelę stron maszyny wirtualnej, ale ponieważ wydajność, rozwidlenie tak naprawdę nie kopiuje żadnych stron, po prostu ustawia wszystkie strony do zapisu tylko do odczytu dla procesu potomnego. Kiedy proces potomny chce napisać coś na tej stronie, zdarza się wyjątek strony i jądro przydzieli nową stronę sklonowaną ze starej strony z uprawnieniami do zapisu. To się nazywa „kopiuj przy zapisie”.

W przypadku vfork pamięć wirtualna jest dziełem dziecka i ojca - właśnie dlatego ojciec i dziecko nie mogą obudzić się jednocześnie, ponieważ będą na siebie wpływać. Tak więc ojciec będzie spał na końcu „do_fork ()” i obudzi się, gdy dziecko wywoła exit () lub execve () od tego czasu będzie właścicielem nowej tabeli stron. Oto kod (w do_fork ()), który śpi ojciec.

if ((clone_flags & CLONE_VFORK) && (retval > 0))
down(&sem);
return retval;

Oto kod (w mm_release () wywoływany przez exit () i execve ()), który wybudza ojca.

up(tsk->p_opptr->vfork_sem);

W przypadku sys_clone () jest bardziej elastyczny, ponieważ można do niego wprowadzać dowolne flagi clone_flags. Więc pthread_create () wywołuje to wywołanie systemowe z wieloma tagami clone_flag:

int clone_flags = (CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGNAL | CLONE_SETTLS | CLONE_PARENT_SETTID | CLONE_CHILD_CLEARTID | CLONE_SYSVSEM);

Podsumowanie: fork (), vfork () i clone () utworzą procesy potomne z różnymi mocowaniami zasobów współużytkowania z procesem ojca. Możemy również powiedzieć, że vfork () i clone () mogą tworzyć wątki (w rzeczywistości są to procesy, ponieważ mają niezależne zadanie_strukt), ponieważ współużytkują tabelę stron maszyny wirtualnej z procesem ojca.

użytkownik991800
źródło
-4

w fork () proces potomny lub nadrzędny zostanie wykonany na podstawie wyboru procesora. Ale w vfork () na pewno dziecko wykona się jako pierwsze. po zakończeniu potomka rodzic wykona polecenie.

Raj Kannan B.
źródło
3
Źle. vfork()można po prostu zaimplementować jako fork().
ninjalj
po AnyFork () nie jest określone, kto uruchamia pierwszego rodzica / dziecka.
AjayKumarBasuthkar
5
@Raj: Masz pewne nieporozumienia pojęciowe, jeśli myślisz, że po rozwidleniu istnieje domniemane pojęcie szeregowego porządku. Forking tworzy nowy proces, a następnie zwraca kontrolę obu procesom (każdy zwraca inny pid) - system operacyjny może zaplanować równoległe uruchomienie nowego procesu, jeśli coś takiego ma sens (np. Wiele procesorów). Jeśli z jakiegoś powodu potrzebujesz, aby te procesy były wykonywane w określonej kolejności szeregowej, potrzebujesz dodatkowej synchronizacji, której nie zapewnia rozwidlenie; Szczerze mówiąc, prawdopodobnie nie chciałbyś nawet widelca.
Andon M. Coleman
W rzeczywistości @AjayKumarBasuthkar i @ninjalj, oboje się mylicie. z vfork(), dziecko biegnie pierwsze. Jest na stronach podręcznika; egzekucja rodziców jest zawieszona do czasu śmierci dziecka lub śmierci exec. I ninjalj sprawdza kod źródłowy jądra. Nie ma sposobu, aby wdrożyć vfork()jako fork()gdyż przechodzą one różne argumenty do_fork()w jądrze. Możesz jednak zaimplementować vforkza pomocą clonesyscall
Zac Wimer
@ZacWimer: patrz komentarz ShadowRanger do innej odpowiedzi stackoverflow.com/questions/4856255/... Stary Linux je zsynchronizował, tak jak robią to BSD inne niż NetBSD (które zazwyczaj są przenoszone na wiele systemów innych niż MMU). Ze strony Linux-a: W 4.4BSD stał się synonimem fork (2), ale NetBSD wprowadził go ponownie; patrz ⟨netbsd.org / Documentation/ kernel/ vfork.html⟩. W Linuksie był odpowiednikiem fork (2) aż do wersji 2.2.0-pre6.
ninjalj