Saltar al contenido
← Blog

systemd 260 elimina SysV: ¿y ahora?

systemd 260 ha borrado el sysv-generator, rc-local.service y systemd-sysv-install. Cada script de /etc/init.d y cada /etc/rc.local del parque tiene ya fecha de caducidad. Así se localizan y así se convierten bien.

·18 min de lectura
  • systemd
  • Linux
  • Administración de sistemas
  • Migración

Durante unos quince años, systemd tradujo en silencio los scripts de /etc/init.d a servicios en cada arranque, sin pedir que nadie se enterara. En la versión 260, publicada en marzo de 2026, dejó de hacerlo. El systemd-sysv-generator, el systemd-rc-local-generator junto con rc-local.service, y el gancho systemd-sysv-install que había detrás de systemctl enable fueron eliminados sin más. En los scripts no ha cambiado nada. Lo que ha cambiado es que ya nadie los lee.

Portada del artículo dividida en dos paneles. A la izquierda, un panel titulado «DELETED IN systemd 260» que enumera systemd-sysv-generator, systemd-rc-local-generator, rc-local.service y systemd-sysv-install. A la derecha, un panel titulado «WHAT REPLACES WHAT» con cuatro pares antes/después: start-stop-daemon --background pasa a Type=exec; --make-pidfile pasa a ningún archivo PID; --chuid acme pasa a User=acme; y /etc/rc[2-5].d/S20name pasa a WantedBy=multi-user.target.
Tres componentes eliminados en una sola versión. La migración en sí no es difícil: lo difícil es saber qué scripts quedan y en qué máquinas antes de que lo diga una actualización desatendida.

Esta es la migración escrita para quien tiene que ejecutarla en servidores reales, no para admirarla desde lejos: cómo inventariar todos los scripts envueltos de un parque entero usando un campo del journal que casi nadie sabe que existe, qué escribía exactamente el generador para poder copiarlo en vez de adivinarlo, la conversión campo a campo de un script de init realista, el error de Type= que hace que una unit informe de éxito con el demonio muerto, y los comandos que sustituyen a init 3. Cada número de versión y cada directiva se han contrastado con el archivo NEWS de systemd y con el código fuente en las etiquetas donde el código eliminado todavía existía, no con la memoria.

La unit no ha fallado. Es que no existe

El síntoma desconcierta más de lo normal, porque es una ausencia y no un error. El servicio no aparece como failed. No aparece en absoluto. systemctl status responde que no encuentra la unit mientras el script sigue en disco, ejecutable, sin tocar desde 2015. Y es el comportamiento correcto: el script nunca fue una unit. Un generador fabricaba una en /run/systemd/generator.late en cada arranque, y ese directorio ahora está vacío.[v260]

Lo que se veQué significaDónde se trata
Unit x.service could not be found, mientras /etc/init.d/x existe y es ejecutableEl generador que inventaba esa unit se ha eliminado. No hay nada roto; simplemente ya no hay traducción.Lo que escribía el generador
Un servicio que arrancó en cada inicio durante años sencillamente no arranca, y en el journal no aparece nadaLa misma causa, vista desde el arranque y no desde la línea de comandos. No hay unit fallida porque no hay unit.Convertir un script
/etc/rc.local sigue siendo ejecutable y ya no se ejecutasystemd-rc-local-generator y rc-local.service se eliminaron junto con el generador de SysV.Sustituir /etc/rc.local
systemctl enable sobre un servicio antiguo ya no hace nada útilsystemd-sysv-install, el gancho que reenviaba enable/disable a chkconfig o a update-rc.d, ha desaparecido.Qué debe entregar un paquete
telinit: command not found, o init 3 no hace nadaEliminado en 258, dos versiones antes y por un motivo distinto: se descartó el concepto de runlevel, no solo los scripts.init 3, telinit y runlevel
Una unit convertida informa de active, pero el proceso no está, o informa de failed mientras el demonio correEl Type= es incorrecto para un demonio que hace fork. Es el fallo de conversión más habitual.Type=forking y archivos PID
# After the upgrade, the service that has started at boot for eleven years
# is simply not there. Not failed - not there.

systemctl status acme-collector
# Unit acme-collector.service could not be found.

ls -l /etc/init.d/acme-collector
# -rwxr-xr-x 1 root root 1284 Mar 14  2015 /etc/init.d/acme-collector
#            ^ the script is still on disk, still executable, still correct.

# The script was never a service. A generator turned it into one on every
# boot, and the generator is gone. Confirm which systemd you are on:
systemctl --version | head -1
# systemd 260 (260.2-1)

# And confirm the generator really is absent rather than just failing:
ls /usr/lib/systemd/system-generators/ | grep -E 'sysv|rc-local'
# (no output on 260 and later; on 259 and earlier you get one or two lines)

# The same disappearance, seen from the other end - nothing was generated:
ls /run/systemd/generator.late/
# (empty, or missing your unit)

Los generadores se ejecutan antes de que el gestor cargue ninguna unit, en cada arranque y en cada daemon-reload, y su salida vive en tmpfs. Por eso nada en disco parece distinto después de actualizar, y por eso no hay nada que reparar: no existe ningún archivo roto, solo un paso de traducción que ya no ocurre. Todo lo que sigue da por hecho que hay acceso por consola a la máquina y que se puede reiniciar una vez al final. Nada de este artículo modifica el estado del sistema hasta que se decide hacerlo; los comandos de inventario solo leen.[generator]

Qué se eliminó y en qué momento exacto

Intervienen tres versiones distintas, y confundirlas es la razón de que tanto consejo sobre este tema sea erróneo. Los scripts de servicio quedaron declarados obsoletos ya en la versión 255. Lo que hizo la versión 258, en septiembre de 2025, fue otra cosa: eliminar las interfaces de estado del sistema de System V, es decir /dev/initctl, los comandos initctl, runlevel y telinit, el control de estado mediante init 3, las units runlevel[0-6].target y el registro de las transiciones de runlevel en utmp y wtmp. La versión 259 revirtió parcialmente una de esas eliminaciones. La versión 260 eliminó ya los propios scripts de servicio.[news][v255][v258]

VersiónFechaQué ocurrió
258Septiembre de 2025Las notas de esta versión reiteran la obsolescencia de los scripts de servicio SysV —declarada en la 255— y todavía sitúan la eliminación en la 259; después se pospuso a la 260. Por separado y de forma inmediata: se eliminan /dev/initctl, los comandos initctl, runlevel y telinit, los cambios de estado al estilo init 3 y las units runlevel[0-6].target, y las transiciones de runlevel dejan de registrarse en utmp/wtmp. En la misma versión se retira el soporte de cgroup v1.
259Diciembre de 2025Se restauran runlevel[0-6].target, pero solo cuando la distribución compila con -Dcompat-sysv-interfaces=yes. Los comandos eliminados no vuelven. Esta es la versión que trae Ubuntu 26.04 LTS.
260Marzo de 2026Se elimina el soporte para scripts de servicio System V. Borrados: systemd-sysv-generator; systemd-rc-local-generator y rc-local.service; systemd-sysv-install. Las opciones de compilación -Drc-local=, -Dsysvinit-path= y -Dsysvrcnd-path= quedan obsoletas. El kernel mínimo sube a 5.10, glibc a 2.34 y OpenSSL a 3.0.
261 y posteriores2026Ningún cambio adicional relacionado con SysV; la capa de compatibilidad simplemente no está. Se espera que las opciones de compilación obsoletas desaparezcan en una versión futura.

La reversión parcial de 259 importa cuando se administra un parque heterogéneo: las units runlevel[0-6].target volvieron, pero solo como una opción que cada distribución tiene que habilitar en tiempo de compilación, y los comandos no volvieron en absoluto. Así que systemctl isolate runlevel3.target puede funcionar en una máquina y no en la siguiente, según cómo haya compilado su paquete cada distribución. Mejor no construir hábitos sobre eso. Dónde están realmente los servidores depende de qué systemd traen:[v259][v260]

Dónde se estásystemdScripts SysV y rc.local
Debian 12 bookworm252Ambos funcionan. Sin aviso por script en el journal.
Debian 13 trixie257Ambos funcionan, y el journal ya avisa por cada script envuelto.
Ubuntu 24.04 LTS255Ambos funcionan, y el journal ya avisa por cada script envuelto.
Ubuntu 26.04 LTS259Ambos funcionan, y el journal avisa por cada script envuelto. Canonical indica que es el último Ubuntu con compatibilidad SysV.
Ubuntu 26.10260 o posteriorEliminado. Los scripts siguen en disco y no se leen nunca.
RHEL 9 / RHEL 10252 / 257Ambos funcionan durante toda la vida de la versión. RHEL 9 envuelve en silencio; RHEL 10 avisa por cada script y sus notas de publicación ya recogen la obsolescencia. El cambio llega con la siguiente versión mayor.
Distribuciones rolling260 o posteriorYa eliminado. Mejor comprobarlo con systemctl --version que suponerlo.

Las fechas convierten esto en algo concreto y no teórico. Las propias notas de publicación de Canonical indican que Ubuntu 26.04 LTS —que trae systemd 259— es la última versión de Ubuntu con compatibilidad System V, y que el cambio entra en vigor en 26.10. Esa versión está prevista para octubre de 2026. Quien esté en las versiones intermedias, o tenga cualquier cosa sobre una distribución rolling, tiene el plazo a semanas vista y no a años. Quien esté en una LTS o en una distribución empresarial tiene hasta la siguiente actualización mayor, que es justo el momento en el que menos apetece descubrir scripts de shell sin dueño.[ubuntu2604][ubuntu2610][rhel10]

Encontrar todos los scripts antes de que los encuentre la actualización

El inventario va primero, y hay que hacerlo mientras el generador siga funcionando, porque es el propio generador quien da la respuesta. Desde la versión 255, cada vez que systemd envuelve un script emite un aviso estructurado con un identificador de mensaje estable, más dos campos propios del journal: SYSVSCRIPT= con la ruta y UNIT= con el nombre que se ha inventado. Eso convierte una auditoría de todo el parque en una única consulta exacta, en lugar de una conjetura montada a base de ls y buena fe.[messages][v255][journalctl]

#!/usr/bin/env bash
# Inventory every SysV script this host still depends on. Run it BEFORE the
# upgrade, on every machine, and keep the output.

echo '== 1. scripts systemd is currently wrapping =========================='
# systemd 255 and later log a structured warning for every script they wrap.
# The message ID is stable, so this is an exact list rather than a guess.
journalctl -b -o json --output-fields=SYSVSCRIPT,UNIT \
  MESSAGE_ID=a8fa8dacdb1d443e9503b8be367a6adb 2>/dev/null \
  | python3 -c 'import sys,json
for l in sys.stdin:
    d = json.loads(l)
    print("%-28s %s" % (d.get("UNIT","?"), d.get("SYSVSCRIPT","?")))'

echo '== 2. fallback for systemd 254 and older ============================='
# Older systemd wraps silently. Ask the manager which units came from a
# generator instead: generated units live under /run/systemd/generator.late
# and carry a SourcePath= pointing back at the script. This also catches
# scripts that were wrapped before the current boot's journal starts.
systemctl list-units --type=service --all --no-legend --plain \
  | awk '{print $1}' \
  | while read -r u; do
      frag=$(systemctl show -p FragmentPath --value "$u" 2>/dev/null)
      case "$frag" in
        /run/systemd/generator*)
          src=$(systemctl show -p SourcePath --value "$u" 2>/dev/null)
          [ -n "$src" ] && printf '%-28s %s\n' "$u" "$src" ;;
      esac
    done

echo '== 3. scripts on disk with no NATIVE unit behind them ================'
# Note the test: `systemctl cat` succeeds for a generated unit too, so asking
# whether the unit exists tells you nothing while the generator is running.
# Ask where the definition lives instead.
for f in /etc/init.d/*; do
  [ -f "$f" ] && [ -x "$f" ] || continue
  n=$(basename "$f"); n=${n%.sh}
  frag=$(systemctl show -p FragmentPath --value "$n.service" 2>/dev/null)
  case "$frag" in
    /etc/systemd/system/*|/usr/lib/systemd/system/*|/lib/systemd/system/*) ;;
    *) echo "no native unit: $f" ;;
  esac
done

echo '== 4. runlevel wiring (this is what set the boot order) ============='
ls -l /etc/rc[1-5].d/S* 2>/dev/null | awk '{print $9, $10, $11}'

echo '== 5. rc.local ======================================================'
ls -l /etc/rc.local 2>/dev/null && \
  { [ -x /etc/rc.local ] && echo 'executable: it runs at boot today'; }

El inventario hay que lanzarlo en todos los hosts, no en uno representativo. Por experiencia, los scripts que sobreviven hasta 2026 nunca son los que conoce el repositorio de gestión de configuración: son el apaño que dejó un externo, el agente de un appliance de terceros, el envoltorio de copias de seguridad anterior al equipo actual. Son exactamente las máquinas que a nadie se le ocurre revisar y exactamente los servicios cuya ausencia se detecta una semana después.

De esa consulta hay que retener dos detalles, más una advertencia sobre su alcance: el paso 1 solo lee el journal del arranque en curso, de modo que un host cuyo aviso ya haya rotado no aparecerá ahí y sigue necesitando el paso 2. systemd solo envolvió jamás archivos regulares ejecutables dentro del directorio de init, de modo que un script dejado en modo 644 ya estaba muerto y no hay que resucitarlo. Y una unit nativa siempre ganaba a un script del mismo nombre: el generador omitía explícitamente cualquier script para el que ya existiera una unit real. Esa segunda regla es la que hace que toda la migración se pueda hacer de forma incremental sin riesgo: se puede instalar la unit nueva con el script todavía presente y todavía habilitado, y el script simplemente deja de consultarse.[gensrc]

Lo que el generador escribía en silencio por ti

Antes de escribir una sola unit, merece la pena coger la que produce el generador y leerla. No es una aproximación burda: es una traducción determinista, y es lo más parecido a una especificación del comportamiento real del script durante el arranque. Hay que capturarla con systemctl cat mientras aún se pueda; después de actualizar esa información desaparece, y reconstruirla a partir de la cabecera LSB es donde se introducen los errores de ordenación.[gensrc][genman]

# Capture the generated unit BEFORE you upgrade. It is the specification for
# the unit you are about to write, produced by the only thing that ever read
# your init script correctly.
systemctl cat acme-collector.service > ~/acme-collector.generated.service

# What comes out looks like this - and every line of it is decided by the
# generator source, not by convention:

# /run/systemd/generator.late/acme-collector.service
# Automatically generated by systemd-sysv-generator

[Unit]
Documentation=man:systemd-sysv-generator(8)
SourcePath=/etc/init.d/acme-collector
Description=LSB: ACME metrics collector
Before=multi-user.target
Before=multi-user.target
Before=multi-user.target
Before=graphical.target
After=remote-fs.target
After=network-online.target
After=time-sync.target
After=postgresql.service
Wants=network-online.target

[Service]
Type=forking
Restart=no
TimeoutSec=5min
IgnoreSIGPIPE=no
KillMode=process
GuessMainPID=no
RemainAfterExit=no
PIDFile=/var/run/acme-collector.pid
SuccessExitStatus=5 6
ExecStart=/etc/init.d/acme-collector start
ExecStop=/etc/init.d/acme-collector stop
ExecReload=/etc/init.d/acme-collector reload

# Yes, Before=multi-user.target really is repeated. The rc2.d, rc3.d and rc4.d
# symlinks each append it and nothing deduplicates the list. Harmless, but it
# tells you the file was machine-written and how.
#
# Note what is NOT there: no [Install] section. The generator wired the unit in
# by dropping symlinks next to it - into multi-user.target.wants for rc2-rc4,
# and into graphical.target.wants for rc5 - which is why `systemctl is-enabled`
# on one of these was never a straight answer.

Y ahora la parte que sorprende a casi todo el mundo, y que se puede comprobar en el propio código del generador. Default-Start: y Default-Stop: de la cabecera LSB nunca se leyeron. El generador solo interpreta Provides:, Required-Start:, Should-Start:, X-Start-Before:, X-Start-After:, los dos campos de descripción y los comentarios al estilo Red Hat # pidfile: y # description:. El cableado de runlevels venía íntegramente de los enlaces simbólicos S??nombre en /etc/rc[1-5].d. Quien lleve una década manteniendo Default-Start lleva una década manteniendo un comentario.[lsb][exitstatus]

En el script de initQué hacía el generador con elloQué escribir en la unit
Provides: nombreSi nombra un servicio, un enlace simbólico de alias. Si nombra una $facility, Before= más Wants= sobre ese target, en un solo sentido. Si repite el nombre del archivo, se ignora.Alias= en [Install], o nada
Required-Start: $networkAfter=network-online.target y Wants=network-online.target, porque ese target es inerte salvo que algo tire de élWants= + After=network-online.target
Required-Start: $remote_fsAfter=remote-fs.targetAfter=remote-fs.target
Required-Start: $namedAfter=nss-lookup.targetAfter=nss-lookup.target
Required-Start: $portmapAfter=rpcbind.targetAfter=rpcbind.target
Required-Start: $timeAfter=time-sync.targetAfter=time-sync.target
Required-Start: $local_fs o $syslogNada. Ambos se descartaban: bajo systemd ya están satisfechos antes de que arranque cualquier servicio ordinario.Nada
Should-Start: fooAfter=foo.service: solo ordenación, nunca un requisitoAfter=foo.service, sin Requires=
X-Start-Before: fooBefore=foo.serviceBefore=foo.service
Default-Start: / Default-Stop:Absolutamente nada. Nunca se interpretó. El cableado de runlevels venía de los enlaces S?? en /etc/rc[1-5].d.WantedBy=multi-user.target en [Install]
Enlace en /etc/rc2.d, rc3.d, rc4.dBefore=multi-user.target y un enlace de tipo wants dentro de ese targetWantedBy=multi-user.target
Enlace en /etc/rc5.dBefore=graphical.target y un enlace de tipo wants dentro de ese targetWantedBy=graphical.target
Enlace en /etc/rc1.dBefore=rescue.target y un enlace de tipo wants dentro de ese target: el generador trata rc1.d igual que rc2.drc5.dCasi nunca es lo que se busca; mejor omitirlo
# pidfile: /ruta (estilo Red Hat)PIDFile=/ruta y RemainAfterExit=no. Sin ninguna línea pidfile, RemainAfterExit=yesBorrar ambos. Usar Type=exec en su lugar
### BEGIN INIT INFO presenteSuccessExitStatus=5 6, los códigos LSB de «no instalado» y «no configurado»Solo si el script sigue saliendo con esos códigos
Una línea de uso que contenga |reload} o similarExecReload=/etc/init.d/x reload. Sin línea de uso, sin verbo reload.ExecReload=/bin/kill -HUP $MAINPID

Dos correspondencias de facilidades de esa tabla merecen una segunda lectura, porque son el origen habitual del clásico «bajo SysV funcionaba y ahora tiene una condición de carrera». $network no se convertía en network.target; se convertía en network-online.target, y el generador añadía tanto After= como Wants=, porque ese target no hace nada salvo que algo tire activamente de él. Por su parte, $local_fs y $syslog no se traducían a nada: se descartaban, con el argumento razonable de que bajo systemd ambos están ya garantizados cuando arranca cualquier servicio normal. Esas decisiones hay que copiarlas en la unit, no improvisar otras.[special]

Convertir un script, campo a campo

Este es un script realista, no un ejemplo de juguete. Tiene cabecera LSB, un comentario de archivo PID al estilo Red Hat, una llamada a start-stop-daemon que manda el proceso a segundo plano y escribe el archivo PID en nombre del demonio, un verbo reload y la línea de uso que es la única razón por la que el generador llegaba a emitir un ExecReload=. La mayoría de los scripts reales que hay por ahí son peores que este.

#!/bin/sh
### BEGIN INIT INFO
# Provides:          acme-collector
# Required-Start:    $remote_fs $network $time
# Required-Stop:     $remote_fs $network
# Should-Start:      postgresql
# Default-Start:     2 3 4 5
# Default-Stop:      0 1 6
# Short-Description: ACME metrics collector
### END INIT INFO
# pidfile: /var/run/acme-collector.pid

DAEMON=/opt/acme/bin/collector
PIDFILE=/var/run/acme-collector.pid
RUNAS=acme
OPTS="--config /etc/acme/collector.conf"

case "$1" in
  start)
    start-stop-daemon --start --quiet --background --make-pidfile \
        --pidfile "$PIDFILE" --chuid "$RUNAS" --exec "$DAEMON" -- $OPTS
    ;;
  stop)
    start-stop-daemon --stop --quiet --pidfile "$PIDFILE" --retry 30
    rm -f "$PIDFILE"
    ;;
  reload)
    kill -HUP "$(cat "$PIDFILE")"
    ;;
  restart)
    "$0" stop; sleep 1; "$0" start
    ;;
  *)
    echo "Usage: $0 {start|stop|restart|reload}" >&2
    exit 2
    ;;
esac
exit 0

Y esto es lo que lo sustituye. Los comentarios dicen aquí más que las directivas: las decisiones interesantes son las que tratan sobre qué no arrastrar. Se va el forking. Se va el archivo PID manual. Se va el envoltorio start-stop-daemon, porque todo lo que hacía —bajar privilegios, mandar a segundo plano, seguir el proceso— es ahora una directiva.[service][exec]

# /etc/systemd/system/acme-collector.service
#
# Everything above the blank line inside [Service] is the translation of the
# init script. Everything below it is what the wrapper could never give you.

[Unit]
Description=ACME metrics collector
Documentation=https://example.internal/acme/collector
# $network in the old header became network-online.target - and that target
# only does something if a unit actively pulls it in, hence the Wants=.
Wants=network-online.target
After=network-online.target
# "Should-Start: postgresql" was ordering only, never a requirement. Keep it
# that way: After= without Requires= means a database outage does not cascade.
After=postgresql.service

[Service]
Type=exec
User=acme
Group=acme
# The single most important line of the migration: stop the daemon forking.
# Almost every daemon has a flag for this - --foreground, -f, -D, --no-detach,
# --nodaemon. Find it and the PID file problem disappears with it.
ExecStart=/opt/acme/bin/collector --config /etc/acme/collector.conf --foreground
ExecReload=/bin/kill -HUP $MAINPID
Restart=on-failure
RestartSec=5s
TimeoutStopSec=30s

# /var/run/acme-collector.pid was in /run all along; let systemd own it.
RuntimeDirectory=acme-collector
StateDirectory=acme-collector
ConfigurationDirectory=acme

NoNewPrivileges=yes
PrivateTmp=yes
PrivateDevices=yes
ProtectSystem=strict
ProtectHome=yes
ProtectKernelTunables=yes
ProtectKernelModules=yes
ProtectControlGroups=yes
RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX
RestrictNamespaces=yes
RestrictSUIDSGID=yes
LockPersonality=yes
MemoryDenyWriteExecute=yes
SystemCallArchitectures=native
SystemCallFilter=@system-service
# Empty means "no capabilities at all". If the daemon binds a port below 1024,
# use CapabilityBoundingSet=CAP_NET_BIND_SERVICE plus the matching
# AmbientCapabilities= instead of dropping this line.
CapabilityBoundingSet=

[Install]
WantedBy=multi-user.target
En el scriptEn la unitNota
start-stop-daemon --background o &Type=exec y el parámetro de primer plano del propio demonioDe eso se trata. No mandar nada a segundo plano.
--make-pidfile, echo $! > …bórralosystemd conoce el PID porque fue él quien bifurcó el proceso.
--chuid, su - usuario -c, runuserUser= y Group=Se aplica a todos los descendientes, no solo al primero.
mkdir -p /run/x; chown …RuntimeDirectory=xSe crea antes de arrancar y se borra tras parar, con el propietario correcto.
mkdir -p /var/lib/xStateDirectory=xSobrevive a los reinicios; para el resto, CacheDirectory=/LogsDirectory=.
ulimit -n 65535LimitNOFILE=65535Un ulimit de shell en el script se aplicaba a la shell, a veces no al demonio.
export FOO=barEnvironment= o EnvironmentFile=EnvironmentFile=-/etc/default/x mantiene válidos los archivos de configuración existentes.
nice -n 10, ioniceNice=10, IOSchedulingClass=O ir más allá con CPUWeight= e IOWeight=.
cd /opt/xWorkingDirectory=/opt/x
>> /var/log/x.log 2>&1bórraloLa salida va al journal, etiquetada con la unit. No hay rotación que escribir.
sleep 5; check_if_upType=notify en el demonio, o una comprobación de salud en una unit aparteUn sleep en un verbo start es una condición de carrera con la mecha más larga.
restart) stop; sleep 1; startbórralosystemctl restart existe y espera como es debido.
Bloque status)bórralosystemctl status e is-active informan del estado real.
trap / limpieza al pararExecStop=, o mejor, gestionar SIGTERM dentro del demonioTimeoutStopSec= acota cuánto se espera antes del SIGKILL.

La correspondencia en términos generales, para los modismos que uno se encuentra de verdad. Donde una fila dice «bórralo», no es una simplificación: el comportamiento lo aporta el gestor, y reimplementarlo en shell es la forma de acabar con dos cosas peleándose por el mismo proceso.[unit]

# Cut over on a system that still has the generator, so you can roll back by
# doing nothing. Order matters here.

# 1. Check the file before systemd ever loads it.
sudo systemd-analyze verify /etc/systemd/system/acme-collector.service

# 2. Install it and reload. The native unit now WINS over the script: the
#    generator skips any script that already has a unit of the same name.
sudo systemctl daemon-reload

# 3. Prove which one is live before you touch the running process.
systemctl show -p FragmentPath -p SourcePath --value acme-collector.service
# /etc/systemd/system/acme-collector.service
#                       <- empty SourcePath = no longer generated. Good.

# 4. Restart through systemd, not through the script.
sudo systemctl restart acme-collector.service
systemctl status acme-collector.service --no-pager

# 5. Enable it explicitly. The generator's implicit wiring is not inherited.
sudo systemctl enable acme-collector.service
systemctl is-enabled acme-collector.service   # -> enabled

# 6. Retire the old wiring. Do NOT delete the script yet - move it aside, so
#    a rollback is one mv away for the length of the change window.
sudo rm -f /etc/rc[0-6].d/[SK]??acme-collector
sudo mv /etc/init.d/acme-collector /root/retired-init.d-acme-collector

# 7. The only real test: reboot, then read the boot rather than the status.
sudo systemctl reboot
journalctl -b -u acme-collector.service
systemd-analyze blame | head -20

Haciendo el cambio en este orden, la vuelta atrás cuesta un mv. El paso más importante con diferencia es el tercero: systemctl show -p FragmentPath -p SourcePath dice sin ambigüedad cuál de las dos definiciones está viva. Un SourcePath no vacío significa que se sigue mirando el envoltorio generado y que el archivo nuevo tiene una colisión de nombre, un error de sintaxis o está en el directorio equivocado.[systemctl]

Sustituir /etc/rc.local sin heredar su peor costumbre

/etc/rc.local merece sección propia porque es lo que todo el mundo va dejando para después, y porque la unit de compatibilidad que pierde tenía una semántica genuinamente rara que más vale conocer antes de copiarla hacia adelante. El rc-local.service real usaba Type=forking con GuessMainPID=no, RemainAfterExit=yes y TimeoutSec=infinity, ordenado únicamente después de network.target, y solo se activaba a sí mismo si el archivo era ejecutable. La propia página de manual del proyecto se molestaba en advertir de que esa ordenación no significa que la red sea usable, y de que todo aquello existía por compatibilidad con determinados sistemas System V y no como un diseño que nadie defendiera.[rclocalunit][rclocalman]

# /etc/rc.local was never one thing. Read yours first and split it: a mount,
# a sysctl, a firewall rule and a background daemon are four different units,
# and only the last one belongs in a service.

# --- 1. the direct replacement, when it really is one script ---------------
# /etc/systemd/system/local-startup.service
[Unit]
Description=Local startup commands (former /etc/rc.local)
ConditionFileIsExecutable=/usr/local/sbin/local-startup
# The old rc-local.service used After=network.target, which upstream's own
# manual page warns does not mean the network is usable. If your script talks
# to the network, use network-online.target and pull it in.
Wants=network-online.target
After=network-online.target

[Service]
Type=oneshot
RemainAfterExit=yes
ExecStart=/usr/local/sbin/local-startup
# The original had TimeoutSec=infinity. Keeping that means a hung script hangs
# the boot forever with no message. Pick a number you would accept waiting.
TimeoutStartSec=90s

[Install]
WantedBy=multi-user.target

# --- 2. the pieces that should not be a service at all ---------------------
# sysctl:      /etc/sysctl.d/90-local.conf
# modules:     /etc/modules-load.d/local.conf
# files/dirs:  /etc/tmpfiles.d/local.conf
# mounts:      /etc/fstab or a .mount unit
# periodic:    a .timer, not a sleep loop

# --- 3. install it --------------------------------------------------------
sudo install -m 0755 /etc/rc.local /usr/local/sbin/local-startup
sudo systemctl daemon-reload
sudo systemctl enable --now local-startup.service
systemctl status local-startup.service --no-pager
# On SELinux systems relabel the script, exactly as the old generator's manual
# page told you to do for /etc/rc.local:
command -v restorecon >/dev/null && sudo restorecon -v /usr/local/sbin/local-startup

Ya puestos, hay dos costumbres que conviene romper. No hay que conservar TimeoutSec=infinity: convierte un comando colgado en un arranque que no termina nunca, en silencio y sin ninguna unit fallida a la que señalar. Y no hay que mantenerlo todo en un solo script. Un rc.local suele ser cuatro cosas sin relación entre sí que acabaron juntas porque solo había un gancho disponible: un sysctl, un montaje, una regla de cortafuegos y algo que debería haber sido un demonio. Como units separadas se pueden ordenar, reintentar y deshabilitar de forma independiente, y cuando una falla se sabe cuál.[special]

Type=forking, archivos PID y demonios que se resisten

Este es el error que cuesta una noche, y no se anuncia. Si el demonio hace fork y la unit declara Type=simple, systemd sigue al proceso padre, lo ve terminar a los pocos milisegundos y entonces, o bien mata a los hijos supervivientes por considerarlos huérfanos, o bien da el arranque por bueno mientras no hay nada en ejecución. Si el demonio hace fork y la unit declara Type=forking pero el archivo PID llega tarde, systemd pierde el proceso principal e informa de MainPID=0: la unit parece activa, Restart= no se dispara nunca y el apagado deja procesos atrás.[daemon][service]

# The failure mode nobody warns you about: the unit reports "active" and the
# daemon is dead, or reports "failed" while the daemon is happily running.
# Both come from getting Type= wrong.

# --- diagnosis -------------------------------------------------------------
systemctl show -p Type -p MainPID -p PIDFile -p ControlGroup --value myd.service
# forking
# 0                <- systemd has no main process: it lost the daemon
# /var/run/myd.pid
# /system.slice/myd.service

# If MainPID is 0 while the process exists, systemd is not supervising it.
# Restart=, watchdogs, and clean shutdown are all silently not working.
systemd-cgls -u myd.service      # who is actually inside the unit's cgroup

# --- fix 1 (preferred): stop forking --------------------------------------
# Type=exec: systemd considers the unit started once the binary has been
# executed. No PID file, no race, no double-fork, no GuessMainPID guesswork.
#   ExecStart=/usr/sbin/myd --foreground
#   Type=exec

# --- fix 2 (best, if the daemon supports it): tell systemd when you are up --
# Type=notify with sd_notify(READY=1) from the daemon, or Type=notify-reload
# if it can also signal that a reload finished. These - together with Type=dbus
# for D-Bus services - are the only readiness protocols precise enough that
# After= on a dependent unit means "the port is actually accepting".
#   Type=notify
#   NotifyAccess=main

# --- fix 3 (last resort): keep forking, but do it correctly ---------------
# PIDFile= must be an absolute path under /run, and the daemon must write it
# BEFORE the parent exits. Anything else is a race you will lose under load.
#   Type=forking
#   PIDFile=/run/myd/myd.pid
#   RuntimeDirectory=myd

# --- do not do this -------------------------------------------------------
# Type=simple with a daemon that forks. systemd will track the parent, see it
# exit immediately, and either kill the children or declare success while the
# service never started. This is the single most common broken conversion.

El arreglo duradero no es un archivo PID más ingenioso: es dejar de hacer fork. Casi todos los demonios escritos en los últimos veinte años tienen un parámetro para permanecer en primer plano, y la propia recomendación del proyecto para demonios de estilo moderno es usarlo. Type=exec elimina entonces toda esa clase de problemas, y Type=notify va más allá: junto con notify-reload, o Type=dbus en servicios de D-Bus, es la única forma fiable de que un After= en una unit dependiente signifique «el socket está aceptando conexiones de verdad» y no «el binario se ha ejecutado». Todo lo demás es una ordenación por esperanza.[incompat]

init 3, telinit y runlevel también han desaparecido

Esta es la parte del cambio que va más allá de los servidores y llega a la memoria muscular y a los procedimientos escritos. La versión 258 eliminó el nodo de dispositivo /dev/initctl y los comandos initctl, runlevel y telinit, eliminó el soporte para cambios de estado mediante init 3, eliminó las units runlevel[0-6].target y dejó de registrar las transiciones de runlevel en utmp y wtmp, porque el concepto de runlevel ya no existe. La versión 259 restauró los targets detrás de la opción de compilación -Dcompat-sysv-interfaces=yes; no restauró los comandos.[v258][v259]

SysVsystemdNota
init 3, telinit 3systemctl isolate multi-user.targettelinit se eliminó en 258. init sigue existiendo —es el PID 1—; lo que desapareció es el control de estado a través de él.
init 5systemctl isolate graphical.target
init 1, telinit Ssystemctl isolate rescue.targetemergency.target baja todavía más.
init 0 / init 6systemctl poweroff / systemctl reboot
runlevelsystemctl get-default, systemctl list-units --type=targetEl comando se eliminó; un target no es un runlevel.
Runlevel por defecto en /etc/inittabsystemctl set-default multi-user.target/etc/inittab lleva años sin leerse.
chkconfig x on, update-rc.d x defaultssystemctl enable x.serviceRequiere una sección [Install] en la unit.
chkconfig x off, update-rc.d -f x removesystemctl disable x.service
(sin equivalente)systemctl mask x.serviceImpide que la unit arranque, incluso como dependencia.
chkconfig --list, service --status-allsystemctl list-unit-files --type=service
service x startsystemctl start x.serviceservice suele seguir existiendo como envoltorio, pero mejor no depender de él.
# v258 removed /dev/initctl and the initctl, runlevel and telinit commands,
# and with them `init 3`. v259 brought the runlevel[0-6].target ALIASES back,
# but only if the distribution builds with -Dcompat-sysv-interfaces=yes, and
# the commands did not come back at all. Do not build habits on them.

# What runlevel am I in?  ->  what is the system aiming at?
systemctl get-default                      # the boot target
systemctl list-units --type=target --state=active

# init 3 / telinit 3     ->
sudo systemctl isolate multi-user.target
# init 5                 ->
sudo systemctl isolate graphical.target
# init 1 / telinit 1 / S ->
sudo systemctl isolate rescue.target
# init 0                 ->
sudo systemctl poweroff
# init 6                 ->
sudo systemctl reboot

# Change the default "runlevel" permanently (this replaces /etc/inittab):
sudo systemctl set-default multi-user.target

# chkconfig foo on / update-rc.d foo defaults ->
sudo systemctl enable foo.service
# chkconfig foo off / update-rc.d -f foo remove ->
sudo systemctl disable foo.service
# ...and the one with no SysV equivalent, for a unit that must never start:
sudo systemctl mask foo.service

# chkconfig --list / service --status-all ->
systemctl list-unit-files --type=service
systemctl list-units --type=service --all

Uno de esos sustitutos no tiene equivalente alguno en SysV y merece aprenderse por méritos propios. systemctl mask hace que una unit no pueda arrancar de ninguna manera, ni siquiera como dependencia de otra cosa; lo más cerca que estuvo SysV de eso era borrar el script y confiar en que el gestor de paquetes no lo volviera a poner. Es la herramienta correcta para retirar un servicio que todavía no se quiere desinstalar y para asegurarse de que un script dado de baja no lo revive un compañero con buena intención.[systemctl]

Lo que se gana y que el envoltorio nunca dio

Con toda honestidad, esta migración es trabajo impuesto y sin ninguna funcionalidad al final. Así que hay que cobrarse la compensación, porque es real y sale gratis: un script de init envuelto no podía usar nada de esto. El envoltorio ejecutaba el script de shell, y el script de shell ejecutaba el demonio; systemd no tenía ni idea de qué era ese proceso, no podía reiniciarlo de forma fiable y no podía restringirlo en absoluto. Una unit nativa consigue todo lo siguiente con añadir líneas:[exec][resctl]

  • Un bucle de supervisión real. Restart=on-failure con RestartSec=, respaldado por StartLimitIntervalSec= para que un bucle de caídas se detenga en lugar de dejar un núcleo clavado. El envoltorio generado fijaba Restart=no para todos los scripts que produjo en su vida.
  • Aislamiento del sistema de archivos. ProtectSystem=strict deja todo el sistema de archivos en solo lectura salvo lo que se nombre, PrivateTmp=yes da al servicio su propio /tmp, y StateDirectory=, RuntimeDirectory= y ConfigurationDirectory= crean, asignan propietario y limpian los directorios que el script creaba a mano con mkdir -p.
  • Reducción de privilegios que se sostiene de verdad. User=, NoNewPrivileges=yes y un CapabilityBoundingSet= explícito sustituyen a su y a start-stop-daemon --chuid, y a diferencia de aquellos se aplican a todos los procesos que el servicio genere, siempre, incluidos los que intente generar un demonio comprometido.
  • Restricción de llamadas al sistema y de espacios de nombres. SystemCallFilter=@system-service, RestrictAddressFamilies=, RestrictNamespaces= y MemoryDenyWriteExecute=yes son una política de seccomp que cabe en cuatro líneas. No había equivalente en shell a ningún precio.
  • Contabilidad de recursos por servicio. MemoryMax=, CPUQuota=, TasksMax= y IOWeight= se aplican al cgroup de la unit, es decir, al demonio y a todo lo que este bifurque: justo lo que un script basado en archivo PID nunca pudo contener.
  • Registros que llegan a algún sitio. La salida estándar y la salida de error van al journal con el nombre de la unit adjunto, de modo que journalctl -u funciona sin que el demonio sepa qué es syslog y sin que un archivo de log llene una partición en silencio porque nadie escribió una regla de rotación.

Aun así, no hay que añadir todo eso a ciegas. Hay que pasar systemd-analyze security sobre la unit y tratar la puntuación como una lista de tareas y no como una nota: nombra cada directiva sin configurar y qué aportaría. Endurecer en pasos pequeños, reiniciar y leer el journal; el modo de fallo del exceso de endurecimiento es un servicio que arranca perfectamente y tres horas después no puede abrir un archivo, que es mucho peor que un servicio que directamente no arranca.[analyze]

Verificar la migración en vez de confiar en que salió bien

No hay que verificar mirando systemctl status y viendo verde. Un script envuelto también salía en verde. Hay que verificar las cuatro cosas que realmente eran distintas: que la definición es un archivo bajo control propio y no uno generado, que ningún SourcePath apunta de vuelta a un script de init, que la unit está habilitada explícitamente y no por cableado heredado, y que systemd conoce el identificador del proceso principal.[analyze]

#!/usr/bin/env bash
# Run after the cut-over and again after the first reboot. Non-zero exit means
# something in the migration is not finished.
set -u
rc=0
units=("$@")   # e.g. ./verify.sh acme-collector.service local-startup.service

for u in "${units[@]}"; do
  echo "--- $u"

  # 1. Is it a real file, not a generated one?
  frag=$(systemctl show -p FragmentPath --value "$u")
  case "$frag" in
    /etc/systemd/system/*|/usr/lib/systemd/system/*|/lib/systemd/system/*) ;;
    *) echo "  FAIL still generated or missing: '$frag'"; rc=1 ;;
  esac

  # 2. No SourcePath = no init script behind it.
  src=$(systemctl show -p SourcePath --value "$u")
  [ -z "$src" ] || { echo "  FAIL SourcePath=$src"; rc=1; }

  # 3. Syntactically valid, with no warnings.
  systemd-analyze verify "$frag" || { echo "  FAIL verify"; rc=1; }

  # 4. Enabled explicitly, not by leftover generator wiring.
  [ "$(systemctl is-enabled "$u")" = enabled ] \
    || { echo "  FAIL not enabled"; rc=1; }

  # 5. Actually supervised: a Type= that forks and loses its child shows
  #    MainPID=0 while looking perfectly healthy.
  mp=$(systemctl show -p MainPID --value "$u")
  ty=$(systemctl show -p Type --value "$u")
  [ "$ty" = oneshot ] || [ "$mp" != 0 ] \
    || { echo "  FAIL Type=$ty but MainPID=0"; rc=1; }

  # 6. Report the sandbox score. Not pass/fail - a number to improve.
  systemd-analyze security "$u" | tail -1
done

# 7. Nothing anywhere is still being generated from an init script.
if ls /run/systemd/generator.late/*.service >/dev/null 2>&1; then
  for g in /run/systemd/generator.late/*.service; do
    grep -q '^SourcePath=/etc/init.d/' "$g" && { echo "STILL WRAPPED: $g"; rc=1; }
  done
fi

# 8. Nothing failed at boot for an ordering reason you introduced.
systemctl --failed --no-legend --no-pager
exit $rc

Después hay que reiniciar, porque toda la clase de fallos que introduce esta migración son fallos de ordenación, y los fallos de ordenación son invisibles en un sistema en marcha donde todas las dependencias ya están levantadas. Hay que leer journalctl -b -u para cada unit convertida y systemd-analyze blame para el arranque en conjunto. Un servicio que antes arrancaba bajo un enlace numerado en la posición 20 y ahora arranca demasiado pronto no fallará: reintentará, o arrancará con una configuración vacía, o hará bind antes de que una interfaz tenga dirección.

Si distribuyes software a servidores ajenos

Quien distribuya software que otras personas instalan lleva ya tiempo sin que esto sea opcional. El generador venía registrando un aviso por cada script envuelto desde la 255, dirigido directamente al empaquetador: please update package to include a native systemd unit file. En systemd 260 ese aviso ya no está, porque no queda nada de lo que avisar: el script se instala y luego no lo ejecuta nadie.[gensrc]

Lo que un paquete debe entregar ahora es un archivo unit en /usr/lib/systemd/system/ con una sección [Install] en condiciones, porque el tercer componente eliminado, systemd-sysv-install, era el gancho que permitía a systemctl enable delegar en chkconfig o en update-rc.d para los servicios basados en scripts. Sin él, habilitar un servicio es puramente cuestión de los enlaces simbólicos que describe [Install]. El script de init puede seguir en el paquete si aún se da soporte a distribuciones antiguas: una unit nativa y un script pueden convivir, y la unit gana allá donde systemd sea lo bastante nuevo como para importar.[unit]

El orden en el que hacer esto

No existe ninguna versión de esto en la que hacerlo más tarde salga más barato. El trabajo es proporcional al número de scripts, que no disminuye, y la ventana la define el calendario de publicación de otros. Lo que varía es cuánto aviso previo se tiene, así que la primera decisión es en cuál de estas situaciones se está:[ubuntu2604]

SituaciónCuánto tiempo quedaQué hacer
Distribución rolling, o systemd 260 ya instaladoNinguno: ya ha desaparecidoInventariar desde copias de seguridad o desde la gestión de configuración y convertir. Los scripts en disco son inertes, pero siguen indicando qué existía.
Serie intermedia de Ubuntu (26.04 → 26.10)SemanasInventariar y capturar las units generadas ahora, mientras siga corriendo la 259. Convertir antes de la publicación de octubre.
Ubuntu 26.04 LTS, con intención de seguir en LTSHasta la siguiente LTSSin urgencia, pero el journal ya está avisando. Convertir de forma oportunista, empezando por todo lo que no tenga dueño.
Debian estable, RHEL 9 o 10Hasta la siguiente versión mayorEl generador sigue presente y haciendo su trabajo. Conviene hacer el inventario igualmente: cuesta un comando y envejece bien.
Se empaqueta software para tercerosNingunoEntregar ya un archivo unit con sección [Install]. Las actualizaciones de los usuarios no están bajo control propio.
Un appliance o agente de un proveedor que no se puede modificarEl mismo que el del hostEscribir la unit uno mismo y aplicar mask al script del proveedor, o plantearlo con el proveedor antes de que su próxima versión deje el servicio tirado.
  1. Inventariar todo el parque esta semana, con la consulta al journal de más arriba, y guardar la salida. Con dos columnas —nombre de host y ruta del script— basta para dimensionar el trabajo y para cazar las máquinas que nadie recuerda.
  2. Capturar la unit generada de cada script con systemctl cat en un directorio que se conserve. Es una operación de solo lectura, lleva minutos, y después de actualizar esa información ya no se puede recuperar.
  3. Clasificar en tres montones: borrar, sustituir, reescribir. Una proporción sorprendente de lo que aparece es un servicio de algo que se dio de baja hace años. Borrarlo es una migración completa y no lleva nada de tiempo.
  4. Convertir primero los fáciles: cualquier cosa ya empaquetada por su proyecto casi seguro tiene una unit oficial, e instalar el paquete actual es más rápido y más correcto que escribir una a mano.
  5. Hacer el cambio mientras el generador siga existiendo, servicio a servicio, en un sistema donde el script aún esté presente. Volver atrás es entonces quitar un archivo y recargar, no restaurar una copia de seguridad con prisas.
  6. Reiniciar y leer el arranque, host por host, antes de dar nada por terminado. Después, aplicar mask a las units retiradas para que nada las devuelva a la vida.

La misma ventana de mantenimiento suele contener el resto: la actualización de servidor de Ubuntu 24.04 a 26.04 cubre la actualización de distribución que va a quitar el generador de debajo de los pies, temporizadores de systemd frente a cron es la otra mitad de la misma limpieza, ya que una máquina con scripts SysV casi siempre tiene crontabs que también deberían ser units, y qué se rompe al pasar a Docker Engine 29 trata el motor de contenedores, que cambió sus propios valores por defecto en el mismo periodo y por motivos relacionados.

Preguntas frecuentes

¿Se puede reinstalar el sysv-generator en systemd 260?

No, y los apaños que parecen funcionar son peores que hacer la migración. El generador se borró del árbol de código fuente, no se desactivó detrás de una opción, así que no hay ningún paquete que instalar ni ninguna opción que activar: las tres opciones de meson obsoletas que quedan (-Drc-local=, -Dsysvinit-path=, -Dsysvrcnd-path=) solo afectan a rutas, no a la presencia del código. En teoría se podría copiar el binario del generador de una compilación de la 259 a /usr/lib/systemd/system-generators/, pero sería ejecutar un generador sin mantenimiento contra un gestor con el que nunca se probó, y la siguiente actualización de systemd lo sobrescribiría en silencio. Escribir el archivo unit lleva menos tiempo que depurar eso una sola vez.

¿Mi script de init sigue funcionando si lo lanzo a mano?

Sí. En el script no ha cambiado nada; es un script de shell corriente y /etc/init.d/x start hará exactamente lo que hizo siempre. Lo que ha desaparecido es la traducción automática a servicio, lo que significa: no arranca en el inicio del sistema, systemctl no sabe que existe, nadie supervisa ni reinicia el proceso, y apagar la máquina no lo detendrá de forma limpia. Esa combinación es peor de lo que suena, porque un script que todavía se puede lanzar a mano no parece roto, así que la gente lo esquiva con una entrada @reboot en un crontab y el problema se queda callado un año.

¿Cómo saber qué servidores están afectados sin entrar en todos?

Consultando el journal en busca del aviso estructurado. Desde systemd 255, cada script envuelto produce una entrada de registro con MESSAGE_ID=a8fa8dacdb1d443e9503b8be367a6adb y dos campos propios, SYSVSCRIPT= y UNIT=. Con journals centralizados, eso es una sola consulta para todo el parque. Si no los hay, se lanza journalctl -b MESSAGE_ID=a8fa8dacdb1d443e9503b8be367a6adb a través de lo que se use para ejecutar comandos en muchos hosts. En systemd 254 y anteriores el envoltorio es silencioso, así que hay que recurrir a preguntar a cada unit por su FragmentPath y su SourcePath, como hace el script de inventario de más arriba.

¿Qué diferencia hay entre Type=simple, Type=exec, Type=forking y Type=notify?

Type=simple da el servicio por arrancado en cuanto systemd ha hecho el fork, antes incluso de que el binario se haya ejecutado necesariamente con éxito. Type=exec espera hasta que el binario se ha ejecutado de verdad, lo que convierte un archivo inexistente o un User= incorrecto en un fallo de arranque en lugar de en una salida inmediata y misteriosa: es el mejor valor por defecto para un demonio en primer plano. Type=forking es para demonios que se van solos a segundo plano y espera un PIDFile=; es lo que usaba el sysv-generator, porque no tenía alternativa. Type=notify espera a que el demonio llame a sd_notify(READY=1), y es el único de los cuatro en el que una dependencia After= significa que el servicio está realmente listo. Lo recomendable es convertir a Type=exec, y usar Type=notify si el demonio lo admite.

¿/etc/rc.local ha desaparecido de verdad o solo está obsoleto?

Ha desaparecido, en systemd 260 y posteriores. Tanto systemd-rc-local-generator como la unit rc-local.service que este activaba fueron eliminados. El archivo seguirá en /etc, seguirá siendo ejecutable, y nada volverá a invocarlo. Ojo con la ruta: era un ajuste de compilación y variaba entre distribuciones —algunas usaban /etc/rc.d/rc.local—, así que hay que comprobar con qué ruta se configuró realmente el generador antes de dar por hecho que se han encontrado todos.

Mi unit dice active (running) pero el proceso no existe. ¿Qué he hecho mal?

Casi con seguridad, Type=forking con un archivo PID que systemd no pudo leer en el momento adecuado, o Type=simple sobre un demonio que hace fork. Conviene ejecutar systemctl show -p Type -p MainPID -p PIDFile --value tuunit.service: si MainPID es 0 mientras el demonio está en marcha, systemd le ha perdido la pista y no se aplica nada de la supervisión. La solución es que el demonio deje de hacer fork: buscar su parámetro de primer plano y usar Type=exec. Si realmente no se puede, hay que asegurarse de que PIDFile= sea una ruta absoluta bajo /run y de que el demonio la escriba antes de que salga el proceso padre.

¿Merece la pena conservar el script de init después de escribir la unit?

Sí durante la ventana de cambio, y luego borrarlo. Mientras coexisten, gana la unit nativa —el generador omitía explícitamente cualquier script que ya tuviera una unit del mismo nombre—, así que tener ambos es seguro y hace trivial la vuelta atrás. Pero dejarlo ahí de forma permanente supone tener dos definiciones del mismo servicio, una de las cuales es una trampa para quien depure esto a las tres de la madrugada. Lo suyo es moverlo a un directorio de material retirado en cuanto el host haya reiniciado limpiamente, y quitar a la vez los enlaces simbólicos de /etc/rc*.d.

¿Hay que preocuparse por esto en RHEL o en Debian estable?

No de forma urgente, pero el inventario merece hacerse igualmente. RHEL 10 y Debian 13 traen ambos systemd 257, así que los dos conservan el generador y los dos ya imprimen el aviso de obsolescencia por script que llegó en la 255; además, las propias notas de publicación de Red Hat indican que el soporte de scripts de servicio System V está obsoleto y se eliminará. El cambio llega con la siguiente versión mayor de cada uno. La razón para actuar ahora es que el paso de inventario es un único comando de solo lectura, es mucho más fácil mientras el generador está en marcha y va indicando qué envuelve, y la alternativa es descubrir un script sin dueño durante una actualización mayor, que es el peor momento posible para hacer ingeniería inversa de la shell de un compañero.

¿Qué sustituye a systemd-sysv-install?

Nada, porque no queda nada que hacer en su lugar. Era el gancho que permitía a systemctl enable, disable e is-enabled delegar en la herramienta propia de la distribución —chkconfig en sistemas Red Hat, update-rc.d en los Debian— cuando el servicio nombrado era un script y no una unit. Eliminado el soporte de scripts, habilitar un servicio es enteramente cuestión de los enlaces simbólicos que describe la sección [Install] de la unit. Si un paquete dependía de esa delegación, ahora necesita un archivo unit de verdad con una sección [Install] de verdad.

¿Se puede convertir un script automáticamente en vez de a mano?

En parte, y el mejor conversor es precisamente el que está a punto de desaparecer: systemctl cat tuservicio.service en un sistema que aún tenga el generador da una traducción mecánicamente correcta de las dependencias, que es la parte fácil de equivocar. Lo que ninguna herramienta puede hacer es la parte que importa: decidir que el demonio debe dejar de hacer fork, que el archivo PID debe desaparecer, que start-stop-daemon --chuid se convierte en User=, y qué directivas de aislamiento son seguras para esta carga de trabajo concreta. Lo sensato es tratar la unit generada como la especificación y escribir la definitiva a partir de ella.

La capa de control de recursos se movió en las mismas versiones: systemd 258 eliminó por completo cgroup v1, así que ahora todos los equipos arrancan con la jerarquía unificada lo pidiera alguien o no. la migración de cgroup v1 a cgroup v2 explica la auditoría, la conversión fichero a fichero y las dos traducciones que cambian el comportamiento y no solo el nombre.

Fuentes

Cada número de versión, directiva y valor por defecto de este artículo se ha leído de las fuentes siguientes, no reproducido de otras publicaciones. Cuando el código ya se ha eliminado, el enlace apunta a la última etiqueta en la que aún existía, para poder comprobarlo de primera mano.

  1. systemd — NEWS: the upstream changelog, and the only authoritative statement of what was removed in which release. Everything dated in this article was checked against it
  2. systemd v260 release notes — "Support for System V service scripts has been removed", with the three components that went with it: systemd-sysv-generator, systemd-rc-local-generator plus rc-local.service, and systemd-sysv-install
  3. systemd v259 release notes — the release that restored runlevel[0-6].target behind the new -Dcompat-sysv-interfaces=yes build option, and the version shipped by Ubuntu 26.04 LTS
  4. systemd v258 release notes — the removal of /dev/initctl and of the initctl, runlevel and telinit commands, the removal of the runlevel[0-6].target units, and the removal of cgroup v1
  5. systemd v255 release notes — where support for System V service scripts was first declared deprecated, and the release whose sysv-generator already logs the structured per-script warning this article uses for the inventory
  6. systemd-sysv-generator source at v259 — the last tag where the code still exists. It is the definitive answer to what the generator read, what it ignored, and exactly which directives it wrote
  7. systemd-sysv-generator(8) — the manual page, including the statement that the wrapper units are always ordered after basic.target and that compatibility was never 100%
  8. systemd-rc-local-generator(8) — the rc.local compatibility generator, its warning that rc-local.service is ordered after network.target and that this does not mean the network works, and the SELinux note about restorecon
  9. rc-local.service at v259 — the actual unit that ran /etc/rc.local, so you can see what semantics you are replacing rather than guessing them
  10. systemd exit-status.h — the LSB start-verb exit codes, and the origin of the SuccessExitStatus=5 6 line the generator emitted for scripts with an LSB header
  11. systemd sd-messages.h — the catalogue of structured log message IDs, including SD_MESSAGE_SYSV_GENERATOR_DEPRECATED, which is what makes the journal-based inventory in this article possible
  12. systemd — Incompatibilities: the upstream list of the places where SysV behaviour and systemd behaviour genuinely differ, worth reading before you assume a script will behave the same way
  13. Linux Standard Base — Init Script Actions: the specification that defines the LSB header fields and the exit codes an init script is supposed to return
  14. systemd.service(5) — Type=, Restart=, PIDFile=, ExecReload=, RemainAfterExit= and the rest of the service options used in the unit files below
  15. systemd.unit(5) — Wants=, After=, Before=, the Condition family and the [Install] section that replaces update-rc.d and chkconfig
  16. systemd.exec(5) — User=, RuntimeDirectory=, StateDirectory= and the whole sandboxing vocabulary, none of which a wrapped init script could ever use
  17. systemd.special(7) — what multi-user.target, graphical.target, network.target and network-online.target actually mean, and why After=network.target does not mean the network is up
  18. systemd.generator(7) — how generators work and where their output lands, which is why the units you are about to lose live under /run/systemd/generator.late
  19. systemctl(1) — enable, mask, cat, show, list-unit-files and isolate: the commands that replace chkconfig, update-rc.d, service and telinit
  20. systemd-analyze(1) — verify, security and blame: the three subcommands that turn this migration from a guess into something you can check
  21. journalctl(1) — matching on MESSAGE_ID= and on arbitrary structured fields, which is how the inventory command in this article finds every wrapped script
  22. daemon(7) — the difference between a SysV-style forking daemon and a new-style daemon, and upstream's own recommendation to stop forking
  23. systemd.resource-control(5) — the per-unit cgroup accounting and limits that come for free once a service is a real unit
  24. Ubuntu 26.04 LTS release notes — the section stating that 26.04 LTS is the last release with System V compatibility in systemd, that the change takes effect in 26.10, and that the release ships systemd 259
  25. Ubuntu 26.10 release schedule — the release date that turns "eventually" into a deadline for anyone tracking the interim series
  26. Red Hat Enterprise Linux 10 release notes — the systemd rebase to 257, the statement that System V service script support is deprecated and will be removed, and the move of default configuration files under /usr/lib/systemd
  27. Debian trixie systemd package — the version currently in Debian stable, for readers deciding how much runway they actually have

¿Te ha resultado útil?