Що є найближчим, що Windows має розщедрити ()?


124

Я думаю, питання все це говорить.

Я хочу розщедритися на Windows. Яка найбільш схожа операція і як її використовувати.

Відповіді:


86

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

В іншому випадку ви можете подивитися, як Cygwin реалізує fork (). З досить давнього документа архітектури Сігвіна :

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

Перше, що відбувається, коли батьківський процес розщеплює дочірній процес, це те, що батько ініціалізує простір у таблиці процесів Cygwin для дитини. Потім створюється призупинений дочірній процес за допомогою виклику Win32 CreateProcess. Далі батьківський процес викликає setjmp для збереження власного контексту і встановлює покажчик на це в області спільної пам’яті Cygwin (поділяється між усіма завданнями Cygwin). Потім він заповнює розділи .data та .bss дитини, копіюючи з власного адресного простору в призупинений адресний простір дитини. Після ініціалізації адресного простору дитини дитина запускається, поки батько чекає на мьютекс. Дитина виявляє, що він був роздвоєним і довгими, скориставшись збереженим буфером стрибків. Потім дитина встановлює мутекс, на який чекає батько, і блокує інший мютекс. Це сигнал для батька скопіювати його стек і купу в дитину, після чого він випустить мутекс, на який чекає дитина, і повертається з виклику вилки. Нарешті, дитина прокидається від блокування останнього мютексу, відтворює будь-які області, нанесені на пам'ять, передані йому через спільну область, і повертається з самої вилки.

Хоча у нас є деякі ідеї, як пришвидшити реалізацію виделки, зменшивши кількість контекстних перемикань між батьківським і дочірнім процесом, форк майже напевно завжди буде неефективним у Win32. На щастя, у більшості випадків сімейство дзвінків, що надається Cygwin, може замінити пару форк / exec лише з невеликими зусиллями. Ці дзвінки чітко відображаються поверх API Win32. Як результат, вони набагато ефективніші. Зміна програми драйверів компілятора на виклик spawn замість fork було тривіальною зміною та збільшило швидкість компіляції на двадцять до тридцяти відсотків у наших тестах.

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

Здається, багато роботи, чи не так? І так, це slooooow.

РЕДАКТИРОВАННЯ: документ є застарілим, дивіться цю відмінну відповідь для оновлення


11
Це хороша відповідь, якщо ви хочете написати програму Cygwin у Windows. Але загалом це не найкраще. По суті, моделі * nix і Windows для процесів і потоків досить різні. CreateProcess () і CreateThread () - це загально еквівалентні API
Foredecker

2
Розробники повинні пам’ятати, що це непідтримуваний механізм, і IIRC дійсно схильний до порушення, коли інший процес у системі використовує введення коду.
Гаррі Джонстон

1
Різне посилання на реалізацію більше не діє.
PythonNut

Відредаговано, щоб залишити лише інше посилання для відповідей
Laurynas Biveinis

@Foredecker, насправді ви не повинні цього робити, навіть якщо ви намагаєтесь написати "додаток cygwin". Він намагається наслідувати Unix, forkале це досягає пропускного рішення, і ви повинні бути готові до несподіваних ситуацій.
Pacerier

66

Я, звичайно, не знаю деталей щодо цього, тому що я ніколи цього не робив, але рідний API NT має можливість розблокувати процес (підсистема 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 не вдалися з тієї чи іншої причини.

Було б дуже приємно, якби цей матеріал був би кращим або взагалі був задокументований, особливо пару структур даних і як підключити процес до підсистеми. Хоча fork - це не концепція 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

Ну, вікна насправді не мають нічого подібного. Тим більше, що вилка може використовуватися для концептуальної створення потоку або процесу в * nix.

Отже, я б сказав:

CreateProcess()/CreateProcessEx()

і

CreateThread()(Я чув, що для додатків С _beginthreadex()краще).


17

Люди намагалися реалізувати fork у 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()було найближчим завданням 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"

  • Ви не можете мати торт і їсти його теж ... in msvcrt.dll, printf () заснований на API консолі, який сам по собі використовує lpc для зв'язку з підсистемою консолі (csrss.exe). З'єднання з csrss ініціюється при запуску процесу, а це означає, що будь-який процес, який починає його виконання "посередині", цей крок буде пропущений. Якщо у вас немає доступу до вихідного коду операційної системи, немає сенсу намагатися підключитися до csrss вручну. Натомість слід створити власну підсистему і відповідно уникати функцій консолей у додатках, які використовують fork ().

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

"Крім того, ви, ймовірно, не повинні використовувати функції Zw *, якщо ви не перебуваєте в режимі ядра, ви, мабуть, замість цього повинні використовувати функції Nt *."

  • Це неправильно. При доступі в користувальницькому режимі немає різниці між Zw *** Nt ***; це лише два різних експортованих імені (ntdll.dll), які посилаються на одну і ту ж (відносну) віртуальну адресу.

ZwGetContextThread (NtCurrentThread () та контекст);

  • отримання контексту поточного (запущеного) потоку шляхом виклику ZwGetContextThread невірно, швидше за все, вийде з ладу, і (завдяки додатковому системному виклику) також не є найшвидшим способом виконання завдання.

2
Це, здається, не відповідає на головне питання, але відповідає на кілька інших різних відповідей, і, ймовірно, краще буде відповісти безпосередньо на кожен, щоб зрозуміти, і щоб було легше слідкувати за тим, що відбувається.
Лі

ви, здається, припускаєте, що printf завжди пише на консоль.
Ясен

3

Семантика fork () необхідна, коли дитині потрібен доступ до фактичного стану пам'яті батьків з моменту виклику миттєвого вилка (). У мене є програмне забезпечення, яке покладається на неявну копію копіювання пам'яті пам'яті, як називається миттєвий форк (), що робить потоки неможливими у використанні. (Це емулюється на сучасних платформах * nix за допомогою семантики копіювання-запису / оновлення-пам’яті таблиці.)

Найближче, що існує в Windows як систематичний виклик, - CreateProcess. Найкраще, що можна зробити, це те, щоб батьки заморозили всі інші потоки протягом часу, коли він копіює пам'ять у новий простір пам’яті, а потім відтають їх. Ані клас Cygwin frok [sic], ані код Scilab, який опублікував Ерік де Кортіс, не роблять нитки заморожування, що я бачу.

Крім того, ви, ймовірно, не повинні використовувати функції Zw *, якщо ви не знаходитесь в режимі ядра, ви, мабуть, замість цього повинні використовувати функції Nt *. Існує додаткова гілка, яка перевіряє, чи перебуваєте ви в режимі ядра, і, якщо ні, виконує перевірку всіх меж і перевірку параметрів, які завжди роблять Nt *. Таким чином, викликати їх з користувальницького режиму дуже малоефективно.


Дуже цікава інформація про експортовані символи Zw *, дякую.
Андон М. Коулман

Зверніть увагу, що функції Zw * з користувальницького простору все ж відображаються на функції Nt * в просторі ядра, для безпеки. Або принаймні вони повинні.
Пол


2

Існує не простий спосіб емуляції fork () в Windows.

Я пропоную вам замість цього використовувати нитки.


Ну, чесно кажучи, реалізація forkбула саме тим , що зробив CygWin. Але якщо ви коли-небудь читаєте про те, як вони це зробили, ваш "непростий шлях" - це грубе неправильне
твердження


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 (). Ось що робить Цигвін. Він створює новий процес, дозволяє ініціалізуватися (включаючи реєстрацію себе за допомогою csrss.exe), потім копіює більшість динамічного стану зі старого процесу та сподівається на найкраще. Мене дивує, що це коли-небудь спрацьовує. Це, безумовно, не працює надійно - навіть якщо це не випадково виходить з ладу через конфлікт адресного простору, будь-яка бібліотека, яку ви використовуєте, може бути мовчки залишена в розбитому стані. Твердження поточної прийнятої відповіді про те, що у Cygwin є "повнофункціональний fork ()" є ... сумнівним.

Підсумок: У середовищі, подібному до Interix, ви можете роздрібнитись, зателефонувавши fork (). В іншому випадку, будь ласка, спробуйте відлучити себе від бажання це зробити. Навіть якщо ви орієнтуєтесь на Cygwin, не використовуйте fork (), якщо вам абсолютно не доведеться.


Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.