A 05-01 vas aprendre a triar amb precisió els fitxers sobre els quals actua el toolkit. Però un script d'operacions no només llegeix i escriu: llança processos. informe-diari.sh invoca grep, bc, tar i consulta la veloz-api; recorre quatre ciutats una darrere l'altra podent fer-ho alhora; i no té cap defensa si l'informe de les 6:00 encara corre quan cron arrenca el de les 6:05 —dos processos escrivint el mateix fitxer, comptadors barrejats i un informe sense sentit—. A 02-06 vas veure el mínim per no perdre el control del terminal: Ctrl-C, Ctrl-Z, jobs, fg, bg, &. Aquí completem aquest quadre i el portem al terreny de l'scripting: veure, buscar, llançar en paral·lel, esperar, senyalitzar, limitar i bloquejar.

Contingut

  1. El model de processos d'Unix
  2. Veure què corre: ps, pstree, top
  3. Buscar processos: pgrep
  4. Primer i segon pla
  5. Sobreviure al tancament de sessió: disown, nohup, setsid
  6. $! i wait: paral·lelisme controlat
  7. Senyals
  8. kill, pkill, killall i timeout
  9. Prioritats amb nice i renice
  10. Una sola instància alhora: flock
  11. Zombis i orfes

  1. El model de processos d'Unix

Un procés és un programa en execució amb la seva pròpia memòria, el seu entorn (les variables exportades de 01-02) i un número que l'identifica: el PID. Tot procés té a més un PPID, el PID de qui l'ha creat. Això converteix el sistema en un arbre l'arrel del qual és init/systemd (PID 1).

Quan a 01-04 vas veure que una ordre externa s'executa en un procés fill, això és exactament el que passa: Bash es duplica (fork) i el duplicat es transforma en l'ordre (exec). D'aquí en surten dues conseqüències que ja coneixes i que ara encaixen: el fill hereta l'entorn del pare, i res del que el fill canviï no afecta el pare —el motiu pel qual cd és un builtin i pel qual el bucle d'una canonada no veu les seves variables (Mòdul 4)—.

echo $$        # 4821  ← PID del teu shell actual
echo $PPID     # 4790  ← qui l'ha llançat (l'emulador de terminal)
bash -c 'echo $$; echo $PPID'   # 5102 / 4821: el fill coneix el seu pare

Els estats en què pot estar un procés apareixen a la columna STAT de ps: R executant-se o llest, S adormit esperant alguna cosa (el normal), D en espera ininterrompible de disc, T aturat, Z zombi.

  1. Veure què corre: ps, pstree, top

ps té dues sintaxis històriques que conviuen; totes dues són correctes i les veuràs totes dues:

ps aux           # estil BSD: tots els processos, amb propietari, sense controlar terminal
ps -ef           # estil System V: tots, format complet
ps -ef --forest  # amb la jerarquia dibuixada

Les columnes de ps aux que de debò es llegeixen:

Columna Què significa
USER Propietari del procés
PID Identificador, el que passaràs a kill
%CPU / %MEM Percentatge d'ús des que va arrencar (no instantani)
VSZ / RSS Memòria virtual / memòria física real en KiB. RSS és la que importa
STAT Estat (R, S, T, Z) més sufixos: s líder de sessió, + en primer pla
START / TIME Quan va arrencar / CPU consumida acumulada
COMMAND La línia d'ordres sencera

ps -ef mostra PPID en comptes de %CPU, cosa que la fa millor per investigar qui ha llançat què. I -o selecciona només els camps que t'interessen, que és el que es fa servir en scripts:

ps -eo pid,ppid,user,rss,etime,comm --sort=-rss | head -5
#   PID  PPID USER       RSS ELAPSED COMMAND
#  2210     1 veloz   184320 3-04:12 veloz-api

--sort=-rss ordena per memòria descendent; etime és el temps transcorregut des de l'arrencada, molt més útil que TIME per saber si alguna cosa fa des d'ahir que està penjada.

pstree -p 2210 dibuixa l'arbre d'un procés i els seus fills, ideal per entendre què ha llançat un script. top (i la seva versió còmoda htop) són interactius: refresquen en viu i serveixen per mirar, no per programar. En un script mai no es fa servir top; es fa servir ps amb -o.

  1. Buscar processos: pgrep

El reflex après és ps aux | grep veloz-api, i té un defecte que causa errors reals:

ps aux | grep veloz-api
# veloz  2210 ... /usr/local/bin/veloz-api
# joan   7788 ... grep --color=auto veloz-api    ← el mateix grep!

El grep apareix en la seva pròpia cerca, perquè ps el llista mentre corre. Un if ps aux | grep -q veloz-api dona sempre cert, encara que l'API estigui caiguda. La solució tradicional és el truc de la classe [v]eloz-api, però l'eina correcta existeix:

pgrep veloz-api            # només PIDs, un per línia
pgrep -a veloz-api         # PID i línia d'ordres
pgrep -c veloz-api         # quants n'hi ha
pgrep -u veloz veloz-api   # d'aquest usuari
pgrep -f 'informe-diari'   # busca a la línia SENCERA, no només al nom
pgrep -x bash              # nom exacte, sense coincidències parcials

-f és imprescindible per a scripts: sense ell, pgrep informe-diari.sh no troba res, perquè el nom del procés és bash, i l'script només és un argument. I pgrep retorna codi 0 si ha trobat alguna cosa i 1 si no, així que encaixa directament en una guarda de les de 03-04:

pgrep -x veloz-api > /dev/null \
    || log_error "veloz-api caiguda: l'informe només farà servir el CSV"

  1. Primer i segon pla

El shell agrupa els processos que llança en treballs (jobs), i els numera. Recapitulant i ampliant 02-06:

tar -czf copia.tar.gz /srv/veloz/dades &   # llançar en segon pla → [1] 8123
jobs -l          # llistar amb PID     # fg %1: portar al primer pla
bg %1            # reprendre en segon pla després d'un Ctrl-Z (que el deixa en estat T)
kill %1          # als treballs se'ls parla amb %

Les referències %1, %+ (el més recent), %- (l'anterior) i %?tar (per text de l'ordre) funcionen amb fg, bg, kill i wait. Un detall que importa: els treballs són un concepte del shell interactiu; dins d'un script no hi ha control de treballs, però &, $! i wait sí que funcionen, i són els que es fan servir.

  1. Sobreviure al tancament de sessió: disown, nohup, setsid

En tancar el terminal, el shell envia SIGHUP (hang up, penjar, herència dels mòdems) als seus treballs, i aquests moren. Tres maneres d'evitar-ho —nohup cmd > sortida 2>&1 &, cmd & disown %1 i setsid cmd > /dev/null 2>&1 &—:

Eina Quan es decideix Què fa
nohup Abans de llançar Ignora SIGHUP i redirigeix la sortida a nohup.out si és un terminal
disown Després de llançar Treu el treball de la llista del shell, que ja no li enviarà SIGHUP
setsid Abans de llançar Crea una sessió nova: el procés deixa de tenir terminal de control

disown és el rescat quan ja has llançat una cosa llarga i t'adones que necessites tancar la sessió. Per a tasques de debò desateses, systemd (07-05) és la resposta correcta; nohup és l'apedaçament ràpid.

  1. $! i wait: paral·lelisme controlat

$! conté el PID de l'últim procés llançat en segon pla, i wait espera que acabi. Amb aquests dos, informe-diari.sh pot calcular les quatre ciutats alhora:

declare -a PIDS=(); fallades=0
for ciutat in Valencia Sevilla Bilbao Madrid; do
    resum_ciutat "$ciutat" > "$TMPDIR/$ciutat.txt" &
    PIDS+=( "$!" )                       # desem el PID de cada fill
done
for pid in "${PIDS[@]}"; do
    wait "$pid" || (( ++fallades ))      # wait retorna el codi de sortida del fill
done
cat "$TMPDIR"/*.txt

Tres coses que fan que aquest patró funcioni i no sigui un simple «llançar coses»:

  • Cada fill escriu en el seu propi fitxer. Si tots escrivissin al mateix, les seves sortides s'entrellaçarien a mitges línies. Un directori de mktemp -d (05-01) és el lloc natural.
  • wait PID retorna el codi de sortida d'aquell fill, així que el paral·lelisme no renuncia al control d'errors.
  • wait sense arguments espera tots els fills, però llavors perds els codis individuals. wait -n retorna així que acaba qualsevol d'ells, que és la base d'una cua amb un nombre màxim de tasques simultànies.

Recorda que cada fill és un procés separat: les variables que modifiqui no tornen al pare. Per això els resultats viatgen per fitxers, exactament igual que en el subshell de la canonada del Mòdul 4.

  1. Senyals

Un senyal és una notificació asíncrona que el nucli lliura a un procés. El procés la pot gestionar, ignorar o deixar que actuï el comportament per defecte —tret de dos que ningú no pot interceptar—.

Senyal Núm. Què significa Interceptable?
SIGHUP 1 Terminal tancat; per conveni, «recarrega la teva configuració»
SIGINT 2 Ctrl-C: interrupció des del teclat
SIGTERM 15 «Acaba ordenadament». El que envia kill per defecte
SIGKILL 9 Mort immediata decidida pel nucli No
SIGSTOP 19 Congelar el procés (Ctrl-Z envia SIGTSTP, el seu cosí interceptable) No
SIGCONT 18 Reprendre un procés aturat
SIGUSR1/SIGUSR2 10/12 Lliures: els defineix la teva aplicació
stateDiagram-v2
    [*] --> Executant: fork + exec
    Executant --> Aturat: SIGSTOP / SIGTSTP (Ctrl-Z)
    Aturat --> Executant: SIGCONT (fg / bg)
    Executant --> Netejant: SIGTERM / SIGINT (interceptable)
    Netejant --> [*]: tanca fitxers i surt
    Executant --> [*]: SIGKILL (sense neteja)
    Aturat --> [*]: SIGKILL

El diagrama conté la lliçó sencera: la ruta per Netejant és la que permet a un procés tancar fitxers, esborrar temporals i deixar el sistema consistent. SIGKILL talla aquesta ruta. Capturar senyals en els teus propis scripts es fa amb trap, i és el tema central de 05-03; aquí ens ocupem d'enviar-los.

  1. kill, pkill, killall i timeout

kill 8123              # envia SIGTERM: "acaba quan puguis"
kill -TERM 8123        # idèntic, explícit
kill -HUP 2210         # recarregar configuració de veloz-api
kill -9 8123           # SIGKILL: últim recurs
kill -l                # llista de senyals disponibles
pkill -f informe-diari      # per patró, com pgrep
pkill -u veloz -TERM veloz-api
killall veloz-api      # per nom exacte d'executable

Per què kill -9 és l'últim recurs: SIGKILL no arriba al procés, l'executa el nucli. El programa no tanca els seus fitxers (queda un CSV a mig escriure), no esborra els seus temporals, no allibera el seu bloqueig, i els seus fills queden orfes. La seqüència correcta és SIGTERM, esperar uns segons comprovant amb kill -0 "$pid" —que no envia cap senyal, només pregunta «existeix i el puc senyalitzar?»— i només si segueix viu, SIGKILL. El muntaràs així a l'exercici 3.

timeout resol l'altra cara del problema: que una cosa no acabi mai.

timeout 30 curl -s http://localhost:8080/salud   # mata als 30 s
timeout -k 5 30 informe-diari.sh detall          # TERM als 30 s, KILL 5 s després

Si expira, timeout retorna 124. Aquest codi és la manera de distingir «ha fallat» de «ha trigat massa», i és imprescindible en qualsevol crida de xarxa dins d'una tasca programada.

  1. Prioritats amb nice i renice

nice -n 10 tar -czf historic.tar.gz /srv/veloz/dades/historic   # baixa prioritat
renice -n 5 -p 8123                                            # canviar en marxa
ionice -c 3 tar -czf ...                                       # prioritat de disc

El valor va de -20 (màxima prioritat) a 19 (mínima). Només root pot baixar el número; qualsevol el pot apujar, és a dir, ser considerat. Per a l'arxivat nocturn de Veloz Envíos, nice -n 10 juntament amb ionice -c 3 evita que la compressió competeixi amb la veloz-api per CPU i disc.

  1. Una sola instància alhora: flock

El problema que obria la lliçó: dos informe-diari.sh simultanis. L'apedaçament casolà és un fitxer PID:

[[ -f /tmp/informe.pid ]] && exit 1      # FRÀGIL
echo $$ > /tmp/informe.pid

És fràgil per dos motius. Entre la comprovació i l'escriptura hi ha una finestra en què un altre procés fa el mateix (condició de cursa, la mateixa família que la del /tmp/fitxer.$$ de 05-01). I si l'script mor de cop, el fitxer es queda allà i bloqueja totes les execucions futures fins que algú l'esborri a mà.

flock fa servir un bloqueig del nucli associat a un descriptor obert, que el sistema allibera sol quan el procés mor, passi el que passi:

readonly LOCK=~/veloz-ops/logs/.informe.lock
exec 9> "$LOCK"          # descriptor 9 associat al fitxer de bloqueig
flock -n 9 || { printf 'Ja hi ha un informe en curs\n' >&2; exit 1; }
main "$@"                # a partir d'aquí som l'única instància

exec 9> obre el fitxer al descriptor 9 (els descriptors són el tema de 05-05; de moment, llegeix-ho com «reserva el canal 9 per a aquest fitxer»). flock -n 9 intenta bloquejar-lo sense esperar. Si prefereixes encuar en comptes d'avortar, flock -w 60 9 espera fins a 60 segons. El bloqueig desapareix en acabar l'script, encara que sigui per SIGKILL. Quan portis això a cron a 07-02 veuràs la variant d'una sola línia, flock -n /ruta/lock ordre, pensada just per al crontab.

  1. Zombis i orfes

Quan un procés acaba, la seva entrada roman a la taula del sistema fins que el seu pare recull el codi de sortida amb wait. En aquest interval és un zombi (STAT = Z): ja no consumeix CPU ni memòria, només una entrada. Són normals i efímers; només són un símptoma si se n'acumulen centenars, cosa que indica un pare que llança fills i mai no fa wait —exactament l'error que evita el patró de la secció 6—.

Un orfe és el contrari: el seu pare ha mort abans que ell. El sistema l'adopta assignant-li init/systemd com a pare, així que segueix funcionant amb normalitat i algú en recollirà el codi en acabar. Un procés llançat amb nohup ... & acaba sent un orfe adoptat, i per això sobreviu.

Errors Habituals i Consells

  • ps aux | grep proces en una condició. Es troba a si mateix i sempre dona cert. Fes servir pgrep.
  • pgrep informe-diari.sh sense -f. El procés es diu bash; sense -f no trobes el teu script.
  • Començar per kill -9. Deixa fitxers a mitges, temporals sense esborrar i bloquejos orfes. SIGTERM primer.
  • Llançar N tasques amb & i no fer wait. L'script acaba abans que els seus fills i els resultats no hi són.
  • Esperar que un fill modifiqui variables del pare. És un altre procés: comunica per fitxers o per stdout.
  • Fitxers PID casolans com a bloqueig. Cursa en crear-los i bloqueig permanent si l'script mor. Fes servir flock.
  • Consell: tota crida a un servei extern dins d'una tasca desatesa va embolcallada en timeout. Un curl sense límit pot deixar penjada la tasca de les 6:00 fins a la de l'endemà.

Exercicis

Exercici 1. Escriu api_viva(), que retorni 0 si el procés veloz-api està corrent i 1 si no, sense caure en el problema del grep que es troba a si mateix, i que a més informi per stderr del PID i dels minuts que fa que està en marxa.

Exercici 2. Modifica el bucle de ciutats d'informe-diari.sh perquè les quatre es calculin en paral·lel, cadascuna al seu fitxer, esperant-les totes i comptant quantes n'han fallat.

Exercici 3. Escriu una funció parar_servei() que enviï SIGTERM a tots els processos veloz-api, esperi fins a 15 segons comprovant cada segon i només llavors recorri a SIGKILL, informant de quin camí ha pres.

Solucions

Solució 1.

api_viva() {
    local pid
    pid=$(pgrep -x -u veloz veloz-api | head -1) || return 1
    printf 'veloz-api viva (PID %s, %s en marxa)\n' \
        "$pid" "$(ps -o etime= -p "$pid" | tr -d ' ')" >&2
    return 0
}

pgrep -x exigeix nom exacte i ja retorna 1 si no hi ha coincidències, així que el || return 1 n'hi ha prou com a guarda (03-04). ps -o etime= amb l'= final imprimeix el valor sense capçalera, i tr -d ' ' treu el farciment d'alineació.

Solució 2.

calcular_ciutats() {
    local tmpdir ciutat pid fallades=0
    tmpdir=$(mktemp -d) || return 1
    local -a pids=()
    for ciutat in "${CIUTATS[@]}"; do
        resum_ciutat "$ciutat" > "$tmpdir/$ciutat.txt" 2>"$tmpdir/$ciutat.err" &
        pids+=( "$!" )
    done
    for pid in "${pids[@]}"; do wait "$pid" || (( ++fallades )); done
    cat "$tmpdir"/*.txt
    (( fallades > 0 )) && log_error "$fallades de ${#pids[@]} ciutats han fallat"
    rm -rf "$tmpdir"
    return 0
}

Els errors de cada fill van al seu propi .err perquè no s'entrellacin. Amb quatre ciutats l'estalvi és de tres quartes parts del temps si el coll d'ampolla és la lectura del CSV. Fixa't que rm -rf "$tmpdir" només s'executa si hi arribem vius; a 05-03 el convertiràs en un trap.

Solució 3.

parar_servei() {
    local i pids
    pids=$(pgrep -x veloz-api) || { printf 'No estava en marxa\n'; return 0; }
    printf 'Enviant SIGTERM a: %s\n' "$pids"
    kill -TERM $pids 2>/dev/null
    for (( i = 1; i <= 15; i++ )); do
        pgrep -x veloz-api > /dev/null || { printf 'Aturat netament en %ds\n' "$i"; return 0; }
        sleep 1
    done
    printf 'No respon després de 15s: SIGKILL\n' >&2
    pkill -x -KILL veloz-api
    return 1
}

$pids va sense cometes expressament —l'únic cas de tot el curs en què això és correcte—: volem que la llista de PIDs es parteixi en diversos arguments per a kill. I el return 1 final permet a qui crida la funció saber que s'ha hagut de forçar la mort, cosa que probablement mereixi una entrada al registre.

Conclusió

Cada ordre externa és un procés amb el seu PID, el seu PPID i el seu lloc en un arbre que neix a systemd. ps aux i ps -ef ho ensenyen tot —amb RSS i etime com a columnes més útils—, ps -eo selecciona camps per a scripts, pstree dibuixa la jerarquia i top/htop serveixen per mirar en viu, mai per programar. pgrep, amb -f i -x, substitueix el ps aux | grep que es troba a si mateix. &, jobs, fg, bg i %1 governen els treballs del shell; nohup, disown i setsid els treuen de l'abast de SIGHUP. La parella $! + wait converteix quatre càlculs seqüencials en quatre de paral·lels sense renunciar als seus codis de sortida. Els senyals són el llenguatge entre processos: SIGTERM demana acabar i permet netejar, SIGKILL no es pot interceptar i per això és l'últim recurs, timeout posa un límit i retorna 124. I flock sobre un descriptor dona l'exclusió mútua que un fitxer PID casolà no donarà mai.

informe-diari.sh ja sap comprovar si l'API és viva, repartir la feina entre quatre processos i negar-se a executar-se dues vegades alhora. Però encara té un forat: si alguna cosa falla a dins —una variable sense definir, un grep sense resultats, un disc ple a mitja escriptura—, l'script continua com si res i produeix un informe silenciosament fals, deixant a més el temporal de mktemp sense esborrar. A 05-03 tanquem aquest forat: set -euo pipefail amb la seva crítica honesta, PIPESTATUS, trap amb EXIT i ERR per a la neteja garantida que fa dues lliçons que prometem, una funció morir() que informa de la línia exacta de la fallada, un conveni de codis de sortida i les eines de depuració bash -x, PS4 i BASH_XTRACEFD.

Curs de Programació en Bash

Mòdul 1: Introducció a Bash

Mòdul 2: Ordres Bàsiques de Bash

Mòdul 3: Fonaments de Scripting

Mòdul 4: Scripting Intermedi

Mòdul 5: Tècniques Avançades de Scripting

Mòdul 6: Treballar amb Eines Externes

Mòdul 7: Automatització i Programació

Mòdul 8: Bones Pràctiques i Optimització

Mòdul 9: Projectes del Món Real

© Copyright 2026. Tots els drets reservats