Qual è la cosa più vicina che Windows deve fare con il fork ()?


124

Immagino che la domanda dica tutto.

Voglio fare il fork su Windows. Qual è l'operazione più simile e come la uso.

Risposte:


86

Cygwin ha fork () completo su Windows. Pertanto, se l'utilizzo di Cygwin è accettabile per te, il problema è risolto nel caso in cui le prestazioni non siano un problema.

Altrimenti puoi dare un'occhiata a come Cygwin implementa fork (). Da un documento di architettura di Cygwin piuttosto vecchio :

5.6. Creazione del processo La chiamata fork in Cygwin è particolarmente interessante perché non si mappa bene sopra l'API Win32. Ciò lo rende molto difficile da implementare correttamente. Attualmente, il fork Cygwin è un'implementazione non-copy-on-write simile a ciò che era presente nelle prime versioni di UNIX.

La prima cosa che accade quando un processo genitore esegue il fork di un processo figlio è che il genitore inizializza uno spazio nella tabella dei processi Cygwin per il figlio. Quindi crea un processo figlio sospeso utilizzando la chiamata Win32 CreateProcess. Successivamente, il processo genitore chiama setjmp per salvare il proprio contesto e imposta un puntatore a questo in un'area di memoria condivisa Cygwin (condivisa tra tutte le attività Cygwin). Quindi riempie le sezioni .data e .bss del figlio copiando dal proprio spazio indirizzo nello spazio indirizzo del figlio sospeso. Dopo che lo spazio degli indirizzi del figlio è stato inizializzato, il figlio viene eseguito mentre il genitore attende un mutex. Il bambino scopre che è stato biforcato e salta in lungo usando il buffer di salto salvato. Il bambino quindi imposta il mutex su cui il genitore sta aspettando e blocca un altro mutex. Questo è il segnale per il genitore di copiare il suo stack e heap nel figlio, dopodiché rilascia il mutex su cui il figlio sta aspettando e ritorna dalla chiamata fork. Infine, il bambino si sveglia dal blocco sull'ultimo mutex, ricrea tutte le aree mappate in memoria che gli sono passate attraverso l'area condivisa e torna dal fork stesso.

Sebbene abbiamo alcune idee su come accelerare l'implementazione del fork riducendo il numero di cambi di contesto tra il processo padre e figlio, il fork quasi certamente sarà sempre inefficiente sotto Win32. Fortunatamente, nella maggior parte dei casi, la famiglia di chiamate spawn fornita da Cygwin può essere sostituita con una coppia fork / exec con un piccolo sforzo. Queste chiamate vengono mappate in modo pulito sopra l'API Win32. Di conseguenza, sono molto più efficienti. Cambiare il programma driver del compilatore per chiamare spawn invece di fork è stato un cambiamento banale e ha aumentato le velocità di compilazione dal 20 al 30 percento nei nostri test.

Tuttavia, spawn ed exec presentano la propria serie di difficoltà. Poiché non è possibile eseguire un vero e proprio exec sotto Win32, Cygwin deve inventare i propri Process ID (PID). Di conseguenza, quando un processo esegue più chiamate exec, ci saranno più PID Windows associati a un singolo PID Cygwin. In alcuni casi, gli stub di ciascuno di questi processi Win32 possono persistere, in attesa che il processo Cygwin eseguito venga chiuso.

Sembra un sacco di lavoro, non è vero? E sì, è slooooow.

EDIT: il documento è obsoleto, vedere questa eccellente risposta per un aggiornamento


11
Questa è una buona risposta se vuoi scrivere un'app Cygwin su Windows. Ma in generale non è la cosa migliore da fare. Fondamentalmente, i modelli di thread e processo * nix e Windows sono piuttosto diversi. CreateProcess () e CreateThread () sono le API generalmente equivalenti
Foredecker

2
Gli sviluppatori dovrebbero tenere presente che si tratta di un meccanismo non supportato e che IIRC è effettivamente incline a interrompersi ogni volta che qualche altro processo sul sistema utilizza l'iniezione di codice.
Harry Johnston

1
Il diverso collegamento di implementazione non è più valido.
PythonNut

Modificato per lasciare solo il link dell'altra risposta
Laurynas Biveinis

@Foredecker, in realtà non dovresti farlo anche se stai cercando di scrivere una "cygwin app". Cerca di imitare Unix, forkma lo fa con una soluzione che perde e devi essere preparato per situazioni impreviste.
Pacerier

66

Certamente non conosco i dettagli su questo perché non l'ho mai fatto, ma l'API NT nativa ha la capacità di eseguire il fork di un processo (il sottosistema POSIX su Windows ha bisogno di questa capacità - non sono sicuro che il sottosistema POSIX è persino più supportato).

Una ricerca di ZwCreateProcess () dovrebbe darti qualche dettaglio in più, ad esempio questo bit di informazioni da Maxim Shatskih :

Il parametro più importante qui è SectionHandle. Se questo parametro è NULL, il kernel eseguirà il fork del processo corrente. In caso contrario, questo parametro deve essere un handle dell'oggetto di sezione SEC_IMAGE creato nel file EXE prima di chiamare ZwCreateProcess ().

Tuttavia, nota che Corinna Vinschen indica che Cygwin ha scoperto che l'utilizzo di ZwCreateProcess () è ancora inaffidabile :

Iker Arizmendi ha scritto:

> 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

Questo documento è piuttosto vecchio, 10 anni circa. Mentre stiamo ancora usando le chiamate Win32 per emulare il fork, il metodo è cambiato notevolmente. In particolare, non creiamo più il processo figlio nello stato sospeso, a meno che specifici dati non necessitino di una gestione speciale nel genitore prima di essere copiati nel figlio. Nell'attuale versione 1.5.25 l'unico caso per un figlio sospeso sono i socket aperti nel genitore. La prossima versione 1.7.0 non verrà sospesa affatto.

Un motivo per non usare ZwCreateProcess era che fino alla versione 1.5.25 supportiamo ancora gli utenti di Windows 9x. Tuttavia, due tentativi di utilizzare ZwCreateProcess su sistemi basati su NT non sono riusciti per un motivo o per l'altro.

Sarebbe davvero bello se questa roba fosse migliore o del tutto documentata, specialmente un paio di strutture dati e come connettere un processo a un sottosistema. Sebbene il fork non sia un concetto Win32, non vedo che sarebbe una brutta cosa rendere il fork più facile da implementare.


Questa è la risposta sbagliata. CreateProcess () e CreateThread () sono gli equivalenti generali.
Foredecker

2
Interix è disponibile in Windows Vista Enterprise / Ultimate come "Sottosistema per applicazioni UNIX": en.wikipedia.org/wiki/Interix
bk1e

15
@Foredecker - questa potrebbe essere una risposta sbagliata, ma anche CreateProcess () / CreateThread () potrebbe essere sbagliato. Dipende se si sta cercando "il modo Win32 di fare le cose" o "il più vicino possibile alla semantica fork ()". CreateProcess () si comporta in modo significativamente diverso da fork (), motivo per cui cygwin aveva bisogno di fare molto lavoro per supportarlo.
Michael Burr

1
@jon: Ho provato a correggere i collegamenti e copiare il testo pertinente nella risposta (quindi i futuri collegamenti interrotti non sono un problema). Tuttavia, questa risposta è di molto tempo fa che non sono sicuro al 100% che la citazione che ho trovato oggi sia ciò a cui mi riferivo nel 2009.
Michael Burr,

4
Se la gente vuole " forkcon immediato exec", allora forse CreateProcess è un candidato. Ma forksenza execè spesso desiderabile e questo è ciò che le persone chiedono per un vero fork.
Aaron McDaid

37

Bene, Windows non ha davvero nulla di simile. Soprattutto perché fork può essere utilizzato per creare concettualmente un thread o un processo in * nix.

Quindi, dovrei dire:

CreateProcess()/CreateProcessEx()

e

CreateThread()(Ho sentito che per le applicazioni C, _beginthreadex()è meglio).


17

Le persone hanno provato a implementare il fork su Windows. Questa è la cosa più vicina che riesco a trovare:

Tratto da: 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;
}

4
Si noti che la maggior parte del controllo degli errori è mancante, ad esempio ZwCreateThread restituisce un valore NTSTATUS che può essere verificato utilizzando le macro SUCCEEDED e FAILED.
BCran

1
Cosa succede se il programma forkva in crash, il programma o il thread si blocca? Se si blocca il programma, non si tratta di un vero fork. Sono solo curioso, perché sto cercando una soluzione reale e spero che questa possa essere un'alternativa decente.
leetNightshade

1
Vorrei notare che c'è un bug nel codice fornito. haveLoadedFunctionsForFork è una funzione globale nell'intestazione, ma una funzione statica nel file c. Entrambi dovrebbero essere globali. E attualmente il fork si blocca, aggiungendo ora il controllo degli errori.
leetNightshade

Il sito è morto e non so come posso compilare l'esempio sul mio sistema. Presumo che mi manchino alcune intestazioni o includo quelle sbagliate, vero? (L'esempio non li mostra.)
Paul Stelian

6

Prima che Microsoft introducesse la loro nuova opzione "Sottosistema Linux per Windows", CreateProcess()era la cosa più vicina a Windowsfork() , ma Windows richiede di specificare un eseguibile da eseguire in quel processo.

La creazione del processo UNIX è molto diversa da Windows. La sua fork()chiamata fondamentalmente duplica il processo corrente quasi in totale, ciascuno nel proprio spazio di indirizzi, e continua a eseguirli separatamente. Sebbene i processi stessi siano diversi, eseguono ancora lo stesso programma. Vedi qui per una buona panoramica del fork/execmodello.

Tornando indietro, l'equivalente di Windows CreateProcess()è la fork()/exec() coppia di funzioni in UNIX.

Se stavi eseguendo il porting del software su Windows e non ti dispiace un livello di traduzione, Cygwin ha fornito la capacità che desideri ma era piuttosto complicata.

Naturalmente, con il nuovo sottosistema Linux , la cosa più vicina di Windows deve fork()è in realtà fork() :-)


2
Quindi, dato WSL, posso usarlo forkin un'applicazione media non WSL?
Cesare


4

"non appena vuoi accedere ai file o printf, io vengono rifiutati"

  • Non puoi avere la tua torta e mangiarla anche tu ... in msvcrt.dll, printf () si basa sull'API della console, che di per sé utilizza lpc per comunicare con il sottosistema della console (csrss.exe). La connessione con csrss viene avviata all'avvio del processo, il che significa che qualsiasi processo che inizia la sua esecuzione "nel mezzo" avrà quel passaggio saltato. A meno che tu non abbia accesso al codice sorgente del sistema operativo, non ha senso provare a connetterti manualmente a csrss. Invece, dovresti creare il tuo sottosistema e di conseguenza evitare le funzioni della console nelle applicazioni che usano fork ().

  • una volta che hai implementato il tuo sottosistema, non dimenticare di duplicare anche tutti gli handle del genitore per il processo figlio ;-)

"Inoltre, probabilmente non dovresti usare le funzioni Zw * a meno che tu non sia in modalità kernel, dovresti probabilmente usare invece le funzioni Nt *."

  • Questo non è corretto. Quando si accede in modalità utente, non c'è assolutamente alcuna differenza tra Zw *** Nt ***; questi sono semplicemente due diversi nomi esportati (ntdll.dll) che fanno riferimento allo stesso indirizzo virtuale (relativo).

ZwGetContextThread (NtCurrentThread (), & context);

  • ottenere il contesto del thread corrente (in esecuzione) chiamando ZwGetContextThread è sbagliato, è probabile che si blocchi e (a causa della chiamata di sistema extra) non è nemmeno il modo più veloce per eseguire l'attività.

2
Questo non sembra rispondere alla domanda principale, ma rispondere a poche altre risposte diverse, e probabilmente sarebbe meglio rispondere direttamente a ciascuna per chiarezza e per rendere più facile seguire cosa sta succedendo.
Leigh

sembra che tu stia supponendo che printf scriva sempre sulla console.
Jasen

3

La semantica fork () è necessaria laddove il bambino ha bisogno di accedere allo stato di memoria effettivo del genitore nel momento in cui viene chiamato fork (). Ho un software che si basa sul mutex implicito della copia della memoria dal momento in cui viene chiamato fork () istantaneo, il che rende i thread impossibili da usare. (Questo è emulato sulle moderne piattaforme * nix tramite la semantica copy-on-write / update-memory-table.)

Il più vicino che esiste su Windows come syscall è CreateProcess. La cosa migliore che si può fare è che il genitore congeli tutti gli altri thread durante il tempo in cui sta copiando la memoria nello spazio di memoria del nuovo processo, quindi li scongeli. Né la classe Cygwin frok [sic] né il codice Scilab che Eric des Courtis ha pubblicato fa il congelamento dei thread, che posso vedere.

Inoltre, probabilmente non dovresti usare le funzioni Zw * a meno che tu non sia in modalità kernel, dovresti probabilmente usare invece le funzioni Nt *. C'è un ramo extra che controlla se sei in modalità kernel e, in caso contrario, esegue tutti i controlli dei limiti e la verifica dei parametri che Nt * fa sempre. Pertanto, è leggermente meno efficiente chiamarli dalla modalità utente.


Informazioni molto interessanti sui simboli esportati Zw *, grazie.
Andon M. Coleman

Si noti che le funzioni Zw * dallo spazio utente sono ancora mappate alle funzioni Nt * nello spazio del kernel, per sicurezza. O almeno dovrebbero.
Paul Stelian


2

Non esiste un modo semplice per emulare fork () su Windows.

Ti suggerisco di usare invece i thread.


Bene, in tutta onestà, l'implementazione è forkstata esattamente ciò che ha fatto CygWin. Ma, se hai mai letto su come l' hanno fatto, il tuo "no easy way" è un grave fraintendimento :-)
paxdiablo


2

Come altre risposte hanno menzionato, NT (il kernel alla base delle versioni moderne di Windows) ha un equivalente di Unix fork (). Non è questo il problema.

Il problema è che la clonazione dell'intero stato di un processo non è generalmente una cosa sensata da fare. Questo è vero nel mondo Unix come lo è in Windows, ma nel mondo Unix, fork () viene utilizzato tutto il tempo e le librerie sono progettate per gestirlo. Le librerie di Windows non lo sono.

Ad esempio, le DLL di sistema kernel32.dll e user32.dll mantengono una connessione privata al processo server Win32 csrss.exe. Dopo un fork, ci sono due processi all'estremità client di quella connessione, che causeranno problemi. Il processo figlio dovrebbe informare csrss.exe della sua esistenza e stabilire una nuova connessione, ma non esiste un'interfaccia per farlo, perché queste librerie non sono state progettate con fork () in mente.

Quindi hai due scelte. Uno è quello di vietare l'uso di kernel32 e user32 e di altre librerie che non sono progettate per essere forkate, comprese tutte le librerie che si collegano direttamente o indirettamente a kernel32 o user32, che sono praticamente tutte. Ciò significa che non puoi interagire affatto con il desktop di Windows e sei bloccato nel tuo mondo Unixy separato. Questo è l'approccio adottato dai vari sottosistemi Unix per NT.

L'altra opzione è ricorrere a una sorta di orribile hack per cercare di far funzionare librerie inconsapevoli con fork (). Questo è ciò che fa Cygwin. Crea un nuovo processo, lo lascia inizializzare (inclusa la registrazione con csrss.exe), quindi copia la maggior parte dello stato dinamico dal vecchio processo e spera per il meglio. Mi stupisce che funzioni mai . Certamente non funziona in modo affidabile, anche se non si verifica un errore casuale a causa di un conflitto di spazio degli indirizzi, qualsiasi libreria che stai utilizzando potrebbe rimanere silenziosamente in uno stato rotto. L'affermazione dell'attuale risposta accettata che Cygwin ha un "fork () completo di tutte le funzionalità" è ... dubbia.

Riepilogo: in un ambiente simile a Interix, è possibile eseguire il fork chiamando fork (). Altrimenti, per favore cerca di liberarti dal desiderio di farlo. Anche se stai prendendo di mira Cygwin, non usare fork () a meno che non sia assolutamente necessario.


Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.