systemd Type=: simple, forking, notify y exec
La unidad aparece activa aunque el daemon terminó. Aprende a elegir Type= entre simple, exec, forking, oneshot y notify, y localizar el PID principal real.
Por qué systemd informa de que una unidad está activa cuando el proceso ha terminado
Una unidad de servicio de systemd permanece active mientras el único proceso que systemd considera el proceso principal siga activo, y Type= en la sección [Service] determina cuál es ese proceso. Si elige un valor incorrecto, systemd termina supervisando un envoltorio de shell o un proceso padre de corta duración, mientras el daemon que le interesa termina dentro de la misma unidad. La unidad informa correctamente sobre el proceso que se le indicó que supervisara.
Cambiar la política de reinicio no resolverá este problema. Restart= actúa cuando termina el proceso principal, por lo que Restart=always nunca se ejecuta mientras el PID principal (identificador de proceso) pertenezca a algo que siga en ejecución. Corrija primero Type=. Lo que hace systemd después de que el proceso principal termine realmente es una decisión independiente, que se explica en la guía sobre Restart= y RestartSec=.
Qué decide realmente Type=
Cada valor de Type= responde a dos preguntas a la vez. ¿Cuándo puede systemd considerar que esta unidad se ha iniciado? ¿Y qué proceso es el principal?
La primera respuesta controla el orden de inicio. Una unidad que incluya la suya en After= espera hasta que systemd considere que la suya se ha iniciado. Un Type= que informe «iniciada» demasiado pronto permite que las unidades dependientes se ejecuten antes de que su servicio pueda responderles.
La segunda respuesta controla la supervisión. systemd coloca cada proceso que inicia una unidad en un cgroup (grupo de control), una función del kernel que agrupa procesos para poder limitarlos y terminarlos juntos. El cgroup es la forma en que systemctl stop realiza la limpieza: KillMode= tiene control-group como valor predeterminado, por lo que detener una unidad envía una señal a todos los procesos que contiene. El PID principal es un concepto más específico. Es el único proceso cuya salida finaliza la unidad y cuyo estado de salida se convierte en el resultado de la unidad. La confusión comienza cuando se interpreta el cgroup como si fuera el PID principal.
Type=simple indica que el servicio se ha iniciado antes de ejecutar el binario
Type=simple es el valor predeterminado cuando se establece ExecStart= y no están presentes Type= ni BusName=. systemd crea el proceso, considera que la unidad se ha iniciado de inmediato y trata ese proceso como el PID principal. Las unidades posteriores comienzan inmediatamente, antes de que se haya ejecutado el binario del servicio.
Este último detalle explica un problema habitual. Un error tipográfico en la ruta ExecStart= sigue produciendo un trabajo de inicio correcto. El fallo aparece un momento después, cuando la ejecución falla. systemd registra este caso con el código de salida 203. Su propia tabla lo denomina EXEC y lo define como un fallo al ejecutar el binario del servicio. Por tanto, que systemctl start termine sin errores no demuestra que el binario exista.
Use simple para un programa que permanece en primer plano y nunca pasa por sí mismo a segundo plano. Esto se aplica a la mayoría de los daemons modernos y prácticamente a cualquier programa que escriba usted mismo.
Type=exec espera a que el programa se inicie realmente
Type=exec es simple con un paso adicional. systemd considera que la unidad se ha iniciado sólo después de que tanto la bifurcación como la ejecución del binario se hayan completado correctamente. Si falta el binario o no se puede resolver un User=, el propio trabajo de inicio falla en lugar de informar de que se ha completado correctamente y fallar silenciosamente un momento después.
Type=exec llegó en systemd 240, por lo que todas las distribuciones de servidor actuales lo incluyen. Ubuntu 24.04 incluye systemd 255 y Debian 13 incluye systemd 257, a fecha de agosto de 2026. Compruebe la suya con systemctl --version.
El coste es un paso adicional de sincronización durante el inicio. La ventaja es un estado de salida fiable de systemctl start. Para un programa en primer plano, prefiera exec frente a simple.
Type=forking y cómo se pierde el PID principal
Type=forking indica a systemd que el proceso de ExecStart= creará un proceso hijo y después finalizará de forma intencionada. systemd espera a que finalice ese primer proceso y sólo entonces considera iniciada la unidad. El proceso hijo que queda es el daemon. Esta práctica procede de la época de SysV, cuando ningún componente supervisaba un daemon después de que terminara el script de init y un archivo PID era el único registro de lo que estaba en ejecución. Esta limitación está estrechamente relacionada con por qué systemd sustituyó los scripts de init.
La dificultad está en identificar el proceso. El proceso que inició systemd ya no existe, por lo que systemd debe determinar cuál de los procesos supervivientes es el principal. Establezca PIDFile= con el archivo que escribe el daemon, normalmente una ruta bajo /run, y systemd leerá el PID desde ese archivo. systemd también comprueba que el PID de ese archivo corresponda a un proceso que ya pertenezca a este servicio. Así, rechaza un archivo obsoleto que señale a un proceso no relacionado, en lugar de confiar en él.
Sin PIDFile=, se aplica GuessMainPID=, cuyo valor predeterminado es yes. Esta estimación sólo es fiable cuando el servicio se estabiliza en un único proceso. El manual indica claramente la limitación: si el daemon consta de más de un proceso, la estimación puede ser incorrecta y la detección de fallos deja de funcionar. Una unidad también puede terminar con un PID principal de 0. Esto significa que systemd no tiene ningún proceso que supervisar.
La mayoría de los daemons que crean procesos hijos también tienen una opción para permanecer en primer plano. Use esa opción con Type=exec y elimine la línea PIDFile=. Menos componentes implican menos formas de perder el PID.
Type=oneshot para trabajos que terminan
Type=oneshot espera que el proceso se ejecute y termine. systemd considera que la unidad se ha iniciado sólo después de que el proceso termina, por lo que oneshot es la forma adecuada para cualquier operación que otra unidad deba esperar. También es el valor predeterminado implícito cuando una unidad no especifica Type= ni ExecStart=.
Dos comportamientos son específicos de oneshot. Es el único tipo que acepta más de una línea ExecStart=, y esas líneas se ejecutan en orden. Su tiempo de espera de inicio también está deshabilitado de forma predeterminada, por lo que un oneshot que se bloquea espera indefinidamente, a menos que establezca TimeoutStartSec= manualmente.
Cuando el proceso termina, la unidad vuelve a estar inactiva. RemainAfterExit=yes la mantiene active sin ningún proceso en ejecución. Esta es la versión deliberada del síntoma descrito al principio de esta página, y es correcta cuando el trabajo de la unidad consiste en dejar un estado configurado en lugar de mantener algo en ejecución: cargar un conjunto de reglas de firewall o iniciar una pila de contenedores. Es el patrón utilizado en una pila de Docker Compose que vuelve tras un reinicio, donde la unidad ejecuta el comando de compose, termina y permanece activa porque los contenedores que inició siguen ejecutándose después de que la unidad finaliza. Una unidad oneshot también es lo que activa una programación, que es la otra parte de ejecutar un trabajo con un temporizador de systemd en lugar de cron.
Type=notify permite que el servicio indique cuándo está listo
Type=notify traslada la decisión al servicio. systemd mantiene abierto el trabajo de inicio hasta que el proceso envía READY=1 a través de un socket Unix cuya ruta recibe en la variable de entorno NOTIFY_SOCKET. La interfaz C es sd_notify(3) y muchos servidores ya la admiten.
Esta es la respuesta precisa a la pregunta «¿está iniciado?». simple y exec informan de que el servicio se ha iniciado antes de que lea su configuración o abra su socket de escucha. Por tanto, una unidad dependiente puede iniciarse demasiado pronto y fallar en su primera conexión. notify informa de que el servicio se ha iniciado en el momento en que el propio servicio indica que está listo.
systemd acepta ese mensaje sólo del proceso principal. Esto es lo que significa NotifyAccess=main, y Type=notify lo implica. Si el mensaje procede de un proceso hijo o auxiliar, establezca NotifyAccess=all. Un script de shell puede invocar systemd-notify --ready, pero se ejecuta como un proceso independiente de corta duración. Por eso necesita NotifyAccess=all, y systemd podría no poder atribuir un mensaje cuyo emisor ya haya terminado. Un servicio que implemente el protocolo directamente es más fiable.
Conviene conocer otros dos ajustes relacionados. Type=notify-reload, disponible desde systemd 253, extiende el mismo intercambio a las recargas. Así, systemctl reload devuelve el control cuando el servicio informa de que la recarga ha terminado, en lugar de hacerlo cuando se envió la señal. WatchdogSec= solicita a un servicio con notificaciones que envíe un mensaje de mantenimiento periódico. systemd considera un plazo incumplido un fallo.
Tipo=dbus y Tipo=idle
Type=dbus espera hasta que el servicio registre un nombre en D-Bus, el bus de mensajes que usan los servicios del sistema y del escritorio para comunicarse entre sí. Requiere BusName= y se convierte en el valor predeterminado en cuanto se establece BusName=. Úselo sólo con un servicio que registre realmente un nombre en el bus.
Type=idle se comporta como simple, pero retrasa la ejecución del programa hasta que se hayan enviado los trabajos en cola, con un límite de cinco segundos. Existe para evitar que la salida de la consola durante el arranque se mezcle con los mensajes de estado. No sirve para establecer el orden de ejecución y no debe usarse en un servicio normal.
Por qué un script envoltorio deja a systemd asociado al PID incorrecto
Esta es la estructura que produce el síntoma original.
[Service]
Type=simple
ExecStart=/opt/app/run.sh#!/bin/bash
export APP_ENV=production
/opt/app/bin/server --config /etc/app.yaml &
/opt/app/bin/exporter --port 9101systemd registra el shell como PID principal. El shell permanece activo mientras exporter se ejecuta en primer plano. Si server termina, el shell no lo detecta. Por tanto, el PID principal sigue activo, la unidad continúa active y Restart= no tiene nada sobre lo que actuar. Ambos procesos permanecen todo el tiempo en el cgroup de la unidad, por lo que systemctl stop sigue haciendo la limpieza correctamente. Lo que se rompió fue la supervisión, no la limpieza.
La solución depende de cuántos procesos de ejecución prolongada tenga realmente la unidad.
Si sólo hay uno, reemplaza el shell por ese proceso.
#!/bin/bash
export APP_ENV=production
exec /opt/app/bin/server --config /etc/app.yamlexec reemplaza el shell por el programa indicado y conserva el mismo PID. Por tanto, el PID que systemd registró ahora pertenece al daemon. Mejor aún, elimina el wrapper. Environment= y EnvironmentFile= proporcionan las variables, y ExecStartPre= ejecuta el paso de preparación. Así, systemd puede iniciar directamente el daemon y conocer su PID desde el principio.
Si hay dos, ningún PID representa la unidad completa. Sepáralos en dos unidades y ordénalas con After= y Wants=. Una unidad por proceso es la estructura que systemd supervisa correctamente. Además, es la única forma de que cada proceso tenga su propio comportamiento de reinicio.
Qué cambia ExitType=cgroup
ExitType= se añadió en systemd 250. El valor predeterminado es main: la unidad se considera detenida cuando termina el proceso principal. Con ExitType=cgroup, la unidad se considera activa mientras siga vivo cualquier proceso de su cgroup.
[Service]
Type=simple
ExitType=cgroup
ExecStart=/opt/app/launcherEsto resuelve un problema concreto. Un lanzador que inicia el trabajo real y después termina haría que systemd considerase detenida la unidad con ExitType=main y terminase los procesos supervivientes. Con ExitType=cgroup, la unidad sigue el grupo completo.
Es importante entender qué no resuelve. ExitType=cgroup mantiene activa una unidad mientras siga vivo al menos un proceso. Por tanto, una unidad que contiene dos daemons permanece activa después de que termine uno de ellos. Corrige el caso del lanzador. No convierte una unidad en un supervisor de varios procesos independientes. ExitType= tampoco se puede combinar con Type=oneshot.
El cgroup también recibe la contabilidad de recursos. Por tanto, límites como MemoryMax= y CPUQuota= se aplican a todos los procesos que haya iniciado la unidad, independientemente de lo que Type= indique sobre el PID principal. Esta parte se explica en limitar la memoria y la CPU de un servicio con systemd.
Cómo encontrar el proceso que systemd supervisa realmente
Trabaje con la unidad que está depurando y siga este orden. Lea lo que cargó systemd, después lo que supervisa y, por último, compare esos datos con la tabla de procesos.
systemctl cat app.service
systemctl show -p Type,ExitType,GuessMainPID,PIDFile,MainPID,RemainAfterExit app.servicesystemctl cat muestra el archivo de unidad junto con todos los drop-ins que se aplican a él. Así puede leer lo que cargó systemd en lugar del archivo que recuerda haber editado. systemctl show muestra los valores efectivos, incluidos los valores predeterminados que no anotó. Anote el valor de MainPID antes de continuar.
systemd-cgls --unit=app.service
ps -o pid,ppid,stat,etime,args -p "$(systemctl show -p MainPID --value app.service)"systemd-cgls muestra todos los procesos del cgroup de la unidad. La línea ps describe el único proceso que systemd supervisa. Lea ambos datos juntos. Un MainPID de 0 significa que systemd no tiene ningún proceso que supervisar. Un MainPID que resuelve a un shell mientras el cgroup también contiene su daemon corresponde al caso del wrapper descrito arriba. Si un cgroup contiene más procesos de los esperados, interviene un lanzador o un daemon que se bifurca.
systemctl status app.service
journalctl -u app.service -bsystemctl status muestra juntas la línea de estado y el árbol del cgroup, por lo que suele responder ambas preguntas a la vez. journalctl -u limitado a este arranque mediante -b muestra los eventos de inicio y detención que systemd registró para la unidad, junto con los códigos de salida que observó. Si el daemon escribe en su propio archivo de registro en lugar de usar el journal, lea también ese archivo, porque systemd sólo puede registrar lo que recibe.
Cuando cambie Type=, recargue y reinicie.
systemd-analyze verify /etc/systemd/system/app.service
sudo systemctl daemon-reload
sudo systemctl restart app.servicesystemd-analyze verify analiza el archivo e informa de los ajustes que no puede aceptar. daemon-reload hace que systemd vuelva a leer los archivos de unidad del disco. Un Type= modificado no se aplica a una unidad que ya está en ejecución, por lo que el reinicio es obligatorio.
Después, pruebe el cambio. Tome el PID del proceso que realmente le interesa desde systemd-cgls y termínelo. Ejecute systemctl is-active app.service inmediatamente después. Si Type= es correcto, la unidad sale del estado activo. Si permanece activa, systemd sigue supervisando otro proceso.
Qué tipo de servicio de systemd debe usar
- Un programa que permanece en primer plano:
Type=exec. - Un programa compatible con notificaciones de disponibilidad:
Type=notify, ynotify-reloadsi también confirma las recargas. - Un daemon que insiste en ejecutarse en segundo plano:
Type=forkingconPIDFile=, o su opción para permanecer en primer plano conType=exec. - Un script que realiza una tarea y termina:
Type=oneshot, además deRemainAfterExit=yescuando el objetivo era dejar el estado persistente. - Un lanzador que termina mientras sus procesos hijos siguen ejecutándose:
Type=simpleconExitType=cgroup.
Si no sabe qué tipo necesita un daemon de terceros, lea primero el archivo de unidad incluido en su paquete. Ejecutar systemctl cat en una unidad distribuida por la distribución muestra el Type= que eligió el proyecto original, y esa elección ha sido probada por más personas que la suya.
FAQ
¿Por qué mi unidad de systemd sigue activa cuando el proceso ha terminado?
Porque el proceso que systemd trata como principal sigue activo. systemd supervisa un solo PID por servicio, elegido según Type=, en lugar de supervisar todos los procesos del cgroup de la unidad. La causa habitual es un script envoltorio iniciado con Type=simple: el shell es el PID principal, por lo que la unidad sigue activa cuando termina un daemon que el shell inició en segundo plano. Ejecute systemctl show -p MainPID app.service, enumere después el cgroup de la unidad con systemd-cgls --unit=app.service y compare ambos resultados.
¿Cuál es la diferencia entre Type=simple y Type=exec?
Type=simple considera que la unidad se ha iniciado en cuanto systemd crea el proceso, antes de ejecutar el binario. Por tanto, una ruta incorrecta en ExecStart= todavía produce un trabajo de inicio correcto seguido de un error. Type=exec espera hasta que la ejecución se completa correctamente, por lo que el propio trabajo de inicio informa de ese error. Ambos tratan el mismo proceso como el PID principal. Type=exec requiere systemd 240 o una versión posterior.
¿Sigo necesitando PIDFile= con Type=forking?
Sí, siempre que el daemon escriba uno. Sin este archivo, systemd recurre a GuessMainPID=, que es una estimación y sólo resulta fiable para un servicio que termina funcionando como un único proceso. Si la estimación es incorrecta o imposible, la detección de errores y el reinicio automático dejan de funcionar para esa unidad. Configure PIDFile= con la ruta exacta en la que escribe el daemon, normalmente dentro de /run.
¿Cuándo debo usar RemainAfterExit=yes?
Cuando el objetivo de la unidad sea cambiar el estado del sistema en lugar de mantener un proceso en ejecución. Una unidad Type=oneshot que carga reglas del firewall o inicia una pila de contenedores termina en cuanto completa su trabajo. Sin RemainAfterExit=yes, la unidad pasa a estar inactiva, por lo que systemctl stop no tiene nada que detener ni forma de ejecutar una limpieza ExecStop=. Con esta opción, la unidad permanece activa sin procesos, que es el comportamiento previsto en este caso.
¿Es necesario ejecutar daemon-reload al cambiar Type=?
Sí, y también debe reiniciar la unidad. systemctl daemon-reload hace que systemd vuelva a leer de disco los archivos de unidad, pero una instancia en ejecución conserva el Type= con el que se inició. Ejecute sudo systemctl daemon-reload y después sudo systemctl restart app.service antes de probar, de lo contrario seguirá observando el comportamiento de supervisión anterior.