145

我想这个问题说明了一切。

我想在 Windows 上分叉。什么是最相似的操作以及如何使用它。

4

14 回答 14

94

Cygwin在 Windows 上具有完整的 fork() 功能。因此,如果您可以接受使用 Cygwin,那么在性能不成问题的情况下问题就解决了。

否则你可以看看 Cygwin 如何实现 fork()。来自一个相当古老的 Cygwin 架构文档

5.6. 进程创建 Cygwin 中的 fork 调用特别有趣,因为它不能很好地映射到 Win32 API 之上。这使得正确实施非常困难。目前,Cygwin fork 是一种非写时复制的实现,类似于早期 UNIX 风格中的实现。

当父进程派生子进程时发生的第一件事是父进程在 Cygwin 进程表中为子进程初始化一个空间。然后它使用 Win32 CreateProcess 调用创建一个挂起的子进程。接下来,父进程调用 setjmp 来保存自己的上下文,并在 Cygwin 共享内存区域(在所有 Cygwin 任务之间共享)中设置指向 this 的指针。然后它通过从自己的地址空间复制到挂起的孩子的地址空间来填充孩子的 .data 和 .bss 部分。子地址空间初始化后,子进程在父进程等待互斥体时运行。孩子发现它已被分叉并使用保存的跳转缓冲区进行长跳转。然后,孩子设置父母正在等待的互斥锁并阻塞另一个互斥锁。这是父级将其堆栈和堆复制到子级的信号,之后它释放子级正在等待的互斥锁并从 fork 调用返回。最后,子进程从最后一个互斥体上的阻塞中醒来,重新创建通过共享区域传递给它的任何内存映射区域,并从 fork 本身返回。

虽然我们对如何通过减少父进程和子进程之间的上下文切换次数来加速我们的 fork 实现有一些想法,但在 Win32 下,fork 几乎肯定总是低效的。幸运的是,在大多数情况下,Cygwin 提供的 spawn 系列调用只需一点点努力就可以替代 fork/exec 对。这些调用清晰地映射在 Win32 API 之上。因此,它们的效率要高得多。将编译器的驱动程序更改为调用 spawn 而不是 fork 是一个微不足道的更改,并且在我们的测试中将编译速度提高了 20% 到 30%。

然而,spawn 和 exec 也有它们自己的困难。因为在 Win32 下无法执行实际的 exec,Cygwin 必须发明自己的进程 ID (PID)。因此,当一个进程执行多个 exec 调用时,将有多个 Windows PID 与单个 Cygwin PID 相关联。在某些情况下,这些 Win32 进程中的每一个的存根可能会逗留,等待它们执行的 Cygwin 进程退出。

听起来工作量很大,不是吗?是的,它很慢。

编辑:文档已过时,请参阅此出色答案以获取更新

于 2009-06-12T07:07:40.613 回答
73

我当然不知道这方面的细节,因为我从来没有这样做过,但是本机 NT API 具有分叉进程的能力(Windows 上的 POSIX 子系统需要这种能力 - 我不确定 POSIX 子系统是否甚至不再支持)。

搜索 ZwCreateProcess() 应该可以获得更多详细信息 - 例如来自 Maxim Shatskih 的以下信息

这里最重要的参数是SectionHandle。如果该参数为NULL,内核将fork当前进程。否则,此参数必须是调用 ZwCreateProcess() 之前在 EXE 文件上创建的 SEC_IMAGE 节对象的句柄。

尽管请注意Corinna Vinschen 表示 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 调用来模拟 fork,但该方法已经发生了显着变化。特别是,我们不再创建处于挂起状态的子进程,除非特定的数据结构在复制到子进程之前需要在父进程中进行特殊处理。在当前的 1.5.25 版本中,暂停子项的唯一情况是父项中的打开套接字。即将发布的 1.7.0 版本根本不会暂停。

不使用 ZwCreateProcess 的一个原因是直到 1.5.25 版本我们仍然支持 Windows 9x 用户。但是,由于某种原因,两次尝试在基于 NT 的系统上使用 ZwCreateProcess 都失败了。

如果这些东西会更好或完全记录在案,尤其是几个数据结构以及如何将进程连接到子系统,那将是非常好的。虽然 fork 不是 Win32 概念,但我认为让 fork 更易于实现并不是一件坏事。

于 2009-06-12T07:57:37.153 回答
42

好吧,windows 并没有真正类似的东西。特别是因为 fork 可用于在概念上在 *nix 中创建线程或进程。

所以,我不得不说:

CreateProcess()/CreateProcessEx()

CreateThread()(我听说对于 C 应用程序,_beginthreadex()更好)。

于 2009-06-12T06:40:41.490 回答
18

人们试图在 Windows 上实现 fork。这是我能找到的最接近它的东西:

取自: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;
}
于 2013-02-01T15:48:19.170 回答
9

在微软推出新的“Windows 的 Linux 子系统”选项之前,CreateProcess()这是 Windows 最接近的选项fork(),但 Windows 要求您指定一个可执行文件以在该进程中运行。

UNIX 进程的创建与 Windows 完全不同。它的fork()调用基本上几乎完全复制了当前进程,每个进程都在自己的地址空间中,并继续分别运行它们。虽然进程本身不同,但它们仍在运行相同的程序。请参阅此处以获得模型的良好概述fork/exec

反过来说,Windows 的等价物CreateProcess()是 UNIX 中的fork()/exec() 对函数。

如果您将软件移植到 Windows 并且您不介意翻译层,Cygwin 提供了您想要的功能,但它相当笨拙。

当然,有了新的Linux 子系统,Windows 最接近的fork()实际上 fork():-)

于 2009-06-12T07:04:27.360 回答
6

以下文档提供了有关将代码从 UNIX 移植到 Win32 的一些信息: https ://msdn.microsoft.com/en-us/library/y23kc048.aspx

除其他外,它表明两个系统之间的流程模型完全不同,并建议在需要类似 fork() 的行为时考虑 CreateProcess 和 CreateThread。

于 2009-06-12T06:49:09.253 回答
4

“只要你想进行文件访问或 printf,就会拒绝 io”

  • 你不能吃蛋糕也不能吃……在 msvcrt.dll 中,printf() 基于控制台 API,它本身使用 lpc 与控制台子系统 (csrss.exe) 进行通信。与 csrss 的连接在进程启动时启动,这意味着任何“在中间”开始执行的进程都将跳过该步骤。除非您可以访问操作系统的源代码,否则尝试手动连接到 csrss 是没有意义的。相反,您应该创建自己的子系统,并相应地避免在使用 fork() 的应用程序中使用控制台功能。

  • 一旦你实现了自己的子系统,不要忘记为子进程复制所有父进程的句柄;-)

“此外,除非您处于内核模式,否则您可能不应该使用 Zw* 函数,而应该使用 Nt* 函数。”

  • 这是不正确的。在用户态访问时,Zw*** Nt*** 绝对没有区别;这些只是引用相同(相对)虚拟地址的两个不同(ntdll.dll)导出名称。

ZwGetContextThread(NtCurrentThread(), &context);

  • 通过调用 ZwGetContextThread 获取当前(正在运行的)线程的上下文是错误的,很可能会崩溃,并且(由于额外的系统调用)也不是完成任务的最快方法。
于 2014-04-06T01:53:56.903 回答
4

正如其他答案所提到的,NT(现代 Windows 版本的内核)相当于 Unix fork()。那不是问题。

问题是克隆一个进程的整个状态通常不是一件明智的事情。这在 Unix 世界中和在 Windows 中一样正确,但在 Unix 世界中,fork() 一直在使用,并且库旨在处理它。Windows 库不是。

例如,系统 DLL kernel32.dll 和 user32.dll 维护到 Win32 服务器进程 csrss.exe 的专用连接。分叉后,该连接的客户端上有两个进程,这将导致问题。子进程应该通知 csrss.exe 它的存在并建立新的连接——但是没有接口可以做到这一点,因为这些库在设计时没有考虑到 fork()。

所以你有两个选择。一种是禁止使用 kernel32 和 user32 以及其他不被设计为 fork 的库——包括任何直接或间接链接到 kernel32 或 user32 的库,这几乎是所有这些库。这意味着您根本无法与 Windows 桌面交互,并被困在自己独立的 Unixy 世界中。这是用于 NT 的各种 Unix 子系统所采用的方法。

另一种选择是诉诸某种可怕的 hack 来尝试让不知情的库与 fork() 一起工作。这就是 Cygwin 所做的。它创建一个新进程,让它初始化(包括使用 csrss.exe 注册自身),然后从旧进程复制大部分动态状态并希望最好。令我惊讶的是,这一直有效。它当然不能可靠地工作——即使它不会由于地址空间冲突而随机失败,您正在使用的任何库都可能会默默地处于损坏状态。当前接受的答案声称 Cygwin 具有“功能齐全的 fork()”的说法是......可疑的。

简介: 在类似 Interix 的环境中,您可以通过调用 fork() 进行分叉。否则,请试着让自己摆脱做这件事的欲望。即使您的目标是 Cygwin,也不要使用 fork(),除非您绝对必须这样做。

于 2020-07-14T05:01:09.473 回答
3

您最好的选择是CreateProcess()CreateThread()这里有更多关于移植的信息。

于 2009-06-12T06:40:43.390 回答
3

在 Windows 上模拟 fork() 没有简单的方法。

我建议你改用线程。

于 2009-06-12T06:44:53.990 回答
3

fork() 语义是必要的,当调用 fork() 时子级需要访问父级的实际内存状态。我有一个软件,它依赖于调用即时 fork() 时内存复制的隐式互斥锁,这使得线程无法使用。(这是通过写时复制/更新内存表语义在现代 *nix 平台上模拟的。)

在 Windows 上作为系统调用存在的最接近的是 CreateProcess。可以做的最好的事情是让父进程在将内存复制到新进程的内存空间期间冻结所有其他线程,然后解冻它们。我可以看到,Cygwin frok [sic] 类和 Eric des Courtis 发布的 Scilab 代码都没有线程冻结。

此外,除非您处于内核模式,否则您可能不应该使用 Zw* 函数,而应该使用 Nt* 函数。有一个额外的分支检查您是否处于内核模式,如果不是,则执行 Nt* 始终执行的所有边界检查和参数验证。因此,从用户模式调用它们的效率会稍低一些。

于 2014-02-06T19:59:16.770 回答
2

你说的最接近...让我想...这一定是 fork() 我猜 :)

有关详细信息,请参阅Interix 是否实现 fork()?

于 2011-03-20T22:27:54.850 回答
1

如果您只关心创建子进程并等待它,那么 process.h 中的 _spawn* API 可能就足够了。以下是有关此的更多信息:

https://docs.microsoft.com/en-us/cpp/c-runtime-library/process-and-environment-control https://en.wikipedia.org/wiki/Process.h

于 2017-09-15T09:24:30.030 回答
0

大多数骇人听闻的解决方案都已过时。Winnie the fuzzer 有一个 fork 版本,可以在当前版本的 Windows 10 上运行(但这需要系统特定的偏移量并且也很容易破坏)。

https://github.com/sslab-gatech/winnie/tree/master/forklib

于 2021-07-23T13:41:46.300 回答