SSD Nodes Learn Hosting plans →
Guías Matt ConnorPor Matt Connor · Actualizado 2026-08-27

Historia de systemd: por qué terminó ganando

Descubre qué no podía hacer SysV init, qué intentaron Upstart y launchd, por qué las distribuciones migraron a systemd en cuatro años y qué críticas eran válidas.

Por qué ganó systemd

La historia de systemd comienza con dos tareas que SysV init no podía realizar. SysV init (System V init, el sistema de arranque que Linux heredó de AT&T Unix) no tenía forma de describir de qué depende un servicio ni de saber qué procesos pertenecen a un servicio una vez que estaba en ejecución. systemd resolvió ambos problemas mediante funciones del kernel que un script de shell no puede utilizar: grupos de control para realizar el seguimiento de procesos y sockets de escucha abiertos previamente para establecer el orden de inicio. El resto de la historia explica cómo esas dos soluciones se extendieron al resto del espacio de usuario. Ahí comenzaron las objeciones, y varias de ellas eran válidas.

Qué hacía realmente SysV init

En un sistema SysV, el PID 1 (el ID de proceso 1, el primer proceso que inicia el kernel) leía /etc/inittab, seleccionaba un nivel de ejecución y ejecutaba los scripts correspondientes a ese nivel. Los scripts estaban en /etc/init.d/. Los enlaces simbólicos de /etc/rc3.d/ determinaban cuáles se ejecutaban y en qué orden, de modo que /etc/rc3.d/S20nginx apuntaba a /etc/init.d/nginx y se ejecutaba con el argumento start.

#!/bin/sh
### BEGIN INIT INFO
# Provides:          nginx
# Required-Start:    $local_fs $remote_fs $network $syslog
# Required-Stop:     $local_fs $remote_fs $network $syslog
# Default-Start:     2 3 4 5
# Default-Stop:      0 1 6
### END INIT INFO

case "$1" in
  start)
    start-stop-daemon --start --quiet --pidfile /run/nginx.pid \
      --exec /usr/sbin/nginx
    ;;
  stop)
    start-stop-daemon --stop --quiet --retry TERM/30/KILL/5 \
      --pidfile /run/nginx.pid
    ;;
esac

El 20 de S20nginx es una posición, no una dependencia. Indica que este script se ejecuta después de S19 y antes de S21. No indica el motivo. Por tanto, nada puede comprobarlo y nada puede ejecutar de forma segura dos scripts no relacionados al mismo tiempo sin que una persona decida que es seguro.

El programa rc ejecutaba cada script por turnos y esperaba a que terminara. Un script que se bloqueaba durante treinta segundos mientras esperaba una dirección de red bloqueaba todo el arranque durante treinta segundos, incluso los servicios que nunca accedían a la red.

La cabecera LSB (Linux Standard Base) situada al principio de ese script intentaba corregir este problema desde dentro. Debian 6.0, en 2011, convirtió insserv en el valor predeterminado: leía Required-Start de cada script, creaba un grafo y renumeraba los enlaces simbólicos. Debian podía entonces ejecutar scripts independientes al mismo tiempo con startpar. Esto ayudó, pero no resolvió el problema más profundo. La dependencia seguía basándose en que terminara un script. Que S20nginx devolviera 0 significa que una función de shell terminó correctamente. No significa que nginx esté aceptando conexiones.

Las cinco cosas que ningún script de init podía resolver

  • Arranque en paralelo. Ordenar por nombre de archivo establece un orden total entre todos los servicios de la máquina, por lo que el arranque tarda como la suma de todas sus partes.
  • Preparación. Un script de inicio termina cuando ha bifurcado el daemon, no cuando el daemon puede atender una solicitud. Por eso, el siguiente script suele iniciarse demasiado pronto.
  • Supervisión. Un daemon se bifurca dos veces y su proceso padre termina. Esto lo desacopla del terminal y hace que PID 1 adopte el proceso. init detecta que termina un proceso hijo, pero no mantiene un vínculo fiable con el proceso que ha sobrevivido.
  • Inicio bajo demanda. inetd (el superservidor de Internet) podía iniciar un daemon cuando llegaba una conexión, pero era un sistema independiente con su propio archivo de configuración. Además, no resolvía el orden del resto de servicios durante el arranque.
  • Control de recursos. Ningún script de init podía limitar la memoria de un servicio ni su proporción de CPU. ulimit se aplicaba a un solo proceso y nice sólo afectaba al planificador. Por eso, un proceso hijo descontrolado de un servicio parecía cualquier otro proceso del sistema.

La falta de supervisión era el problema que más afectaba al trabajo diario. El archivo PID era la solución provisional: el daemon escribía su identificador de proceso en /run/nginx.pid y la función de detención volvía a leerlo. Si el daemon terminaba de forma forzada, el archivo permanecía. Después, el kernel reutilizaba ese número para otro proceso y start-stop-daemon --stop --pidfile enviaba una señal al proceso que lo tuviera en ese momento. Un archivo PID obsoleto es la forma en que un script de init termina el proceso equivocado.

launchd resolvió primero el problema de los sockets

Apple incluyó launchd en Mac OS X 10.4 en 2005. Dave Zarzycki lo escribió. Un proceso sustituyó a init, rc, xinetd, crond y watchdogd.

La idea que merece la pena copiar es la activación mediante sockets. launchd crea primero todos los sockets de escucha y después inicia los demonios. Un cliente que se conecta a un demonio que todavía no se ha iniciado no recibe un rechazo de conexión, porque el kernel mantiene la conexión en la cola de espera de ese socket hasta que el demonio llama a accept(). El orden entre dos demonios deja de ser algo que deba declarar una persona. El socket se encarga de ello.

launchd se creó sobre Mach IPC (comunicación entre procesos), que forma parte del kernel XNU de Apple y no tiene un equivalente en Linux. Portar el código nunca fue realista. La idea sí se adoptó.

Upstart convirtió los eventos en la unidad de trabajo

Upstart, de Canonical, escrito por Scott James Remnant, se incluyó en Ubuntu 6.10 en octubre de 2006. Fedora 9 a Fedora 14 lo utilizaron, al igual que RHEL 6 y Chrome OS. Sustituyó el nivel de ejecución por un evento, y un job indicaba qué eventos debían iniciarlo y detenerlo.

# /etc/init/example.conf
start on filesystem and net-device-up IFACE!=lo
stop on runlevel [!2345]
respawn
respawn limit 10 5
exec /usr/local/bin/exampled

A medida que aumentó el número de jobs, aparecieron dos problemas. El primero era la dirección de las dependencias. Un job indica «iníciame cuando ocurra esto», por lo que la información sobre qué depende de qué queda en el archivo equivocado: un servicio sabe qué necesita, pero no puede saber quién lo necesitará el próximo año. Añadir un servicio solía requerir editar un job existente para que emitiera un evento nuevo.

El segundo era el seguimiento. Upstart supervisaba un daemon que se bifurcaba contando las llamadas fork() mediante ptrace, configurado como expect fork o expect daemon. Si el número de bifurcaciones era incorrecto, Upstart supervisaba un proceso que ya había terminado o esperaba una bifurcación que ya se había producido. El síntoma era que initctl start se quedaba bloqueado sin mostrar ningún error, y el archivo del job no ofrecía ninguna forma de explicar la causa.

Upstart también exigía a los colaboradores firmar el acuerdo de colaboración de Canonical. No era un fallo de ingeniería, pero sí influyó en quién trabajaba en el proyecto.

Replanteamiento de PID 1, abril de 2010

El 30 de abril de 2010 Lennart Poettering publicó un artículo titulado "Rethinking PID 1". Kay Sievers trabajó con él en el proyecto. El argumento tenía cuatro partes.

  • Iniciar menos servicios. Muchos servicios pueden esperar hasta que algo los solicite.
  • Dejar de declarar el orden cuando un socket puede deducirlo. Abrir todos los sockets en una sola pasada y, después, iniciar todo a la vez.
  • Seguir los procesos con grupos de control en lugar de archivos PID.
  • Describir un servicio en un archivo declarativo, para que una misma descripción funcione en todas las distribuciones.

La primera versión se publicó ese mismo año. Fedora 14 ofreció systemd como opción en noviembre de 2010 y Fedora 15 lo convirtió en la opción predeterminada en mayo de 2011.

Por qué cgroups hicieron fiable la supervisión

Un cgroup (grupo de control) es una función del kernel para agrupar procesos. Se integró en Linux 2.6.24, en 2008. systemd coloca cada servicio en su propio cgroup. Un proceso hijo hereda el cgroup de su proceso padre, y un proceso sin privilegios no puede salir de él. Por tanto, hacer doble fork no oculta nada: PID 1 mantiene en todo momento el conjunto exacto de procesos que pertenece a una unidad. Detener un servicio significa matar todo lo que haya en su cgroup. Eso es lo que hace el valor predeterminado de KillMode=control-group.

systemctl status muestra ese grupo:

● nginx.service - A high performance web server and a reverse proxy server
     Loaded: loaded (/usr/lib/systemd/system/nginx.service; enabled; preset: enabled)
     Active: active (running) since Tue 2026-08-11 09:14:22 UTC; 3min ago
   Main PID: 1042 (nginx)
      Tasks: 3 (limit: 4653)
     Memory: 6.1M (peak: 7.4M)
     CGroup: /system.slice/nginx.service
             ├─1042 "nginx: master process /usr/sbin/nginx"
             ├─1043 "nginx: worker process"
             └─1044 "nginx: worker process"

Ese bloque explica por completo el problema de los archivos PID obsoletos. No hay ningún archivo que pueda quedar obsoleto, porque la lista forma parte del estado del kernel.

El mismo árbol también aplica límites, porque los cgroups se diseñaron para llevar contabilidad antes de que se usaran para hacer seguimiento de procesos. MemoryMax=, CPUQuota= y TasksMax= ocupan una línea cada uno. Aplicar un límite estricto de memoria y CPU a un servicio consiste hoy en crear un archivo drop-in. En 2009, era un parche para un script de shell que nadie había escrito.

Por qué todas las distribuciones cambiaron entre 2011 y 2015

  • Fedora 15, mayo de 2011.
  • openSUSE 12.1, noviembre de 2011.
  • Mageia 2, mayo de 2012.
  • Arch Linux, opción predeterminada para las instalaciones nuevas desde octubre de 2012.
  • RHEL 7, junio de 2014.
  • SLES 12, octubre de 2014.
  • Debian 8, abril de 2015.
  • Ubuntu 15.04, abril de 2015.

La fecha de RHEL 7 se prolongó más que las demás porque CentOS 7 la reconstruyó. Ahí fue donde la mayoría de los administradores conoció por primera vez un archivo de unidad, un episodio de la historia más larga que va de Red Hat Linux a CentOS y luego a Rocky y AlmaLinux.

Las razones eran bastante prosaicas, por eso el cambio fue rápido.

  • Un archivo de unidad funciona en todas las distribuciones. Por eso, los proyectos upstream empezaron a distribuir un archivo .service y las distribuciones dejaron de mantener un script de shell por paquete y por versión.
  • El seguimiento de las sesiones de escritorio pasó a systemd-logind después de que ConsoleKit dejara de mantenerse alrededor de 2012. GNOME necesitaba logind, así que una distribución sin systemd tenía que buscar un reemplazo. Ese reemplazo, elogind, es el logind de systemd extraído y mantenido por separado.
  • udev, el gestor de dispositivos, se integró en el árbol de código fuente de systemd en abril de 2012. Las distribuciones que distribuían udev pasaron a seguir el repositorio de systemd. Gentoo creó eudev como bifurcación en respuesta.
  • Los contenedores hicieron más importante el seguimiento fiable de procesos y los límites por servicio, ya que ambos son funciones de cgroup. La cuestión de qué supervisor controla un proceso de contenedor sigue vigente cuando hace que una pila de Docker Compose vuelva a iniciarse después de un reinicio.

La decisión de Debian fue la más visible. El Technical Committee votó en febrero de 2014, la votación terminó en empate y el presidente, Bdale Garbee, emitió el voto decisivo a favor de systemd. Ubuntu anunció unos días después que seguiría a Debian en lugar de continuar con Upstart. Un grupo de desarrolladores de Debian creó Devuan como bifurcación de la distribución en noviembre de 2014 y publicó Devuan 1.0 en mayo de 2017.

Las objeciones, expuestas con precisión

Alcance. Un solo proyecto incluye ahora PID 1, el daemon de registro, la gestión de sesiones de inicio de sesión, el gestor de dispositivos, un daemon de configuración de red, un resolvedor DNS (sistema de nombres de dominio), un cliente NTP (protocolo de tiempo de red), un ejecutor de contenedores y un cargador de arranque. La defensa habitual, según la cual son binarios independientes que no es necesario instalar, es cierta, pero no responde a la objeción. Cuando un escritorio necesita logind y logind se separa del árbol de systemd, la elección deja de ser libre. Eso es lo que significaba el acoplamiento en el argumento, y ocurrió.

El journal binario. journald escribe un formato binario indexado en lugar de texto plano. Ofrece cosas que el texto nunca proporcionó: filtrado por unidad y prioridad, campos estructurados y metadatos que el programa emisor no puede falsificar, porque journald registra por sí mismo la unidad y el cgroup. journalctl -u nginx -p err --since "-1h" sustituye a grep por una expresión regular de fecha. El coste también es real. En una máquina que no arranca, no puede leer el registro con less desde un shell de recuperación. En su lugar, indique a journalctl el disco montado:

sudo journalctl --directory /mnt/var/log/journal --boot -1 --priority err

Hay otra trampa que afecta a muchos usuarios al menos una vez. journald guarda los registros en /run/log/journal, que es memoria, salvo que exista /var/log/journal. En un equipo donde no existe, journalctl -b -1 no tiene nada que mostrar después de un reinicio, justo cuando más lo necesita. Compruébelo y corríjalo:

journalctl --disk-usage
sudo mkdir -p /var/log/journal
sudo systemd-tmpfiles --create --prefix /var/log/journal
sudo systemctl restart systemd-journald

journalctl --disk-usage debería informar ahora de los journals archivados en /var/log/journal. Si también quiere texto plano, establezca ForwardToSyslog=yes en /etc/systemd/journald.conf y mantenga rsyslog instalado.

Depuración del arranque. Cuando una unidad se bloquea, la consola muestra una línea y nada más:

[  *** ] A start job is running for Wait for Network to be Configured (1min 32s / no limit)

Las herramientas para continuar con la investigación sí existen: systemctl list-jobs mientras está bloqueada, systemd-analyze blame y systemd-analyze critical-chain después, y systemd.log_level=debug en la línea de comandos del kernel. La versión equilibrada de la queja es que cualquiera que conociera sh podía leer un script de inicio de principio a fin, mientras que una unidad bloqueada exige saber cuál de una docena de comandos debe utilizar. Es un coste real. Cada administrador lo paga una vez, y muchos administradores lo pagaron al mismo tiempo.

Un valor predeterminado que cambia para todos. systemd 230, en 2016, cambió el valor predeterminado de logind para que los procesos de usuario restantes se terminaran al cerrar la sesión. Las sesiones tmux y screen separadas de la terminal terminaban cuando finalizaba la sesión que las había iniciado. Las distribuciones incluían KillUserProcesses=no en /etc/systemd/logind.conf, y la respuesta compatible es loginctl enable-linger <user>. Un valor predeterminado de un proyecto cambió un hábito del que dependían millones de personas. Eso es lo que significa en la práctica tener "demasiado espacio de usuario concentrado en un solo lugar".

Una dependencia predeterminada es una superficie de seguridad. En marzo de 2024, la puerta trasera de xz-utils tenía como objetivo sshd en Debian y Ubuntu. OpenSSH de upstream no enlaza con libsystemd. Esas distribuciones lo modificaron para que sshd pudiera informar de su disponibilidad a systemd, y libsystemd incorporó liblzma, donde estaba la puerta trasera. El protocolo de disponibilidad es por sí mismo un único datagrama enviado al socket indicado en $NOTIFY_SOCKET, por lo que nunca fue necesaria ninguna biblioteca. La respuesta de systemd fue cargar las bibliotecas de compresión con dlopen, de modo que ya no se enlazan de forma predeterminada. Una clase relacionada de errores muestra el mismo patrón: en 2017, un valor de User= que empezaba por un dígito se consideraba no válido y la unidad se ejecutaba como root en lugar de fallar, por lo que un error tipográfico se convertía en una escalada de privilegios. Las versiones posteriores rechazan iniciar la unidad.

Historial en su propio indicador de systemctl

Cada problema anterior es ahora una directiva en un archivo que puede leer.

  • El arranque en serie se convirtió en After= y Wants=, y systemd-analyze critical-chain muestra qué retuvo realmente el arranque.
  • La preparación se convirtió en Type=notify, donde el servicio escribe READY=1 en $NOTIFY_SOCKET cuando puede atender peticiones. Type=forking con PIDFile= todavía existe para daemons antiguos, y es el tipo que falla con start operation timed out. Terminating. cuando el archivo PID nunca aparece. Elegir el tipo incorrecto hace que una unidad informe de que está activa mientras el daemon que inició ya ha terminado, por lo que conviene saber qué Type= coincide con la forma real de inicio del daemon antes de escribir esa línea.
  • La supervisión pasó al cgroup, de modo que Restart=on-failure con RestartSec= sustituye a un script envoltorio, y StartLimitBurst= evita que un bucle de fallos se ejecute indefinidamente.
  • inetd se convirtió en una unidad .socket situada junto a la unidad .service.
  • ulimit se convirtió en MemoryMax=, CPUQuota= y TasksMax=.
  • La línea su - appuser -c de un script de init se convirtió en User=, NoNewPrivileges=yes y ProtectSystem=strict, por lo que ejecutar un servicio con un usuario sin privilegios es la estructura predeterminada de una unidad y no una tarea adicional.
[Unit]
Description=Example API
Wants=network-online.target
After=network-online.target postgresql.service
Requires=postgresql.service

[Service]
Type=notify
ExecStart=/usr/local/bin/exampled
Restart=on-failure
RestartSec=2
MemoryMax=512M
CPUQuota=50%
TasksMax=128
User=exampled
NoNewPrivileges=yes
PrivateTmp=yes
ProtectSystem=strict
StateDirectory=exampled

[Install]
WantedBy=multi-user.target

Una línea de ese archivo es el error que todo el mundo comete una vez. Requires=postgresql.service es un requisito, no una regla de orden: indica que la unidad falla si Postgres falla, pero no indica que Postgres deba iniciarse primero. Sin After=postgresql.service, ambos se inician al mismo tiempo y el servicio intenta conectarse a un puerto en el que todavía no escucha ningún proceso. Son directivas separadas de forma intencionada, porque a veces se necesita una sin la otra. ProtectSystem=strict monta el sistema de archivos en modo de sólo lectura para este servicio. Por eso existe StateDirectory=: proporciona al servicio una ruta con permisos de escritura bajo /var/lib.

El lugar más claro para ver 2005 en un servidor de 2026 es SSH en Ubuntu 24.04, que incluye systemd 255 en agosto de 2026. ssh.service se activa mediante socket de forma predeterminada: ssh.socket mantiene el socket de escucha y sshd se inicia cuando llega una conexión. Por eso Port 2222 en /etc/ssh/sshd_config no tiene ningún efecto: sshd no es el proceso que abrió el puerto. El cambio debe hacerse en la unidad de socket.

sudo systemctl edit ssh.socket
[Socket]
ListenStream=
ListenStream=2222

El ListenStream= vacío borra el valor heredado de la unidad incluida en el paquete. Si se omite, se obtienen ambos puertos, porque systemd añade elementos a una lista en lugar de sustituirla. A continuación, aplique los cambios y compruébelos. Mantenga abierta una segunda sesión SSH durante todo el proceso:

sudo systemctl daemon-reload
sudo systemctl restart ssh.socket
sudo ss -lntp | grep 2222

ss debería mostrar un socket en el puerto 2222 propiedad de systemd, no de sshd. Ese es el diseño de launchd, veinte años después, en su VPS. Si prefiere el comportamiento anterior, sudo systemctl disable --now ssh.socket seguido de sudo systemctl enable --now ssh.service le proporciona un sshd de ejecución prolongada que vuelve a leer Port desde su propia configuración.

Los detalles que encontrará dependen de la versión que ejecute. Por eso conviene conocer la diferencia entre una versión LTS y una versión intermedia de Ubuntu antes de planificar una actualización. Cuando administra más de una máquina, que el archivo de unidad sea idéntico en todas ellas es la razón por la que administrar varios servidores desde un solo lugar es ahora un problema de configuración y no de scripts de shell. Y cuando escriba sus propias unidades, el par de servicio y temporizador realiza la tarea que en 2009 habría dividido entre un script de init y una línea de cron.

FAQ

¿Por qué las distribuciones Linux sustituyeron SysV init por systemd?

Por dos razones de ingeniería y una razón de mantenimiento. SysV init ordenaba los servicios por nombre de archivo, que indica una posición y no una dependencia, y perdía el seguimiento de cualquier daemon que se bifurcara de su proceso padre. Por eso, los archivos PID obsoletos podían terminar el proceso equivocado. systemd resolvió el orden mediante la activación por sockets y las directivas de dependencias, y resolvió el seguimiento mediante grupos de control. La razón de mantenimiento determinó la velocidad del cambio: un archivo de unidad funciona en todas las distribuciones, por lo que los proyectos upstream distribuían un archivo .service y los mantenedores de las distribuciones dejaron de escribir un script de shell por paquete. Fedora 15 cambió en mayo de 2011 y Ubuntu 15.04 fue el último gran sistema en adoptar el cambio, en abril de 2015.

¿Es systemd un único binario gigante?

No. El árbol de código fuente compila muchos programas independientes. PID 1 es /usr/lib/systemd/systemd, mientras que journald, logind y udevd son procesos independientes con sus propios binarios; ejecute ls /usr/lib/systemd/ para verlos en su propio equipo. La crítica que sigue siendo válida se refiere al acoplamiento de las versiones y no al tamaño del binario: estos programas se publican juntos y comparten interfaces privadas, por lo que las distribuciones suelen adoptarlos como un conjunto, y software como GNOME empezó a esperar específicamente logind.

¿Todavía puedo ejecutar Linux sin systemd?

Sí. Devuan distribuye sysvinit, Gentoo usa OpenRC de forma predeterminada, Void usa runit, Alpine usa busybox init con OpenRC y Slackware mantiene scripts de estilo BSD. El coste es el trabajo de compatibilidad. El software de escritorio que espera logind necesita elogind, que es el logind de systemd mantenido como paquete independiente, y una cantidad cada vez mayor de software de servidor sólo distribuye un archivo .service, por lo que debe escribir y mantener el script de inicio usted mismo.

¿Por qué el journal es binario en lugar de ser un archivo de texto plano?

Porque journald almacena campos estructurados con un índice. Esto permite filtrar por unidad y prioridad, además de conservar metadatos que el programa que envía el mensaje no puede falsificar: journald registra por sí mismo la unidad, el cgroup y el UID real, en lugar de confiar en la línea del registro. El coste es que necesita journalctl para leerlo, incluso desde un sistema de rescate, donde debe indicarle el disco montado mediante journalctl --directory /mnt/var/log/journal. Si también quiere texto, establezca ForwardToSyslog=yes en /etc/systemd/journald.conf.

¿Qué sustituyó la edición de mi script /etc/init.d?

Los archivos drop-in. No edite la unidad en /usr/lib/systemd/system/, porque una actualización del paquete la sobrescribe. Ejecute sudo systemctl edit nginx.service y systemd creará /etc/systemd/system/nginx.service.d/override.conf, que se fusiona con la unidad proporcionada por el paquete. systemctl cat nginx.service muestra el resultado fusionado y systemd-delta enumera todas las anulaciones del equipo. Después de editar cualquier archivo manualmente, ejecute sudo systemctl daemon-reload o el siguiente comando mostrará Warning: The unit file, source configuration file or drop-ins of nginx.service changed on disk.

#systemd#linux#init#sysvinit#history