Hasta ahora todo lo que has manejado estaba quieto: archivos, permisos, texto. Esta lección cambia de objeto y se ocupa de lo que está vivo. Un proceso es un programa en ejecución, con su propia memoria, sus descriptores de archivo abiertos, su identidad y su lugar en un árbol genealógico que arranca en systemd. Entenderlos es lo que te permite responder a la pregunta que antes o después te va a hacer Marta: «¿por qué el servidor va lento?».

En la lección anterior descubriste que el 65 % de los errores de acceso.log se concentran en la franja de las 03:00, y que todo apunta a que algo compite por las conexiones de la base de datos durante la ventana nocturna. Aquí tienes las herramientas para averiguar qué es ese algo, y para actuar sobre ello sin tirar el servicio abajo.

Contenido

  1. Qué es un proceso
  2. El árbol de procesos
  3. Ciclo de vida: fork, exec, wait, exit
  4. Zombis y huérfanos
  5. Estados de proceso
  6. ps de verdad
  7. top y htop: leer la cabecera
  8. pgrep y pkill
  9. Señales
  10. Prioridades: nice, renice e ionice
  11. Control de trabajos en la sesión interactiva
  12. lsof y fuser
  13. Caso Tramontana: la app colgada de madrugada

  1. Qué es un proceso

Cada proceso tiene una ficha en el kernel con, entre otras cosas:

Atributo Qué es
PID identificador único
PPID PID del proceso padre
UID/GID efectivos identidad con la que se comprueban los permisos
Estado R, S, D, T o Z (apartado 5)
Prioridad (PRI) y nice (NI) cuánta CPU le toca
VSZ / RSS memoria virtual reservada / memoria física realmente usada
Directorio de trabajo el cwd con el que resuelve rutas relativas
Descriptores abiertos los que viste en /proc/<pid>/fd en 03-04

La distinción VSZ frente a RSS es la que más malentendidos causa: VSZ incluye memoria reservada pero nunca tocada y bibliotecas compartidas, así que sumar los VSZ de todos los procesos da un número muy superior a la RAM instalada sin que eso signifique nada. El número que importa es RSS.

  1. El árbol de procesos

Todo proceso desciende de otro. La raíz es systemd, con PID 1, el primer proceso que arranca el kernel.

operador@srv-tramontana:~$ pstree -p | head -8
systemd(1)─┬─cron(742)
           ├─dbus-daemon(701)
           ├─ejecutable(1284)─┬─{ejecutable}(1285)
           │                  ├─{ejecutable}(1286)
           │                  └─{ejecutable}(1287)
           ├─sshd(889)───sshd(1502)───sshd(1508)───bash(1509)───pstree(1620)
           └─systemd-journald(412)

Se lee mucho. ejecutable(1284) es la aplicación de Tramontana con tres hilos (entre llaves, no son procesos independientes). La cadena de sshd muestra tu propia conexión: el demonio principal, el proceso de la conexión, el de la sesión ya autenticada, tu bash y el pstree que acabas de lanzar como hijo suyo. Ahí está la herencia del entorno de 03-01, hecha diagrama.

pstree -ps 1284 muestra los ancestros de un PID concreto y devuelve systemd(1)───ejecutable(1284): la app cuelga directamente de systemd, no de una sesión de usuario. Eso significa que sobrevive a que cierres el SSH, y es la diferencia entre un servicio y un programa lanzado a mano.

  1. Ciclo de vida: fork, exec, wait, exit

flowchart LR
    P["Padre (bash)"] -->|"fork()"| C["Hijo: copia del padre<br/>PID nuevo, mismo código"]
    C -->|"exec()"| N["El hijo se reemplaza<br/>por el nuevo programa"]
    N -->|"exit(código)"| Z["Zombi: solo queda<br/>el código de salida"]
    P -->|"wait()"| Z
    Z --> F["Liberado de la tabla<br/>de procesos"]

Cuando escribes ls en Bash ocurre esto: Bash hace fork(), que crea un hijo idéntico a él; el hijo llama a exec(), que sustituye su contenido por el binario de ls conservando el PID y los descriptores abiertos —ahí es donde encajan las redirecciones que Bash preparó antes—; ls termina llamando a exit(0); y Bash, que estaba en wait(), recoge ese código de salida, que es lo que luego lees en $?.

Esa separación entre fork y exec es la que hace posible la redirección tal como la estudiaste: hay un momento, entre las dos llamadas, en que el hijo ya existe y todavía no es el programa nuevo, y es ahí donde el shell reconfigura los descriptores.

  1. Zombis y huérfanos

Son dos situaciones opuestas y conviene no confundirlas.

Un zombi (estado Z) es un proceso que ya ha terminado pero cuyo padre no ha llamado a wait(). No consume CPU ni memoria: solo ocupa una entrada en la tabla de procesos con su código de salida, esperando a que alguien lo recoja.

A un zombi no se le puede matar. Ya está muerto; kill -9 sobre él no hace nada, porque las señales las reciben procesos vivos. La única forma de eliminarlo es que su padre lo recoja o que el padre muera. Un puñado de zombis es irrelevante; miles indican un padre mal programado, y la acción correcta es reiniciar al padre.

Para buscarlos: ps -eo stat,ppid,pid,comm | awk '$1 ~ /^Z/'. En srv-tramontana no devuelve nada.

Un huérfano es lo contrario: un proceso vivo cuyo padre ha muerto. No es un problema. El kernel lo reasigna a systemd (PPID pasa a 1), que sí llama a wait() correctamente. Es el mecanismo en el que se apoyan nohup y los demonios.

  1. Estados de proceso

Estado Nombre Significa
R Running / runnable ejecutándose o listo para hacerlo
S Interruptible sleep esperando algo (E/S, red, un temporizador); la mayoría
D Uninterruptible sleep esperando E/S de disco, no acepta señales
T Stopped detenido por Ctrl+Z o SIGSTOP
Z Zombie terminado, pendiente de recoger

Modificadores que verás pegados: s líder de sesión, l multihilo, + en primer plano, < prioridad alta, N prioridad baja.

Por qué un proceso en D no se puede matar. Está bloqueado dentro del kernel esperando que se complete una operación de disco o de red, y el kernel no le entrega señales hasta que esa operación termine. kill -9 se queda encolado: la señal se entregará cuando el proceso vuelva a estado S o R. Si un proceso lleva minutos en D, el problema no es el proceso: es el almacenamiento —un disco fallando, un montaje NFS caído— y ahí es donde hay que mirar.

  1. ps de verdad

ps tiene dos sintaxis históricas que conviven: BSD (sin guion) y UNIX (con guion). Las dos combinaciones que se usan:

operador@srv-tramontana:~$ ps aux | head -3
USER         PID %CPU %MEM    VSZ   RSS TTY   STAT START   TIME COMMAND
root           1  0.0  0.4 168404 12856 ?     Ss   08:31   0:03 /sbin/init
svc-tram+   1284  2.1 12.4 982340 483920 ?    Ssl  08:32   1:47 /opt/tramontana/app/ejecutable
Columna Significa
USER dueño efectivo (truncado a 8 caracteres: svc-tram+)
%CPU porcentaje de CPU promediado desde que arrancó, no instantáneo
%MEM porcentaje de RAM física
VSZ / RSS memoria virtual / residente, en KiB
TTY terminal asociado; ? significa que no tiene, típico de un servicio
STAT estado más modificadores
START / TIME hora de arranque / CPU total consumida

Ojo con %CPU: es una media desde el arranque del proceso. Un proceso que devoró la CPU hace ocho horas y ahora duerme puede seguir mostrando un porcentaje alto. Para saber qué consume ahora, usa top.

ps -ef da la vista UNIX, con PPID explícito, que es lo que quieres para seguir relaciones padre-hijo. Y ps -o construye la salida a medida:

operador@srv-tramontana:~$ ps -eo pid,ppid,user,ni,stat,rss,etime,cmd --sort=-rss | head -4
    PID    PPID USER      NI STAT   RSS     ELAPSED CMD
   1284       1 svc-tram   0 Ssl 483920    04:12:33 /opt/tramontana/app/ejecutable
    889       1 root       0 Ss   12404    04:13:05 sshd: /usr/sbin/sshd -D
    412       1 root       0 Ss   11208    04:13:11 systemd-journald

--sort=-rss ordena por memoria residente descendente (el - invierte). etime da el tiempo transcurrido desde el arranque, mucho más útil que la hora absoluta cuando estás diagnosticando.

Filtros: -u operador por usuario, -C ejecutable por nombre de comando, -p 1284 por PID, --forest para ver la jerarquía en la propia salida de ps.

  1. top y htop: leer la cabecera

top - 12:44:18 up  4:13,  2 users,  load average: 3.42, 1.87, 0.94
Tareas: 132 total,   2 ejecutar, 130 hibernar,   0 parar,   0 zombie
%Cpu(s): 12,3 us,  4,1 sy,  0,0 ni, 18,2 id, 64,8 wa,  0,3 hi,  0,3 si,  0,0 st
MiB Mem :   3844,0 total,    198,4 free,   2914,2 used,    731,4 buff/cache
MiB Swap:   2048,0 total,   1620,0 free,    428,0 used.    612,8 avail Mem

El load average

Los tres números son la media de procesos en estado R o D durante 1, 5 y 15 minutos. Dos consecuencias que casi nadie tiene claras:

  1. No es un porcentaje. Hay que compararlo con el número de núcleos: en la VM de srv-tramontana, con 2 vCPU, un load de 2,00 es plena ocupación y 3,42 significa que hay más trabajo del que la máquina puede atender.
  2. Incluye los procesos en D, es decir, los que esperan disco. Un load de 3,42 con la CPU casi ociosa no indica falta de CPU: indica que hay procesos atascados esperando E/S.

Y la comparación entre los tres números da la tendencia: 3.42, 1.87, 0.94 es una curva ascendente. El problema está empeorando ahora mismo, no remitiendo.

El desglose de CPU

Sigla Significa
us tiempo en código de usuario
sy tiempo en el kernel
ni procesos con nice modificado
id ocioso
wa esperando E/S: la CPU está libre pero no puede avanzar
hi / si interrupciones hardware / software
st steal: CPU que el hipervisor dio a otra VM

Ese 64,8 wa es el dato de la cabecera de arriba. La CPU no está saturada: está esperando al disco o a la base de datos. Perseguir un proceso que consume CPU sería buscar en el sitio equivocado. Y st alto en una máquina virtual significa que el problema no está en tu VM sino en el anfitrión, algo que conviene saber antes de optimizar código.

Sobre memoria: buff/cache no es memoria perdida, es caché de disco que el kernel libera en cuanto alguien la necesita. La cifra que importa es avail Mem. Ver poca memoria libre en Linux es normal y deseable.

Teclas útiles en top: M ordena por memoria, P por CPU, 1 desglosa por núcleo, k envía una señal, u filtra por usuario, c muestra la línea de comandos completa. htop hace lo mismo con colores, ratón y árbol con F5; se instala aparte pero merece la pena.

  1. pgrep y pkill

operador@srv-tramontana:~$ pgrep -a ejecutable
1284 /opt/tramontana/app/ejecutable --config /etc/tramontana/app.conf

-a muestra la línea de comandos, -c cuenta, -u filtra por usuario, -f casa contra la línea completa y no solo el nombre, -x exige coincidencia exacta, -n/-o el más nuevo / el más antiguo.

Por qué son mejores que ps | grep | awk | kill: esa tubería tiene dos defectos. El primero es que el propio grep aparece en la lista y puedes acabar matando un PID que ya no existe o, peor, uno reciclado. El segundo es que una coincidencia parcial se lleva por delante procesos que no querías. pgrep consulta directamente /proc y no se incluye a sí mismo.

Antes de pkill, ejecuta siempre el pgrep equivalente. Es la misma convención que ls antes de rm:

operador@srv-tramontana:~$ pgrep -a -f 'ejecutable --config'
1284 /opt/tramontana/app/ejecutable --config /etc/tramontana/app.conf
operador@srv-tramontana:~$ pkill -f 'ejecutable --config'

  1. Señales

Una señal es una notificación asíncrona que el kernel entrega a un proceso.

Nº Nombre Efecto por defecto ¿Se puede capturar?
1 SIGHUP terminar; por convención, recargar configuración sí
2 SIGINT interrumpir (Ctrl+C) sí
3 SIGQUIT terminar y volcar core (Ctrl+\) sí
9 SIGKILL matar de inmediato no
15 SIGTERM terminar ordenadamente (por defecto) sí
18/19 SIGCONT / SIGSTOP reanudar / detener CONT sí, STOP no
10/12 SIGUSR1 / SIGUSR2 libres, definidas por la aplicación sí

kill -l lista las 64 señales del sistema. kill 1284 envía SIGTERM. Para otra: kill -TERM 1284, kill -15 1284 o kill -s SIGTERM 1284, todas equivalentes. killall nombre actúa por nombre en lugar de por PID —con el riesgo evidente de alcanzar más de lo que pretendías.

La regla profesional: SIGTERM, esperar, y solo entonces SIGKILL

SIGTERM es una petición: el proceso la recibe, ejecuta su rutina de cierre y termina cuando ha acabado. SIGKILL es una ejecución sumaria: el kernel destruye el proceso sin avisarle. El proceso no se entera y por tanto no puede hacer nada.

Qué se pierde con -9:

  • Los datos en búferes que aún no se habían escrito a disco.
  • Las transacciones abiertas en la base de datos, que quedan sin confirmar y bloqueando filas hasta que expiren.
  • Las peticiones HTTP en curso, que se cortan en seco: el cliente ve un error.
  • Los ficheros temporales y los ficheros de bloqueo, que quedan huérfanos y pueden impedir el siguiente arranque.
  • La oportunidad de que el proceso escriba en el log por qué se cerró.

El procedimiento correcto:

operador@srv-tramontana:~$ kill -TERM 1284
operador@srv-tramontana:~$ sleep 10; pgrep -c ejecutable
0

Diez segundos es un margen razonable para una aplicación web; si el proceso ya no está, hemos terminado bien. Solo si sigue vivo tras ese plazo se recurre a kill -9, y entonces se documenta como incidente, porque significa que la rutina de cierre de la aplicación no funciona y eso es un fallo que hay que corregir.

SIGHUP merece una mención: por convención, muchos demonios lo interpretan como «relee tu configuración sin reiniciar». Cuando funciona, es la forma de aplicar un cambio en app.conf sin cortar ni una petición.

  1. Prioridades: nice, renice e ionice

El valor nice va de -20 (máxima prioridad) a 19 (mínima). El nombre viene de «ser amable»: cuanto más alto, más cede el proceso ante los demás. Un usuario normal solo puede subirlo —bajar su propia prioridad—; reducirlo exige root.

operador@srv-tramontana:~$ nice -n 15 tar -czf /srv/tramontana/backups/envios/datos.tar.gz /home/operador/datos
operador@srv-tramontana:~$ sudo renice -n 5 -p 1284
1284 (id de proceso) prioridad antigua 0, prioridad nueva 5
operador@srv-tramontana:~$ ionice -c 3 tar -czf /srv/tramontana/backups/envios/datos.tar.gz /home/operador/datos

nice lanza un comando con prioridad modificada; renice cambia la de un proceso ya en marcha, también por usuario (-u) o por grupo. Y para la E/S, que en nuestro caso es el recurso escaso, está ionice. Su clase 3 es idle: la copia solo lee del disco cuando nadie más lo necesita. Con un wa del 64,8 %, ionice resuelve más que nice: bajar la prioridad de CPU de un proceso que no usa CPU no sirve de nada. Diagnosticar antes de actuar también significa elegir la palanca correcta.

  1. Control de trabajos en la sesión interactiva

Acción Cómo
Lanzar en segundo plano comando &
Listar los trabajos de la sesión jobs -l
Traer al primer plano fg %1
Continuar en segundo plano bg %1
Suspender el actual Ctrl+Z (envía SIGSTOP)
Desligar de la sesión disown -h %1
Inmune al cierre de sesión nohup comando &
operador@srv-tramontana:~$ tar -czf /tmp/copia.tar.gz /opt/tramontana/releases/3.1.0 &
[1] 2041
operador@srv-tramontana:~$ jobs -l
[1]+  2041 Ejecutando   tar -czf /tmp/copia.tar.gz /opt/tramontana/releases/3.1.0 &

Al cerrar la sesión, el shell envía SIGHUP a sus trabajos. nohup los inmuniza y redirige la salida a nohup.out; disown -h consigue lo mismo con un trabajo ya lanzado.

Y ahora la advertencia importante: nada de esto sirve para un servicio de verdad. Un nohup ./ejecutable & no se reinicia si el proceso muere, no arranca al encender el servidor, no tiene control de recursos, no gestiona sus logs y no puede pararse de forma ordenada por nadie que no seas tú. El control de trabajos es para tareas largas de tu sesión —una copia, una compilación—, no para producción. Los servicios se gestionan con systemd, y eso es la lección 05-05.

  1. lsof y fuser

lsof («list open files») responde a la pregunta «¿quién tiene esto abierto?». Como en Linux casi todo es un archivo, sirve también para sockets y puertos.

Caso «no puedo desmontar».

operador@srv-tramontana:~$ sudo umount /srv/tramontana/backups
umount: /srv/tramontana/backups: destino ocupado.
operador@srv-tramontana:~$ sudo lsof +D /srv/tramontana/backups
COMMAND   PID  USER   FD   TYPE DEVICE SIZE/OFF   NODE NAME
bash     1509 opera  cwd    DIR  8,1      4096 262148 /srv/tramontana/backups/envios
tar      2041 opera    3w   REG  8,1  10485760 262203 /srv/tramontana/backups/envios/datos.tar.gz

Dos culpables: un bash cuyo directorio de trabajo (cwd) está dentro —basta un cd fuera— y un tar escribiendo (3w, descriptor 3 en modo escritura). Con eso ya sabes qué esperar y a quién avisar, en lugar de forzar el desmontaje.

Caso «el puerto 8080 está ocupado».

operador@srv-tramontana:~$ sudo lsof -i :8080
COMMAND     PID           USER   FD   TYPE  DEVICE NODE NAME
ejecutable 1284 svc-tramontana   7u  IPv4 1284091  TCP *:http-alt (LISTEN)

El PID 1284 tiene el puerto en escucha. Es la respuesta directa a «no puedo arrancar la aplicación nueva porque el puerto está en uso»: la vieja sigue viva.

fuser es más escueto y muy práctico para actuar: fuser -v /ruta lista los procesos, fuser -k /ruta los mata (con cuidado) y fuser -k -TERM 8080/tcp envía SIGTERM a quien ocupe ese puerto.

/proc/<pid>/ es la fuente de verdad de la que beben todas estas herramientas: cmdline la orden exacta, environ el entorno con el que arrancó, cwd y exe como enlaces simbólicos, fd/ los descriptores, status un resumen legible, limits los límites de recursos. Cuando una herramienta te dé un dato dudoso, ve al fichero.

  1. Caso Tramontana: la app colgada de madrugada

03:05. La aplicación no responde. Marta te escribe. Procedimiento, con la conclusión de 03-05 como hipótesis de partida.

Paso 1: ¿está viva y en qué estado?

operador@srv-tramontana:~$ ps -o pid,stat,ni,rss,etime,pcpu -p $(pgrep -x ejecutable)
    PID STAT  NI   RSS     ELAPSED %CPU
   1284 Dsl    0 483920    04:12:33  2.1

Estado D: no está colgada en un bucle, está bloqueada esperando E/S. Con solo 2,1 % de CPU, no es un problema de cálculo. Un kill -9 aquí ni siquiera surtiría efecto inmediato.

Paso 2: ¿qué dice la carga? El load average: 3.42, 1.87, 0.94 con 64,8 wa de la cabecera del apartado 7 confirma lo mismo desde otro ángulo: cola creciente y espera de E/S, no falta de CPU.

Paso 3: ¿quién compite?

operador@srv-tramontana:~$ ps -eo pid,user,stat,pcpu,etime,cmd --sort=-pcpu | head -4
    PID USER     STAT %CPU     ELAPSED CMD
   2210 operador D    38.4       05:12 tar -czf /srv/tramontana/backups/envios/nocturna.tar.gz /opt/tramontana
   1284 svc-tram Dsl   2.1    04:12:33 /opt/tramontana/app/ejecutable

Ahí está. Una copia de seguridad nocturna lleva cinco minutos leyendo /opt/tramontana entero —incluidos los cuatro releases con sus ejecutable de 48 MB— y satura el disco de la VM. La aplicación, que necesita el disco para atender consultas, se queda esperando.

Paso 4: aliviar sin matar nada. La copia es legítima; lo que está mal es su prioridad.

operador@srv-tramontana:~$ sudo ionice -c 3 -p 2210
operador@srv-tramontana:~$ sudo renice -n 19 -p 2210
2210 (id de proceso) prioridad antigua 0, prioridad nueva 19

Ambos cambios se aplican al proceso en marcha, sin interrumpirlo. Dos minutos después el wa baja y la aplicación vuelve a responder.

Paso 5: si hubiera hecho falta reiniciar la app, el procedimiento habría sido kill -TERM $(pgrep -x ejecutable), esperar a que las peticiones en curso terminen, verificar con pgrep que ya no está y solo entonces arrancarla de nuevo. Nunca kill -9 de entrada: cortaría las reservas que estuvieran confirmándose en ese momento, y una reserva a medias es un problema con cliente delante.

Informe para Marta. La caída no fue un fallo de la aplicación sino una competencia por el disco: la copia de seguridad nocturna se ejecutaba con la misma prioridad que el servicio y lo dejaba sin acceso al almacenamiento. La medida aplicada —darle a la copia la prioridad de E/S más baja— protege frente a que vuelva a bloquear el servicio, y no protege frente a un aumento general de la carga: si crece el número de reservas, harán falta más recursos o separar la base de datos. Además, la copia incluye los cuatro releases enteros cuando bastaría con el activo, lo que la hace innecesariamente pesada. Pendiente: ajustar el alcance de la copia y su horario, que es exactamente lo que veremos en la lección siguiente.

Errores Comunes y Consejos

  • Usar kill -9 como primera opción. Es la última. SIGTERM, esperar, verificar.
  • Intentar matar un zombi. Ya está muerto. Actúa sobre el padre.
  • Insistir con un proceso en D. No recibirá la señal hasta que la E/S termine. Investiga el almacenamiento.
  • Leer %CPU de ps como valor instantáneo. Es una media desde el arranque. Usa top.
  • Alarmarse por poca memoria libre. buff/cache es caché reutilizable. Mira avail Mem.
  • Comparar el load average con 100. Compáralo con el número de núcleos, y recuerda que incluye la espera de E/S.
  • ps | grep | kill sobre un nombre corto. El propio grep sale en la lista y una coincidencia parcial mata de más. Usa pgrep -a y luego pkill.
  • Dejar un servicio con nohup &. No sobrevive a un reinicio ni se recupera solo.
  • Consejo: ante «el servidor va lento», mira siempre primero el wa de top. Si es alto, el cuello de botella es el disco y la mitad de las hipótesis se descartan de golpe.
  • Consejo: ps -eo acepta cualquier combinación de columnas; guárdate un alias con las tuyas en ~/.bashrc, como aprendiste en 03-01.

Ejercicios

Ejercicio 1. Averigua con qué línea de comandos exacta, con qué directorio de trabajo y con qué variables de entorno arrancó el proceso de Tramontana, sin usar ps. Explica qué te dice cada dato.

Ejercicio 2. Lanza una tarea larga en segundo plano, suspéndela, reanúdala en segundo plano, bájale la prioridad de CPU y de E/S, y desligala de tu sesión para que sobreviva a un cierre de SSH. Muestra la comprobación de cada paso.

Ejercicio 3. Simula el diagnóstico de «el puerto 8080 no acepta la nueva versión»: identifica qué proceso lo ocupa, de quién es, desde cuándo, y detén ese proceso de forma ordenada verificando que ha terminado antes de dar el paso por bueno.

Soluciones

Solución 1.

operador@srv-tramontana:~$ PID=$(pgrep -x ejecutable)
operador@srv-tramontana:~$ tr '\0' ' ' < /proc/$PID/cmdline; echo
/opt/tramontana/app/ejecutable --config /etc/tramontana/app.conf
operador@srv-tramontana:~$ sudo ls -l /proc/$PID/cwd /proc/$PID/exe
lrwxrwxrwx 1 svc-tramontana tramontana 0 ago 18 12:52 /proc/1284/cwd -> /opt/tramontana/app
lrwxrwxrwx 1 svc-tramontana tramontana 0 ago 18 12:52 /proc/1284/exe -> /opt/tramontana/releases/3.2.1/ejecutable
operador@srv-tramontana:~$ sudo tr '\0' '\n' < /proc/$PID/environ | grep -E 'PATH|LANG'
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
LANG=C.UTF-8

cmdline guarda los argumentos separados por bytes nulos, de ahí el tr. Confirma que la app lee /etc/tramontana/app.conf, así que un cambio ahí le afecta. El cwd es /opt/tramontana/app, el enlace simbólico: significa que sus rutas relativas se resuelven a través de él.

El dato revelador es exe, que apunta a /opt/tramontana/releases/3.2.1/ejecutable, la ruta ya resuelta. Es la prueba de que el despliegue atómico funciona como esperábamos: el kernel fijó el binario real al arrancar, de modo que cambiar el enlace app a otro release no afecta al proceso en marcha. Sabe qué versión está sirviendo de verdad, no cuál dice el enlace ahora mismo. Y environ muestra un PATH mínimo y LANG=C.UTF-8: el servicio no hereda tu entorno interactivo, cosa que ya sabías desde 03-01 y que volverá a aparecer en la lección siguiente.

Solución 2.

operador@srv-tramontana:~$ tar -czf /tmp/rel.tar.gz /opt/tramontana/releases &
[1] 2311
operador@srv-tramontana:~$ fg %1
tar -czf /tmp/rel.tar.gz /opt/tramontana/releases
^Z
[1]+  Detenido                tar -czf /tmp/rel.tar.gz /opt/tramontana/releases
operador@srv-tramontana:~$ ps -o pid,stat -p 2311
    PID STAT
   2311 T
operador@srv-tramontana:~$ bg %1
[1]+ tar -czf /tmp/rel.tar.gz /opt/tramontana/releases &
operador@srv-tramontana:~$ renice -n 15 -p 2311 && ionice -c 3 -p 2311
2311 (id de proceso) prioridad antigua 0, prioridad nueva 15
operador@srv-tramontana:~$ disown -h %1
operador@srv-tramontana:~$ ps -o pid,ni,stat -p 2311
    PID  NI STAT
   2311  15 SN

Cada comprobación aporta algo: STAT en T confirma que Ctrl+Z lo detuvo de verdad (SIGSTOP), y tras bg y los ajustes, SN indica que duerme con prioridad baja (N) y NI es 15. renice a 15 no ha necesitado sudo porque subir el propio nice está permitido a cualquier usuario; bajarlo requeriría privilegios. disown -h no lo saca de la lista de trabajos, pero marca que no debe recibir SIGHUP al cerrar la sesión. Si supiéramos de antemano que la tarea es larga, lo limpio habría sido nohup ... & desde el principio.

Solución 3.

operador@srv-tramontana:~$ sudo lsof -i :8080 -sTCP:LISTEN
COMMAND     PID           USER   FD   TYPE  DEVICE NODE NAME
ejecutable 1284 svc-tramontana   7u  IPv4 1284091  TCP *:http-alt (LISTEN)
operador@srv-tramontana:~$ ps -o pid,user,etime,cmd -p 1284
    PID USER         ELAPSED CMD
   1284 svc-tram    04:31:12 /opt/tramontana/app/ejecutable --config /etc/tramontana/app.conf

Ya tenemos las tres respuestas: lo ocupa ejecutable, pertenece a svc-tramontana y lleva cuatro horas y media en marcha. -sTCP:LISTEN filtra los sockets en escucha y descarta las conexiones establecidas, que serían ruido.

operador@srv-tramontana:~$ sudo kill -TERM 1284
operador@srv-tramontana:~$ sleep 10; pgrep -x ejecutable || echo 'terminado correctamente'
terminado correctamente
operador@srv-tramontana:~$ sudo lsof -i :8080
operador@srv-tramontana:~$ echo "puerto libre: $?"
puerto libre: 1

Dos verificaciones distintas y ambas necesarias. pgrep confirma que el proceso ya no existe; lsof sin salida confirma que el puerto está libre, que no es exactamente lo mismo: un socket puede quedar unos segundos en TIME_WAIT tras cerrarse el proceso, e intentar arrancar la versión nueva en ese hueco fallaría por un motivo distinto y desconcertante. Comprobar el recurso que de verdad necesitas, y no solo el proceso, es lo que evita ese diagnóstico erróneo. El estado TIME_WAIT y el resto de estados TCP los verás en 03-08.

Conclusión

Has dejado de mirar archivos para mirar lo que se está ejecutando, y con eso has resuelto una incidencia real de principio a fin.

  • Sabes qué guarda el kernel de cada proceso —PID, PPID, UID efectivo, estado, prioridad, VSZ y RSS— y que el número de memoria que importa es RSS.
  • Recorres el árbol de procesos con pstree desde systemd (PID 1) y entiendes el ciclo fork / exec / wait / exit, que es donde encaja la redirección de 03-04.
  • Distingues zombi de huérfano: al zombi no se le puede matar y se actúa sobre el padre; el huérfano lo adopta systemd y no es problema.
  • Conoces los estados R, S, D, T, Z y por qué un proceso en D no responde a ninguna señal, ni siquiera a -9.
  • Lees ps aux y ps -ef columna a columna, construyes vistas con ps -eo ... --sort=, y sabes que el %CPU de ps es una media desde el arranque.
  • Interpretas la cabecera de top: el load average frente al número de núcleos y su tendencia, el desglose us/sy/ni/id/wa/hi/si/st —con wa como la señal de que el cuello de botella es el disco— y que buff/cache no es memoria perdida.
  • Usas pgrep/pkill en vez de ps | grep | kill, comprobando siempre antes de matar, y aplicas la regla profesional de las señales: SIGTERM, esperar, verificar y solo entonces SIGKILL, sabiendo qué se pierde con -9.
  • Ajustas prioridades con nice, renice e ionice, eligiendo la palanca según dónde esté el cuello de botella.
  • Controlas trabajos con &, jobs, fg, bg, Ctrl+Z, disown y nohup, y tienes claro que nada de eso vale para un servicio de producción.
  • Respondes con lsof y fuser a «¿quién tiene esto abierto?» y «¿quién ocupa este puerto?», y acudes a /proc/<pid>/ cuando quieres la verdad sin intermediarios.

El diagnóstico ha dejado una tarea pendiente muy concreta: la copia nocturna se ejecuta a la hora equivocada, con la prioridad equivocada y sobre más datos de los necesarios. Arreglarla es el tema de la siguiente lección. Programación de Tareas con Cron te enseñará la sintaxis de los cinco campos hasta sus casos más retorcidos, los tres fallos clásicos que causan el 90 % de las incidencias —el PATH mínimo, el correo de salida que nadie lee y la zona horaria—, cómo depurar una tarea que «funciona a mano y en cron no», y cómo evitar con flock que dos ejecuciones se solapen, que es precisamente lo que puede haber pasado esta madrugada.

Curso de Linux: De Principiante a Administrador de Sistemas

Módulo 1: Introducción a Linux

Módulo 2: Comandos Básicos de Linux

Módulo 3: Habilidades Avanzadas en la Línea de Comandos

Módulo 4: Scripting en Shell

Módulo 5: Administración del Sistema

Módulo 6: Redes y Seguridad

Módulo 7: Temas Avanzados

Módulo 8: Proyectos Prácticos

© Copyright 2026. Todos los derechos reservados