Administración de Linux
Procesos de Linux y systemd: ps, señales, unidades, journalctl y timers
Intermedio10 min de lecturaActualizado
Esta página se tradujo del inglés con ayuda de IA y se cotejó con el original. Leer la versión en inglés
Un sistema Linux en marcha es un árbol de procesos supervisado por systemd. Esta guía empieza por los procesos —estados, ps y top, señales, trabajos en segundo plano y prioridad de planificación— y pasa después a systemd: qué es una unidad, cómo escribir y endurecer un servicio, cómo las dependencias y los targets ordenan el arranque y cuándo un timer es mejor opción que una entrada de cron.
Lo que aprenderás
- Interpretar las columnas de estado, memoria y CPU en la salida de ps y top
- Enviar la señal adecuada para detener, recargar o inspeccionar un proceso y entender qué señales no se pueden capturar
- Controlar trabajos en primer y segundo plano y desvincular del terminal las tareas de larga duración
- Escribir, habilitar y endurecer una unidad de servicio de systemd y leer sus logs con journalctl
- Expresar el orden de arranque con dependencias y targets, y programar tareas con timers de systemd
Todo lo que se ejecuta en una máquina Linux es un proceso, y cada proceso, salvo el primero, lo ha creado otro. Ese simple hecho explica muchísimas cosas: por qué una variable del shell no sobrevive a un script, por qué cerrar un terminal mata lo que se lanzó desde él y por qué la gestión de servicios trata en realidad de parentesco y ciclos de vida, más que de arrancar programas.
Durante sus primeras décadas, Linux gestionaba ese ciclo de vida con scripts de shell en /etc/init.d. Hoy todas las distribuciones arrancan con systemd, que supervisa directamente los servicios, sigue sus procesos mediante control groups, recoge su salida en un journal estructurado y arranca las cosas en paralelo según las dependencias declaradas. Esta guía cubre primero los conceptos de procesos que necesitas en el shell y después la capa de systemd que se apoya en ellos.
La vida de un proceso
Un proceso nace cuando otro ya existente llama a fork(), que produce una copia casi idéntica, y normalmente la copia llama a execve() para sustituir su imagen de memoria por otro programa. El hijo hereda del padre el entorno, los descriptores de fichero abiertos, el directorio de trabajo y la umask; esa herencia es precisamente lo que hace que funcionen las redirecciones del shell, como se explica en fundamentos del shell.
Cuando un proceso termina, se convierte en zombie hasta que su padre llama a wait() para recoger el código de salida. Si el padre muere antes, el huérfano pasa a depender del PID 1, que se encarga de recogerlo. En un host con systemd, el PID 1 es el propio systemd; dentro de un contenedor es el entrypoint de tu imagen, sea cual sea. Por eso los contenedores que ejecutan un script de shell como PID 1 acumulan zombies e ignoran las señales, a menos que el script esté escrito para esa función.
La columna de estado que muestran las herramientas indica qué está esperando el kernel:
| Estado | Significado |
|---|---|
R | en ejecución o ejecutable: en una CPU o en la cola de ejecución |
S | espera interrumpible: aguardando una entrada, un temporizador, un socket |
D | espera no interrumpible: normalmente bloqueado en E/S de disco o NFS; no se puede matar |
T | detenido, típicamente con Ctrl-Z o SIGSTOP |
Z | zombie: terminado, con el código de salida aún sin recoger |
Un montón de procesos en D es la firma clásica de un problema de almacenamiento, no de CPU. Esa distinción importa al interpretar el load average, y se analiza con más detalle en logs y resolución de problemas.
Ver qué se está ejecutando
ps muestra una instantánea. Dos invocaciones cubren casi todo: ps aux, al estilo BSD, para una visión general amplia, y ps -ef para la misma información al estilo UNIX. La vista en árbol (forest) es la que merece la pena memorizar, porque la pregunta suele ser quién es el padre de quién.
$ ps -eo pid,ppid,user,stat,pcpu,pmem,rss,etime,cmd --sort=-pcpu | head -5
PID PPID USER STAT %CPU %MEM RSS ELAPSED CMD
1842 1 postgres Ss 7.4 9.1 748120 3-04:11:52 /usr/lib/postgresql/16/bin/postgres
2310 1842 postgres Ss 2.1 1.4 118004 3-04:11:50 postgres: checkpointer
9921 1 www-data S 1.8 0.6 52208 02:14:07 php-fpm: pool www
$ ps -ef --forest | grep -A3 sshd
RSS es el tamaño del conjunto residente (resident set size), la memoria física realmente mapeada; VSZ (tamaño virtual) cuenta el espacio de direcciones y suele ser enorme y carente de sentido en una JVM o una base de datos. Compara procesos por RSS, no por VSZ.
Para una vista en directo, top se refresca cada pocos segundos: pulsa 1 para desglosar el uso de CPU por núcleo, M para ordenar por memoria, P por CPU, k para enviar una señal a un proceso y f para elegir columnas. htop, si está instalado, añade un árbol desplazable, soporte de ratón y envío de señales a un proceso desde un menú. pidstat 2, del paquete sysstat, muestrea la CPU, la memoria y la E/S de cada proceso a intervalos fijos, algo mucho más informativo que una única instantánea cuando persigues un pico intermitente.
Para encontrar un proceso por nombre está pgrep, y pgrep -a muestra la línea de comandos completa:
$ pgrep -a -u www-data php-fpm
9921 php-fpm: pool www
9922 php-fpm: pool www
Señales
Una señal es una pequeña notificación asíncrona que se entrega a un proceso. La mayoría se pueden capturar con un manejador, ignorar o dejar con su acción por defecto.
| Señal | Número | Acción por defecto | Uso típico |
|---|---|---|---|
SIGHUP | 1 | terminar | cuelgue del terminal; por convención, «recargar la configuración» en los demonios |
SIGINT | 2 | terminar | Ctrl-C |
SIGQUIT | 3 | terminar + core | Ctrl-\; una JVM imprime en su lugar un thread dump |
SIGKILL | 9 | terminar | no se puede capturar; último recurso |
SIGTERM | 15 | terminar | la señal educada por defecto de kill y systemctl stop |
SIGSTOP / SIGCONT | 19 / 18 | detener / continuar | pausa y reanudación que no se pueden capturar |
SIGUSR1 / SIGUSR2 | 10 / 12 | terminar | definidas por la aplicación, a menudo para rotar logs o activar el modo debug |
kill 9921 # SIGTERM by default
kill -HUP $(pgrep -o nginx)
pkill -u bob -TERM # every process belonging to bob
kill -QUIT <jvm-pid> # thread dump into the JVM's stdout
kill -0 9921 && echo "still alive" # signal 0 tests existence and permission only
Recurrir primero a kill -9 es el mal hábito más común en este terreno. Le niega al proceso cualquier oportunidad de vaciar un búfer de escritura, confirmar una transacción o limpiar un fichero de bloqueo, y deja atrás sockets y ficheros PID obsoletos. Envía SIGTERM, dale unos segundos y luego escala. Si un proceso ignora SIGTERM una y otra vez, es un bug que merece la pena reportar, no un motivo para seguir usando -9.
SIGKILL tampoco sirve de nada con un proceso atascado en estado D: está bloqueado dentro del kernel y la señal solo se entrega cuando vuelve al espacio de usuario. Arregla el almacenamiento o el montaje de red al que está esperando.
Control de trabajos en el shell
Los shells interactivos agrupan los procesos de una línea de comandos en un trabajo (job) para que puedas suspenderlos y reanudarlos.
$ ./import-data.sh
^Z
[1]+ Stopped ./import-data.sh
$ bg # resume it in the background
$ jobs -l
[1]+ 21507 Running ./import-data.sh &
$ fg %1 # bring it back to the foreground
& lanza un comando directamente en segundo plano; wait se bloquea hasta que terminan los trabajos en segundo plano. Nada de esto sobrevive a la sesión: cuando se cierra el terminal, SIGHUP llega al trabajo. nohup cmd & y disown %1 lo esquivan, pero en un sistema moderno las respuestas honestas son un multiplexor de terminal (tmux, screen) para el trabajo interactivo y una unidad transitoria para cualquier cosa desatendida:
systemd-run --unit=import-once --collect /usr/local/bin/import-data.sh
journalctl -u import-once -f
Prioridad de planificación
El planificador del kernel pondera los procesos según su valor nice, desde -20 (el más favorable) hasta 19 (el menos). Solo root puede bajar un valor nice.
nice -n 10 tar -czf /backup/site.tar.gz /var/www # start a batch job politely
renice -n 5 -p 21507 # adjust a running one
ionice -c 3 -p 21507 # idle I/O class: only when nothing else wants the disk
Los valores nice son una indicación, no una cuota. Cuando necesites un límite real, usa los límites de control groups que systemd expone en cualquier unidad —CPUQuota=, MemoryMax=, IOWeight=—, porque los impone el kernel en lugar de negociarlos el planificador.
systemd: las unidades y el fichero de unidad
systemd gestiona unidades, cada una descrita por un fichero de estilo INI. El sufijo determina el tipo: .service para un demonio o una tarea puntual, .socket para la activación por socket, .timer para tareas programadas, .mount y .automount para sistemas de ficheros, .target para un punto de agrupación, .path para disparadores del sistema de ficheros y .slice para el reparto de recursos.
Los ficheros de unidad proceden de tres lugares, en orden creciente de prioridad: /usr/lib/systemd/system/ (los instalan los paquetes; nunca los edites), /etc/systemd/system/ (los tuyos) y los directorios drop-in como /etc/systemd/system/nginx.service.d/override.conf, que sobrescriben unos pocos ajustes de una unidad empaquetada. systemctl edit nginx crea el drop-in por ti; systemctl cat nginx muestra la configuración efectiva ya combinada.
Aquí tienes una unidad completa para una aplicación Java, del tipo que desplegarías junto con el empaquetado descrito en empaquetado y despliegue:
# /etc/systemd/system/orders-api.service
[Unit]
Description=Orders API
Documentation=https://example.com/runbooks/orders-api
After=network-online.target postgresql.service
Wants=network-online.target
[Service]
Type=exec
User=orders
Group=orders
WorkingDirectory=/opt/orders-api
EnvironmentFile=/etc/orders-api/env
ExecStart=/usr/lib/jvm/java-21-openjdk-amd64/bin/java -XX:MaxRAMPercentage=70 -jar app.jar
ExecReload=/bin/kill -HUP $MAINPID
Restart=on-failure
RestartSec=5s
TimeoutStopSec=30s
# Hardening: each line removes something the service does not need
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=/var/lib/orders-api
MemoryMax=2G
[Install]
WantedBy=multi-user.target
Tres campos merecen comentario. Type= le dice a systemd cuándo se considera arrancado el servicio: exec en cuanto se ejecuta el binario, simple en cuanto se hace el fork, notify cuando el programa avisa de que está listo mediante sd_notify, forking para demonios a la antigua que se pasan solos a segundo plano y oneshot para una tarea que se ejecuta y termina. Equivocarse aquí es la razón por la que a veces una unidad dependiente arranca antes de que el servicio esté realmente escuchando; notify es la opción precisa cuando el programa la soporta.
Restart=on-failure reinicia tras un código de salida distinto de cero o una señal, pero no tras una parada limpia. Combínalo con StartLimitIntervalSec= y StartLimitBurst= en [Unit] para que un bucle de caídas acabe rindiéndose en lugar de machacar la máquina.
WantedBy=multi-user.target es aquello sobre lo que actúa systemctl enable. Habilitar crea un enlace simbólico en multi-user.target.wants/, y por eso habilitar y arrancar son operaciones separadas: una afecta al próximo arranque y la otra, al momento actual.
Manejar systemd
sudo systemctl daemon-reload # after adding or editing any unit file
sudo systemctl enable --now orders-api # start it and start it at boot
systemctl status orders-api # state, PID, cgroup, last log lines
systemctl is-active orders-api # scriptable: exit code plus one word
sudo systemctl restart orders-api
sudo systemctl reload orders-api # runs ExecReload, no downtime if supported
systemctl list-units --type=service --state=running
systemctl list-unit-files --state=enabled
systemctl show orders-api -p MemoryMax -p Restart
Dos comandos se ganan su sitio cuando algo va lento o falla en el arranque: systemd-analyze blame ordena las unidades por tiempo de arranque y systemd-analyze critical-chain muestra la cadena de dependencias que realmente determinó la duración del arranque. systemctl list-dependencies orders-api imprime el árbol en la dirección contraria.
systemctl mask es más contundente que disable: enlaza la unidad a /dev/null para que nada, ni siquiera la dependencia de otra unidad, pueda arrancarla. Acuérdate de hacer unmask antes de preguntarte por qué el servicio de un paquete no arranca.
Leer los logs de una unidad
systemd captura la salida estándar y la salida de error de cada unidad en el journal, etiquetadas con el nombre de la unidad, el PID, el identificador de arranque y la prioridad. journalctl sirve para consultarlo.
journalctl -u orders-api -f # follow, like tail -f
journalctl -u orders-api --since "1 hour ago" -p err
journalctl -b -1 -u orders-api # previous boot
journalctl -u orders-api -o json-pretty | head -40
journalctl --disk-usage
La retención del journal y si sobrevive a un reinicio se configuran en /etc/systemd/journald.conf; la cuestión del almacenamiento y su relación con /var/log se tratan en la guía de resolución de problemas.
Targets, dependencias y orden
Un target es una unidad que agrupa a otras y sustituye a los antiguos runlevels. multi-user.target es un sistema completamente arrancado sin sesión gráfica; graphical.target lo incluye y añade un display manager; rescue.target es el modo monousuario. systemctl get-default y set-default controlan cuál se usa al arrancar.
Las dependencias y el orden son conceptos separados a propósito:
Requires=yWants=arrastran otra unidad: conRequires, tu unidad falla si la otra falla; conWants, no.After=yBefore=solo ordenan unidades que ya se están arrancando; no arrastran nada.
Una unidad con After=postgresql.service pero sin Wants= arrancará después de la base de datos si la base de datos se está arrancando de todos modos, e inmediatamente si no. Casi todos los reportes de «mi servicio arranca demasiado pronto» se deben a un Wants= que falta o a usar network.target (interfaces configuradas) cuando lo que se quería era network-online.target (una dirección realmente utilizable).
Timers en lugar de cron
Una unidad timer activa otra unidad según una programación. Necesita dos ficheros: la tarea y el disparador.
# /etc/systemd/system/db-backup.service
[Unit]
Description=Nightly database dump
[Service]
Type=oneshot
User=postgres
ExecStart=/usr/local/bin/db-backup.sh
# /etc/systemd/system/db-backup.timer
[Unit]
Description=Run the nightly database dump
[Timer]
OnCalendar=*-*-* 02:30:00
RandomizedDelaySec=15m
Persistent=true
[Install]
WantedBy=timers.target
sudo systemctl enable --now db-backup.timer
systemctl list-timers --all
systemd-analyze calendar "Mon..Fri 09:00" # verify an expression before trusting it
sudo systemctl start db-backup.service # run it once, now, to test
Persistent=true ejecuta una tarea perdida cuando la máquina vuelve a arrancar: la propiedad que le falta a cron y la razón por la que las copias de seguridad se saltan un día sin avisar en portátiles e instancias spot. RandomizedDelaySec evita que cien máquinas golpeen el mismo endpoint a las 02:30:00 en punto. OnUnitActiveSec= expresa «cada 15 minutos desde la última ejecución» para tareas cuya duración varía. Y como la tarea es una unidad corriente, su salida acaba en el journal en lugar de enviarse por correo a un buzón que nadie lee.
Cron sigue existiendo y está bien para un puñado de tareas personales; crontab -e edita tu propia tabla y /etc/cron.d/ guarda entradas del sistema con una columna de usuario. Solo recuerda que cron se ejecuta con un entorno mínimo y sin ficheros de login, así que los scripts necesitan rutas absolutas y un PATH explícito.
Errores comunes
- Editar una unidad empaquetada en
/usr/lib/systemd/system. La próxima actualización la sobrescribe. Usasystemctl edito un fichero en/etc/systemd/system. - Olvidar
daemon-reload. systemd guarda en caché los ficheros de unidad; un cambio es invisible hasta que recargas, ystatusno te dirá por qué. - Confundir
enableconstart. Habilitar solo afecta al arranque. Usaenable --nowcuando quieras ambas cosas. - Usar
Type=simplepara un programa que hace fork. systemd sigue el PID equivocado,MAINPIDes incorrecto y la recarga y la parada se comportan mal. After=network.targetpara cualquier cosa que escuche en una dirección pública. Usanetwork-online.targetcon elWants=correspondiente.- Matar con
-9por reflejo. Pierdes el apagado limpio que el servicio ya implementa. - Confiar en nice para proteger una máquina cargada. Define en su lugar
CPUQuota=yMemoryMax=en la unidad.
Puntos clave
- Los procesos forman un árbol: los hijos heredan el entorno y los descriptores, el PID 1 adopta a los huérfanos y un zombie es un código de salida sin recoger, no un programa en ejecución.
ps -eocon columnas explícitas,top/htopypgrepresponden a la mayoría de las preguntas de tipo «qué se está ejecutando y por qué»; compara RSS, no VSZ.- Envía
SIGTERMy espera antes de recurrir aSIGKILL;SIGHUPrecarga la configuración por convención, y nada llega a un proceso atascado en E/S no interrumpible. - Un fichero de unidad de systemd declara cómo arrancar un programa, cuándo se considera arrancado, cómo reiniciarlo y qué puede tocar; los drop-ins sobrescriben de forma segura las unidades empaquetadas.
Wants=/Requires=arrastran unidades,After=/Before=solo las ordenan, ynetwork-online.targetes lo que de verdad necesitan los servicios de red.- El journal contiene la salida de todas las unidades;
journalctl -u … -p err --sincees el camino más rápido para saber qué ha fallado. - Los timers dan a las tareas programadas las mismas unidades, el mismo logging, las mismas dependencias y la misma semántica de recuperación que todo lo demás que ejecuta systemd.
Fuentes y lecturas recomendadas
Preguntas frecuentes
- ¿Por qué mi servicio muere cuando cierro la sesión SSH?
- Un programa lanzado desde un shell de login pertenece a esa sesión. Cuando el terminal desaparece, el kernel envía SIGHUP al grupo en primer plano y la mayoría de los programas terminan. Anteponer nohup o mandarlo a segundo plano con un ampersand solo tapa el problema. Todo lo que deba sobrevivir tiene que ir en una unidad de systemd o, como mínimo, en systemd-run --user --scope, que lo saca del cgroup de la sesión.
- ¿Cuál es la diferencia práctica entre SIGTERM y SIGKILL?
- SIGTERM es una petición educada: el proceso puede capturarla, vaciar sus búferes, cerrar sockets, borrar su fichero PID y terminar limpiamente. SIGKILL no se puede capturar, bloquear ni ignorar: el kernel destruye el proceso al instante, así que las transacciones abiertas se pierden y los ficheros temporales se quedan. Envía siempre SIGTERM primero, espera y reserva SIGKILL para un proceso que de verdad haya dejado de responder.
- ¿Por qué un proceso aparece como defunct o zombie y cómo lo elimino?
- Un zombie es un proceso terminado cuyo código de salida su padre todavía no ha recogido con wait(). No ocupa memoria ni CPU, solo una entrada en la tabla de procesos. No puedes matarlo porque ya está muerto. Arregla el padre —reinícialo o envíale SIGCHLD— y las entradas desaparecen. Si el padre termina, init adopta a los hijos y los recoge.
- ¿Debo usar un timer de systemd o una tarea de cron?
- Los timers se integran con el resto de systemd: la tarea se ejecuta como una unidad, su salida va al journal, se aplican dependencias y límites de recursos, Persistent=true recupera las ejecuciones perdidas tras una parada y RandomizedDelaySec reparte la carga entre toda una flota. Cron implica menos ficheros y basta para un script periódico sencillo en una máquina que gestionas a mano. En un servidor que ya administras con unidades, prefiere los timers por coherencia.