Quelle est la chose la plus proche de Windows à fork ()?


124

Je suppose que la question dit tout.

Je veux bifurquer sur Windows. Quelle est l'opération la plus similaire et comment l'utiliser.

Réponses:


86

Cygwin a un fork () complet sur Windows. Ainsi, si l'utilisation de Cygwin est acceptable pour vous, le problème est résolu dans le cas où les performances ne sont pas un problème.

Sinon, vous pouvez voir comment Cygwin implémente fork (). D'après un assez vieux doc d' architecture de Cygwin :

5.6. Création de processus L'appel de fourche dans Cygwin est particulièrement intéressant car il ne correspond pas bien à l'API Win32. Cela rend la mise en œuvre correcte très difficile. Actuellement, le fork Cygwin est une implémentation sans copie sur écriture similaire à ce qui était présent dans les premières versions d'UNIX.

La première chose qui se produit lorsqu'un processus parent forque un processus enfant est que le parent initialise un espace dans la table de processus Cygwin pour l'enfant. Il crée ensuite un processus enfant suspendu à l'aide de l'appel Win32 CreateProcess. Ensuite, le processus parent appelle setjmp pour enregistrer son propre contexte et place un pointeur sur celui-ci dans une zone de mémoire partagée Cygwin (partagée entre toutes les tâches Cygwin). Il remplit ensuite les sections .data et .bss de l'enfant en copiant depuis son propre espace d'adressage dans l'espace d'adressage de l'enfant suspendu. Une fois l'espace d'adressage de l'enfant initialisé, l'enfant est exécuté pendant que le parent attend un mutex. L'enfant découvre qu'il a été forké et fait des sauts longs en utilisant le tampon de saut enregistré. L'enfant définit ensuite le mutex que le parent attend et bloque sur un autre mutex. C'est le signal pour le parent de copier sa pile et son tas dans l'enfant, après quoi il libère le mutex que l'enfant attend et revient de l'appel de fork. Enfin, l'enfant se réveille du blocage sur le dernier mutex, recrée toutes les zones mappées en mémoire qui lui sont transmises via la zone partagée, et revient de fork lui-même.

Bien que nous ayons quelques idées sur la façon d'accélérer notre implémentation de fork en réduisant le nombre de changements de contexte entre le processus parent et enfant, fork sera presque certainement toujours inefficace sous Win32. Heureusement, dans la plupart des cas, la famille d'appels spawn fournie par Cygwin peut être remplacée par une paire fork / exec avec seulement un petit effort. Ces appels correspondent parfaitement à l'API Win32. En conséquence, ils sont beaucoup plus efficaces. Changer le programme pilote du compilateur pour appeler spawn au lieu de fork a été un changement trivial et a augmenté la vitesse de compilation de vingt à trente pour cent dans nos tests.

Cependant, spawn et exec présentent leurs propres difficultés. Puisqu'il n'y a aucun moyen de faire une exécution réelle sous Win32, Cygwin doit inventer ses propres ID de processus (PID). Par conséquent, lorsqu'un processus exécute plusieurs appels d'exécution, plusieurs PID Windows seront associés à un seul PID Cygwin. Dans certains cas, les stubs de chacun de ces processus Win32 peuvent persister, attendant que leur processus Cygwin exécuté se termine.

Cela ressemble à beaucoup de travail, n'est-ce pas? Et oui, c'est slooooow.

EDIT: la doc est obsolète, veuillez voir cette excellente réponse pour une mise à jour


11
C'est une bonne réponse si vous souhaitez écrire une application Cygwin sous Windows. Mais en général, ce n'est pas la meilleure chose à faire. Fondamentalement, les modèles de processus et de threads * nix et Windows sont assez différents. CreateProcess () et CreateThread () sont les API généralement équivalentes
Foredecker

2
Les développeurs doivent garder à l'esprit qu'il s'agit d'un mécanisme non pris en charge et que l'IIRC est en effet enclin à s'interrompre chaque fois qu'un autre processus du système utilise l'injection de code.
Harry Johnston

1
Le lien d'implémentation différent n'est plus valide.
PythonNut

Modifié pour ne laisser que l'autre lien de réponse
Laurynas Biveinis

@Foredecker, En fait, vous ne devriez pas le faire même si vous essayez d'écrire une "application cygwin". Il essaie d'imiter Unix forkmais il accomplit cela avec une solution qui fuit et vous devez être préparé à des situations inattendues.
Pacerier

66

Je ne connais certainement pas les détails à ce sujet car je ne l'ai jamais fait, mais l'API NT native a la capacité de créer un processus (le sous-système POSIX sur Windows a besoin de cette capacité - je ne suis pas sûr que le sous-système POSIX est même plus pris en charge).

Une recherche sur ZwCreateProcess () devrait vous donner plus de détails - par exemple cette information de Maxim Shatskih :

Le paramètre le plus important ici est SectionHandle. Si ce paramètre est NULL, le noyau bifurquera le processus en cours. Sinon, ce paramètre doit être un handle de l'objet de section SEC_IMAGE créé sur le fichier EXE avant d'appeler ZwCreateProcess ().

Cependant, notez que Corinna Vinschen indique que Cygwin trouvé en utilisant ZwCreateProcess () n'est toujours pas fiable :

Iker Arizmendi a écrit:

> 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

Ce document est assez vieux, 10 ans environ. Bien que nous utilisions toujours des appels Win32 pour émuler fork, la méthode a sensiblement changé. Surtout, nous ne créons plus le processus enfant dans l'état suspendu, à moins que des infrastructures de données spécifiques nécessitent une gestion spéciale dans le parent avant d'être copiées sur l'enfant. Dans la version 1.5.25 actuelle, le seul cas pour un enfant suspendu est l'ouverture de sockets dans le parent. La prochaine version 1.7.0 ne sera pas suspendue du tout.

Une des raisons de ne pas utiliser ZwCreateProcess était que jusqu'à la version 1.5.25, nous prenons toujours en charge les utilisateurs de Windows 9x. Cependant, deux tentatives d'utilisation de ZwCreateProcess sur des systèmes NT ont échoué pour une raison ou une autre.

Ce serait vraiment bien que ce truc soit meilleur ou pas du tout documenté, en particulier quelques structures de données et comment connecter un processus à un sous-système. Bien que fork ne soit pas un concept Win32, je ne vois pas que ce serait une mauvaise chose de rendre fork plus facile à implémenter.


C'est la mauvaise réponse. CreateProcess () et CreateThread () sont les équivalents généraux.
Foredecker

2
Interix est disponible dans Windows Vista Enterprise / Ultimate en tant que «sous-système pour les applications UNIX»: en.wikipedia.org/wiki/Interix
bk1e

15
@Foredecker - cela peut être une mauvaise réponse, mais CreateProcess () / CreateThread () pourrait aussi être faux. Cela dépend si l'on recherche «la manière Win32 de faire les choses» ou «aussi proche que possible de la sémantique de fork ()». CreateProcess () se comporte considérablement différemment de fork (), raison pour laquelle cygwin a dû faire beaucoup de travail pour le supporter.
Michael Burr

1
@jon: J'ai essayé de réparer les liens et de copier le texte pertinent dans la réponse (donc les futurs liens rompus ne sont pas un problème). Cependant, cette réponse date d'assez longtemps que je ne suis pas sûr à 100% que la citation que j'ai trouvée aujourd'hui soit celle à laquelle je faisais référence en 2009.
Michael Burr

4
Si les gens veulent " forkavec immédiat exec", alors peut-être que CreateProcess est un candidat. Mais forksans execc'est souvent souhaitable et c'est ce que les conducteurs demandent un vrai fork.
Aaron McDaid

37

Eh bien, les fenêtres n'ont vraiment rien de tel. D'autant plus que fork peut être utilisé pour créer conceptuellement un thread ou un processus dans * nix.

Alors, je dois dire:

CreateProcess()/CreateProcessEx()

et

CreateThread()(J'ai entendu dire que pour les applications C, _beginthreadex()c'est mieux).


17

Les gens ont essayé d'implémenter fork sur Windows. C'est la chose la plus proche que je puisse trouver:

Tiré de: 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
Notez que la plupart des vérifications d'erreurs sont manquantes - par exemple, ZwCreateThread renvoie une valeur NTSTATUS qui peut être vérifiée à l'aide des macros SUCCEEDED et FAILED.
BCran

1
Que se passe-t-il si le système forkplante, plante le programme ou le thread plante-t-il simplement? Si cela plante le programme, ce n'est pas vraiment une fourche. Juste curieux, car je cherche une vraie solution, et j'espère que cela pourrait être une alternative décente.
leetNightshade

1
Je voudrais noter qu'il y a un bogue dans le code fourni. haveLoadedFunctionsForFork est une fonction globale dans l'en-tête, mais une fonction statique dans le fichier c. Ils devraient tous deux être mondiaux. Et actuellement la fourche se bloque, ajoutant la vérification des erreurs maintenant.
leetNightshade

Le site est mort et je ne sais pas comment je peux compiler l'exemple sur mon propre système. Je suppose que je manque certains en-têtes ou que j'inclus les mauvais. (l'exemple ne les montre pas.)
Paul Stelian

6

Avant que Microsoft n'introduise sa nouvelle option «sous-système Linux pour Windows», CreateProcess()c'était la chose la plus proche que Windows doit faire fork(), mais Windows vous oblige à spécifier un exécutable à exécuter dans ce processus.

La création de processus UNIX est assez différente de Windows. Son fork()appel duplique essentiellement le processus actuel presque au total, chacun dans son propre espace d'adressage, et continue de les exécuter séparément. Bien que les processus eux-mêmes soient différents, ils exécutent toujours le même programme. Voir ici pour un bon aperçu du fork/execmodèle.

Pour revenir en arrière, l'équivalent de Windows CreateProcess()est la fork()/exec() paire de fonctions sous UNIX.

Si vous portiez un logiciel sur Windows et que la couche de traduction ne vous dérangeait pas, Cygwin fournissait la capacité que vous souhaitiez, mais c'était plutôt dérangeant.

Bien sûr, avec le nouveau sous - système Linux , la chose la plus proche de Windows doit fork()est en fait fork() :-)


2
Donc, étant donné WSL, puis-je utiliser forkdans une application moyenne non WSL?
Caesar

6

Le document suivant fournit des informations sur le portage du code d'UNIX vers Win32: https://msdn.microsoft.com/en-us/library/y23kc048.aspx

Entre autres choses, il indique que le modèle de processus est assez différent entre les deux systèmes et recommande de considérer CreateProcess et CreateThread là où un comportement semblable à fork () est requis.


4

"dès que vous souhaitez accéder aux fichiers ou imprimer, les io sont refusés"

  • Vous ne pouvez pas avoir votre gâteau et le manger aussi ... dans msvcrt.dll, printf () est basé sur l'API de la console, qui en elle-même utilise lpc pour communiquer avec le sous-système de la console (csrss.exe). La connexion avec csrss est initiée au démarrage du processus, ce qui signifie que tout processus qui commence son exécution "au milieu" verra cette étape ignorée. Sauf si vous avez accès au code source du système d'exploitation, il est inutile d'essayer de vous connecter manuellement à csrss. Au lieu de cela, vous devez créer votre propre sous-système, et par conséquent éviter les fonctions de console dans les applications qui utilisent fork ().

  • une fois que vous avez implémenté votre propre sous-système, n'oubliez pas de dupliquer également tous les descripteurs du parent pour le processus enfant ;-)

"De plus, vous ne devriez probablement pas utiliser les fonctions Zw * sauf si vous êtes en mode noyau, vous devriez probablement utiliser les fonctions Nt * à la place."

  • Ceci est une erreur. Lorsqu'on accède en mode utilisateur, il n'y a absolument aucune différence entre Zw *** Nt ***; ce sont simplement deux noms exportés différents (ntdll.dll) qui font référence à la même adresse virtuelle (relative).

ZwGetContextThread (NtCurrentThread (), & context);

  • obtenir le contexte du thread actuel (en cours d'exécution) en appelant ZwGetContextThread est incorrect, risque de planter et (en raison de l'appel système supplémentaire) n'est pas non plus le moyen le plus rapide d'accomplir la tâche.

2
Cela ne semble pas répondre à la question principale mais à quelques autres réponses différentes, et il serait probablement préférable de répondre directement à chacune pour plus de clarté et pour faciliter le suivi de ce qui se passe.
Leigh

vous semblez supposer que printf écrit toujours sur la console.
Jasen

3

La sémantique de fork () est nécessaire là où l'enfant a besoin d'accéder à l'état réel de la mémoire du parent dès que l'instant fork () est appelé. J'ai un logiciel qui repose sur le mutex implicite de la copie de mémoire à partir du moment où l'instant fork () est appelé, ce qui rend les threads impossibles à utiliser. (Ceci est émulé sur les plates-formes * nix modernes via la sémantique copie sur écriture / mise à jour-mémoire-table.)

Le plus proche qui existe sous Windows en tant qu'appel système est CreateProcess. Le mieux que l'on puisse faire est que le parent fige tous les autres threads pendant le temps qu'il copie de la mémoire vers l'espace mémoire du nouveau processus, puis les dégèle. Ni la classe Cygwin frok [sic] ni le code Scilab qu'Eric des Courtis a posté n'effectuent le blocage des threads, ce que je peux voir.

De plus, vous ne devriez probablement pas utiliser les fonctions Zw * sauf si vous êtes en mode noyau, vous devriez probablement utiliser les fonctions Nt * à la place. Il y a une branche supplémentaire qui vérifie si vous êtes en mode noyau et, sinon, effectue toutes les vérifications de limites et de paramètres que Nt * fait toujours. Ainsi, il est très légèrement moins efficace de les appeler depuis le mode utilisateur.


Informations très intéressantes concernant les symboles exportés Zw *, merci.
Andon M. Coleman

Notez que les fonctions Zw * de l'espace utilisateur sont toujours mappées aux fonctions Nt * dans l'espace noyau, par sécurité. Ou du moins, ils devraient.
Paul Stelian


2

Il n'y a pas de moyen simple d'émuler fork () sous Windows.

Je vous suggère d'utiliser des threads à la place.


Eh bien, en toute honnêteté, la mise en œuvre forkétait exactement ce que CygWin a fait. Mais, si jamais vous lisez comment ils l'ont fait, votre "pas facile" est un gros malentendu :-)
paxdiablo


2

Comme d'autres réponses l'ont mentionné, NT (le noyau sous-jacent aux versions modernes de Windows) a un équivalent de Unix fork (). Ce n'est pas le problème.

Le problème est que le clonage de l'état entier d'un processus n'est généralement pas une chose saine à faire. C'est aussi vrai dans le monde Unix que dans Windows, mais dans le monde Unix, fork () est utilisé tout le temps et les bibliothèques sont conçues pour y faire face. Les bibliothèques Windows ne le sont pas.

Par exemple, les DLL système kernel32.dll et user32.dll maintiennent une connexion privée au processus serveur Win32 csrss.exe. Après un fork, il y a deux processus du côté client de cette connexion, ce qui va poser des problèmes. Le processus enfant doit informer csrss.exe de son existence et établir une nouvelle connexion - mais il n'y a pas d'interface pour le faire, car ces bibliothèques n'ont pas été conçues avec fork () à l'esprit.

Vous avez donc deux choix. La première consiste à interdire l'utilisation de kernel32 et user32 et d'autres bibliothèques qui ne sont pas conçues pour être fourchues - y compris toutes les bibliothèques qui lient directement ou indirectement à kernel32 ou user32, qui sont pratiquement toutes. Cela signifie que vous ne pouvez pas du tout interagir avec le bureau Windows et que vous êtes coincé dans votre propre monde Unixy séparé. C'est l'approche adoptée par les différents sous-systèmes Unix pour NT.

L'autre option est de recourir à une sorte de piratage horrible pour essayer de faire fonctionner des bibliothèques inconscientes avec fork (). C'est ce que fait Cygwin. Il crée un nouveau processus, le laisse s'initialiser (y compris s'enregistrer avec csrss.exe), puis copie la majeure partie de l'état dynamique de l'ancien processus et espère le meilleur. Cela m'étonne que cela fonctionne toujours . Cela ne fonctionne certainement pas de manière fiable - même s'il n'échoue pas de manière aléatoire en raison d'un conflit d'espace d'adressage, toute bibliothèque que vous utilisez peut être laissée silencieusement dans un état cassé. L'affirmation de la réponse actuellement acceptée selon laquelle Cygwin a un "fork complet ()" est ... douteuse.

Résumé: Dans un environnement de type Interix, vous pouvez fork en appelant fork (). Sinon, essayez de vous sevrer du désir de le faire. Même si vous ciblez Cygwin, n'utilisez pas fork () sauf si vous le devez absolument.


En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.