Jaka jest najbliższa rzecz, którą Windows musi rozwidlić ()?

124

Myślę, że to pytanie mówi wszystko.

Chcę rozwidlać w systemie Windows. Jaka jest najbardziej podobna operacja i jak z niej korzystam.

rlbond
źródło

Odpowiedzi:

86

Cygwin ma w pełni funkcjonalny fork () w systemie Windows. Tak więc, jeśli używanie Cygwin jest dla Ciebie akceptowalne, problem zostanie rozwiązany w przypadku, gdy wydajność nie jest problemem.

W przeciwnym razie możesz przyjrzeć się, jak Cygwin implementuje fork (). Z dość starego dokumentu architektury Cygwin :

5.6. Tworzenie procesu Wywołanie fork w Cygwin jest szczególnie interesujące, ponieważ nie mapuje dobrze w górnej części API Win32. To bardzo utrudnia prawidłowe wdrożenie. Obecnie rozwidlenie Cygwin jest implementacją bez kopiowania przy zapisie, podobną do tej, która była obecna we wczesnych wersjach UNIX.

Pierwszą rzeczą, która się dzieje, gdy proces nadrzędny forkuje proces potomny, jest inicjowanie przez niego spacji w tabeli procesów Cygwin. Następnie tworzy zawieszony proces potomny przy użyciu wywołania Win32 CreateProcess. Następnie proces nadrzędny wywołuje setjmp, aby zapisać swój własny kontekst i ustawia wskaźnik do tego w obszarze pamięci współdzielonej Cygwin (współdzielonej przez wszystkie zadania Cygwin). Następnie wypełnia sekcje .data i .bss dziecka, kopiując z własnej przestrzeni adresowej do przestrzeni adresowej zawieszonego dziecka. Po zainicjowaniu przestrzeni adresowej dziecka, dziecko jest uruchamiane, podczas gdy rodzic czeka na muteks. Dziecko odkrywa, że ​​został rozwidlony i wykonuje długie skoki, korzystając z zapisanego bufora skoku. Następnie dziecko ustawia muteks, na który czeka rodzic, i blokuje inny muteks. Jest to sygnał dla rodzica, aby skopiował swój stos i stertę do dziecka, po czym zwalnia muteks, na który czeka dziecko, i wraca z wywołania fork. Wreszcie, dziecko budzi się z blokowania na ostatnim muteksie, odtwarza wszystkie obszary mapowane w pamięci przekazane do niego przez wspólny obszar i wraca z samego forka.

Chociaż mamy kilka pomysłów, jak przyspieszyć implementację rozwidlenia poprzez zmniejszenie liczby przełączeń kontekstu między procesem nadrzędnym i podrzędnym, rozwidlenie prawie na pewno zawsze będzie nieefektywne pod Win32. Na szczęście w większości przypadków rodzina wywołań spawn dostarczana przez Cygwin może zostać zastąpiona parą fork / exec przy niewielkim wysiłku. Te wywołania są dokładnie odwzorowywane w górnej części interfejsu API Win32. Dzięki temu są znacznie wydajniejsze. Zmiana programu sterownika kompilatora tak, aby wywoływał spawn zamiast fork, była trywialną zmianą i zwiększyła prędkość kompilacji o dwadzieścia do trzydziestu procent w naszych testach.

Jednak spawn i exec stwarzają własny zestaw trudności. Ponieważ nie ma sposobu na wykonanie faktycznego exec pod Win32, Cygwin musi wymyślić własne identyfikatory procesów (PID). W rezultacie, gdy proces wykonuje wiele wywołań exec, z jednym PID Cygwin będzie powiązanych wiele identyfikatorów PID systemu Windows. W niektórych przypadkach, kody pośredniczące każdego z tych procesów Win32 mogą pozostawać w oczekiwaniu na zamknięcie ich wykonanego procesu Cygwin.

Brzmi jak dużo pracy, prawda? I tak, jest powolny.

EDYTUJ: dokument jest nieaktualny, zapoznaj się z tą doskonałą odpowiedzią na aktualizację

Laurynas Biveinis
źródło
11
To dobra odpowiedź, jeśli chcesz napisać aplikację Cygwin w systemie Windows. Ale ogólnie nie jest to najlepsza rzecz do zrobienia. Zasadniczo modele procesów i wątków * nix i Windows są bardzo różne. CreateProcess () i CreateThread () to ogólnie równoważne interfejsy API
Foredecker
2
Programiści powinni pamiętać, że jest to nieobsługiwany mechanizm, a IIRC jest rzeczywiście skłonny do zerwania, gdy jakiś inny proces w systemie używa wstrzykiwania kodu.
Harry Johnston,
1
Inne łącze implementacji nie jest już ważne.
PythonNut
Zmieniono, aby pozostawić tylko drugie łącze odpowiedzi
Laurynas Biveinis
@Foredecker, Właściwie nie powinieneś tego robić, nawet jeśli próbujesz napisać „aplikację cygwin”. Próbuje naśladować Uniksa, forkale osiąga to za pomocą nieszczelnego rozwiązania i musisz być przygotowany na nieoczekiwane sytuacje.
Pacerier
66

Na pewno nie znam szczegółów na ten temat, ponieważ nigdy tego nie robiłem, ale natywne API NT ma możliwość rozwidlenia procesu (podsystem POSIX w Windows potrzebuje tej możliwości - nie jestem pewien, czy podsystem POSIX jest już nawet obsługiwany).

Poszukiwanie ZwCreateProcess () powinno dać ci więcej szczegółów - na przykład ta informacja od Maxima Shatskiha :

Najważniejszym parametrem jest tutaj SectionHandle. Jeśli ten parametr ma wartość NULL, jądro rozwidli bieżący proces. W przeciwnym razie ten parametr musi być uchwytem obiektu sekcji SEC_IMAGE utworzonego w pliku EXE przed wywołaniem funkcji ZwCreateProcess ().

Chociaż zauważ, że Corinna Vinschen wskazuje, że Cygwin stwierdził, że używanie ZwCreateProcess () jest nadal niewiarygodne :

Iker Arizmendi napisał:

> Because the Cygwin project relied solely on Win32 APIs its fork
> implementation is non-COW and inefficient in those cases where a fork
> is not followed by exec.  It's also rather complex. See here (section
> 5.6) for details:
>  
> http://www.redhat.com/support/wpapers/cygnus/cygnus_cygwin/architecture.html

Ten dokument jest dość stary, mniej więcej 10 lat. Chociaż nadal używamy wywołań Win32 do emulowania rozwidlenia, metoda znacznie się zmieniła. W szczególności nie tworzymy już procesu potomnego w stanie zawieszonym, chyba że określone dane wymagają specjalnej obsługi przez rodzica, zanim zostaną skopiowane do dziecka. W obecnej wersji 1.5.25 jedynym przypadkiem dla zawieszonego dziecka są otwarte gniazda w rodzicu. Nadchodząca wersja 1.7.0 w ogóle nie zostanie zawieszona.

Jednym z powodów, dla których nie warto używać ZwCreateProcess, było to, że do wersji 1.5.25 nadal obsługujemy użytkowników Windows 9x. Jednak dwie próby użycia ZwCreateProcess w systemach opartych na systemie NT nie powiodły się z tego czy innego powodu.

Byłoby naprawdę miło, gdyby te rzeczy były lepsze lub w ogóle udokumentowane, zwłaszcza kilka struktur danych i jak podłączyć proces do podsystemu. Chociaż fork nie jest koncepcją Win32, nie sądzę, aby uczynienie go łatwiejszym do wdrożenia byłoby złą rzeczą.

Michael Burr
źródło
To jest zła odpowiedź. CreateProcess () i CreateThread () to ogólne odpowiedniki.
Foredecker
2
Interix jest dostępny w systemie Windows Vista Enterprise / Ultimate jako „Podsystem dla aplikacji UNIX”: en.wikipedia.org/wiki/Interix
bk1e
15
@Foredecker - to może być zła odpowiedź, ale CreateProcess () / CreateThread () też może się mylić. Zależy to od tego, czy poszukuje się „sposobu działania Win32”, czy „możliwie najbardziej zbliżonego do semantyki fork ()”. CreateProcess () zachowuje się znacząco inaczej niż fork (), co jest powodem, dla którego cygwin potrzebował dużo pracy, aby go obsługiwać.
Michael Burr
1
@jon: Próbowałem naprawić linki i skopiować odpowiedni tekst do odpowiedzi (więc przyszłe uszkodzone linki nie stanowią problemu). Jednak ta odpowiedź jest z wystarczająco długo temu, że nie jestem w 100% pewien cytat znalazłem dzisiaj jest to, co miał na myśli w 2009 roku
Michael Burr
4
Jeśli ludzie chcą „ forknatychmiastowego exec”, być może kandydatem jest CreateProcess. Ale forkbez execjest często pożądane i właśnie o to kierowcy ludzie proszą o prawdziwe fork.
Aaron McDaid,
37

Cóż, okna tak naprawdę nie mają czegoś podobnego. Zwłaszcza, że ​​fork może służyć do koncepcyjnego tworzenia wątku lub procesu w * nix.

Muszę więc powiedzieć:

CreateProcess()/CreateProcessEx()

i

CreateThread()(Słyszałem, że dla aplikacji C _beginthreadex()jest lepsze).

Evan Teran
źródło
17

Ludzie próbowali wdrożyć fork w systemie Windows. To jest najbliższa rzecz, jaką mogę znaleźć:

Zaczerpnięte z: http://doxygen.scilab.org/5.3/d0/d8f/forkWindows_8c_source.html#l00216

static BOOL haveLoadedFunctionsForFork(void);

int fork(void) 
{
    HANDLE hProcess = 0, hThread = 0;
    OBJECT_ATTRIBUTES oa = { sizeof(oa) };
    MEMORY_BASIC_INFORMATION mbi;
    CLIENT_ID cid;
    USER_STACK stack;
    PNT_TIB tib;
    THREAD_BASIC_INFORMATION tbi;

    CONTEXT context = {
        CONTEXT_FULL | 
        CONTEXT_DEBUG_REGISTERS | 
        CONTEXT_FLOATING_POINT
    };

    if (setjmp(jenv) != 0) return 0; /* return as a child */

    /* check whether the entry points are 
       initilized and get them if necessary */
    if (!ZwCreateProcess && !haveLoadedFunctionsForFork()) return -1;

    /* create forked process */
    ZwCreateProcess(&hProcess, PROCESS_ALL_ACCESS, &oa,
        NtCurrentProcess(), TRUE, 0, 0, 0);

    /* set the Eip for the child process to our child function */
    ZwGetContextThread(NtCurrentThread(), &context);

    /* In x64 the Eip and Esp are not present, 
       their x64 counterparts are Rip and Rsp respectively. */
#if _WIN64
    context.Rip = (ULONG)child_entry;
#else
    context.Eip = (ULONG)child_entry;
#endif

#if _WIN64
    ZwQueryVirtualMemory(NtCurrentProcess(), (PVOID)context.Rsp,
        MemoryBasicInformation, &mbi, sizeof mbi, 0);
#else
    ZwQueryVirtualMemory(NtCurrentProcess(), (PVOID)context.Esp,
        MemoryBasicInformation, &mbi, sizeof mbi, 0);
#endif

    stack.FixedStackBase = 0;
    stack.FixedStackLimit = 0;
    stack.ExpandableStackBase = (PCHAR)mbi.BaseAddress + mbi.RegionSize;
    stack.ExpandableStackLimit = mbi.BaseAddress;
    stack.ExpandableStackBottom = mbi.AllocationBase;

    /* create thread using the modified context and stack */
    ZwCreateThread(&hThread, THREAD_ALL_ACCESS, &oa, hProcess,
        &cid, &context, &stack, TRUE);

    /* copy exception table */
    ZwQueryInformationThread(NtCurrentThread(), ThreadBasicInformation,
        &tbi, sizeof tbi, 0);
    tib = (PNT_TIB)tbi.TebBaseAddress;
    ZwQueryInformationThread(hThread, ThreadBasicInformation,
        &tbi, sizeof tbi, 0);
    ZwWriteVirtualMemory(hProcess, tbi.TebBaseAddress, 
        &tib->ExceptionList, sizeof tib->ExceptionList, 0);

    /* start (resume really) the child */
    ZwResumeThread(hThread, 0);

    /* clean up */
    ZwClose(hThread);
    ZwClose(hProcess);

    /* exit with child's pid */
    return (int)cid.UniqueProcess;
}
static BOOL haveLoadedFunctionsForFork(void)
{
    HANDLE ntdll = GetModuleHandle("ntdll");
    if (ntdll == NULL) return FALSE;

    if (ZwCreateProcess && ZwQuerySystemInformation && ZwQueryVirtualMemory &&
        ZwCreateThread && ZwGetContextThread && ZwResumeThread &&
        ZwQueryInformationThread && ZwWriteVirtualMemory && ZwClose)
    {
        return TRUE;
    }

    ZwCreateProcess = (ZwCreateProcess_t) GetProcAddress(ntdll,
        "ZwCreateProcess");
    ZwQuerySystemInformation = (ZwQuerySystemInformation_t)
        GetProcAddress(ntdll, "ZwQuerySystemInformation");
    ZwQueryVirtualMemory = (ZwQueryVirtualMemory_t)
        GetProcAddress(ntdll, "ZwQueryVirtualMemory");
    ZwCreateThread = (ZwCreateThread_t)
        GetProcAddress(ntdll, "ZwCreateThread");
    ZwGetContextThread = (ZwGetContextThread_t)
        GetProcAddress(ntdll, "ZwGetContextThread");
    ZwResumeThread = (ZwResumeThread_t)
        GetProcAddress(ntdll, "ZwResumeThread");
    ZwQueryInformationThread = (ZwQueryInformationThread_t)
        GetProcAddress(ntdll, "ZwQueryInformationThread");
    ZwWriteVirtualMemory = (ZwWriteVirtualMemory_t)
        GetProcAddress(ntdll, "ZwWriteVirtualMemory");
    ZwClose = (ZwClose_t) GetProcAddress(ntdll, "ZwClose");

    if (ZwCreateProcess && ZwQuerySystemInformation && ZwQueryVirtualMemory &&
        ZwCreateThread && ZwGetContextThread && ZwResumeThread &&
        ZwQueryInformationThread && ZwWriteVirtualMemory && ZwClose)
    {
        return TRUE;
    }
    else
    {
        ZwCreateProcess = NULL;
        ZwQuerySystemInformation = NULL;
        ZwQueryVirtualMemory = NULL;
        ZwCreateThread = NULL;
        ZwGetContextThread = NULL;
        ZwResumeThread = NULL;
        ZwQueryInformationThread = NULL;
        ZwWriteVirtualMemory = NULL;
        ZwClose = NULL;
    }
    return FALSE;
}
Eric des Courtis
źródło
4
Zauważ, że brakuje większości sprawdzania błędów - np. ZwCreateThread zwraca wartość NTSTATUS, którą można sprawdzić za pomocą makr SUCCEEDED i FAILED.
BCran
1
Co się stanie, jeśli forkawaria ulegnie awarii, spowoduje awarię programu lub czy wątek po prostu się zawiesi? Jeśli to zawiesza program, to tak naprawdę nie jest rozwidlenie. Po prostu ciekawy, ponieważ szukam prawdziwego rozwiązania i mam nadzieję, że to może być przyzwoita alternatywa.
leetNightshade
1
Chciałbym zauważyć, że w podanym kodzie jest błąd. haveLoadedFunctionsForFork to funkcja globalna w nagłówku, ale funkcja statyczna w pliku c. Oba powinny być globalne. Obecnie widelec ulega awarii, dodając teraz sprawdzanie błędów.
leetNightshade
Witryna nie działa i nie wiem, jak skompilować przykład na moim własnym systemie. Zakładam, że brakuje mi niektórych nagłówków lub uwzględniam niewłaściwe, prawda? (przykład ich nie pokazuje.)
Paul Stelian
6

Zanim Microsoft wprowadził nową opcję „podsystemu Linux dla Windows”, CreateProcess()była to najbliższa rzecz, jaką musiał wykonać fork()system Windows, ale system Windows wymaga określenia pliku wykonywalnego do uruchomienia w tym procesie.

Tworzenie procesu w systemie UNIX przebiega zupełnie inaczej niż w systemie Windows. Jego fork()wywołanie zasadniczo powiela bieżący proces prawie w całości, każdy w swojej własnej przestrzeni adresowej i kontynuuje uruchamianie ich oddzielnie. Chociaż same procesy są różne, nadal korzystają z tego samego programu. Zobacz tutaj dobry przegląd fork/execmodelu.

Wracając w drugą stronę, odpowiednikiem Windowsa CreateProcess()jest fork()/exec() para funkcji w systemie UNIX.

Jeśli przenosisz oprogramowanie na Windows i nie masz nic przeciwko warstwie tłumaczeniowej, Cygwin zapewnił ci możliwość, ale była raczej niezdarna.

Oczywiście, z nowego podsystemu Linux , najbliższa rzecz Windows ma się fork()to rzeczywiście fork() :-)

paxdiablo
źródło
2
Czy więc mając WSL, mogę używać forkw przeciętnej aplikacji innej niż WSL?
Caesar
6

Poniższy dokument zawiera informacje na temat przenoszenia kodu z systemu UNIX na Win32: https://msdn.microsoft.com/en-us/library/y23kc048.aspx

Między innymi wskazuje, że model procesu jest zupełnie inny między dwoma systemami i zaleca rozważenie CreateProcess i CreateThread, gdzie wymagane jest zachowanie podobne do fork ().

Brandon E. Taylor
źródło
4

„gdy tylko zechcesz uzyskać dostęp do pliku lub printf, odmówi się dostępu we / wy”

  • Nie możesz mieć ciastka i też go zjeść ... w msvcrt.dll printf () jest oparte na API konsoli, które samo w sobie używa lpc do komunikacji z podsystemem konsoli (csrss.exe). Połączenie z csrss jest inicjowane przy starcie procesu, co oznacza, że ​​każdy proces rozpoczynający wykonywanie „w środku” będzie miał pominięty ten krok. Jeśli nie masz dostępu do kodu źródłowego systemu operacyjnego, nie ma sensu próbować łączyć się ręcznie z csrss. Zamiast tego powinieneś stworzyć swój własny podsystem i odpowiednio unikać funkcji konsoli w aplikacjach używających fork ().

  • po zaimplementowaniu własnego podsystemu nie zapomnij również zduplikować wszystkich uchwytów rodzica dla procesu potomnego ;-)

„Poza tym prawdopodobnie nie powinieneś używać funkcji Zw *, chyba że jesteś w trybie jądra, prawdopodobnie powinieneś zamiast tego używać funkcji Nt *”.

  • To jest niepoprawne. W przypadku dostępu w trybie użytkownika nie ma absolutnie żadnej różnicy między Zw *** Nt ***; są to tylko dwie różne wyeksportowane nazwy (ntdll.dll), które odnoszą się do tego samego (względnego) adresu wirtualnego.

ZwGetContextThread (NtCurrentThread (), & kontekst);

  • uzyskanie kontekstu bieżącego (działającego) wątku przez wywołanie ZwGetContextThread jest błędne, prawdopodobnie ulegnie awarii, a także (z powodu dodatkowego wywołania systemowego) nie jest najszybszym sposobem wykonania zadania.
user3502619
źródło
2
Wydaje się, że nie jest to odpowiedź na główne pytanie, ale odpowiedź na kilka innych różnych odpowiedzi i prawdopodobnie lepiej byłoby odpowiedzieć bezpośrednio na każdą z nich, aby uzyskać jasność i ułatwić śledzenie tego, co się dzieje.
Leigh,
wydajesz się zakładać, że printf zawsze zapisuje na konsoli.
Jasen
3

Semantyka fork () jest konieczna, gdy dziecko potrzebuje dostępu do aktualnego stanu pamięci rodzica w momencie wywołania funkcji fork (). Mam oprogramowanie, które opiera się na niejawnym muteksie kopiowania pamięci od momentu wywołania natychmiastowego fork (), co uniemożliwia użycie wątków. (Jest to emulowane na nowoczesnych platformach * nix za pomocą semantyki kopiowania przy zapisie / aktualizacji pamięci tabeli).

Najbliższy, który istnieje w systemie Windows jako wywołanie systemowe, to CreateProcess. Najlepsze, co można zrobić, to zamrozić wszystkie inne wątki w czasie kopiowania pamięci do przestrzeni pamięci nowego procesu, a następnie je rozmrozić. Ani klasa Cygwin frok [sic], ani kod Scilab, który opublikował Eric des Courtis, nie zamrażają wątków, które widzę.

Ponadto prawdopodobnie nie powinieneś używać funkcji Zw *, chyba że jesteś w trybie jądra, prawdopodobnie powinieneś zamiast tego używać funkcji Nt *. Istnieje dodatkowa gałąź, która sprawdza, czy jesteś w trybie jądra, a jeśli nie, wykonuje wszystkie sprawdzanie granic i weryfikację parametrów, które zawsze wykonuje Nt *. W związku z tym wywoływanie ich z trybu użytkownika jest nieco mniej wydajne.

sjcaged
źródło
Bardzo ciekawe informacje dotyczące eksportowanych symboli Zw *, dziękujemy.
Andon M. Coleman,
Zwróć uwagę, że funkcje Zw * z przestrzeni użytkownika nadal mapują na funkcje Nt * w przestrzeni jądra, dla bezpieczeństwa. A przynajmniej powinni.
Paul Stelian
2

Nie ma łatwego sposobu na emulację fork () w systemie Windows.

Proponuję zamiast tego używać wątków.

VVS
źródło
Cóż, uczciwie mówiąc , wdrożenie forkbyło dokładnie tym , co zrobił CygWin. Ale jeśli kiedykolwiek przeczytasz, jak oni to zrobili, twój „niełatwy sposób” to rażące nieporozumienie :-)
paxdiablo
2

Jak wspomniały inne odpowiedzi, NT (jądro będące podstawą nowoczesnych wersji Windows) ma odpowiednik Unix fork (). To nie jest problem.

Problem polega na tym, że klonowanie całego stanu procesu nie jest na ogół rozsądną rzeczą. Jest to tak samo prawdziwe w świecie uniksowym, jak w systemie Windows, ale w świecie uniksowym fork () jest używany przez cały czas i biblioteki są zaprojektowane tak, aby sobie z tym radzić. Biblioteki systemu Windows nie są.

Na przykład systemowe biblioteki DLL kernel32.dll i user32.dll utrzymują prywatne połączenie z procesem serwera Win32 csrss.exe. Po rozwidleniu istnieją dwa procesy po stronie klienta tego połączenia, co spowoduje problemy. Proces potomny powinien poinformować csrss.exe o swoim istnieniu i nawiązać nowe połączenie - ale nie ma takiego interfejsu, ponieważ te biblioteki nie zostały zaprojektowane z myślą o fork ().

Masz więc dwie możliwości. Jednym z nich jest zakaz korzystania z kernel32 i user32 oraz innych bibliotek, które nie są przeznaczone do rozwidlania - w tym bibliotek, które bezpośrednio lub pośrednio łączą się z kernel32 lub user32, czyli praktycznie wszystkimi z nich. Oznacza to, że nie możesz w ogóle wchodzić w interakcję z pulpitem Windows i utkniesz w swoim własnym, oddzielnym świecie Unixy. Jest to podejście przyjęte przez różne podsystemy Unix dla NT.

Inną opcją jest uciekanie się do jakiegoś okropnego hackowania, aby spróbować zmusić nieświadome biblioteki do pracy z fork (). To właśnie robi Cygwin. Tworzy nowy proces, pozwala mu się zainicjować (w tym rejestruje się w csrss.exe), a następnie kopiuje większość stanu dynamicznego ze starego procesu i ma nadzieję na najlepsze. Zadziwia mnie, że to zawsze działa. Z pewnością nie działa niezawodnie - nawet jeśli nie przypadkowo zawiedzie z powodu konfliktu przestrzeni adresowej, każda używana biblioteka może po cichu zostać zepsuta. Twierdzenie obecnie akceptowanej odpowiedzi, że Cygwin ma „w pełni funkcjonalny fork ()” jest ... wątpliwe.

Podsumowanie: W środowisku podobnym do Interix można forować, wywołując fork (). W przeciwnym razie spróbuj odzwyczaić się od chęci zrobienia tego. Nawet jeśli celujesz w Cygwin, nie używaj fork (), chyba że absolutnie musisz.

benrg
źródło