Windows最接近fork()的东西是什么?


124

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

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

Answers:


86

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

否则,您可以看看Cygwin如何实现fork()。从一个相当老的Cygwin的体系结构文档中可以得出

5.6。流程创建Cygwin中的fork调用特别有趣,因为它无法在Win32 API上很好地映射。这使得正确实施非常困难。当前,Cygwin分支是一种非写时复制实现,类似于UNIX早期版本中提供的实现。

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

尽管我们对如何通过减少父进程和子进程之间的上下文切换次数来加快fork的实现有一些想法,但是在Win32下,fork几乎总是效率低下。幸运的是,在大多数情况下,由Cygwin提供的派生调用系列可以轻松完成,以替代fork / exec对。这些调用完全在Win32 API上映射。结果,它们效率更高。在我们的测试中,将编译器的驱动程序更改为调用spawn而不是fork是一个微不足道的更改,并且将编译速度提高了20%到30%。

但是,spawn和exec会遇到一系列困难。由于无法在Win32下执行实际的exec,因此Cygwin必须发明自己的进程ID(PID)。结果,当一个进程执行多个exec调用时,将有多个与单个Cygwin PID关联的Windows PID。在某些情况下,每个Win32进程的存根可能会徘徊,等待其执行的Cygwin进程退出。

听起来需要做很多工作,不是吗?是的,它是slooooow。

编辑:该文档已过时,请查看此出色的答案以进行更新


11
如果要在Windows上编写Cygwin应用程序,这是一个很好的答案。但总的来说,这不是最好的选择。从根本上说,* nix和Windows进程和线程模型是完全不同的。CreateProcess()和CreateThread()是通常等效的API
Foredecker 2009年

2
开发人员应记住,这是不受支持的机制,并且IIRC确实倾向于在系统上的某些其他进程使用代码注入时中断该机制。
哈里·约翰斯顿

1
不同的实现链接不再有效。
PythonNut 2014年

编辑后仅留下其他答案链接
Laurynas Biveinis 2014年

@Foredecker,实际上即使您正在尝试编写“ cygwin应用程序” 也不应这样做。它试图模仿Unix,fork但是它通过泄漏的解决方案来实现这一点,因此您必须为意外情况做好准备。
Pacerier,2015年

66

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

搜索ZwCreateProcess()应该会为您提供更多详细信息-例如,Maxim Shatskih提供的以下信息

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

尽管请注意Corinna Vinschen指出Cygwin使用ZwCreateProcess()发现仍然不可靠

艾克·阿里兹门迪(Iker Arizmendi)写道:

> 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易于实现并不是一件坏事。


这是错误的答案。CreateProcess()和CreateThread()是通用的等效项。
Foredecker 2009年

2
Interix在Windows Vista Enterprise / Ultimate中可作为“ UNIX应用程序子系统”使用:en.wikipedia.org/wiki/Interix
bk1e

15
@Foredecker-这可能是错误的答案,但是CreateProcess()/ CreateThread()也很可能是错误的。这取决于是要寻找“ Win32做事方式”还是“尽可能接近fork()语义”。CreateProcess()的行为与fork()显着不同,这就是cygwin需要做大量工作来支持它的原因。
Michael Burr

1
@jon:我已经尝试修复链接并将相关文本复制到答案中(这样以后断开的链接就不成问题了)。但是,这个答案来自很久以前,我不确定我今天发现的报价是我在2009
Michael Burr 2013年

4
如果人们希望“ fork立即exec”,那么也许CreateProcess是一个候选者。但是常常fork没有exec希望,这是促使人们要求真实的东西fork
2014年


17

人们试图在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;
}

4
请注意,大多数错误检查都丢失了-例如ZwCreateThread返回一个NTSTATUS值,可以使用SUCCEEDED和FAILED宏进行检查。
BCran

1
如果fork崩溃,该程序崩溃还是该线程刚刚崩溃怎么办?如果它使程序崩溃,那么这并不是真正的分叉。只是好奇,因为我正在寻找一个真正的解决方案,希望这可能是一个不错的选择。
leetNightshade 2014年

1
我想指出的是,所提供的代码中有一个错误。在标题中,haveLoadedFunctionsForFork是全局函数,但在c文件中是静态函数。他们俩都应该是全球性的。当前分叉崩溃,现在添加错误检查。
leetNightshade 2014年

该站点已死,我不知道如何在自己的系统上编译该示例。我认为我缺少某些标题或包含错误的标题吗?(该示例未显示它们。)
Paul Stelian

6

在Microsoft引入其新的“ Windows的Linux子系统”选项之前,Windows CreateProcess()是最接近的fork(),但是Windows要求您指定一个可执行文件才能在该进程中运行。

UNIX进程的创建与Windows完全不同。它的fork()调用基本上将当前过程几乎全部复制,每个过程都在各自的地址空间中进行,然后继续单独运行它们。尽管进程本身不同,但它们仍在运行同一程序。有关fork/exec模型的详细概述,请参见此处

换一种说法,Windows相当于UNIX中CreateProcess()fork()/exec() 对函数。

如果您要将软件移植到Windows而又不介意翻译层,Cygwin提供了您想要的功能,但是这很麻烦。

当然,的Linux子系统,Windows有到最近的事情fork()实际 fork() :-)


2
那么,给定WSL,我可以fork在普通的非WSL应用程序中使用吗?
凯撒


4

“只要您想进行文件访问或printf,那么io就会被拒绝”

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

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

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

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

ZwGetContextThread(NtCurrentThread(),&context);

  • 通过调用ZwGetContextThread获取当前(正在运行的)线程的上下文是错误的,很可能会崩溃,并且(由于进行了额外的系统调用)也不是完成任务的最快方法。

2
这似乎不是在回答主要问题,而是在回答其他几个不同的答案,并且可能最好直接回答每个问题,以使内容更清楚,并使后续操作变得更容易。
Leigh 2014年

您似乎在假设printf总是写入控制台。
杰森

3

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

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

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


关于Zw *导出符号的非常有趣的信息,谢谢。
Andon M. Coleman

请注意,出于安全考虑,用户空间中的Zw *函数仍映射到内核空间中的Nt *函数。或者至少他们应该。
Paul Stelian


2

在Windows上没有简单的方法来仿真fork()。

我建议您改用线程。


那么,在公平,实现fork究竟什么CygWin的那样。但是,如果你阅读了关于如何他们做到了,侑“没有简单的方法”是一个总misunderstatement :-)
paxdiablo


2

正如其他答案所提到的,NT(Windows的现代基础内核)与Unix fork()等效。那不是问题。

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

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

因此,您有两种选择。一种是禁止使用kernel32和user32以及其他并非旨在分叉的库–包括直接或间接链接到kernel32或user32的所有库,实际上它们都是库。这意味着您根本无法与Windows桌面进行交互,而被困在自己的单独Unixy世界中。这是各种Unix NT子系统所采用的方法。

另一种选择是求助于某种可怕的黑客,以尝试使不知道的库与fork()一起使用。那就是Cygwin所做的。它创建一个新进程,使其进行初始化(包括使用csrss.exe注册自身),然后从旧进程中复制大部分动态状态,并希望达到最佳状态。令我惊讶的是,它曾经奏效。它当然不能可靠地工作-即使它不会由于地址空间冲突而随机失败,您正在使用的任何库也可能会默默地处于损坏状态。目前公认的答案称Cygwin具有“功能齐全的fork()”是令人怀疑的。

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


By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.