Что в Windows ближе всего к fork ()?


124

Думаю, в этом вопросе все сказано.

Хочу форк на Windows. Какая операция наиболее похожа и как ее использовать.

Ответы:


86

Cygwin имеет полнофункциональную fork () в Windows. Таким образом, если использование Cygwin для вас приемлемо, тогда проблема решается, если производительность не является проблемой.

В противном случае вы можете посмотреть, как Cygwin реализует fork (). Из довольно старого архитектурного документа Cygwin :

5.6. Создание процесса. Вызов fork в Cygwin особенно интересен, потому что он плохо отображается поверх Win32 API. Это очень затрудняет правильную реализацию. В настоящее время вилка Cygwin - это реализация без копирования при записи, аналогичная той, что присутствовала в ранних версиях UNIX.

Первое, что происходит, когда родительский процесс разветвляет дочерний процесс, - это то, что родитель инициализирует пространство в таблице процессов Cygwin для дочернего процесса. Затем он создает приостановленный дочерний процесс с помощью вызова Win32 CreateProcess. Затем родительский процесс вызывает setjmp для сохранения своего собственного контекста и устанавливает указатель на него в общей области памяти Cygwin (совместно используемой всеми задачами Cygwin). Затем он заполняет дочерние разделы .data и .bss, копируя из своего собственного адресного пространства в приостановленное дочернее адресное пространство. После инициализации адресного пространства дочернего элемента дочерний элемент запускается, пока родитель ждет мьютекса. Ребенок обнаруживает, что он был разветвлен, и прыгает в длину, используя сохраненный буфер перехода. Затем дочерний элемент устанавливает мьютекс, которого ожидает родитель, и блокирует другой мьютекс. Это сигнал для родителя скопировать свой стек и кучу в дочерний, после чего он освобождает мьютекс, которого ожидает дочерний элемент, и возвращается из вызова fork. Наконец, дочерний элемент просыпается от блокировки на последнем мьютексе, воссоздает все отображенные в память области, переданные ему через общую область, и возвращается из самого fork.

Хотя у нас есть некоторые идеи относительно того, как ускорить реализацию нашей вилки за счет уменьшения количества переключений контекста между родительским и дочерним процессом, вилка почти всегда будет неэффективной в Win32. К счастью, в большинстве случаев семейство вызовов spawn, предоставляемое Cygwin, может быть заменено парой fork / exec с небольшим усилием. Эти вызовы четко отображаются поверх Win32 API. В результате они намного эффективнее. Изменение программы драйвера компилятора для вызова spawn вместо fork было тривиальным изменением и увеличило скорость компиляции на двадцать-тридцать процентов в наших тестах.

Однако spawn и exec представляют свой набор трудностей. Поскольку в Win32 невозможно выполнить реальный запуск, Cygwin должен изобретать свои собственные идентификаторы процессов (PID). В результате, когда процесс выполняет несколько вызовов exec, будет несколько идентификаторов Windows PID, связанных с одним Cygwin PID. В некоторых случаях заглушки каждого из этих процессов Win32 могут задерживаться, ожидая завершения их процесса exec'd Cygwin.

Похоже, много работы, не так ли? И да, это медленно.

РЕДАКТИРОВАТЬ: документ устарел, см. Этот отличный ответ для обновления


11
Это хороший ответ, если вы хотите написать приложение Cygwin для Windows. Но в целом это не самое лучшее. По сути, модели процессов и потоков * nix и Windows сильно различаются. CreateProcess () и CreateThread () - в целом эквивалентные API-интерфейсы
Foredecker

2
Разработчикам следует иметь в виду, что это неподдерживаемый механизм, и IIRC действительно склонен ломаться, когда какой-либо другой процесс в системе использует внедрение кода.
Гарри Джонстон

1
Ссылка на другую реализацию больше не действует.
PythonNut

Отредактировано, чтобы оставить только другую ссылку для ответа
Лауринас Бивейнис

@Foredecker, на самом деле вам не стоит этого делать, даже если вы пытаетесь написать "приложение cygwin". Он пытается имитировать Unix, forkно достигает этого с помощью ненадежного решения, и вы должны быть готовы к неожиданным ситуациям.
Pacerier

66

Я, конечно, не знаю подробностей об этом, потому что я никогда этого не делал, но собственный NT API имеет возможность разветвлять процесс (подсистема POSIX в Windows нуждается в этой возможности - я не уверен, что подсистема POSIX даже не поддерживается).

Поиск по ZwCreateProcess () должен дать вам более подробную информацию - например, эту информацию от Максима Шацких :

Самый важный параметр здесь - SectionHandle. Если этот параметр равен NULL, ядро ​​будет форкнуть текущий процесс. В противном случае этот параметр должен быть дескриптором объекта раздела SEC_IMAGE, созданного в EXE-файле перед вызовом ZwCreateProcess ().

Однако обратите внимание, что Коринна Виншен указывает, что Cygwin, обнаруженный с помощью ZwCreateProcess (), все еще ненадежен :

Икер Арисменди писал:

> 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

Этот документ довольно старый, лет 10 или около того. Хотя мы все еще используем вызовы Win32 для имитации вилки, метод заметно изменился. В частности, мы больше не создаем дочерний процесс в приостановленном состоянии, если только определенные данные не нуждаются в особой обработке в родительском процессе, прежде чем они будут скопированы в дочерний. В текущем выпуске 1.5.25 единственным случаем для приостановленного дочернего элемента являются открытые сокеты в родительском. Предстоящий выпуск 1.7.0 вообще не будет приостановлен.

Одна из причин не использовать ZwCreateProcess заключалась в том, что до выпуска 1.5.25 мы все еще поддерживали пользователей Windows 9x. Однако две попытки использовать ZwCreateProcess в системах на базе NT по той или иной причине не удались.

Было бы действительно хорошо, если бы этот материал был лучше или вообще задокументирован, особенно пара структур данных и способы подключения процесса к подсистеме. Хотя вилка не является концепцией Win32, я не думаю, что было бы плохо упростить реализацию вилки.


Это неправильный ответ. CreateProcess () и CreateThread () являются общими эквивалентами.
Foredecker

2
Interix доступен в Windows Vista Enterprise / Ultimate как «Подсистема для приложений UNIX»: en.wikipedia.org/wiki/Interix
bk1e

15
@Foredecker - это может быть неправильный ответ, но CreateProcess () / CreateThread () тоже может быть неправильным. Это зависит от того, ищете ли вы «способ работы с Win32» или «максимально приближенный к семантике fork ()». CreateProcess () ведет себя значительно иначе, чем fork (), поэтому cygwin пришлось проделать большую работу для его поддержки.
Майкл Берр,

1
@jon: Я попытался исправить ссылки и скопировать соответствующий текст в ответ (чтобы в будущем неработающие ссылки не были проблемой). Однако этот ответ пришел достаточно давно, и я не уверен на 100%, что цитата, которую я нашел сегодня, - это то, о чем я имел в виду в 2009 году.
Майкл Берр,

4
Если люди хотят « forkс немедленным exec», то, возможно, подойдет CreateProcess. Но forkбез этого execчасто желательно, и именно это заставляет людей просить настоящих fork.
Аарон МакДэйд

37

Что ж, в windows действительно нет ничего похожего. Тем более, что fork можно использовать для концептуального создания потока или процесса в * nix.

Итак, я должен сказать:

CreateProcess()/CreateProcessEx()

и

CreateThread()(Я слышал, что для приложений C _beginthreadex()лучше).


17

Люди пытались реализовать форк в Windows. Это самое близкое к нему, что я могу найти:

Взято с: 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
Обратите внимание, что большая часть проверки ошибок отсутствует - например, ZwCreateThread возвращает значение NTSTATUS, которое можно проверить с помощью макросов SUCCEEDED и FAILED.
BCran

1
Что произойдет, если произойдет forkсбой, произойдет сбой программы или просто произойдет сбой потока? Если это приводит к сбою программы, то на самом деле это не разветвление. Просто любопытно, потому что я ищу реальное решение и надеюсь, что это может быть достойной альтернативой.
leetNightshade

1
Хочу отметить ошибку в предоставленном коде. haveLoadedFunctionsForFork - это глобальная функция в заголовке, но статическая функция в файле c. Оба они должны быть глобальными. И в настоящее время форк вылетает, теперь добавлена ​​проверка ошибок.
leetNightshade

Сайт мертв, и я не знаю, как скомпилировать пример в моей собственной системе. Я предполагаю, что мне не хватает некоторых заголовков или я включаю неправильные? (пример не показывает их.)
Пол Стелиан

6

До того, как Microsoft представила свою новую опцию «Подсистема Linux для Windows», это CreateProcess()было ближе всего к Windows.fork() , но Windows требует, чтобы вы указали исполняемый файл для запуска в этом процессе.

Процесс создания UNIX сильно отличается от Windows. Его fork()вызов практически полностью дублирует текущий процесс, каждый в своем адресном пространстве, и продолжает их запускать отдельно. Хотя сами процессы разные, они по-прежнему выполняют одну и ту же программу. См. Здесь хороший обзор fork/execмодели.

Возвращаясь назад, эквивалент Windows CreateProcess()- это fork()/exec() пара функций в UNIX.

Если вы переносили программное обеспечение на Windows и не возражаете против уровня перевода, Cygwin предоставил вам нужные возможности, но это было довольно беспорядочно.

Конечно, с новой подсистемой Linux самое близкое к Windows fork()- это fork() :-)


2
Итак, учитывая WSL, могу ли я использовать его forkв обычном приложении, отличном от WSL?
Цезарь

6

В следующем документе представлена ​​некоторая информация о переносе кода с UNIX на Win32: https://msdn.microsoft.com/en-us/library/y23kc048.aspx

Среди прочего, он указывает на то, что модель процесса в двух системах сильно различается, и рекомендует рассмотреть CreateProcess и CreateThread, где требуется поведение, подобное fork ().


4

"как только вы захотите получить доступ к файлу или printf, тогда io будет отказано"

  • Вы не можете съесть свой торт и съесть его ... в msvcrt.dll printf () основан на API консоли, который сам по себе использует lpc для связи с подсистемой консоли (csrss.exe). Соединение с csrss инициируется при запуске процесса, что означает, что любой процесс, который начинает свое выполнение «посередине», будет пропускать этот шаг. Если у вас нет доступа к исходному коду операционной системы, нет смысла пытаться подключиться к csrss вручную. Вместо этого вы должны создать свою собственную подсистему и, соответственно, избегать функций консоли в приложениях, использующих fork ().

  • как только вы реализовали свою собственную подсистему, не забудьте также продублировать все родительские дескрипторы для дочернего процесса ;-)

«Кроме того, вам, вероятно, не следует использовать функции Zw *, если вы не находитесь в режиме ядра, вместо этого вам, вероятно, следует использовать функции Nt *».

  • Это неверно. При доступе в пользовательском режиме нет абсолютно никакой разницы между Zw *** Nt ***; это просто два разных (ntdll.dll) экспортированных имени, которые относятся к одному и тому же (относительному) виртуальному адресу.

ZwGetContextThread (NtCurrentThread (), & контекст);

  • получение контекста текущего (запущенного) потока путем вызова ZwGetContextThread неверно, может привести к сбою и (из-за дополнительного системного вызова) также не самый быстрый способ выполнения задачи.

2
Кажется, это не ответ на главный вопрос, а ответ на несколько других разных ответов, и, вероятно, было бы лучше ответить напрямую на каждый для ясности и облегчения отслеживания того, что происходит.
Ли

похоже, вы предполагаете, что printf всегда пишет в консоль.
Jasen

3

Семантика fork () необходима там, где дочернему элементу требуется доступ к фактическому состоянию памяти родительского объекта на момент вызова fork (). У меня есть программа, которая полагается на неявный мьютекс копирования памяти при вызове функции fork (), что делает невозможным использование потоков. (Это эмулируется на современных платформах * nix с помощью семантики таблицы копирования при записи / обновления в памяти.)

Ближайшим из существующих в Windows системных вызовов является CreateProcess. Лучшее, что можно сделать, - это чтобы родительский объект заморозил все другие потоки на время, когда он копирует память в пространство памяти нового процесса, а затем разморозил их. Я вижу, что ни класс Cygwin frok, ни код Scilab, опубликованный Эриком де Курти, не останавливают потоки.

Кроме того, вам, вероятно, не следует использовать функции Zw *, если вы не находитесь в режиме ядра, вместо этого вам, вероятно, следует использовать функции Nt *. Есть дополнительная ветка, которая проверяет, находитесь ли вы в режиме ядра, и, если нет, выполняет все проверки границ и параметров, которые всегда выполняет Nt *. Таким образом, вызывать их из пользовательского режима немного менее эффективно.


Очень интересная информация об экспортируемых символах Zw *, спасибо.
Андон М. Коулман

Обратите внимание, что для безопасности функции Zw * из пользовательского пространства по-прежнему отображаются на функции Nt * в пространстве ядра. По крайней мере, должны.
Пол Стелиан


2

Нет простого способа эмулировать fork () в Windows.

Я предлагаю вам вместо этого использовать потоки.


Честно говоря, реализация forkбыла именно тем , что сделала CygWin. Но, если вы когда-нибудь читали о том, как они это сделали, ваш «нелегкий путь» будет большим недоразумением :-)
paxdiablo


2

Как уже упоминалось в других ответах, NT (ядро, лежащее в основе современных версий Windows) имеет эквивалент Unix fork (). Проблема не в этом.

Проблема в том, что клонирование всего состояния процесса, как правило, неразумно. Это так же верно в мире Unix, как и в Windows, но в мире Unix fork () используется постоянно, и библиотеки предназначены для работы с этим. Библиотеки Windows - нет.

Например, системные библиотеки DLL kernel32.dll и user32.dll поддерживают частное соединение с серверным процессом Win32 csrss.exe. После вилки на клиентской стороне этого соединения есть два процесса, которые могут вызвать проблемы. Дочерний процесс должен сообщить csrss.exe о своем существовании и установить новое соединение, но для этого нет интерфейса, поскольку эти библиотеки не были разработаны с учетом fork ().

Итак, у вас есть два варианта. Один из них - запретить использование kernel32 и user32 и других библиотек, которые не предназначены для разветвления, включая любые библиотеки, которые прямо или косвенно ссылаются на kernel32 или user32, то есть практически все из них. Это означает, что вы вообще не можете взаимодействовать с рабочим столом Windows и застряли в своем собственном отдельном мире Unixy. Это подход, используемый различными подсистемами Unix для NT.

Другой вариант - прибегнуть к какой-то ужасной хитрости, чтобы попытаться заставить неосведомленные библиотеки работать с fork (). Это то, что делает Cygwin. Он создает новый процесс, позволяет ему инициализироваться (включая регистрацию с помощью csrss.exe), затем копирует большую часть динамического состояния из старого процесса и надеется на лучшее. Меня поражает, что это когда-либо работает. Он определенно не работает надежно - даже если он не выходит из строя случайным образом из-за конфликта адресного пространства, любая используемая вами библиотека может оставаться в неработающем состоянии. Утверждение текущего принятого ответа о том, что Cygwin имеет «полнофункциональную вилку ()» ... сомнительно.

Резюме: В среде, подобной Interix, вы можете выполнить вилку, вызвав fork (). В противном случае попробуйте отучить себя от этого желания. Даже если вы нацелены на Cygwin, не используйте fork (), если в этом нет крайней необходимости.


Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.