Qué queda después de que muere el padre — mantener los procesos hijos en un Job Object

· Actualizado el: · · Windows, Desarrollo en Windows, C#, C++, Win32 API, Medición y control

Historial de revisiones (primera versión, publicada el 29 Aug 2026)
Primera publicación

Terminó la IU en el Administrador de tareas y, aun así, la cámara no se puede volver a abrir. La aplicación de supervisión ya no está y, sin embargo, el ayudante del SDK sigue sosteniendo el puerto COM. Reiniciar el padre produce una segunda instancia, y aparecen problemas en la memoria compartida y las canalizaciones con nombre. Una vez que aísla un SDK de dispositivo en un proceso aparte, se encuentra con estos fallos «después de que muere el padre».

El punto de partida para pensar la causa es que en Windows, los procesos hijos y nietos no terminan automáticamente cuando el proceso padre sale. La relación padre-hijo en el arranque y la gestión de la vida a la salida son dos cosas distintas. La herramienta que cubre este hueco es el Job Object.

Este artículo confirma primero por qué el tratamiento de salida del padre por sí solo no basta, y luego organiza cómo meter procesos en un Job, la política de terminación y el método de supervisión. Por último, lo conecta con los fallos que ocurren en la integración de dispositivos y con el procedimiento de investigación.

Los lectores previstos son desarrolladores de WinForms / WPF / servicios que aíslan un SDK de dispositivo en un proceso aparte. El entorno previo es Windows 10/11 (para las partes que usan Nested Jobs y PROC_THREAD_ATTRIBUTE_JOB_LIST), y el código se muestra en C++ (Win32 API) y C# (.NET 6 o posterior). La dificultad es intermedia.

Este artículo prolonga los artículos «No responde», apagado, suspensión/reanudación y canalizaciones con nombre, y trata la vida fuera del proceso.

1. Primero la conclusión

El punto de partida del diseño es decidir «qué no debe quedar atrás y qué quiere conservar» antes de decidir «cómo terminar».

Un Job Object es un mecanismo que convierte un árbol de procesos en una unidad. Sin embargo, terminar a la fuerza a los descendientes en el momento en que el padre desaparece y conservar los volcados o el estado final de esos descendientes no van, tal cual, juntos. Decida si está recuperando el dispositivo que sostiene el proceso o si está preservando primero el material de diagnóstico.

  • La unidad que gestiona la vida es el Job, no el linaje padre-hijo. Adjunta límites, notificaciones y terminación en bloque a un grupo de procesos. Una vez asignado, un proceso no puede salir hasta que termina, y a partir de Windows 8 los Jobs pueden ser Nested Jobs.12
  • Fije la pertenencia al Job antes de que el hijo se ejecute. Si hace Assign después del arranque, pierde a los nietos nacidos en el intervalo. CREATE_SUSPENDED y JOB_LIST en la creación cierran ventanas de carrera distintas (capítulo 4).
  • La recuperación automática y la conservación de información de diagnóstico se eligen como política de terminación. La condición de disparo de KillOnJobClose es «se cierra el último handle de Job». Es fuerte contra una caída del padre pero débil para el análisis post mortem de los descendientes que se terminan a la fuerza con él, así que si necesita ambos, el lado de supervisión recoge primero y termina después (capítulo 5).3

Lo que quiere una aplicación de medición no es matar en sí. Es no dejar un proceso que sostiene el dispositivo, y poder observar las salidas anómalas.

Lo que quiere saber Capítulos que leer
Por qué el tratamiento de salida del padre por sí solo no basta Capítulos 2–3: la relación padre-hijo y el papel del Job
Cómo gestionar a los descendientes sin perder a ninguno Capítulos 4–6: creación, política de terminación, supervisión
Qué se convierte en un problema con SDK y servicios Capítulos 7–9: casos de fallo, límites de recursos, Nested Jobs y breakaway
Qué investigar en el terreno, y cómo elegir Capítulos 10–11: procedimiento de investigación y tabla de decisión

El mapa de conocimiento de debajo sirve para revisar cómo se relacionan los elementos. Si prefiere partir del mecanismo, siga al capítulo 2.

En el diagrama, una línea continua marca una relación que siempre se cumple y una línea discontinua una relación condicional (las condiciones están en la explicación de cada relación en la página de detalle). La lista completa de relaciones (21 en total, con evidencia y grado de certeza) y las definiciones de los conceptos principales están reunidas en la página de detalle del mapa de conocimiento (en japonés). Datos: JSON-LD / Turtle

2. Por qué WaitForExit no basta

«Esperar» una salida y atar las vidas son cosas distintas

Process.WaitForExit() es una API para que «el padre espere a que el hijo salga». Lo que este artículo considera es la dirección opuesta: qué hacer con el hijo cuando el padre muere primero. Con la sola relación padre-hijo de Windows, la salida del padre no se transmite al hijo.

Process.Kill() y CloseMainWindow() tampoco se ocupan, por sí solos, de los procesos nietos ni de los handles de dispositivo que esos nietos sostienen. Los handles del proceso que salió se liberan, pero el problema son los handles que sostienen los descendientes que sobrevivieron.

Kill(entireProcessTree: true) de .NET recorre a los descendientes y los termina, pero puede perder procesos creados durante la enumeración o después de que el padre muriera primero, y nunca se llama una vez que el propio padre se ha caído. Necesita un mecanismo que no dependa solo del código de limpieza del padre.

Para cada una de las razones principales por las que un padre sale, esto es lo que queda en el terreno.

Por qué muere el padre Qué le ocurre al hijo Qué queda en el terreno
IU cerrada con el botón X, tratamiento de salida incompleto Nada (el objeto Process simplemente se libera) Proceso ayudante, bloqueo de la cámara
Solo el padre matado en el Administrador de tareas El hijo sigue viviendo Puerto COM, USB, memoria compartida
Caída por una excepción no controlada No hay garantía de que se ejecute el finally del padre Archivos temporales, bloqueos exclusivos
Tiempo de espera al detener el servicio El SCM solo se ocupa del padre Hijos que quedan en la sesión 0
El desfase entre la vida del padre y la vida de ocupación del dispositivoCuando el proceso padre sale, desaparecen la espera del padre y su objeto Process, pero los procesos hijos y nietos siguen viviendo, y también permanece su retención de handles de dispositivo, canalizaciones con nombre y archivos de bloqueoEl proceso padre saleSe va: espera del padre, objeto ProcessQueda: procesos hijos y nietosRetención de handles de dispositivoLado servidor de las canalizaciones con nombreArchivos de bloqueo, memoria compartida

Figura 1: La vida del padre y la vida de ocupación del dispositivo están desfasadas. El código de limpieza del padre es el que menos corre precisamente cuando el padre muere de una muerte anómala.

El objetivo es el caso en que «el sistema operativo está en marcha y solo salió el padre»

Los flujos en los que el apagado o la suspensión detienen todo el sistema operativo pertenecen al artículo sobre el apagado y al artículo de suspensión/reanudación. Este artículo trata el caso en que el sistema operativo sigue sano y solo sale el padre. En entornos de medición este es el caso más frecuente, y los procesos residuales son más difíciles de notar.

3. Qué es un Job Object

Las operaciones básicas son crear, asignar, establecer y consultar

Un Job Object es un objeto del kernel que gestiona un grupo de procesos como una unidad. Dividir las operaciones básicas por papel da las cuatro siguientes.14

API Papel
CreateJobObject Crear un Job al que aún no pertenece ningún proceso
AssignProcessToJobObject Asignar un proceso al Job
SetInformationJobObject Establecer límites y otros ajustes
QueryInformationJobObject Leer información de contabilidad como tiempo de CPU, errores de página y recuento de procesos

La pertenencia es irreversible; un proceso no puede salir hasta que termina. Además, la información de contabilidad incluye los totales acumulados por procesos que ya han salido.

Un hijo que un proceso miembro crea con CreateProcess pertenece al mismo Job por defecto. En otras palabras, el núcleo del valor de un Job es que nietos y bisnietos entran automáticamente.1 Los caminos que escapan a la pertenencia, como el breakaway y los arranques por poder mediante WMI, se examinan por separado en el capítulo 9.

Estructura básica de un Job ObjectUn hijo y un nieto pertenecen al Job Object que creó el proceso padre, y el Job aplica límites, envía notificaciones a un puerto de finalización y realiza terminación en bloque por árbol de procesosProceso padreJob ObjectHijo (host del SDK de dispositivo)Nieto (ayudante del fabricante)Límites (memoria, CPU)Notificaciones (puerto de finalización)Terminación en bloque

Figura 2: Un Job es un contenedor que ofrece tres cosas por árbol de procesos: límites, notificaciones y terminación en bloque.

Diferencias de generación del sistema operativo, y en qué se distingue de un entorno aislado

En Windows 7 y anteriores un proceso solo podía pertenecer a un job; a partir de Windows 8 se hicieron posibles los Nested Jobs (pertenencia múltiple).5 El cuerpo de este artículo supone Windows 10/11; los puntos a vigilar en Windows 7 y anteriores se cubren en el capítulo 9 y en las FAQ.

Meter un proceso en un Job no lo convierte en un contenedor ni en un entorno aislado. El acceso a la red no se puede restringir, y el token de acceso (los privilegios) es un mecanismo aparte. Las restricciones de IU por sí solas tampoco pueden crear un límite de seguridad. El papel en este artículo es estrictamente hacer de la vida y los recursos de un árbol de procesos una unidad.

4. La forma correcta de entrar — La carrera entre creación y asignación

Si hace Assign después del arranque, nacen nietos en el intervalo

Un proceso hijo puede engendrar nietos en los primeros milisegundos después de empezar a ejecutarse. Un SDK que lanza su ayudante es el caso típico. Si obtiene el PID después de Process.Start() y luego hace Assign, cualquier nieto nacido antes del Assign acaba fuera del Job.

Asignar después de que el hijo empieza a ejecutarse pierde a los nietosEl hijo ya se está ejecutando justo después de Process.Start, y cualquier nieto que engendra durante la ventana de carrera antes de llamar a AssignProcessToJobObject acaba fuera del JobEl hijo arranca en Process.StartVentana de carrera hasta AssignNietos nacidos en esta ventanaSiguen ejecutándose fuera del JobAssignProcessToJobObjectSolo entran los nietos nacidos después

Figura 3: La ventana de carrera puede ser de solo unos milisegundos, pero el arranque del ayudante del SDK ocurre exactamente ahí.

Procedimiento A: crear suspendido, asignar y luego ejecutar

El método clásico con la compatibilidad más amplia usa CREATE_SUSPENDED. Cierra la ventana en la que el hijo se ejecuta primero y engendra nietos con el siguiente orden.67

  1. Crear el Job con CreateJobObject
  2. Establecer los límites primero con SetInformationJobObject
  3. Llamar a CreateProcess con CREATE_SUSPENDED (el hilo inicial no se ejecuta)
  4. Meterlo con AssignProcessToJobObject
  5. Si eso falla, no haga Resume; llame a TerminateProcess en el acto (no deje que se ejecute ni una instrucción fuera del Job)
  6. Ejecutarlo con ResumeThread

Lo que cierra el procedimiento A es la ventana de carrera contra la creación de nietos. La ventana contra una caída del propio padre permanece. Si el padre se cae entre los pasos 3 y 4, queda un hijo suspendido que aún no está en el Job. No se ejecuta, pero tampoco desaparece por sí solo.

Si quiere cerrar esta ventana incluyendo la resiliencia a una caída del padre, use el procedimiento B de debajo.

Procedimiento para arrancar con SUSPENDED y luego meter al hijo en el JobCrear el Job y establecer límites, arrancar al hijo con CREATE_SUSPENDED, asignarlo con AssignProcessToJobObject, detenerlo con TerminateProcess sin reanudar si eso falla, y ejecutarlo con ResumeThread si tiene éxitoéxitofalloCrear el Job con CreateJobObjectLímites con SetInformationJobObjectArrancar al hijo con CREATE_SUSPENDEDAssignProcessToJobObjectEjecutar con ResumeThreadTerminar de inmediato, sin Resume

Figura 4: El esqueleto del procedimiento A. Una implementación que omite la rama «si falla, no lo ejecute» produce un proceso suelto solo cuando las cosas van mal.

// C++: el núcleo mínimo del procedimiento A (el tratamiento de errores es solo esqueleto)
HANDLE job = CreateJobObjectW(nullptr, nullptr);   // Sin nombre vale. No lo haga heredable
if (!job) return HRESULT_FROM_WIN32(GetLastError());

JOBOBJECT_EXTENDED_LIMIT_INFORMATION limits = {};
limits.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE;
if (!SetInformationJobObject(job, JobObjectExtendedLimitInformation,
                             &limits, sizeof(limits))) {
    DWORD err = GetLastError();         // Guardarlo antes de que CloseHandle lo sobrescriba
    CloseHandle(job);                   // No ejecutar al hijo en un Job sin límites
    return HRESULT_FROM_WIN32(err);
}

STARTUPINFOW si = { sizeof(si) };
PROCESS_INFORMATION pi = {};
if (!CreateProcessW(exePath, cmdline, nullptr, nullptr, FALSE,
                    CREATE_SUSPENDED, nullptr, nullptr, &si, &pi)) {
    DWORD err = GetLastError();
    CloseHandle(job);                   // No filtrar el handle de Job en reintentos de arranque
    return HRESULT_FROM_WIN32(err);
}

if (!AssignProcessToJobObject(job, pi.hProcess)) {
    DWORD err = GetLastError();         // Guardarlo antes de que Terminate lo sobrescriba
    TerminateProcess(pi.hProcess, 1);   // No dejarlo ejecutarse fuera del Job
    // Cerrar los handles y elevar err como el error
} else if (ResumeThread(pi.hThread) == (DWORD)-1) {
    DWORD err = GetLastError();
    TerminateProcess(pi.hProcess, 1);   // No dejarlo atrás suspendido
    // Cerrar los handles y elevar err como el error
}
CloseHandle(pi.hThread);
// En caso de éxito, la propiedad de job y pi.hProcess pasa al objeto de
// gestión de vida del llamador (el equivalente del contenedor C# del capítulo 4).
// Cerrar job dispara KillOnJobClose, y pi.hProcess se usa en el capítulo 6
// para resolver «quién murió»

Procedimiento B: en Windows 10 y posteriores, crear el proceso ya dentro del Job

Ponga el handle de Job en la lista de atributos de STARTUPINFOEX con PROC_THREAD_ATTRIBUTE_JOB_LIST. Luego pásela a CreateProcess con la marca EXTENDED_STARTUPINFO_PRESENT. Sin la marca, la estructura no se interpreta como la extendida y los atributos se ignoran.89

Con este método, el proceso pertenece al Job antes de que se ejecute su hilo inicial. La ventana de carrera de «creado, pero aún no miembro» ya no existe en absoluto, así que no hacen falta ni SUSPENDED ni la rama que hace Assign después de la creación y trata el fallo.

Método Cuándo queda fijada la pertenencia Advertencias restantes
Procedimiento A: SUSPENDED → Assign → Resume Después de crear al hijo, antes de que se ejecute su hilo inicial Si el padre se cae antes del Assign, queda un hijo detenido
Procedimiento B: crear con JOB_LIST En la creación del proceso Requiere Windows 10 o posterior. Indicar la lista de atributos y la marca de arranque extendido
En qué se diferencian las ventanas de carrera de los procedimientos A y BEl procedimiento A crea el proceso suspendido y luego hace Assign y Resume, así que necesita una rama que termina si falla, mientras que el procedimiento B crea el proceso con el Job en la lista de atributos, así que ya es miembro en el momento de nacer y no tiene ni ventana de carrera ni rama de falloProcedimiento A: crear suspendidoUnirse con AssignArrancar con ResumeRama de fallo obligatoriaProcedimiento B: crear con lista de atributosMiembro en el momento de nacerSin ventana de carrera ni rama de fallo

Figura 5: El procedimiento A es «meterlo y luego ejecutarlo»; el procedimiento B es «nace ya dentro». Si puede suponer Windows 10 o posterior, la razón para elegirlo es precisamente la presencia o ausencia de la ventana de carrera y de la rama de fallo.

Tres implementaciones que evitar en cualquiera de los dos procedimientos

  • Obtener el PID después de Process.Start() y luego meterlo (los nietos salen primero)
  • Dejar que el hijo herede el handle de Job (el hijo sigue sosteniendo el handle incluso después de que muere el padre, así que KillOnJobClose deja de dispararse — capítulo 5)
  • Tragar un fallo de Assign y seguir operando (un proceso de dispositivo fuera del Job es lo que impulsa el siguiente fallo)

En .NET, expresar al propietario del handle de Job en el código

System.Diagnostics.Process no tiene el concepto de Job, y no hay un contenedor oficial. Escriba un contenedor delgado con P/Invoke o CsWin32.

El punto es envolver el handle de Job en un SafeHandle y hacerlo IDisposable. Cerrar el último handle de Job en Dispose() dispara KillOnJobClose. La intención de diseño de que «la vida del contenedor es la vida del árbol hijo» se puede expresar como propiedad.

Debajo hay un esqueleto que muestra la propiedad del handle. La creación se hace en el lado P/Invoke de los procedimientos A/B, y un proyecto real también necesita configuración de CsWin32, designaciones unsafe, etc.

// C#: un contenedor delgado responsable solo de poseer el handle de Job (la creación pasa por el P/Invoke de los procedimientos A/B)
sealed class ChildProcessJob : IDisposable
{
    private readonly SafeFileHandle _job;    // Seguir sosteniéndolo en un campo

    public ChildProcessJob()
    {
        _job = PInvoke.CreateJobObject(default, null);
        if (_job.IsInvalid) throw new Win32Exception();
        var limits = new JOBOBJECT_EXTENDED_LIMIT_INFORMATION();
        limits.BasicLimitInformation.LimitFlags =
            JOB_OBJECT_LIMIT.JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE;
        if (!PInvoke.SetInformationJobObject(_job,
            JOBOBJECTINFOCLASS.JobObjectExtendedLimitInformation,
            &limits, (uint)sizeof(JOBOBJECT_EXTENDED_LIMIT_INFORMATION)))
        {
            int err = Marshal.GetLastWin32Error();  // Guardarlo antes de Dispose
            _job.Dispose();              // No entregar un Job sin KillOnJobClose
            throw new Win32Exception(err);
        }
    }

    public void Dispose() => _job.Dispose();  // El árbol de debajo termina aquí
}

A la inversa, si cierra el handle sin pretender cerrarlo, termina el árbol hijo. Conserve una referencia al contenedor durante toda la vida del padre. Si no lo hace, el árbol hijo se borra sin motivo en el momento en que el GC recoge el SafeHandle.

5. KillOnJobClose — «mantener» y «morir juntos»

El disparo es «el cierre del último handle», no «la muerte del padre»

JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE es una marca de límite que termina cada proceso bajo el Job cuando se cierra el último handle de Job.3

Tanto si el padre sale por una excepción, lo matan en el Administrador de tareas o se detiene a la fuerza como servicio, el kernel cierra todos los handles de ese proceso. Si ese era el último handle de Job, los hijos y nietos terminan. La fuerza de este mecanismo es que no supone que se ejecute el tratamiento de salida del padre.10

Sin embargo, si deja que el hijo herede el handle de Job, el handle permanece después de que el padre sale. En ese caso no es «el último handle», y el árbol hijo no termina.

Línea de tiempo de KillOnJobCloseTanto si el padre sale con normalidad, se cae o se termina a la fuerza, el kernel cierra todos los handles del padre, y si ese era el último handle de Job el Job se cierra y el árbol de procesos de debajo se termina de una vezNo (heredado)El padre desaparece (incluida una caída)El kernel cierra todos los handles¿Último handle de Job?Terminar todo el árbol de debajoLos hijos siguen viviendo

Figura 6: El disparo es «se cerró el último handle», no «murió el padre». Por eso el handle no debe heredarlo el hijo.

Elegir entre recuperar de inmediato y preservar el material de diagnóstico

Lo que pierde con una terminación forzada no es solo la retención del dispositivo. También pierde la ocasión de tomar un volcado de memoria de los hijos y nietos que se terminan con el padre, el último fotograma válido y la ocasión de vaciar un archivo de medición a medio escribir.

El volcado del propio padre es otro asunto. WER trata la excepción no controlada mientras el padre sigue vivo, así que el volcado del padre se puede escribir antes de que se cierren los handles. De lo que se trata aquí es el material post mortem de los hijos y nietos del lado que se termina a la fuerza.

Política Adecuada para Lo que pierde
Con KillOnJobClose Sitios donde una doble apertura del dispositivo es el peor resultado Material post mortem, las últimas muestras
Sin (solo supervisión) Sitios donde los volcados y registros son un activo Procesos huérfanos y puertos retenidos si se descuida
Sin, más un proceso de supervisión que llama a TerminateJobObject Cuando existe un servicio de control aparte La implementación se duplica

Como entrada para la decisión, liste primero «qué no debe quedar atrás» y «qué quiere conservar».

Qué no debe quedar atrás: una cámara o un digitalizador abiertos, el uso exclusivo de un puerto serie o USB, el lado servidor de una canalización con nombre, una sesión de dongle de licencia, memoria compartida y archivos de bloqueo.

Qué quiere conservar: volcados de memoria, el último fotograma o contadores válidos, la ocasión de enviar un comando que devuelva el dispositivo a un estado seguro (si es posible, enviarlo antes de matar).

¿Matar, o solo supervisar?Si una doble apertura del dispositivo es el peor resultado, establezca KillOnJobClose; si los volcados y el último fotograma son un activo, no lo establezca y supervise en su lugar; si existe un servicio de control aparte, llame a TerminateJobObject desde ahíLiberar el dispositivo va primeroLos volcados y el estado final son un activoExiste un servicio de control aparte¿Qué protege en el momento en que muere?Con KillOnJobCloseSolo supervisar (no matar)El lado de supervisión llama a TerminateJobObject

Figura 7: Elija según «qué protege en el momento en que muere», no según «matar o no». Si quiere ambos, acaba en el arreglo de la tercera fila, en el que el lado de supervisión toma primero el volcado y luego desmonta el árbol.

Entregar el handle de Job al lado de supervisión mientras el padre está vivo

La tercera fila de la tabla, el arreglo en el que un proceso de supervisión termina el árbol con TerminateJobObject, necesita preparación. El lado de supervisión debe obtener el handle de Job antes de que muera el padre.

El Job que crea el procedimiento de este artículo no tiene nombre, así que no hay forma de alcanzarlo desde fuera una vez que el padre se ha ido. Hay dos formas de entregarlo.

Método Qué hacer mientras el padre está vivo
Duplicar el handle del Job sin nombre Pasar el handle al proceso de supervisión con DuplicateHandle
Usar un Job con nombre Crearlo con nombre desde el principio; el lado de supervisión lo abre con OpenJobObject y lo sostiene

Como los nombres pueden chocar de forma global, incluya un GUID único o similar. Si olvida esta preparación, el lado de supervisión no tiene medio de terminar el árbol ni siquiera cuando detecta la anomalía del padre.

Tomar volcados y poner el dispositivo en estado seguro antes de la terminación forzada

Un hijo terminado por KillOnJobClose no recibe aviso, igual que con TerminateProcess. No se produce ninguna excepción no controlada, así que aunque WER (LocalDumps) esté configurado en el hijo, no queda ningún volcado de esa terminación forzada. Lo que WER puede atrapar es el caso en que el hijo sale por su propia caída.

Si el requisito es «tanto recuperación automática como volcados», mueva la responsabilidad de terminar al lado de supervisión. El orden es: tomar el volcado mientras el objetivo está vivo, devolver el dispositivo a un estado seguro si hace falta y, por último, terminar con TerminateJobObject.

Desmontar de modo que coexistan la recuperación automática y los volcadosCuando el proceso de supervisión detecta una anomalía, toma primero un volcado, envía si hace falta un comando que devuelve el dispositivo a un estado seguro y, por último, desmonta el árbol con TerminateJobObject, de modo que coexistan la recuperación automática y el análisis post mortemEl lado de supervisión detecta una anomalíaTomar primero el volcadoDevolver el dispositivo a un estado seguroDesmontar con TerminateJobObject

Figura 8: La única respuesta a «tanto recuperación automática como volcados». Invierta el orden y el objetivo a volcar ya no existe.

En un arreglo en el que KillOnJobClose termina a la fuerza al hijo en el momento en que desaparece el padre, el hijo tampoco tiene ocasión de enviar un comando «devolver el dispositivo a un estado seguro». Para dispositivos que lo necesitan, elija el arreglo de la tercera fila y haga que el lado de supervisión envíe primero el comando de puesta en seguro y luego llame a TerminateJobObject.11

6. Esperar «vacío» con un puerto de finalización

Esperar solo en el handle de Job no puede confirmar que el árbol ha salido

El handle de Job no pasa a estado señalado cuando han salido todos los procesos de debajo. Solo se señala cuando todos los procesos se terminaron porque se superó el límite de tiempo del job.12

Para «seguir adelante una vez que el árbol hijo está vacío», asocie un puerto de finalización de E/S (IOCP) al Job.1314 Haga la asociación mientras el Job está vacío, antes de meter ningún proceso. Si la asocia a mitad de camino, puede perder las notificaciones de procesos cuyo estado cambió durante la asociación.15

Observar creación, salida, anomalía y cero con cuatro mensajes

Mensaje Lo que le dice
JOB_OBJECT_MSG_NEW_PROCESS Un proceso se unió al Job. También detecta la creación de nietos
JOB_OBJECT_MSG_EXIT_PROCESS Un proceso salió
JOB_OBJECT_MSG_ABNORMAL_EXIT_PROCESS Un proceso salió con un código de salida anómalo, como una violación de acceso. Especialmente importante en aplicaciones de medición13
JOB_OBJECT_MSG_ACTIVE_PROCESS_ZERO El recuento de procesos activos llegó a 0

Todo lo que lleva un paquete NEW_PROCESS es el PID nuevo. No le dice la relación padre-hijo, es decir, quién engendró el proceso. Si necesita el linaje, añada otro medio como ETW.

Flujo de las notificaciones del puerto de finalizaciónEl Job deposita mensajes de creación, salida, salida anómala y cero en el puerto de finalización, un hilo de supervisión dedicado los recibe con GetQueuedCompletionStatus, y solo los resultados se pasan al hilo de IUHilo de IUHilo de supervisiónPuerto de finalizaciónJob ObjectHilo de IUHilo de supervisiónPuerto de finalizaciónJob ObjectNEW / EXIT_PROCESSABNORMAL_EXIT / ZEROEsperar el paquete de finalizaciónMensaje y PIDPasar solo la notificación de resultado

Figura 9: Ejecute GetQueuedCompletionStatus en un hilo dedicado. Espere en el hilo de IU y la IU pasa a «No responde» cada vez que un hijo se comporta mal.

Esperar en un hilo dedicado y un puerto dedicado, y consultar la contabilidad al agotar el tiempo

Ejecute este bucle de supervisión en un puerto de finalización creado solo para las notificaciones del Job. Si se cuelga del mismo puerto que E/S existentes, el paquete ya se ha extraído para cuando GetQueuedCompletionStatus regresa. Si lo descarta con continue porque la clave difiere, el propietario de esa E/S espera su finalización para siempre. Lo mismo vale para la rama de paquetes fallidos.

Si comparte, necesita un mecanismo aparte que entregue a cada propietario por clave. Este artículo separa los puertos y solo pasa resultados al hilo de IU.

También supone que un Job es una generación de arranque, recreado en cada arranque. Si lo reutiliza entre reintentos, TotalProcesses incluye la generación anterior, y la guarda que distingue un Job vacío antes del arranque deja de funcionar.

// C++: esqueleto del hilo de supervisión (tiempo de espera + contabilidad como seguro contra notificaciones perdidas)
DWORD msg; ULONG_PTR key; LPOVERLAPPED info;
bool treeEmpty = false;
while (!treeEmpty) {
    if (!GetQueuedCompletionStatus(iocp, &msg, &key, &info, 5000)) {
        if (info != nullptr) continue;                // Paquete de finalización de una E/S fallida. Seguir supervisando
        if (GetLastError() != WAIT_TIMEOUT) break;    // Puerto destruido y similares: parar
        JOBOBJECT_BASIC_ACCOUNTING_INFORMATION acct = {};
        if (QueryInformationJobObject(job, JobObjectBasicAccountingInformation,
                                      &acct, sizeof(acct), nullptr))
            treeEmpty = (acct.TotalProcesses > 0 &&   // No tomar un Job vacío previo al arranque por finalización
                         acct.ActiveProcesses == 0);  // Seguro para una notificación ZERO perdida
            // Supuesto: el Job se recrea en cada arranque (1 Job = 1 generación de arranque).
            // Si el Job se reutiliza entre reintentos, TotalProcesses sigue contando la
            // generación anterior, y esta guarda no puede separar las generaciones
        continue;                        // Si falla la consulta, no concluir que está vacío
    }
    if ((HANDLE)key != job) continue;    // Contrastar con el CompletionKey usado en la asociación.
                                         // Se supone que este puerto se crea solo para supervisar el Job
                                         // (véase el texto de debajo. Descartar así en un puerto compartido
                                         //  hace esperar para siempre a los propietarios de otras E/S)
    DWORD pid = (DWORD)(UINT_PTR)info;   // Algunos mensajes llevan un PID
    switch (msg) {
    case JOB_OBJECT_MSG_NEW_PROCESS:          /* registrar la creación de nietos */ break;
    case JOB_OBJECT_MSG_EXIT_PROCESS:         /* registrar la salida */ break;
    case JOB_OBJECT_MSG_ABNORMAL_EXIT_PROCESS:/* salida anómala: ir a comprobar un volcado */ break;
    case JOB_OBJECT_MSG_ACTIVE_PROCESS_ZERO:  treeEmpty = true; break;
    }                                    // El break del switch por sí solo no termina la espera
}

Notificaciones, contabilidad y handles resuelven cada uno información distinta

Las notificaciones no llevan garantía de entrega en principio. Las únicas garantizadas son las notificaciones de los límites definidos con JobObjectNotificationLimitInformation. No puede concluir «no hay notificación = no ocurrió».15

El estado agregado como «¿se ha quedado vacío?» se confirma también sondeando la información de contabilidad. Pero la contabilidad es un conjunto de contadores agregados, así que no puede reconstruir el PID, el código de salida ni si la salida fue anómala de un EXIT/ABNORMAL_EXIT perdido. Eso es el dominio de sostener handles de proceso y de ETW.

ACTIVE_PROCESS_ZERO tampoco es prueba de una salida normal. El recuento puede haber llegado a cero por terminación forzada, y el propio mensaje no distingue. Juzgue la calidad de la salida por EXIT/ABNORMAL_EXIT y el código de salida.

No decida que es el mismo proceso solo por el PID

El PID de un paquete de finalización se reutiliza. A menos que sostenga un handle de proceso, no hay garantía de que el PID siga refiriéndose al mismo proceso.13

Lo que el pi.hProcess sostenido en el capítulo 4 puede clavar es solo el PID del hijo que arrancó directamente. Para los nietos que engendra el SDK, llame a OpenProcess en el momento en que llega NEW_PROCESS para obtener un handle, y a partir de entonces contraste contra ese handle.

Aun así, queda una ventana corta entre la notificación y el Open. Si el PID se reutiliza en ese intervalo, agarra un proceso distinto. Cuando se requiere una identificación estricta del proceso individual, contraste con telemetría que lleve una hora de creación, como los eventos de arranque de proceso de ETW.

Supervisión que no se apoya solo en las notificacionesComo los mensajes del puerto de finalización sirven para notificar y no llevan garantía de entrega, combínelos con el sondeo de la información de contabilidad y con sostener handles de proceso para prepararse ante pérdidas y reutilización de PIDNotificaciones del puerto de finalizaciónSin garantía de entrega (fin de notificación)Sondear también la información de contabilidadLos PID se reutilizanSostener handles de proceso

Figura 10: Las notificaciones son el camino principal; la contabilidad y los handles son el seguro. Solo con ambos puede decir que está «observando».

De paso, circularon implementaciones que «matan con TerminateThread el hilo que espera a que el Job se vacíe», pero con un puerto de finalización no hace falta terminar a la fuerza el hilo de espera. Raymond Chen también ha publicado un artículo que reescribe este patrón antiguo hacia el enfoque del puerto de finalización.16

7. Fallos que ocurren de verdad en medición e integración de dispositivos

Aquí los mecanismos cubiertos hasta ahora se aplican a seis fallos que ocurren en entornos de dispositivos. En cada caso, además de la causa y el remedio, confirmamos «qué quedó atrás».

Fallo 1: solo muere el padre y la cámara sigue abierta

En un montaje en el que el SDK del fabricante tiene un proceso ayudante para la transferencia de fotogramas, matar la IU padre en el Administrador de tareas deja solo al ayudante. El padre reiniciado obtiene device busy durante la inicialización del SDK. En algunos sitios no se recupera nada hasta que se corta la alimentación del dispositivo.

Qué quedó: el proceso ayudante y la apertura exclusiva de la cámara.

Solo muere el padre y la cámara sigue abiertaTerminar a la fuerza la IU quita al padre, pero el proceso ayudante del SDK permanece sosteniendo el handle de la cámara, así que el padre reiniciado falla al reabrir con device busyTerminar la IU en el Administrador de tareasEl padre desapareceEl ayudante del SDK permaneceSigue sosteniendo la cámaradevice busy tras el reinicio

Figura 11: La cara verdadera de «el proceso se ha ido y, aun así, el dispositivo no se puede abrir». El culpable a menudo no es el proceso cuyo nombre se mostró en el Administrador de tareas.

Fallo 2: un nieto acaba fuera del Job

Hay dos caminos, y los remedios deben separarse.

(a) El SDK usa su propio Job. Este es el caso en que la otra parte ya está en un Job distinto. En Windows 7 es un job por proceso, así que su Assign falla. En Windows 8 y posteriores un Nested Job puede recogerlo, pero si su Job tiene restricciones de IU, el Nested Job en sí es imposible (capítulo 9).

(b) El SDK engendra al nieto con CREATE_BREAKAWAY_FROM_JOB. Esto solo funciona cuando su Job permite BREAKAWAY_OK, y el nieto nace fuera del árbol desde el principio. Un Nested Job tampoco puede recogerlo. Si no lo permite, falla la creación del SDK, así que si la supervisión es la prioridad, el enfoque básico es no permitirlo y detectarlo como un fallo.

Qué quedó: un nieto que se ejecuta fuera de la supervisión, y un Assign cuyo fallo se tragó.

Dos caminos por los que un nieto acaba fuera del JobEn el camino en que el SDK usa su propio Job, un Nested Job puede recogerlo en Windows 8 y posteriores pero falla si hay restricciones de IU; en el camino en que el SDK engendra al nieto con breakaway, solo funciona si usted lo permitió y el nieto está fuera del árbol desde el principioEl nieto acaba fuera del Job(a) El SDK usa su propio Job(b) Creado con breakawayWin8+ lo recoge por Nested JobFalla con restricciones de IUSolo funciona si se permiteNieto fuera del árbol desde el principio

Figura 12: El mismo «salir», pero (a) deja margen para recogerlo por Nested Job, mientras que (b) queda zanjado como irrecuperable en el momento en que lo permite. El remedio empieza por identificar el camino.

Fallo 3: un proceso de dispositivo arrancado desde un servicio

Cuando un servicio se detiene, el SCM solo espera al padre; los hijos y nietos engendrados en la sesión 0 quedan fuera del alcance del tratamiento de detención. Job + KillOnJobClose cubre incluso esta zona fuera de alcance. El diseño de un arreglo que abarca un servicio y la sesión interactiva se deja al artículo sobre el límite de usuario.

Qué quedó: procesos residuales en la sesión 0, y un falso positivo en la comprobación de instancia duplicada en el siguiente arranque.

Fallo 4: un hijo que muere un mes después

Si no registra periódicamente la contabilidad del Job (PeakJobMemoryUsed, contadores de E/S, recuento total de procesos), no puede rastrear después «qué generación del ayudante empezó a hincharse, y cuándo».17 La estructura de morir un mes después por una fuga de handles es la diseccionada en el artículo sobre el fallo de larga duración de una cámara industrial, y la contabilidad del Job es el punto de entrada a esa investigación.

Qué quedó: registros insuficientes para resolver la causa.

Fallo 5: el tratamiento de salida del padre espera a que el hijo salga

Si llama a WaitForExit o espera a que el árbol salga en el hilo de IU, el padre pasa a «No responde» el día en que el hijo se congela. Deje la espera de salida al hilo IOCP y ponga en la IU solo el progreso y un botón de abortar. El mecanismo es el descrito en el artículo «No responde».

Qué quedó: un padre colgado junto con el hijo.

No esperar la salida en el hilo de IU del padreEsperar a que el hijo salga en el hilo de IU propaga un cuelgue del hijo al No responde del padre, así que deje la espera de salida al hilo de supervisión IOCP y ponga en el hilo de IU solo una visualización de progreso y un botón de abortarEsperar la salida del hijo en el hilo de IUEl día en que el hijo se cuelgaEl padre también No responde (arrastrado)Esperar en el hilo IOCPLa IU solo tiene progreso y abortar

Figura 13: El lado que observa la anomalía del hijo no debe congelarse por la anomalía del hijo. Solo separar dónde espera quita el cuelgue colateral.

Fallo 6: falla solo bajo un depurador

Las herramientas de desarrollo y los lanzadores pueden haber metido ya su proceso padre en algún Job. En Windows 8 y posteriores un Nested Job suele salvarle, pero en PC de equipo con 7 o anterior el Assign se convierte en ERROR_ACCESS_DENIED, lo que produce diferencias como «solo no funciona en la máquina de desarrollo» o «solo en producción». El primer paso habitual es comprobar la propia pertenencia con IsProcessInJob.18

Qué quedó: tiempo de verificación gastado sin poder clavar la causa de la diferencia de entorno.

8. Qué límites adjuntar

Los límites tienen un propósito y efectos secundarios

Restringiendo la lista a los límites que importan en una aplicación de medición, aquí están las razones para usar cada uno y los efectos secundarios.319

Límite Por qué lo usa Si se pasa
KILL_ON_JOB_CLOSE Liberar el dispositivo cuando desaparece el padre Desaparece el material post mortem
ACTIVE_PROCESS Detener una multiplicación desbocada de los hijos del SDK Incluso a los ayudantes legítimos se les niega la creación
JOB_MEMORY / PROCESS_MEMORY Un techo a las fugas de larga duración Empieza a fallar la asignación de búferes de imagen enormes
DIE_ON_UNHANDLED_EXCEPTION Sin cuadros de error en máquinas desatendidas La depuración interactiva se vuelve penosa
Control de tasa de CPU Evitar que un hijo de procesamiento de imagen mate de hambre a la IU Se pierden los plazos de fotograma
BREAKAWAY_OK Dejar una vía de escape a un SDK que necesita su propio job Los procesos desaparecen de la supervisión
Restricciones de IU Apretón de tipo entorno aislado Los Nested Jobs se rompen (capítulo 9)

Los límites de notificación sirven para observar; los límites impuestos sirven para rechazar y terminar

Separe «límites holgados para notificación» de «límites que detienen las cosas cuando se superan». JobObjectNotificationLimitInformation solo le notifica el exceso; el proceso sigue ejecutándose.15

Los límites de Extended Limit se imponen, pero la forma de imposición difiere por límite.3

Límite Qué ocurre cuando se supera
Límite de memoria Falla la operación de commit que lo superaría. El propio proceso sigue vivo
ACTIVE_PROCESS Falla la creación o asignación que lo superaría. Un proceso que lo superó por asignación se termina
Tiempo de proceso (PROCESS_TIME) Solo se termina el proceso que lo superó
Tiempo de job (JOB_TIME) Un límite sobre el valor agregado; por defecto se terminan todos los procesos bajo el Job

Establezca esto sin conocer la diferencia y diagnosticará mal «un ayudante que desaparece justo después de la creación» como otro fallo. Para operación de larga duración, el orden seguro es observar primero con límites de notificación y decidir los límites impuestos una vez conocida la tendencia.

Límites para notificación y límites para imposiciónUn límite de JobObjectNotificationLimitInformation solo notifica el exceso y el proceso sigue, mientras que los límites de Extended Limit se imponen: un límite de memoria hace fallar la operación, ACTIVE_PROCESS hace fallar creación y asignación, y los límites de tiempo terminan el procesoObservarDetener¿Propósito del límite?Límite de notificación: sigue si se superaLímite impuesto: rechazar o terminarIdentificar la generación a partir de los registros de contabilidadFallo de commit, creación denegada, terminación

Figura 14: El mismo «límite», pero notificación e imposición son cosas distintas, y la imposición funciona de forma distinta por límite. Ponga un límite impuesto sin observar primero y se dispara en falso en un pico de operación normal.

La relación entre el control de tasa de CPU y el procesamiento periódico se deja al artículo de tiempo real suave; aquí no vamos más allá de una contramedida para «el proceso de dispositivo se come la IU».

9. Nested Jobs, breakaway y una contraparte que ya está en un Job

Piense los Nested Jobs como «contención de conjuntos de procesos»

Las reglas de los Nested Jobs en Windows 8 y posteriores se pueden organizar en cuatro.5

  • El job padre es el conjunto más amplio, y el job hijo es un subconjunto de él (asignar en un orden que rompe esta contención falla)
  • Para los límites de recursos principales, el más restrictivo a lo largo de la cadena entra en vigor
  • Un job con restricciones de IU no puede ser un Nested Job
  • Las notificaciones también se entregan a los puertos de finalización de cada job padre a lo largo de la cadena (el job hijo no necesita un puerto)
Jerarquía de Nested Jobs y límites efectivosEl job padre es el conjunto más amplio y el job hijo es un subconjunto de él, y para los límites de recursos principales entra en vigor el valor más restrictivo a lo largo de la cadena. Un job con restricciones de IU no puede ser un Nested JobJob padre (conjunto más amplio)Job hijo (subconjunto)Procesos miembrosLímite efectivo = valor más restrictivoJob con restricciones de IUNo puede ser Nested Job

Figura 15: Piense los Nested Jobs como «contención de conjuntos». Las restricciones de IU rompen los Nested Jobs, así que es más seguro no adjuntarlas a un Job usado para gestionar la vida.

El breakaway es el camino para crear un proceso fuera del Job desde el principio

El breakaway es el camino legítimo por el que los descendientes creados con CreateProcess salen del árbol.3

Ajuste del lado del Job Condición para que el hijo nazca fuera del Job
JOB_OBJECT_LIMIT_BREAKAWAY_OK Crearlo con CREATE_BREAKAWAY_FROM_JOB indicado
SILENT_BREAKAWAY_OK No hace falta marca. Todo hijo nace fuera

A veces hace falta porque el SDK usa un Job propio. Sin embargo, un proceso nacido por ese camino queda excluido tanto de la terminación en bloque como de la supervisión. Si lo permite, decida también quién gestiona lo que se ha escapado.

El camino fuera del árbol por breakawayCuando BREAKAWAY_OK está definido en el Job, un nieto creado con CREATE_BREAKAWAY_FROM_JOB nace fuera del Job y desaparece del alcance de la terminación en bloque y la supervisiónCreación normalCreación con BREAKAWAYJob (con BREAKAWAY_OK)Proceso hijoEl nieto también en el JobEl nieto sale del JobFuera de la supervisión y la terminación en bloque

Figura 16: El breakaway tiene dos caras: «una vía de escape para un SDK que la necesita» y «un agujero en la supervisión». Si lo adjunta, decida quién cuida lo que se ha escapado.

Un arranque por poder mediante WMI no se puede impedir ni prohibiendo el breakaway

Los caminos en los que un proceso de terceros arranca en su nombre, como Win32_Process.Create de WMI, son otro asunto. El padre real es el proveedor WMI, así que el proceso nacido está fuera del Job desde el principio. Prohibir el breakaway no cierra este agujero.1

Si el SDK usa este camino se comprueba no en el registro NEW_PROCESS, sino en las relaciones padre-hijo de Process Explorer.

Comprobar primero la pertenencia a un Job existente y las restricciones de Windows 7 y anteriores

Cuando la contraparte ya está en un Job (fallo 6), el procedimiento es: comprobar con IsProcessInJob → si se puede montar un Nested Job, Assign tal cual → si no se puede (Windows 7, o restricciones de IU), cambiar el diseño.18

En Windows 7 y anteriores no puede hacer Assign una segunda vez a una contraparte que ya pertenece a otro Job. BREAKAWAY_OK no es una marca que quite después un proceso ya asignado. Esa vía de escape solo funciona cuando el lado del SDK pide breakaway al crear al hijo. Si eso no se puede esperar, cambie el diseño antes del arranque bajo el supuesto de «un proceso, un job».12

Decidir al final si meter al propio padre en el Job

Por último, una palabra sobre el diseño de «meter su propio proceso en su propio Job». Si el propio padre también se coloca debajo, entonces cuando el padre se cae también queda incluido en los objetivos de KillOnJobClose, y la vida de todo el árbol coincide por completo. Pero es un arma de doble filo que se convierte en una terminación masiva no intencionada si se equivoca en el manejo del handle de Job, así que es más seguro empezar con «padre fuera, solo el árbol hijo dentro».

10. Cómo investigar

Investigue en el orden pertenencia → contabilidad y registros de notificación → ocupación del dispositivo. Este es el procedimiento de confirmación para no concluir «no queda nada» solo con mirar nombres de proceso.

  • Process Explorer: las propiedades del proceso tienen una pestaña Job, que muestra el Job al que pertenece el proceso y sus límites. Es la forma más rápida de confirmar «en qué Job está este ayudante»
  • IsProcessInJob: el punto de entrada para comprobar la propia pertenencia o la de la contraparte desde el código18
  • QueryInformationJobObject: registrar periódicamente Basic Accounting (recuento total de procesos, tiempo de CPU) y Extended Limit (PeakJobMemoryUsed y demás)417
  • Conservar el registro del puerto de finalización en un archivo: la línea temporal NEW_PROCESS / EXIT / ABNORMAL_EXIT se convierte en la única prueba en una investigación un mes después
  • No comprobar restos por PID: lo que hay que mirar son handles de dispositivo, nombres de canalización y archivos de bloqueo. «Ningún proceso visible en el Administrador de tareas» no significa «el dispositivo se ha liberado»
Procedimiento para investigar restosPrimero confirmar la pertenencia con IsProcessInJob y la pestaña Job de Process Explorer, leer la contabilidad con QueryInformationJobObject y, por último, juzgar los restos por handles de dispositivo, nombres de canalización y archivos de bloqueo, no por si existe un procesoConfirmar la pertenencia con IsProcessInJobPestaña Job de Process ExplorerContabilidad con QueryInformationJobObjectJuzgar restos por handles de dispositivo y nombres de canalización

Figura 17: Investigue en el orden «pertenencia → contabilidad → ocupación». No concluya «no queda nada» solo con mirar nombres de proceso.

11. Una guía aproximada para elegir (tabla de decisión)

Aquí las elecciones hasta ahora se resumen por situación. Vuelva al capítulo 5 para la política de terminación, al capítulo 6 para los supuestos de supervisión y al capítulo 9 para la relación con Jobs existentes.

Situación Recomendación
El cuerpo de IU y el SDK de dispositivo se han separado en procesos distintos Meterlos en un Job y supervisar con un puerto de finalización
El peor caso es que el dispositivo quede retenido después de que desaparece el padre Establecer KillOnJobClose
El fotograma o el volcado en el momento de la caída es un activo No establecer KillOnJobClose; el lado de supervisión pone en seguro y luego llama a TerminateJobObject
El SDK del fabricante engendra ayudantes Crear con SUSPENDED o JOB_LIST, y registrar NEW_PROCESS
Assign devuelve ERROR_ACCESS_DENIED Comprobar primero el job existente y si es posible un Nested Job. Sospechar restricciones de IU
Engendrar hijos en la sesión interactiva desde un servicio No adjuntar restricciones de IU. Volver al diseño del artículo sobre el límite de usuario
Esperar a que salgan los nietos en el hilo de IU Parar. Moverlo al hilo IOCP

12. Resumen

La vida de un hijo no es la vida del objeto Process del padre. Tampoco se transmite automáticamente al hijo la salida del padre. Un Job Object es el mecanismo que convierte este árbol de procesos en una unidad y trata límites, notificaciones y terminación en bloque.

Para gestionar hasta los nietos, fije la pertenencia antes de que el hijo se ejecute. El procedimiento A es SUSPENDED + Assign; el procedimiento B, en Windows 10 y posteriores, es JOB_LIST. KillOnJobClose funciona por el cierre del último handle de Job sea cual sea la razón por la que salió el padre, pero también pierde el material post mortem de los descendientes terminados con él.

Por eso exactamente debe listar primero «qué no debe quedar atrás después de la caída» y «qué quiere conservar», y elegir entre recuperación inmediata y terminación después de que el lado de supervisión haya recogido y puesto en seguro. Este es el procedimiento de diseño que este artículo quiere transmitir.

Lo siguiente a escribir serían los detalles de arrancar procesos a través de la sesión 0 y la sesión interactiva, o esperar en dispositivos con E/S superpuesta. Una vez que tiene bajo control la «vida exterior» de un proceso, a continuación espera la vida de la E/S.

Artículos relacionados

Áreas de consultoría relacionadas

En KomuraSoft LLC nos encargamos del diseño de aislamiento de procesos de aplicaciones Windows que trabajan con cámaras, instrumentos de medición y dispositivos serie/USB, de la investigación de problemas de ocupación de dispositivo como ayudantes del SDK residuales y device busy, y de construir mecanismos de supervisión y recuperación automática para aplicaciones de larga duración. Consúltenos incluso a partir de un solo caso de «reiniciar el padre y el dispositivo no se puede abrir».

Referencias

  1. Microsoft Learn, Job Objects. Sobre que un Job Object es un objeto del kernel que gestiona un grupo de procesos como una unidad, que los procesos hijos creados por un proceso miembro se asocian al mismo Job por defecto (salvo mediante Win32_Process.Create), las dos marcas de límite para breakaway, la terminación en bloque con TerminateJobObject, y cómo gestionar un árbol de procesos en entornos donde los Nested Jobs no están disponibles.  2 3 4 5

  2. Microsoft Learn, AssignProcessToJobObject function (jobapi2.h). Sobre que la asociación entre un proceso y un Job es irreversible, un job por proceso en Windows 7 y anteriores con pertenencia múltiple (Nested Jobs) posible a partir de Windows 8, y los límites efectivos y la propagación del breakaway bajo Nested Jobs.  2

  3. Microsoft Learn, JOBOBJECT_BASIC_LIMIT_INFORMATION structure (winnt.h). Sobre las marcas de límite JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE (terminar todos los procesos cuando se cierra el último handle de Job), ACTIVE_PROCESS (un techo de procesos activos simultáneos), JOB_MEMORY (un techo de commit para todo el job), DIE_ON_UNHANDLED_EXCEPTION, y BREAKAWAY_OK / SILENT_BREAKAWAY_OK.  2 3 4 5

  4. Microsoft Learn, JOBOBJECT_BASIC_ACCOUNTING_INFORMATION structure (winnt.h). Sobre que el Job conserva información de contabilidad como recuento total de procesos, tiempo de CPU y recuento de errores de página, incluidos los totales acumulados por procesos que han salido, y que se obtiene con QueryInformationJobObject.  2

  5. Microsoft Learn, Nested Jobs. Sobre que los Nested Jobs forman una jerarquía padre-hijo (el job hijo es un subconjunto de los procesos del job padre), que un job con restricciones de IU no puede ser Nested Job, que el límite efectivo es el valor más restrictivo a lo largo de la cadena, que las notificaciones se envían a todos los puertos de finalización de la cadena de jobs padres, y que la jerarquía se termina desde el nivel más bajo.  2

  6. Microsoft Learn, Process Creation Flags. Sobre CREATE_SUSPENDED (crear el hilo inicial suspendido y no ejecutarlo hasta ResumeThread) y CREATE_BREAKAWAY_FROM_JOB (el Job del llamador debe tener JOB_OBJECT_LIMIT_BREAKAWAY_OK). 

  7. Raymond Chen, Closing the race window between creating a suspended process and putting it in a job (The Old New Thing). Sobre el procedimiento clásico de crear con CREATE_SUSPENDED y luego meter el proceso en un Job, y cómo cerrar su ventana de carrera. 

  8. Microsoft Learn, UpdateProcThreadAttribute function (processthreadsapi.h). Sobre que PROC_THREAD_ATTRIBUTE_JOB_LIST asigna handles de Job al proceso hijo creado en el orden especificado, y que se admite en Windows 10 / Windows Server 2016 y posteriores. 

  9. Raymond Chen, A more direct and mistake-free way of creating a process in a job object (The Old New Thing). Sobre usar PROC_THREAD_ATTRIBUTE_JOB_LIST para hacer que un proceso pertenezca a un Job desde el momento de la creación. 

  10. Raymond Chen, Destroying all child processes (and grandchildren) when the parent exits (The Old New Thing). Sobre el arreglo que termina a los descendientes juntos cuando el padre desaparece usando un Job con KILL_ON_JOB_CLOSE, y la importancia de no dejar que se herede el handle de Job. 

  11. Microsoft Learn, TerminateJobObject function (jobapi2.h). Sobre terminar a la fuerza todos los procesos asociados al Job, como si se hubiera llamado a TerminateProcess en cada uno por separado. 

  12. Microsoft Learn, Job Objects - Managing Job Objects. Sobre que el objeto Job pasa a estado señalado cuando se terminan todos los procesos por superar el límite de tiempo del job, que el Job se destruye cuando se cierra el último handle, y que el cierre provoca la terminación de todos los procesos miembros cuando se especifica KILL_ON_JOB_CLOSE. 

  13. Microsoft Learn, JOBOBJECT_ASSOCIATE_COMPLETION_PORT structure (winnt.h). Sobre la lista de mensajes enviados al puerto de finalización como JOB_OBJECT_MSG_NEW_PROCESS / EXIT_PROCESS / ABNORMAL_EXIT_PROCESS / ACTIVE_PROCESS_ZERO, los códigos de salida juzgados como salidas anómalas, que no se puede descartar la reutilización de PID en mensajes que devuelven un PID a menos que se sostenga un handle de proceso, y que la entrega de notificaciones no está garantizada.  2 3

  14. Raymond Chen, How do I wait until all processes in a job have exited? (The Old New Thing). Sobre que esperar en el handle de Job no puede detectar «se quedó vacío», y la necesidad de esperar JOB_OBJECT_MSG_ACTIVE_PROCESS_ZERO en el puerto de finalización. 

  15. Microsoft Learn, Job Objects - Job Limits and Notifications. Sobre que asociar el puerto de finalización mientras el Job está inactivo es preferible (reducir la posibilidad de perder notificaciones de procesos cuyo estado cambia durante la asociación), que la entrega de mensajes no está garantizada salvo para los límites definidos con JobObjectNotificationLimitInformation, y que los procesos siguen ejecutándose después de superar un límite de notificación.  2 3

  16. Raymond Chen, Removing the TerminateThread from code that waits for a job object to empty (The Old New Thing). Sobre reescribir el patrón antiguo de matar el hilo de espera con TerminateThread en una espera basada en el puerto de finalización. 

  17. Microsoft Learn, JOBOBJECT_EXTENDED_LIMIT_INFORMATION structure (winnt.h). Sobre establecer límites de memoria por proceso y por job, y obtener la memoria de pico con PeakProcessMemoryUsed / PeakJobMemoryUsed.  2

  18. Microsoft Learn, IsProcessInJob function (jobapi.h). Sobre determinar si un proceso se está ejecutando en el Job especificado (o en cualquier Job).  2 3

  19. Microsoft Learn, JOBOBJECT_CPU_RATE_CONTROL_INFORMATION structure (winnt.h). Sobre controlar la tasa de CPU (parte de ciclos o peso) por Job. 

Artículos recientes con las mismas etiquetas para profundizar en temas cercanos.

Estas páginas sitúan el tema en un contexto más amplio de servicios y decisiones.

El artículo está directamente relacionado con los siguientes servicios.

Preguntas frecuentes

Preguntas habituales en las consultas sobre el tema del artículo.

¿Un Job Object es un contenedor o un entorno aislado?
No. Un Job Object es un objeto del kernel que adjunta límites, notificaciones y terminación en bloque a un grupo de procesos. Puede imponer techos de memoria, CPU y número de procesos, pero no puede restringir el acceso a la red, y el token de acceso (los privilegios) no cambia. Tiene restricciones de IU, pero esas solas no lo convierten en un límite de seguridad. Si el objetivo es el aislamiento o la seguridad, hay que combinarlo con otro mecanismo como AppContainer o contenedores. Este artículo cubre el uso de tratar la vida y los recursos de un árbol de procesos como una unidad.
¿No basta con Process.Kill()?
Process.Kill() termina solo ese proceso; no llega a los procesos nietos. Kill(entireProcessTree: true) en .NET Core 3.0 y posteriores recorre los descendientes y los termina, pero como enumera el árbol de procesos a partir de las relaciones padre-hijo de ese momento, puede perder procesos nacidos durante la enumeración y procesos cuya línea se cortó porque el padre murió primero. Además, ninguno de los dos métodos se llama cuando el propio padre se cae. Si quiere recuperar a los descendientes tanto si el padre vive como si muere, un Job Object más KillOnJobClose, que confía la vida al kernel, es la opción fiable.
Si el padre está en un Job, ¿el hijo entra en el Job automáticamente?
Por defecto, sí. Un proceso hijo creado con CreateProcess por un proceso que pertenece a un Job pertenece automáticamente al mismo Job. La excepción es el breakaway. Si JOB_OBJECT_LIMIT_BREAKAWAY_OK está definido en el Job y el hijo se crea con la marca CREATE_BREAKAWAY_FROM_JOB, o si está definido JOB_OBJECT_LIMIT_SILENT_BREAKAWAY_OK, el hijo nace fuera del Job. Tenga en cuenta también que los procesos creados mediante Win32_Process.Create de WMI no se asocian al Job.
¿Se puede sacar un proceso de un Job una vez que se ha metido?
No. La asociación hecha por AssignProcessToJobObject es irreversible, y la pertenencia continúa hasta que el proceso sale. Las opciones de diseño son por tanto tres: no meterlo, crearlo fuera desde el principio con breakaway, o usar un Job aparte como Nested Job. No hay un "sacarlo después". Esta irreversibilidad es también la razón de preparar el Job antes de la creación.
¿KillOnJobClose funciona incluso cuando el padre se cae?
Sí. JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE es un mecanismo que termina los procesos de debajo cuando se cierra el último handle de Job, y tanto si el padre sale con normalidad, muere por una excepción no controlada o lo matan en el Administrador de tareas, el kernel cierra los handles como parte de la limpieza del proceso, así que se dispara. Sin embargo, si deja que el proceso hijo herede el handle de Job, el handle que sostiene el hijo sobrevive a la muerte del padre, así que no es el último handle y no se dispara. No deje que se herede el handle de Job.
¿Las notificaciones del puerto de finalización se entregan siempre?
No siempre. La documentación oficial indica explícitamente que, salvo las notificaciones de los límites definidos con JobObjectNotificationLimitInformation, la entrega de mensajes al puerto de finalización no está garantizada. Una notificación que no llegó no significa que el suceso no ocurriera. Para una supervisión que necesita certeza, combínela con el sondeo de la información de contabilidad mediante QueryInformationJobObject, y conserve usted mismo los handles de proceso para resolver si un proceso está vivo o muerto.
¿.NET tiene una API oficial de Job Object?
No. System.Diagnostics.Process no tiene el concepto de Job, y la BCL tampoco tiene un contenedor. La respuesta práctica es llamar a CreateJobObject / SetInformationJobObject / AssignProcessToJobObject mediante P/Invoke, o generar las firmas con CsWin32, el generador de origen de Microsoft, y escribir un contenedor delgado. Si envuelve el handle de Job en un SafeHandle y lo cierra en Dispose de IDisposable, el sentido de KillOnJobClose, "la vida del contenedor = la vida del árbol de procesos hijos", aparece directamente en el código.
¿Cómo debo diseñar para un PC de equipo que ejecuta Windows 7?
En Windows 7 y anteriores un proceso solo puede pertenecer a un Job, y los Nested Jobs no son posibles. Si el SDK de la otra parte usa un Job propio, su AssignProcessToJobObject falla. JOB_OBJECT_LIMIT_BREAKAWAY_OK es una marca que permite a un proceso que ya está en su Job crear un hijo fuera del Job con CREATE_BREAKAWAY_FROM_JOB; no es magia que deje pasar un segundo Assign después de que el proceso ya está dentro. En otras palabras, la vía de escape solo existe cuando el lado del SDK pide breakaway en el momento de la creación. Si eso no se puede esperar, la única opción es cambiar el diseño antes del arranque bajo el supuesto de que hay un solo Job, el suyo. La documentación de Microsoft también muestra cómo gestionar el árbol con las dos marcas de límite de breakaway en entornos donde los Nested Jobs no están disponibles. Dicho esto, es un sistema operativo fuera de soporte, así que si es posible, la migración va primero.

Perfil del autor

Página de presentación del autor del artículo.

Go Komura

Representante de KomuraSoft LLC

Especializado en desarrollo de software para Windows, consultoría técnica e investigación de fallos, sobre todo en proyectos con sistemas existentes y errores difíciles de reproducir.

Volver al blog