Qual é a coisa mais próxima que o Windows tem do fork ()?

124

Eu acho que a pergunta diz tudo.

Eu quero bifurcar no Windows. Qual é a operação mais semelhante e como eu a uso.

rlbond
fonte

Respostas:

86

O Cygwin possui o fork () completo no Windows. Portanto, se o uso do Cygwin for aceitável para você, o problema será resolvido caso o desempenho não seja um problema.

Caso contrário, você pode dar uma olhada em como o Cygwin implementa o fork (). Do documento de arquitetura de Cygwin bastante antigo :

5.6 Criação de processos A chamada de bifurcação no Cygwin é particularmente interessante porque não é bem mapeada no topo da API do Win32. Isso dificulta a implementação correta. Atualmente, o fork do Cygwin é uma implementação sem cópia na gravação, semelhante à que estava presente nos primeiros sabores do UNIX.

A primeira coisa que acontece quando um processo pai bifurca um processo filho é que o pai inicializa um espaço na tabela de processos Cygwin para o filho. Em seguida, ele cria um processo filho suspenso usando a chamada Win32 CreateProcess. Em seguida, o processo pai chama setjmp para salvar seu próprio contexto e define um ponteiro para isso em uma área de memória compartilhada do Cygwin (compartilhada entre todas as tarefas do Cygwin). Em seguida, preenche as seções .data e .bss da criança, copiando de seu próprio espaço de endereço para o espaço de endereço da criança suspensa. Depois que o espaço de endereço do filho é inicializado, o filho é executado enquanto o pai aguarda um mutex. A criança descobre que foi bifurcada e saltos longos usando o buffer de salto salvo. A criança define o mutex em que o pai está esperando e bloqueia outro mutex. Esse é o sinal para o pai copiar sua pilha e acumular no filho, após o qual libera o mutex em que o filho está esperando e retorna da chamada de bifurcação. Por fim, a criança acorda do bloqueio no último mutex, recria todas as áreas mapeadas na memória passadas para ela através da área compartilhada e retorna do próprio fork.

Embora tenhamos algumas idéias sobre como acelerar a implementação do fork, reduzindo o número de alternâncias de contexto entre o processo pai e filho, o fork quase sempre será sempre ineficiente no Win32. Felizmente, na maioria das circunstâncias, a família de chamadas geradas por Cygwin pode substituir um par de garfo / executivo com apenas um pouco de esforço. Essas chamadas são mapeadas corretamente na API do Win32. Como resultado, eles são muito mais eficientes. Alterar o programa de driver do compilador para chamar spawn em vez de fork foi uma mudança trivial e aumentou as velocidades de compilação em vinte a trinta por cento em nossos testes.

No entanto, spawn e exec apresentam seu próprio conjunto de dificuldades. Como não há como executar um exec real no Win32, o Cygwin precisa inventar seus próprios IDs de processo (PIDs). Como resultado, quando um processo executa várias chamadas executivas, haverá vários PIDs do Windows associados a um único PID Cygwin. Em alguns casos, os stubs de cada um desses processos do Win32 podem demorar, aguardando a saída do processo Cygwin executivo.

Parece muito trabalho, não é? E sim, é slooooow.

EDIT: o documento está desatualizado, consulte esta excelente resposta para uma atualização

Laurynas Biveinis
fonte
11
Esta é uma boa resposta se você quiser escrever um aplicativo Cygwin no Windows. Mas, em geral, não é a melhor coisa a fazer. Fundamentalmente, os modelos de processos e threads * nix e Windows são bem diferentes. CreateProcess () e CreateThread () são os APIs geralmente equivalentes
Foredecker
2
Os desenvolvedores devem ter em mente que este é um mecanismo não suportado, e o IIRC está realmente inclinado a quebrar sempre que algum outro processo no sistema estiver usando injeção de código.
Harry Johnston
1
O link de implementação diferente não é mais válido.
PythonNut
Editado para deixar apenas o outro link de resposta
Laurynas Biveinis 28/11
@Foredecker, na verdade você não deve fazer isso mesmo se estiver tentando escrever um "aplicativo cygwin". Ele tenta imitar o Unix, forkmas consegue isso com uma solução com vazamento e você deve estar preparado para situações inesperadas.
Pacerier 13/08/2015
66

Certamente não conheço os detalhes sobre isso porque nunca fiz isso, mas a API nativa do NT tem uma capacidade de bifurcar um processo (o subsistema POSIX no Windows precisa dessa capacidade - não tenho certeza se o subsistema POSIX ainda é suportado).

Uma pesquisa por ZwCreateProcess () deve fornecer mais detalhes - por exemplo, esta informação de Maxim Shatskih :

O parâmetro mais importante aqui é SectionHandle. Se este parâmetro for NULL, o kernel irá bifurcar o processo atual. Caso contrário, esse parâmetro deverá ser um identificador do objeto de seção SEC_IMAGE criado no arquivo EXE antes de chamar ZwCreateProcess ().

Porém, observe que Corinna Vinschen indica que Cygwin encontrou usando ZwCreateProcess () ainda não confiável :

Iker Arizmendi escreveu:

> 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

Este documento é bastante antigo, 10 anos ou mais. Enquanto ainda estamos usando chamadas Win32 para emular fork, o método mudou notavelmente. Especialmente, não criamos mais o processo filho no estado suspenso, a menos que estruturas de dados específicas precisem de um tratamento especial no pai antes de serem copiadas para o filho. Na versão 1.5.25 atual, o único caso de um filho suspenso são os soquetes abertos no pai. A próxima versão 1.7.0 não será suspensa.

Um motivo para não usar o ZwCreateProcess foi que, até a versão 1.5.25, ainda estamos suportando usuários do Windows 9x. No entanto, duas tentativas de usar o ZwCreateProcess em sistemas baseados em NT falharam por um motivo ou outro.

Seria muito bom se esse material fosse melhor ou de todo documentado, especialmente algumas estruturas de dados e como conectar um processo a um subsistema. Embora o fork não seja um conceito do Win32, não acho que seria ruim facilitar a implementação do fork.

Michael Burr
fonte
Esta é a resposta errada. CreateProcess () e CreateThread () são os equivalentes gerais.
Foredecker
2
Interix está disponível no Windows Vista Enterprise / Ultimate como "Subsistema para Aplicativos UNIX": en.wikipedia.org/wiki/Interix
bk1e
15
@Foredecker - esta pode ser uma resposta errada, mas CreateProcess () / CreateThread () também pode estar errado. Depende se alguém está procurando 'a maneira Win32 de fazer as coisas' ou 'o mais próximo possível da semântica fork ()'. CreateProcess () se comporta de maneira significativamente diferente do fork (), que é a razão pela qual o cygwin precisava fazer muito trabalho para suportá-lo.
22711 Michael Burr
1
@ Jon: Eu tentei consertar os links e copiar o texto relevante na resposta (para futuros links quebrados não são um problema). No entanto, esta resposta é de tempo suficiente atrás que eu não estou 100% certo de que a citação que eu encontrei hoje é o que eu estava referindo-se em 2009.
Michael Burr
4
Se as pessoas querem " forkcom imediato exec", talvez o CreateProcess seja um candidato. Mas o forksem execé muitas vezes desejável e é isso que leva as pessoas a pedir um real fork.
Aaron McDaid
37

Bem, o Windows realmente não tem nada parecido. Especialmente porque o fork pode ser usado para criar conceitualmente um thread ou um processo no * nix.

Então, eu teria que dizer:

CreateProcess()/CreateProcessEx()

e

CreateThread()(Ouvi dizer que, para aplicativos C, _beginthreadex()é melhor).

Evan Teran
fonte
17

As pessoas tentaram implementar o fork no Windows. Esta é a coisa mais próxima que posso encontrar:

Retirado 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;
}
Eric des Courtis
fonte
4
Observe que a maior parte da verificação de erros está ausente - por exemplo, ZwCreateThread retorna um valor NTSTATUS que pode ser verificado usando as macros SUCCEEDED e FAILED.
BCRAN
1
O que acontece se a forkfalha ocorrer , o programa falhar ou o thread simplesmente falhar? Se travar o programa, isso não é realmente uma bifurcação. Apenas curioso, porque estou procurando uma solução real e espero que essa seja uma alternativa decente.
precisa saber é o seguinte
1
Gostaria de observar que há um erro no código fornecido. haveLoadedFunctionsForFork é uma função global no cabeçalho, mas uma função estática no arquivo c. Ambos devem ser globais. Atualmente, o fork falha, adicionando a verificação de erros agora.
LeetNightshade
O site está morto e não sei como posso compilar o exemplo no meu próprio sistema. Suponho que estou perdendo alguns cabeçalhos ou incluindo os errados, estou? (o exemplo não mostrá-los.)
Paul Stelian
6

Antes da Microsoft apresentar sua nova opção "Subsistema Linux para Windows", CreateProcess()era a coisa mais próxima do Windows fork(), mas o Windows exige que você especifique um executável para executar nesse processo.

A criação do processo UNIX é bem diferente do Windows. Sua fork()chamada basicamente duplica quase todo o processo atual, cada um em seu próprio espaço de endereço, e continua executando-os separadamente. Embora os processos em si sejam diferentes, eles ainda estão executando o mesmo programa. Veja aqui uma boa visão geral do fork/execmodelo.

Voltando ao contrário, o equivalente do Windows CreateProcess()é o fork()/exec() par de funções no UNIX.

Se você estava portando software para o Windows e não se importa com uma camada de tradução, o Cygwin forneceu a capacidade que você deseja, mas foi bastante desagradável.

Obviamente, com o novo subsistema Linux , a coisa mais próxima do Windows fork()é na verdade fork() :-)

paxdiablo
fonte
2
Portanto, dada a WSL, posso usar forkem um aplicativo médio que não seja da WSL?
César
6

O documento a seguir fornece algumas informações sobre como transportar código do UNIX para o Win32: https://msdn.microsoft.com/en-us/library/y23kc048.aspx

Entre outras coisas, indica que o modelo de processo é bastante diferente entre os dois sistemas e recomenda a consideração de CreateProcess e CreateThread onde o comportamento do tipo fork () é necessário.

Brandon E Taylor
fonte
4

"assim que você desejar acessar ou imprimir arquivos, io será recusado"

  • Você não pode comer o seu bolo e comê-lo também ... no msvcrt.dll, printf () é baseado na API do console, que por si só usa lpc para se comunicar com o subsistema do console (csrss.exe). A conexão com o csrss é iniciada na inicialização do processo, o que significa que qualquer processo que inicie sua execução "no meio" terá essa etapa ignorada. A menos que você tenha acesso ao código fonte do sistema operacional, não faz sentido tentar se conectar ao csrss manualmente. Em vez disso, você deve criar seu próprio subsistema e, consequentemente, evitar as funções do console em aplicativos que usam fork ().

  • Depois de implementar seu próprio subsistema, não se esqueça de duplicar todos os identificadores dos pais para o processo filho ;-)

"Além disso, você provavelmente não deve usar as funções Zw *, a menos que esteja no modo kernel, provavelmente deve usar as funções Nt *."

  • Isto está incorreto. Quando acessado no modo de usuário, não há absolutamente nenhuma diferença entre Zw *** Nt ***; esses são apenas dois nomes exportados diferentes (ntdll.dll) que se referem ao mesmo endereço virtual (relativo).

ZwGetContextThread (NtCurrentThread (), e contexto);

  • obter o contexto do thread atual (em execução) chamando ZwGetContextThread está errado, provavelmente trava e (devido à chamada extra do sistema) também não é a maneira mais rápida de realizar a tarefa.
user3502619
fonte
2
Isso não parece estar respondendo à pergunta principal, mas respondendo a algumas outras respostas diferentes, e provavelmente seria melhor responder diretamente a cada uma para maior clareza e facilitar o acompanhamento do que está acontecendo.
Leigh
você parece estar imaginando que o printf sempre grava no console.
Jasen
3

a semântica fork () é necessária onde o filho precisa acessar o estado de memória real do pai, a partir do momento em que o fork () é chamado. Eu tenho um software que se baseia no mutex implícito da cópia de memória a partir do momento em que o fork instantâneo () é chamado, o que torna os threads impossíveis de usar. (Isso é emulado nas modernas plataformas * nix por meio da semântica de copiar na gravação / atualizar a memória da tabela.)

O mais próximo que existe no Windows como syscall é CreateProcess. O melhor que pode ser feito é que o pai congele todos os outros encadeamentos durante o tempo em que estiver copiando a memória para o espaço de memória do novo processo e os descongele. Nem a classe Cygwin frok [sic] nem o código Scilab publicado por Eric des Courtis fazem o congelamento de threads, que eu posso ver.

Além disso, você provavelmente não deve usar as funções Zw *, a menos que esteja no modo kernel, provavelmente deve usar as funções Nt *. Há uma ramificação extra que verifica se você está no modo kernel e, se não, executa todas as verificações de limites e verificação de parâmetros que o Nt * sempre faz. Portanto, é um pouco menos eficiente chamá-los do modo de usuário.

sjcaged
fonte
Informações muito interessantes sobre os símbolos exportados do Zw *, obrigado.
precisa
Observe que as funções Zw * do espaço do usuário ainda são mapeadas para as funções Nt * no espaço do kernel, por segurança. Ou pelo menos eles deveriam.
Paul Stelian
2

Não há uma maneira fácil de emular o fork () no Windows.

Eu sugiro que você use tópicos em seu lugar.

VVS
fonte
Bem, para ser justo, implementar forkfoi exatamente o que CygWin fez. Mas, se você sempre ler sobre como eles fizeram isso, yor "nenhuma maneira fácil" é um misunderstatement bruta :-)
paxdiablo
2

Como outras respostas mencionaram, o NT (o kernel subjacente às versões modernas do Windows) possui um equivalente ao fork do Unix (). Esse não é o problema.

O problema é que a clonagem de todo o estado de um processo geralmente não é uma coisa sensata a ser feita. Isso é tão verdadeiro no mundo Unix quanto no Windows, mas no mundo Unix, fork () é usado o tempo todo e as bibliotecas são projetadas para lidar com isso. As bibliotecas do Windows não são.

Por exemplo, as DLLs do sistema kernel32.dll e user32.dll mantêm uma conexão privada com o processo do servidor Win32 csrss.exe. Após uma bifurcação, há dois processos no lado cliente dessa conexão, que causam problemas. O processo filho deve informar o csrss.exe de sua existência e fazer uma nova conexão - mas não há interface para fazer isso, porque essas bibliotecas não foram projetadas com o fork () em mente.

Então você tem duas opções. Uma é proibir o uso do kernel32 e user32 e outras bibliotecas que não foram projetadas para serem bifurcadas - incluindo quaisquer bibliotecas que se vinculam direta ou indiretamente ao kernel32 ou user32, que é praticamente todas elas. Isso significa que você não pode interagir com a área de trabalho do Windows e está preso no seu próprio mundo Unixy. Essa é a abordagem adotada pelos vários subsistemas Unix para NT.

A outra opção é recorrer a algum tipo de truque horrível para tentar fazer com que as bibliotecas inconscientes funcionem com o fork (). Isso é o que Cygwin faz. Ele cria um novo processo, permite inicializar (incluindo o registro com o csrss.exe), depois copia a maior parte do estado dinâmico do processo antigo e espera o melhor. Surpreende-me que isso sempre funcione. Certamente não funciona de maneira confiável - mesmo que não falhe aleatoriamente devido a um conflito no espaço de endereço, qualquer biblioteca que você está usando pode ficar silenciosamente em um estado interrompido. A alegação da resposta atualmente aceita de que Cygwin tem um "fork completo ()" é ... duvidosa.

Resumo: Em um ambiente semelhante ao Interix, você pode bifurcar-se chamando fork (). Caso contrário, tente se afastar do desejo de fazê-lo. Mesmo se você estiver segmentando o Cygwin, não use fork (), a menos que seja absolutamente necessário.

benrg
fonte