SSD Nodes Learn 🎉 VPS desde $5.50/mes
Guías Matt ConnorPor Matt Connor

systemd Type=: simple, forking, notify y exec

La unidad figura activa, pero el daemon terminó. Elige Type= entre simple, exec, forking, oneshot y notify, y encuentra el PID principal real.

¿Por qué systemd indica que una unidad está activa cuando el proceso ha terminado?

Una unidad de servicio de systemd permanece active mientras está activo el único proceso que systemd considera el proceso principal, y Type=, en la sección [Service], determina cuál es ese proceso. Si elige un valor incorrecto, systemd termina supervisando un wrapper 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 del estado del 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) corresponda a algo que siga ejecutándose. 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 iniciada esta unidad? ¿Y cuál es el proceso principal?

La primera respuesta controla el orden de inicio. Una unidad que incluye la tuya en After= espera hasta que systemd considera iniciada la tuya. Un Type= que informa de que está «iniciada» demasiado pronto permite que las unidades dependientes se ejecuten antes de que tu 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 limitarlo y terminarlo conjuntamente. El cgroup es la forma en que systemctl stop realiza la limpieza: KillMode= tiene control-group de forma predeterminada, por lo que detener una unidad envía una señal a todos los procesos que contiene. El PID principal es 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 informa de que el servicio se inició 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 se inician inmediatamente, antes de que se haya ejecutado el binario del servicio.

Este último detalle explica una sorpresa habitual. Un error tipográfico en la ruta ExecStart= aún produce un trabajo de inicio correcto, y el fallo llega un momento después, cuando la ejecución falla. systemd registra este caso con el código de salida 203, que su propia tabla denomina EXEC y 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 a casi 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 el fork y la ejecución del binario hayan terminado correctamente. Si falta el binario o no se puede resolver un User=, el propio trabajo de inicio falla en ese momento, en lugar de informar de éxito y fallar silenciosamente un momento después.

Type=exec se incorporó en systemd 240, por lo que todas las distribuciones actuales para servidores 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 en lugar de 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 terminará de forma intencionada. systemd espera a que termine ese primer proceso y sólo entonces considera iniciada la unidad. El proceso hijo que queda es el daemon.

La dificultad es identificarlo. El proceso que inició systemd ya no existe, así que systemd debe determinar cuál de los procesos supervivientes es el principal. Configure 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. Por tanto, rechaza un archivo obsoleto que nombre un proceso no relacionado en lugar de confiar en él.

Sin PIDFile=, se aplica GuessMainPID= y su valor predeterminado es yes. La estimación sólo es fiable cuando el servicio termina funcionando como un único proceso. El manual indica claramente el límite: 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, lo que 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 elementos implican menos formas de perder el PID.

Type=oneshot para tareas que terminan

Type=oneshot espera que el proceso se ejecute y termine. systemd marca la unidad como iniciada sólo después de que el proceso haya terminado, por lo que oneshot es la opción adecuada para cualquier tarea que otra unidad deba esperar. También es el valor predeterminado implícito cuando una unidad no especifica Type= ni ExecStart=.

Hay dos comportamientos 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 bloquee esperará indefinidamente a menos que establezca TimeoutStartSec= manualmente.

Después de que el proceso termine, la unidad vuelve a estar inactiva. RemainAfterExit=yes la mantiene active sin ningún proceso en ejecución. Esta es la versión intencionada del síntoma descrito al principio de esta página, y es correcta cuando el trabajo de la unidad consiste en dejar un estado persistente en lugar de mantener algo en ejecución: cargar un conjunto de reglas del firewall o iniciar un conjunto de contenedores. Es el patrón utilizado por una pila de Docker Compose que vuelve a iniciarse después de 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 ella. Una unidad oneshot también es la que activa una programación, que es la otra mitad de ejecutar una tarea 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 en 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 se ha iniciado en el momento en que el propio servicio indica que está listo.

systemd acepta ese mensaje únicamente del proceso principal. Eso 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 llamar a systemd-notify --ready, pero se ejecuta como un proceso independiente de corta duración. Por eso necesita NotifyAccess=all, y systemd puede no atribuir el mensaje si el proceso emisor ya ha terminado. Un servicio que implemente el protocolo directamente es más fiable.

Conviene conocer otras dos opciones relacionadas. Type=notify-reload, disponible desde systemd 253, amplía el mismo intercambio de confirmación a las recargas. Así, systemctl reload retorna cuando el servicio informa de que la recarga ha terminado, en lugar de retornar cuando se envió la señal. WatchdogSec= solicita a un servicio que envía notificaciones que envíe un mensaje de mantenimiento de actividad a intervalos. systemd considera un plazo incumplido como un fallo.

Type=dbus y Type=idle

Type=dbus espera hasta que el servicio adquiere 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 realmente registre un nombre de 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 arranque y no debe usarse en un servicio normal.

Por qué un script envoltorio deja a systemd asociado al PID equivocado

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 9101

systemd 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 realizando la limpieza correctamente. El problema está en la supervisión, no en la limpieza.

La solución depende de cuántos procesos de ejecución prolongada tenga realmente la unidad.

Si sólo hay uno, sustituya el shell por ese proceso.

#!/bin/bash
export APP_ENV=production
exec /opt/app/bin/server --config /etc/app.yaml

exec sustituye el shell por el programa indicado y conserva el mismo PID. Por tanto, el PID que systemd registró pertenece ahora al daemon. Mejor aún, elimine el wrapper. Environment= y EnvironmentFile= transportan las variables, y ExecStartPre= ejecuta el paso de configuració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árelos en dos unidades y ordénelas con After= y Wants=. Una unidad por proceso es la estructura que systemd supervisa correctamente, y 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 haya algún proceso vivo en su cgroup.

[Service]
Type=simple
ExitType=cgroup
ExecStart=/opt/app/launcher

Esto 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 haya al menos un proceso vivo, por lo que una unidad que contenga dos daemons seguirá activa después de que uno termine. Esto 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 lo que 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. Este aspecto se explica en limitar la memoria y la CPU de un servicio con systemd.

Cómo encontrar el proceso que systemd supervisa realmente

Siga este procedimiento, en orden, en la unidad que está depurando. Primero lea lo que cargó systemd. Después lea lo que supervisa. Por último, compare esa información con la tabla de procesos.

systemctl cat app.service
systemctl show -p Type,ExitType,GuessMainPID,PIDFile,MainPID,RemainAfterExit app.service

systemctl cat muestra el archivo de unidad junto con todos los drop-ins que se aplican a ella. 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 dejó escritos. 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 supervisa systemd. Lea ambas cosas conjuntamente. Un MainPID de 0 significa que systemd no tiene ningún proceso que supervisar. Un MainPID que se resuelve en un shell mientras el cgroup también contiene su daemon corresponde al caso del wrapper descrito anteriormente. Si el cgroup contiene más procesos de los esperados, interviene un lanzador o un daemon que crea procesos secundarios.

systemctl status app.service
journalctl -u app.service -b

systemctl status muestra conjuntamente la línea de estado y el árbol de cgroups. Por eso, a menudo responde a ambas preguntas de una 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 hacerlo en el journal, lea también ese archivo, porque systemd sólo puede registrar lo que recibe.

Cuando cambie Type=, recargue la configuración y reinicie la unidad.

systemd-analyze verify /etc/systemd/system/app.service
sudo systemctl daemon-reload
sudo systemctl restart app.service

systemd-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 tanto, el reinicio es obligatorio.

Después, pruebe el cambio. Obtenga el PID del proceso que realmente le interesa mediante systemd-cgls y termínelo. Ejecute systemctl is-active app.service inmediatamente después. Si Type= es correcto, la unidad deja el estado activo. Si permanece activa, systemd sigue supervisando otro proceso.

Qué tipo Type= de systemd debe usar

  • Un programa que permanece en primer plano: Type=exec.
  • Un programa que admite notificaciones de disponibilidad: Type=notify, y notify-reload si también confirma las recargas.
  • Un daemon que insiste en pasar a segundo plano: Type=forking con PIDFile=, o su opción para ejecutarse en primer plano con Type=exec.
  • Un script que realiza una tarea y termina: Type=oneshot, además de RemainAfterExit=yes cuando el objetivo era dejar un estado persistente.
  • Un lanzador que termina mientras sus procesos secundarios siguen ejecutándose: Type=simple con ExitType=cgroup.

Si no sabe qué tipo necesita un daemon de terceros, lea primero el archivo de unidad incluido con el paquete. Ejecutar systemctl cat en una unidad distribuida por la distribución muestra el valor de Type= que eligió el proyecto original. 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 considera principal sigue activo. systemd supervisa un único 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 ha creado el proceso, antes de ejecutar el binario. Por tanto, una ruta incorrecta en ExecStart= produce un trabajo de inicio correcto seguido de un fallo. Type=exec espera hasta que la ejecución se haya realizado correctamente, por lo que el propio trabajo de inicio informa del fallo. Ambos tratan el mismo proceso como PID principal. Type=exec requiere systemd 240 o posterior.

¿Sigo necesitando PIDFile= con Type=forking?

Sí, siempre que el daemon escriba uno. Sin él, systemd recurre a GuessMainPID=, que es una estimación y sólo resulta fiable para un servicio que termina funcionando como un único proceso. Cuando la estimación es incorrecta o no es posible realizarla, dejan de funcionar la detección de fallos y el reinicio automático de esa unidad. Configure PIDFile= con la ruta exacta que escribe el daemon, normalmente bajo /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 inactiva, por lo que systemctl stop no tiene nada que detener ni ninguna 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 los archivos de unidad del disco, 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 realizar las pruebas. De lo contrario, seguirá observando el comportamiento de supervisión anterior.