¿Qué es lo más cercano que Windows tiene para bifurcar ()?

124

Supongo que la pregunta lo dice todo.

Quiero bifurcar en Windows. ¿Cuál es la operación más similar y cómo la uso?

rlbond
fuente

Respuestas:

86

Cygwin ha presentado fork () en Windows. Por lo tanto, si usar Cygwin es aceptable para usted, entonces el problema se resuelve en caso de que el rendimiento no sea un problema.

De lo contrario, puede echar un vistazo a cómo Cygwin implementa fork (). De un documento de arquitectura bastante antiguo de Cygwin :

5.6. Creación de procesos La llamada fork en Cygwin es particularmente interesante porque no se asigna bien en la parte superior de la API Win32. Esto hace que sea muy difícil de implementar correctamente. Actualmente, la bifurcación Cygwin es una implementación sin copia en escritura similar a la que estaba presente en los primeros sabores de UNIX.

Lo primero que sucede cuando un proceso padre bifurca un proceso hijo es que el padre inicializa un espacio en la tabla de proceso Cygwin para el hijo. Luego crea un proceso hijo suspendido utilizando la llamada Win32 CreateProcess. Luego, el proceso padre llama a setjmp para guardar su propio contexto y establece un puntero a esto en un área de memoria compartida de Cygwin (compartida entre todas las tareas de Cygwin). Luego completa las secciones .data y .bss del niño copiando desde su propio espacio de direcciones en el espacio de direcciones del niño suspendido. Después de que se inicializa el espacio de direcciones del niño, el niño se ejecuta mientras el padre espera en un mutex. El niño descubre que se ha bifurcado y saltos largos utilizando el búfer de salto guardado. Luego, el niño establece el mutex que el padre está esperando y bloquea otro mutex. Esta es la señal para que el padre copie su pila y su montón en el niño, después de lo cual libera el mutex que el niño está esperando y regresa de la llamada fork. Finalmente, el niño se despierta del bloqueo en el último mutex, recrea cualquier área asignada a la memoria que se le haya pasado a través del área compartida y regresa de la bifurcación misma.

Si bien tenemos algunas ideas sobre cómo acelerar nuestra implementación de la bifurcación reduciendo el número de cambios de contexto entre el proceso primario y secundario, la bifurcación casi siempre será ineficiente en Win32. Afortunadamente, en la mayoría de los casos, la familia de llamadas generadas por Cygwin puede ser sustituida por un par tenedor / ejecutivo con solo un pequeño esfuerzo. Estas llamadas se asignan limpiamente en la parte superior de la API Win32. Como resultado, son mucho más eficientes. Cambiar el programa del controlador del compilador para generar spawn en lugar de fork fue un cambio trivial y aumentó las velocidades de compilación en un veinte a treinta por ciento en nuestras pruebas.

Sin embargo, spawn y exec presentan su propio conjunto de dificultades. Debido a que no hay forma de hacer un ejecutivo real en Win32, Cygwin tiene que inventar sus propios ID de proceso (PID). Como resultado, cuando un proceso realiza múltiples llamadas ejecutivas, habrá múltiples PID de Windows asociados con un solo PID de Cygwin. En algunos casos, los resguardos de cada uno de estos procesos Win32 pueden persistir, esperando que salga su proceso Cygwin ejecutado.

Suena a mucho trabajo, ¿no? Y sí, es muy lento.

EDITAR: el documento está desactualizado, vea esta excelente respuesta para una actualización

Laurynas Biveinis
fuente
11
Esta es una buena respuesta si desea escribir una aplicación Cygwin en Windows. Pero en general no es lo mejor que se puede hacer. Básicamente, los modelos de procesos y subprocesos * nix y Windows son bastante diferentes. CreateProcess () y CreateThread () son las API generalmente equivalentes
Foredecker
2
Los desarrolladores deben tener en cuenta que este es un mecanismo no compatible, y el IIRC está inclinado a romperse cada vez que algún otro proceso en el sistema está usando inyección de código.
Harry Johnston el
1
El enlace de implementación diferente ya no es válido.
PythonNut
Editado para dejar el otro enlace de respuesta solamente
Laurynas Biveinis
@Foredecker, en realidad no deberías hacerlo incluso si estás intentando escribir una "aplicación cygwin". Intenta imitar a Unix, forkpero lo logra con una solución con fugas y debe estar preparado para situaciones inesperadas.
Pacerier
66

Ciertamente no conozco los detalles sobre esto porque nunca lo he hecho, pero la API nativa de NT tiene la capacidad de bifurcar un proceso (el subsistema POSIX en Windows necesita esta capacidad; no estoy seguro de si el subsistema POSIX incluso es más compatible).

Una búsqueda de ZwCreateProcess () debería proporcionarle más detalles, por ejemplo, este bit de información de Maxim Shatskih :

El parámetro más importante aquí es SectionHandle. Si este parámetro es NULL, el núcleo bifurcará el proceso actual. De lo contrario, este parámetro debe ser un identificador del objeto de sección SEC_IMAGE creado en el archivo EXE antes de llamar a ZwCreateProcess ().

Sin embargo, tenga en cuenta que Corinna Vinschen indica que Cygwin encontró que usar ZwCreateProcess () todavía no es confiable :

Iker Arizmendi escribió:

> 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 es bastante antiguo, más o menos 10 años. Si bien todavía estamos usando llamadas Win32 para emular fork, el método ha cambiado notablemente. Especialmente, ya no creamos el proceso hijo en el estado suspendido, a menos que los datos específicos necesiten un manejo especial en el padre antes de que se copien en el niño. En la versión actual 1.5.25, el único caso para un niño suspendido son los enchufes abiertos en el padre. La próxima versión 1.7.0 no se suspenderá en absoluto.

Una razón para no usar ZwCreateProcess fue que hasta la versión 1.5.25 todavía admitimos usuarios de Windows 9x. Sin embargo, dos intentos de usar ZwCreateProcess en sistemas basados ​​en NT fallaron por una razón u otra.

Sería realmente bueno si este material fuera mejor o estuviera documentado, especialmente un par de estructuras de datos y cómo conectar un proceso a un subsistema. Si bien fork no es un concepto de Win32, no veo que sea malo hacer que fork sea más fácil de implementar.

Michael Burr
fuente
Esta es la respuesta incorrecta. CreateProcess () y CreateThread () son los equivalentes generales.
Foredecker
2
Interix está disponible en Windows Vista Enterprise / Ultimate como "Subsistema para aplicaciones UNIX": en.wikipedia.org/wiki/Interix
bk1e
15
@Foredecker: esta puede ser una respuesta incorrecta, pero CreateProcess () / CreateThread () también podría estar equivocado. Depende de si uno está buscando 'la forma Win32 de hacer las cosas' o 'la semántica de fork () lo más cerca posible'. CreateProcess () se comporta de manera significativamente diferente a fork (), que es la razón por la que cygwin necesitaba hacer mucho trabajo para soportarlo.
Michael Burr
1
@ Jon: He intentado arreglar los enlaces y copiar el texto relevante en la respuesta (para que los enlaces rotos futuros no sean un problema). Sin embargo, esta respuesta es de hace tanto tiempo que no estoy 100% seguro de que la cita que encontré hoy es a lo que me refería en 2009.
Michael Burr
44
Si la gente quiere " forkcon inmediato exec", entonces quizás CreateProcess sea un candidato. Pero forksin a execmenudo es deseable y esto es lo que impulsa a las personas a pedir un real fork.
Aaron McDaid el
37

Bueno, Windows realmente no tiene nada parecido. Especialmente porque fork puede usarse para crear conceptualmente un hilo o un proceso en * nix.

Entonces, tendría que decir:

CreateProcess()/ /CreateProcessEx()

y

CreateThread()(He escuchado que para aplicaciones C, _beginthreadex()es mejor).

Evan Teran
fuente
17

La gente ha intentado implementar fork en Windows. Esto es lo más cercano que puedo encontrar:

Tomado 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
fuente
44
Tenga en cuenta que falta la mayor parte de la comprobación de errores, por ejemplo, ZwCreateThread devuelve un valor NTSTATUS que puede verificarse utilizando las macros SUCCEEDED y FAILED.
BCran
1
¿Qué sucede si se forkbloquea, se bloquea el programa o el hilo simplemente se bloquea? Si se bloquea el programa, entonces esto no es realmente bifurcación. Solo por curiosidad, porque estoy buscando una solución real y espero que esta sea una alternativa decente.
leetNightshade
1
Me gustaría señalar que hay un error en el código proporcionado. haveLoadedFunctionsForFork es una función global en el encabezado, pero una función estática en el archivo c. Ambos deberían ser globales. Y actualmente la bifurcación se bloquea, agregando comprobación de errores ahora.
leetNightshade
El sitio está muerto y no sé cómo puedo compilar el ejemplo en mi propio sistema. Supongo que me faltan algunos encabezados o incluir los incorrectos, ¿verdad? (el ejemplo no los muestra.)
Paul Stelian
6

Antes de que Microsoft presentara su nueva opción "Subsistema Linux para Windows", CreateProcess()era lo más parecido a lo que Windows tenía que hacer fork(), pero Windows requiere que especifique un ejecutable para ejecutarlo en ese proceso.

La creación del proceso UNIX es bastante diferente a la de Windows. Su fork()llamada básicamente duplica el proceso actual casi en total, cada uno en su propio espacio de direcciones, y continúa ejecutándolos por separado. Si bien los procesos en sí son diferentes, todavía ejecutan el mismo programa. Vea aquí para una buena descripción del fork/execmodelo.

Volviendo al otro lado, el equivalente de Windows CreateProcess()es el fork()/exec() par de funciones en UNIX.

Si estaba transfiriendo software a Windows y no le importaba una capa de traducción, Cygwin le brindó la capacidad que deseaba, pero era bastante burda.

Por supuesto, con el nuevo subsistema de Linux , lo más parecido a Windows tiene fork()es en realidad fork() :-)

paxdiablo
fuente
2
Entonces, dado WSL, ¿puedo usarlo forken una aplicación promedio que no sea WSL?
César
6

El siguiente documento proporciona información sobre el código de portabilidad de UNIX a Win32: https://msdn.microsoft.com/en-us/library/y23kc048.aspx

Entre otras cosas, indica que el modelo de proceso es bastante diferente entre los dos sistemas y recomienda considerar CreateProcess y CreateThread donde se requiere un comportamiento similar a fork ().

Brandon E Taylor
fuente
4

"Tan pronto como desee acceder a los archivos o imprimir, entonces se rechazarán".

  • No puede tener su pastel y comérselo también ... en msvcrt.dll, printf () se basa en la API de la consola, que en sí misma utiliza lpc para comunicarse con el subsistema de la consola (csrss.exe). La conexión con csrss se inicia al inicio del proceso, lo que significa que cualquier proceso que comience su ejecución "en el medio" tendrá ese paso omitido. A menos que tenga acceso al código fuente del sistema operativo, entonces no tiene sentido intentar conectarse a csrss manualmente. En su lugar, debe crear su propio subsistema y, en consecuencia, evitar las funciones de la consola en aplicaciones que usan fork ().

  • una vez que haya implementado su propio subsistema, no olvide duplicar también todos los identificadores del padre para el proceso secundario ;-)

"Además, probablemente no deberías usar las funciones Zw * a menos que estés en modo kernel, probablemente deberías usar las funciones Nt * en su lugar".

  • Esto es incorrecto. Cuando se accede en modo de usuario, no hay absolutamente ninguna diferencia entre Zw *** Nt ***; estos son simplemente dos nombres exportados diferentes (ntdll.dll) que se refieren a la misma dirección virtual (relativa).

ZwGetContextThread (NtCurrentThread (), & context);

  • obtener el contexto del subproceso actual (en ejecución) llamando a ZwGetContextThread es incorrecto, es probable que se bloquee y (debido a la llamada adicional al sistema) tampoco es la forma más rápida de realizar la tarea.
user3502619
fuente
2
Esto no parece estar respondiendo a la pregunta principal, sino respondiendo a algunas otras respuestas diferentes, y probablemente sería mejor responder directamente a cada una para mayor claridad y para que sea más fácil seguir lo que está sucediendo.
Leigh
parece estar asumiendo que printf siempre escribe en la consola.
Jasen
3

la semántica fork () es necesaria cuando el niño necesita acceso al estado de memoria real del padre a partir del momento en que se llama fork (). Tengo un software que se basa en el mutex implícito de la copia de memoria a partir de la llamada fork instantánea (), lo que hace que los hilos sean imposibles de usar. (Esto se emula en las plataformas modernas * nix a través de la semántica copiar-escribir-escribir / actualizar-tabla-memoria).

Lo más cercano que existe en Windows como syscall es CreateProcess. Lo mejor que se puede hacer es que el padre congele todos los otros subprocesos durante el tiempo que está copiando la memoria en el espacio de memoria del nuevo proceso, luego descongelarlos. Ni la clase Cygwin frok [sic] ni el código Scilab que Eric des Courtis publicó hace el congelamiento de hilos, que puedo ver.

Además, probablemente no debería usar las funciones Zw * a menos que esté en modo kernel, probablemente debería usar las funciones Nt * en su lugar. Hay una rama adicional que verifica si está en modo kernel y, si no, realiza todas las verificaciones de límites y la verificación de parámetros que Nt * siempre hace. Por lo tanto, es un poco menos eficiente llamarlos desde el modo de usuario.

sjcaged
fuente
Información muy interesante sobre los símbolos exportados Zw *, gracias.
Andon M. Coleman
Tenga en cuenta que las funciones Zw * del espacio del usuario todavía se asignan a las funciones Nt * en el espacio del kernel, por seguridad. O por lo menos ellos deberian.
Paul Stelian
2

No hay una manera fácil de emular fork () en Windows.

Te sugiero que uses hilos en su lugar.

VVS
fuente
Bueno, para ser justos, la implementación forkfue exactamente lo que hizo CygWin. Pero, si alguna vez leíste cómo lo hicieron, tu "no es una manera fácil" es un gran malentendido :-)
paxdiablo 05 de
2

Como han mencionado otras respuestas, NT (el núcleo subyacente a las versiones modernas de Windows) tiene un equivalente de Unix fork (). Ese no es el problema.

El problema es que clonar el estado completo de un proceso generalmente no es algo sensato. Esto es tan cierto en el mundo de Unix como lo es en Windows, pero en el mundo de Unix, fork () se usa todo el tiempo y las bibliotecas están diseñadas para manejarlo. Las bibliotecas de Windows no lo son.

Por ejemplo, las DLL del sistema kernel32.dll y user32.dll mantienen una conexión privada al proceso del servidor Win32 csrss.exe. Después de una bifurcación, hay dos procesos en el extremo del cliente de esa conexión, lo que causará problemas. El proceso secundario debe informar a csrss.exe de su existencia y establecer una nueva conexión, pero no hay una interfaz para hacerlo, porque estas bibliotecas no se diseñaron teniendo en cuenta fork ().

Entonces tienes dos opciones. Una es prohibir el uso de kernel32 y user32 y otras bibliotecas que no están diseñadas para ser bifurcadas, incluidas las bibliotecas que se vinculan directa o indirectamente con kernel32 o user32, que es prácticamente todas ellas. Esto significa que no puede interactuar con el escritorio de Windows en absoluto, y está atascado en su propio mundo de Unixy. Este es el enfoque adoptado por los diversos subsistemas de Unix para NT.

La otra opción es recurrir a algún tipo de truco horrible para tratar de hacer que las bibliotecas inconscientes funcionen con fork (). Eso es lo que hace Cygwin. Crea un nuevo proceso, le permite inicializarse (incluido el registro con csrss.exe), luego copia la mayor parte del estado dinámico del proceso anterior y espera lo mejor. Me sorprende que esto alguna vez funcione . Ciertamente no funciona de manera confiable, incluso si no falla aleatoriamente debido a un conflicto de espacio de direcciones, cualquier biblioteca que esté usando puede quedar silenciosamente en un estado roto. La afirmación de la respuesta actual aceptada de que Cygwin tiene un "tenedor con todas las funciones ()" es ... dudosa.

Resumen: en un entorno similar a Interix, puede bifurcar llamando a fork (). De lo contrario, intenta alejarte del deseo de hacerlo. Incluso si está apuntando a Cygwin, no use fork () a menos que sea absolutamente necesario.

benrg
fuente