Al final de la lliçó anterior va quedar plantejat un problema concret. ingestor, agregador i meteo-api són processos separats, i aquella decisió d'arquitectura tenia bones raons: aïllament davant de fallades, separació de privilegis sobre /var/lib/meteora/lectures/, i la possibilitat de moure una peça a una altra màquina. Però té una conseqüència immediata: els seus espais d'adreces són independents. La MMU que vas estudiar al mòdul 2 garanteix que un punter de l'ingestor no significa res a l'agregador. No es poden passar un array de Lectura compartint un punter, com feien els quatre fils.

Necessiten un mecanisme explícit, proporcionat pel nucli, per parlar entre ells. Aquest conjunt de mecanismes és la comunicació entre processos o IPC (Inter-Process Communication).

Aquesta lliçó recorre els sis mecanismes que realment s'utilitzen a Linux —canonades, FIFO, cues de missatges, memòria compartida, sockets i senyals—, amb exemples que funcionen i amb els detalls que la documentació superficial omet: què passa quan una memòria intermèdia s'omple, què passa si el lector tanca, per què només unes poques funcions són segures dins d'un gestor de senyal. I acaba amb una guia de decisió, perquè triar malament el mecanisme d'IPC condiciona el rendiment i la fiabilitat de tot un sistema. Un avís des del principi, perquè és la font número u d'errors: la memòria compartida transporta dades però no coordina ningú; sincronitzar-la és el tema de la lliçó següent, i aquí ho assenyalarem explícitament allà on toqui.

Contingut

  1. Els dos models fonamentals
  2. Canonades anònimes: què són per dins
  3. Quan la memòria intermèdia s'omple i quan el lector tanca
  4. Canonades amb nom: FIFO
  5. Cues de missatges POSIX
  6. Memòria compartida POSIX i /dev/shm/meteora-cache
  7. Sockets: de domini UNIX i de xarxa
  8. Senyals com a mecanisme de notificació
  9. Comunicació bloquejant i no bloquejant
  10. Comparativa i guia de decisió

Els dos models fonamentals

Per sota de la varietat de mecanismes només hi ha dos models conceptuals, i tota l'enginyeria d'IPC és una conseqüència de triar entre ells.

Pas de missatges. Els processos intercanvien unitats de dades discretes a través del nucli. L'emissor crida una funció d'enviament, el nucli copia les dades a una memòria intermèdia seva, i el receptor crida una funció de recepció que les copia al seu espai. Els processos no comparteixen mai memòria.

Memòria compartida. El nucli fa que una regió de memòria física aparegui mapejada a l'espai d'adreces de diversos processos. A partir d'aquell moment, escriure en aquella regió és escriure amb una instrucció mov normal, i l'altre procés ho veu immediatament. El nucli intervé una vegada, en establir el mapatge, i després desapareix.

graph LR
    A1[Procés A] -->|"1. send: còpia<br/>usuari→nucli"| K1[Memòria intermèdia del nucli]
    K1 -->|"2. recv: còpia<br/>nucli→usuari"| B1[Procés B]
    A2[Procés A] -->|"mov directe"| P[Pàgina física compartida]
    B2[Procés B] -->|"mov directe"| P

A dalt, pas de missatges: dues còpies i el nucli enmig. A baix, memòria compartida: els dos processos escriuen a la mateixa pàgina física. Les diferències són sistemàtiques:

Pas de missatges Memòria compartida
Còpies per transferència 2 (usuari→nucli→usuari) 0
Latència típica (4 KB) ~5-15 µs ~0,1 µs
Cost segons la mida Creix amb els bytes copiats Constant
Sincronització Implícita: el mecanisme la dona Cap: és cosa teva
Funciona entre màquines Sí (sockets de xarxa) No
Complexitat d'ús Baixa Alta
Risc de corrupció Baix Alt: un punter mal posat trenca l'altre
Delimitació de missatges La dona el mecanisme Te l'has d'inventar

Els dos primers números expliquen per què existeix la memòria compartida: per moure 17 MB de l'ingestor a l'agregador, el pas de missatges copiaria 34 MB (17 de pujada i 17 de baixada) i trigaria uns 180 ms; la memòria compartida costa un mmap i zero còpies. I la fila de sincronització explica per què no s'utilitza sempre: amb pas de missatges, si el receptor llegeix, o hi ha un missatge complet o no n'hi ha cap, perquè el nucli garanteix l'atomicitat del lliurament; amb memòria compartida no hi ha garantia de res, i l'agregador pot llegir una struct Lectura mentre l'ingestor l'escriu i obtenir 8 bytes nous i 16 de vells. La memòria compartida és el mecanisme més ràpid i l'únic que no resol cap problema de coordinació per si sol.

Canonades anònimes: què són per dins

Una canonada (pipe) és un canal unidireccional de bytes entre dos processos emparentats. És el mecanisme d'IPC més antic d'UNIX i continua sent el més utilitzat, encara que gairebé ningú no l'anomeni pel seu nom: cada vegada que escrius cat fitxer | grep patró a l'intèrpret d'ordres, n'estàs creant una.

Per dins, una canonada és exactament això: una memòria intermèdia circular a la memòria del nucli, amb dos descriptors de fitxer apuntant-hi, un per llegir i un altre per escriure. A Linux aquesta memòria intermèdia és de 65.536 bytes (16 pàgines) per defecte, ajustable amb fcntl(fd, F_SETPIPE_SZ, mida) fins al límit de /proc/sys/fs/pipe-max-size.

/* canonada.c — l'ingestor envia un lot de lectures a un fill agregador */
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>

struct Lectura { unsigned int estacio_id; unsigned long timestamp;
                 float temperatura, humitat, pressio; };

int main(void) {
    int fd[2];                      /* fd[0] = lectura, fd[1] = escriptura */
    if (pipe(fd) == -1) { perror("pipe"); exit(1); }
    pid_t pid = fork();             /* el fill HERETA els dos descriptors */

    if (pid == 0) {                 /* ---- FILL: agregador, només llegeix ---- */
        close(fd[1]);               /* CRÍTIC: tancar l'extrem que no fa servir */
        struct Lectura l; double suma = 0; int n = 0;
        while (read(fd[0], &l, sizeof l) == sizeof l) { suma += l.temperatura; n++; }
        /* read() ha retornat 0: el pare ha tancat el seu extrem d'escriptura (EOF) */
        printf("[agregador] %d lectures, mitjana %.2f C\n", n, suma / n);
        close(fd[0]); _exit(0);
    }

    close(fd[0]);                   /* ---- PARE: ingestor, només escriu ---- */
    for (int i = 0; i < 1000; i++) {
        struct Lectura l = { .estacio_id = 41, .timestamp = 1756684800 + i,
                             .temperatura = 21.0f + (i % 50) * 0.1f,
                             .humitat = 62.0f, .pressio = 1013.2f };
        if (write(fd[1], &l, sizeof l) != sizeof l) { perror("write"); break; }
    }
    close(fd[1]);                   /* en tancar, el fill rep EOF */
    wait(NULL);
    return 0;
}

En executar-lo: [agregador] 1000 lectures, mitjana 23.45 C. Quatre detalls que cal entendre, perquè cadascun és un error clàssic si s'oblida:

L'ordre és pipe() i després fork(), mai a l'inrevés. La canonada només es comparteix perquè el fill hereta la taula de descriptors del pare en el fork() (mòdul 2). Si fas fork() primer, cada procés crearà la seva pròpia canonada sense relació amb la de l'altre. D'aquí que les canonades anònimes només serveixin entre processos emparentats: pare-fill, o germans que van heretar del mateix pare.

Tancar l'extrem que no s'utilitza és obligatori, no una neteja. Si el fill no tanca fd[1], la canonada continua tenint un escriptor obert: quan el pare tanqui el seu, el fill no rebrà EOF i el seu read() es quedarà bloquejat per sempre. És una de les causes més freqüents de penjades amb canonades, amb un símptoma enganyós: el programa funciona però no acaba.

El read() retorna 0 quan es tanca l'últim escriptor. Aquest zero és l'EOF. Mentre hi hagi almenys un descriptor d'escriptura obert en qualsevol procés, un read() sobre una canonada buida es bloqueja en lloc de retornar 0.

La canonada és un flux de bytes, no de missatges. El nucli no conserva les fronteres dels teus write(): un de 24 bytes es pot llegir com dos read() de 12, i tres de 24 es poden llegir en un read() de 72. A l'exemple funciona perquè struct Lectura fa 24 bytes i la canonada garanteix atomicitat fins a PIPE_BUF (4.096 bytes a Linux), però amb lectures parcials cal insistir en un bucle, i si els teus missatges són de mida variable t'has d'inventar un delimitador o una capçalera de longitud. És el preu de treballar amb un flux.

L'equivalent a l'intèrpret d'ordres és exactament el mateix mecanisme. Quan escrius cat /var/lib/meteora/lectures/2026-08-31.dat | ./agregador --mitjana-horaria, l'intèrpret crida pipe(), fa dos fork(), i a cada fill fa servir dup2(fd[1], STDOUT_FILENO) i dup2(fd[0], STDIN_FILENO) respectivament abans de l'execve(). Per això cat i agregador no necessiten saber res de canonades: escriuen i llegeixen dels seus descriptors estàndard. Aquesta indirecció és una de les idees més elegants d'UNIX.

Quan la memòria intermèdia s'omple i quan el lector tanca

Les dues situacions límit d'una canonada són les que separen qui l'ha fet servir de qui l'ha entesa.

La memòria intermèdia s'omple. Si l'ingestor escriu més ràpid del que l'agregador llegeix, els 65.536 bytes s'esgoten. Aleshores write() es bloqueja fins que hi hagi lloc. El procés passa a estat S (o D per a algunes variants) i el planificador el treu de la cua de llestos.

Això no és una fallada, és una característica extraordinàriament útil: s'anomena contrapressió (backpressure). El productor es frena automàticament al ritme del consumidor, sense que ningú no ho programi. És el que fa que cat fitxer_de_50GB | grep patró no consumeixi 50 GB de RAM: cat es bloqueja tan bon punt grep es queda enrere. S'observa en directe amb ps -o pid,stat,wchan -C productor, que mostra l'estat S i WCHAN: pipe_write — la funció exacta del nucli on dorm, esperant espai. Mirar WCHAN per saber en què està bloquejat un procés val per a qualsevol bloqueig i el faràs servir molt a Monitoratge i Diagnòstic de Rendiment.

El lector tanca el seu extrem. Aquí el comportament és més agressiu. Si l'agregador mor o tanca fd[0] i l'ingestor intenta escriure, el nucli envia SIGPIPE a l'escriptor; l'acció per defecte d'aquesta senyal és acabar el procés sense missatge d'error; i només si el procés la ignora o la captura, el write() retorna -1 amb errno == EPIPE.

Que l'acció per defecte sigui matar el procés sembla brutal, però té sentit a l'intèrpret d'ordres: a cat fitxer_enorme | head -3, quan head ha imprès tres línies i acaba, no té sentit que cat continuï llegint gigabytes que ningú no llegirà. En un servei de llarga vida, en canvi, aquesta mort silenciosa és inacceptable, i el patró professional és sempre aquest:

signal(SIGPIPE, SIG_IGN);            /* en arrencar l'ingestor */

/* i a partir d'aquí, comprovar EPIPE a cada escriptura */
if (write(fd, &lectura, sizeof lectura) == -1) {
    if (errno == EPIPE) { fprintf(stderr, "agregador tancat; reconnectant\n"); reconnectar(); }
    else perror("write");
}

Ignorar la senyal converteix una mort sobtada en un codi d'error que pots tractar. Aquest mateix patró s'aplica als sockets, on escriure en una connexió que l'altre extrem ha tancat també produeix SIGPIPE. Tot servidor de xarxa seriós fa signal(SIGPIPE, SIG_IGN) a les primeres línies de main(); oblidar-ho significa que el teu servei mor en silenci la primera vegada que un client penja en mal moment.

Canonades amb nom: FIFO

La limitació de les canonades anònimes —només entre processos emparentats— es resol donant-los un nom al sistema de fitxers. Això és una FIFO (First In, First Out) o canonada amb nom.

Una FIFO és una entrada al sistema de fitxers amb tipus p, que qualsevol procés amb permisos pot obrir. No emmagatzema dades al disc: el fitxer és només un punt de trobada; la memòria intermèdia continua sent a la memòria del nucli, igual que en una canonada anònima.

Muntarem el canal entre l'ingestor i l'agregador de Meteora:

$ sudo mkfifo -m 0660 /run/meteora/lectures.fifo
$ sudo chown meteora:meteora /run/meteora/lectures.fifo
$ ls -l /run/meteora/lectures.fifo
prw-rw---- 1 meteora meteora 0 sep  1 09:14 /run/meteora/lectures.fifo
# ↑ la 'p' del principi indica "pipe"; la mida és 0 i sempre ho serà

# Terminal A — l'agregador es posa a l'escolta
$ ./agregador --entrada /run/meteora/lectures.fifo   # es bloqueja a open()

# Terminal B — l'ingestor bolca el seu lot
$ ./ingestor --bolcar-lot /run/meteora/lectures.fifo

El codi del costat del lector és una canonada normal, amb la diferència de com s'obté el descriptor:

/* agregador_fifo.c (fragment) */
/* open() es BLOQUEJA fins que un altre procés obri per escriure.
   És la sincronització de trobada (rendezvous) de les FIFO. */
int fd = open("/run/meteora/lectures.fifo", O_RDONLY);
if (fd == -1) { perror("open"); return 1; }

struct Lectura l; ssize_t n; double suma = 0; long compte = 0;
while ((n = read(fd, &l, sizeof l)) > 0) {
    if (n != sizeof l) { fprintf(stderr, "lectura parcial\n"); continue; }
    suma += l.temperatura; compte++;
}
printf("[agregador] %ld lectures, mitjana %.2f C\n", compte, suma / compte);
close(fd);

Quatre peculiaritats de les FIFO que cal conèixer:

L'open() bloqueja fins que aparegui l'altre extrem. Obrir en només lectura es bloqueja fins que algú obri per escriptura, i a l'inrevés: és un mecanisme de trobada incorporat, molt còmode. Si no el vols, O_NONBLOCK; compte amb l'asimetria, obrir en escriptura no bloquejant sense lector falla amb ENXIO, mentre que obrir en lectura no bloquejant sense escriptor té èxit.

Diversos escriptors són possibles i les seves escriptures s'intercalen de manera segura, sempre que cada write() sigui com a molt de PIPE_BUF (4.096 bytes): per sota d'aquesta mida el nucli garanteix que l'escriptura és atòmica i no es barreja amb la d'un altre escriptor; per sobre es pot fragmentar i rebràs brossa. Com que una struct Lectura fa 24 bytes, les 800 estacions podrien escriure a la mateixa FIFO sense corrompre's. És una garantia molt valuosa i poc coneguda.

No hi ha diversos lectors útils. Si dos processos llegeixen de la mateixa FIFO, cada byte va a un dels dos, arbitràriament. No és difusió: és repartiment.

La FIFO és persistent però les dades no. El fitxer sobreviu als processos; el contingut de la memòria intermèdia es perd tan bon punt es tanquen tots els extrems. I si ningú no llegeix, l'escriptor es bloqueja; si ningú no escriu, el lector es bloqueja. Una FIFO no guarda res quan no hi ha ningú a l'altre costat.

Aquest últim punt és exactament la limitació que motiva el mecanisme següent.

Cues de missatges POSIX

Una cua de missatges és una bústia gestionada pel nucli on els processos dipositen i recullen missatges discrets i amb prioritat. Resol tres limitacions de les canonades: conserva les fronteres entre missatges, permet prioritats, i persisteix encara que no hi hagi ningú connectat.

Linux ofereix dues API: la de System V (msgget/msgsnd, antiga, amb identificadors numèrics incòmodes) i la POSIX (mq_open/mq_send, amb noms tipus ruta i descriptors que funcionen amb select/poll). Fes servir sempre la POSIX.

/* cua_alertes.c — l'ingestor envia alertes a l'agregador amb prioritat */
#include <stdio.h>
#include <string.h>
#include <fcntl.h>
#include <mqueue.h>

#define CUA "/meteora-alertes"       /* el nom HA de començar per '/' */

int main(int argc, char **argv) {
    struct mq_attr attr = { .mq_flags = 0,
                            .mq_maxmsg  = 10,    /* 10 missatges a la cua */
                            .mq_msgsize = 256,   /* 256 bytes per missatge */
                            .mq_curmsgs = 0 };

    if (argc > 1 && strcmp(argv[1], "enviar") == 0) {
        mqd_t mq = mq_open(CUA, O_WRONLY | O_CREAT, 0660, &attr);
        if (mq == (mqd_t)-1) { perror("mq_open"); return 1; }
        /* prioritat 9 = urgent; 0 = rutina. Com més gran el número, abans es lliura. */
        mq_send(mq, "ESTACIO 41 SENSE DADES 15 MIN", 29, 9);
        mq_send(mq, "estacio 12 calibracio ok",      24, 0);
        mq_send(mq, "ESTACIO 07 TEMP FORA DE RANG",  28, 9);
        mq_close(mq);
    } else {
        mqd_t mq = mq_open(CUA, O_RDONLY | O_CREAT, 0660, &attr);
        char buf[256]; unsigned int prio;
        for (int i = 0; i < 3; i++) {
            ssize_t n = mq_receive(mq, buf, sizeof buf, &prio);
            printf("[prio %u] %.*s\n", prio, (int)n, buf);
        }
        mq_close(mq); mq_unlink(CUA);   /* unlink esborra la cua del sistema */
    }
    return 0;
}
$ gcc cua_alertes.c -o cua -lrt          # compte: cal enllaçar amb -lrt
$ ./cua enviar
$ ./cua
[prio 9] ESTACIO 41 SENSE DADES 15 MIN
[prio 9] ESTACIO 07 TEMP FORA DE RANG
[prio 0] estacio 12 calibracio ok

Fixa't en el resultat: encara que el missatge de calibració es va enviar segon, es rep l'últim. mq_receive() sempre retorna el missatge de més prioritat, i entre els de la mateixa prioritat respecta l'ordre d'arribada. Aquest és l'avantatge funcional que cap canonada no dona: si l'agregador va saturat, les alertes crítiques s'atenen abans que el soroll de fons.

Les cues POSIX viuen en un sistema de fitxers virtual que pots muntar i examinar, i els seus límits vénen del nucli:

$ sudo mount -t mqueue none /dev/mqueue
$ cat /dev/mqueue/meteora-alertes
QSIZE:81         NOTIFY:0     SIGNO:0     NOTIFY_PID:0

$ cat /proc/sys/fs/mqueue/msg_max        # 10  → missatges màxims per cua
$ cat /proc/sys/fs/mqueue/msgsize_max    # 8192 → bytes màxims per missatge
$ cat /proc/sys/fs/mqueue/queues_max     # 256  → cues màximes al sistema

QSIZE són els bytes pendents ara mateix: una finestra de diagnòstic excel·lent, perquè si creix sense parar és que el consumidor no dona l'abast. I deu missatges per cua és molt poc: superar-ho fa que mq_send() es bloquegi (o falli amb EAGAIN en mode no bloquejant). Es pot pujar amb sysctl fs.mqueue.msg_max=100, però la conclusió pràctica és una altra: les cues POSIX són per a notificacions i ordres de control, no per a cabal de dades. Per moure 17 MB de lectures al dia, el mecanisme correcte és un altre.

Un extra útil: mq_notify() permet demanar que el nucli t'avisi —amb una senyal o creant un fil— quan arribi un missatge a una cua buida, sense haver d'estar bloquejat esperant.

Memòria compartida POSIX i /dev/shm/meteora-cache

Arribem al mecanisme més ràpid i al que ja coneixes de nom des del mòdul 2. La memòria compartida consisteix en què diversos processos mapegin les mateixes pàgines físiques als seus espais d'adreces.

El procediment POSIX té tres passos: crear un objecte de memòria compartida amb shm_open(), donar-li mida amb ftruncate(), i mapejar-lo amb mmap(). A partir d'aquí, és memòria normal.

/* cache_meteora.c — crear i fer servir /dev/shm/meteora-cache */
#include <stdio.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/mman.h>

#define NOM_SHM "/meteora-cache"         /* → /dev/shm/meteora-cache */

struct Lectura { unsigned int estacio_id; unsigned long timestamp;
                 float temperatura, humitat, pressio; };

struct cache_meteora {
    unsigned long peticions_totals;      /* l'escriuen els 4 treballadors */
    unsigned long ultima_agregacio;      /* l'escriu l'agregador */
    unsigned int  n_ultimes;
    struct Lectura ultimes[1024];        /* 24 KB de lectures recents */
};

int main(int argc, char **argv) {
    /* 1. Crear (o obrir) l'objecte de memòria compartida */
    int fd = shm_open(NOM_SHM, O_CREAT | O_RDWR, 0660);
    if (fd == -1) { perror("shm_open"); return 1; }

    /* 2. Fixar-ne la mida. Només cal la primera vegada. */
    if (ftruncate(fd, sizeof(struct cache_meteora)) == -1) { perror("ftruncate"); return 1; }

    /* 3. Mapejar-lo. MAP_SHARED fa les escriptures visibles als altres. */
    struct cache_meteora *c = mmap(NULL, sizeof *c, PROT_READ | PROT_WRITE,
                                   MAP_SHARED, fd, 0);
    if (c == MAP_FAILED) { perror("mmap"); return 1; }
    close(fd);            /* el mapatge sobreviu al tancament del descriptor */

    if (argc > 1 && strcmp(argv[1], "escriure") == 0) {
        c->ultimes[c->n_ultimes].estacio_id = 41;
        c->ultimes[c->n_ultimes].temperatura = 23.4f;
        c->n_ultimes++;                  /* ⚠ CARRERA si hi ha diversos escriptors */
        c->ultima_agregacio = 1756684800;
        printf("[escriptor] n_ultimes=%u\n", c->n_ultimes);
    } else {
        printf("[lector] n_ultimes=%u  ultima_agregacio=%lu  temp[0]=%.1f\n",
               c->n_ultimes, c->ultima_agregacio, c->ultimes[0].temperatura);
    }
    munmap(c, sizeof *c);
    /* shm_unlink(NOM_SHM); ← només quan ja ningú no el necessiti */
    return 0;
}
$ gcc cache_meteora.c -o cache -lrt
$ ./cache escriure        → [escriptor] n_ultimes=1
$ ./cache                 → [lector] n_ultimes=1  ultima_agregacio=1756684800  temp[0]=23.4
$ ls -l /dev/shm/         → -rw-rw---- 1 meteora meteora 24596 sep 1 09:31 meteora-cache

Punts clau del codi:

/dev/shm és un tmpfs, un sistema de fitxers que viu íntegrament a la RAM (i pot anar a swap). Per això shm_open("/meteora-cache") crea un fitxer visible a /dev/shm/meteora-cache que pots inspeccionar amb ls, esborrar amb rm i dimensionar amb df -h /dev/shm. No hi ha màgia: és un fitxer a la RAM mapejat amb mmap.

MAP_SHARED és el que ho fa compartit. Amb MAP_PRIVATE, cada procés rebria la seva pròpia còpia tan bon punt escrivís, per copy-on-write, i les modificacions no serien visibles per a ningú. És un error d'una sola paraula amb un símptoma desconcertant: tot funciona però l'altre procés no veu mai els canvis.

close(fd) no destrueix el mapatge, que roman fins al munmap() o fins que el procés acabi; tancar el descriptor és bona pràctica per no gastar-ne. I l'objecte persisteix fins a shm_unlink() o fins al reinici de la màquina: és persistència de nucli, un avantatge (l'agregador es pot reiniciar sense perdre la memòria cau) i una fuita potencial (si ningú no fa shm_unlink, la RAM queda ocupada indefinidament).

I ara l'advertència que cal subratllar tres vegades, la del comentari ⚠ CARRERA. La memòria compartida no sincronitza absolutament res. Aquest c->n_ultimes++ és exactament el mateix comptador++ de Conceptes de Concurrència, amb les mateixes tres instruccions i la mateixa pèrdua d'increments. I és pitjor que això: escriure una struct Lectura de 24 bytes són almenys tres instruccions, i l'agregador la pot llegir a mitges. Allà on una cua de missatges garanteix que un missatge arriba sencer o no arriba, aquí no hi ha cap garantia, i allà on una cua no perd mai un element, aquí sí que es perden increments: la memòria compartida sempre necessita primitives addicionals.

El que falta —mutexos, semàfors, variables de condició, col·locats dins de la mateixa regió compartida perquè els vegin tots els processos— és el contingut íntegre de Sincronització i Exclusió Mútua. Fins aleshores, considera tot el codi d'aquest apartat com a incomplet expressament.

Sockets: de domini UNIX i de xarxa

Un socket és un extrem de comunicació bidireccional. És el mecanisme d'IPC més versàtil perquè, amb la mateixa API, comunica processos de la mateixa màquina o de màquines diferents.

Hi ha dues famílies que importen aquí:

Socket de domini UNIX (AF_UNIX) Socket de xarxa (AF_INET/AF_INET6)
Àmbit La mateixa màquina Qualsevol màquina abastable
Adreça Una ruta: /run/meteora/api.sock IP + port: 10.0.4.7:9200
Recorregut de les dades Només memòria del nucli Pila TCP/IP completa
Latència (anada i tornada) ~5-10 µs ~50 µs en LAN, ~30 ms a Internet
Cabal màxim ~10 GB/s Limitat per la xarxa
Control d'accés Permisos del sistema de fitxers Tallafocs, TLS
Pot passar descriptors (SCM_RIGHTS) No
Pot identificar el parell (SO_PEERCRED: PID, UID, GID) No de manera fiable

El socket de domini UNIX és entre 5 i 10 vegades més ràpid que un de xarxa local perquè se salta tota la pila TCP/IP —sense sumes de comprovació, capçaleres, control de congestió ni fragmentació: el nucli copia de la memòria intermèdia de l'emissor a la del receptor—. I aporta dues capacitats úniques: passar descriptors de fitxer oberts entre processos (així reparteix nginx les connexions acceptades entre els seus treballadors) i conèixer les credencials del procés de l'altre extrem sense que aquest les declari, cosa que permet autenticació fiable en local.

Ara el cas de Meteora: l'ingestor escoltant les lectures de les 800 estacions. Aquí sí que cal un socket de xarxa, perquè les estacions són fora de la màquina.

/* ingestor_socket.c — rep lectures de les estacions per UDP */
#include <stdio.h>
#include <arpa/inet.h>
#include <sys/socket.h>

struct Lectura { unsigned int estacio_id; unsigned long timestamp;
                 float temperatura, humitat, pressio; };

int main(void) {
    int s = socket(AF_INET, SOCK_DGRAM, 0);      /* UDP: datagrames */
    if (s == -1) { perror("socket"); return 1; }

    struct sockaddr_in adr = { .sin_family = AF_INET, .sin_port = htons(9200),
                               .sin_addr.s_addr = htonl(INADDR_ANY) };
    if (bind(s, (struct sockaddr *)&adr, sizeof adr) == -1) { perror("bind"); return 1; }

    /* Ampliar la memòria intermèdia de recepció: amb 800 estacions, les ràfegues
       simultànies omplen els 208 KB per defecte i el nucli descarta. */
    int mida = 4 * 1024 * 1024;
    setsockopt(s, SOL_SOCKET, SO_RCVBUF, &mida, sizeof mida);

    struct Lectura l;
    struct sockaddr_in origen; socklen_t olen = sizeof origen;
    while (1) {
        ssize_t n = recvfrom(s, &l, sizeof l, 0, (struct sockaddr *)&origen, &olen);
        if (n != sizeof l) continue;              /* datagrama malformat */
        printf("estacio %u des de %s: %.1f C\n", l.estacio_id,
               inet_ntoa(origen.sin_addr), l.temperatura);
        /* ... aquí aniria l'emmagatzematge a /var/lib/meteora/lectures/ ... */
    }
}

Decisions de disseny que convé justificar:

UDP (SOCK_DGRAM) i no TCP. Cada lectura és un datagrama independent de 24 bytes, i perdre'n una no és dramàtic: n'arribarà una altra d'aquí a un minut. TCP hi afegiria el cost de mantenir 800 connexions obertes amb les seves memòries intermèdies i retransmissions per garantir un lliurament que no necessitem. A més, amb SOCK_DGRAM es conserven les fronteres de missatge: un recvfrom() retorna exactament un datagrama, cosa que elimina el problema del trossejat de les canonades i de TCP.

Ampliar SO_RCVBUF. Aquesta línia connecta directament amb el mòdul 2: si les 800 estacions envien alhora i l'ingestor està ocupat escrivint al disc, els datagrames s'acumulen a la memòria intermèdia del socket, i quan s'omple el nucli els descarta en silenci —només ho veuràs com a rx_missed_errors o als descartaments de netstat -su—. Els 208 KB per defecte donen per a 8.600 lectures; 4 MB, per a 175.000. És la mateixa lògica de dimensionament que vam veure amb NAPI.

htons i htonl converteixen a l'ordre de bytes de xarxa (big-endian) des del del processador (little-endian a x86); oblidar-ho converteix el port 9200 en el 61. I compte: l'exemple envia la struct Lectura en cru, cosa que només funciona si emissor i receptor comparteixen arquitectura i alineament; en producció caldria serialitzar els camps explícitament.

Senyals com a mecanisme de notificació

Una senyal és una notificació asíncrona que el nucli lliura a un procés. No transporta dades (llevat de sigqueue, que permet un enter): és un avís que alguna cosa ha passat. És, en essència, una interrupció programari dirigida a un procés.

Ja n'has trobat unes quantes al curs: SIGSEGV quan un procés toca memòria que no li correspon (mòdul 2), SIGKILL quan l'OOM killer decideix sacrificar algú, SIGPIPE fa dos apartats.

Senyal Número Acció per defecte Ús habitual
SIGHUP 1 Acabar Recarregar configuració (conveni universal)
SIGINT / SIGQUIT 2 / 3 Acabar Ctrl+C / Ctrl+\ (el segon, amb bolcat)
SIGKILL 9 Acabar No es pot capturar ni ignorar
SIGSEGV 11 Acabar + bolcat Accés invàlid a memòria
SIGPIPE 13 Acabar Escriure sense lector
SIGTERM 15 Acabar Petició educada d'aturada
SIGCHLD 17 Ignorar Un fill ha acabat (evita zombis)
SIGSTOP/SIGCONT 19/18 Aturar/Continuar Control de treballs
SIGUSR1/SIGUSR2 10/12 Acabar Lliures per a la teva aplicació

El cas de Meteora és el conveni més estès d'UNIX: recarregar /etc/meteora/meteora.conf amb SIGHUP sense reiniciar el servei ni perdre les connexions en curs.

/* recarrega_config.c — recarregar la configuració amb SIGHUP */
#define _POSIX_C_SOURCE 200809L
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <string.h>

/* L'ÚNICA variable que el gestor pot tocar amb seguretat */
volatile sig_atomic_t recarrega_pendent = 0;

void gestor_hup(int sig) { (void)sig; recarrega_pendent = 1; }   /* només la bandera */

int main(void) {
    struct sigaction sa;
    memset(&sa, 0, sizeof sa);
    sa.sa_handler = gestor_hup;
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = SA_RESTART;    /* reintenta les crides interrompudes */
    if (sigaction(SIGHUP, &sa, NULL) == -1) { perror("sigaction"); return 1; }
    signal(SIGPIPE, SIG_IGN);    /* com hem vist abans */

    printf("meteo-api arrencat, PID %d\n", getpid());
    while (1) {
        if (recarrega_pendent) {
            recarrega_pendent = 0;
            /* AQUÍ, al bucle principal, es pot fer de tot:
               obrir fitxers, reservar memòria, escriure al registre... */
            printf("Rellegint /etc/meteora/meteora.conf\n");
            llegir_configuracio("/etc/meteora/meteora.conf");
        }
        atendre_una_peticio();
    }
}
$ ./meteo-api &
meteo-api arrencat, PID 8421
$ kill -HUP 8421          # o: kill -s HUP 8421
Rellegint /etc/meteora/meteora.conf

El patró que veus —el gestor només posa una bandera; la feina es fa al bucle principal— és obligatori, i la raó és la restricció més important de les senyals.

Un gestor de senyal s'executa interrompent el procés en un punt arbitrari. Pot interrompre enmig d'un malloc(), quan les estructures internes de l'assignador són inconsistents. Si el gestor crida printf(), que internament reserva memòria, es produeix un interbloqueig o una corrupció del munt. Per això POSIX defineix una llista curta de funcions segures en gestors (async-signal-safe):

Segures en un gestor Prohibides en un gestor
write, read, open, close printf, fprintf, sprintf
_exit, kill, signal, sigaction malloc, free, calloc, realloc
time, getpid, sem_post pthread_mutex_lock, syslog
Assignacions a volatile sig_atomic_t Qualsevol funció de stdio

El tipus volatile sig_atomic_t és l'única variable que pots tocar amb garanties: sig_atomic_t assegura que l'assignació és una sola instrucció i volatile obliga el compilador a rellegir-la a cada iteració del bucle en lloc de guardar-la en un registre. (Aquí volatile sí que és correcte perquè el gestor s'executa al mateix fil que el bucle: no hi ha dos nuclis ni memòries cau implicades. En concurrència real entre fils no n'hi ha prou, i ho veurem a la lliçó següent.)

Dos apunts més. SA_RESTART: quan arriba una senyal mentre el procés està bloquejat en un read() lent, la crida s'avorta amb EINTR; amb aquest flag el nucli la reintenta automàticament i el teu codi no ha d'envoltar cada crida bloquejant en un bucle. I sigaction en lloc de signal: signal() té un comportament històricament inconsistent entre sistemes —en alguns reinstal·la el gestor després de cada senyal, en d'altres el restableix a l'acció per defecte—, mentre que sigaction() és explícita i portable; fes servir signal() només per al cas trivial de SIG_IGN.

Per a processos multifil, el patró professional que vam anticipar a la lliçó anterior: bloquejar les senyals a tots els fils amb pthread_sigmask() i dedicar un fil a recollir-les amb sigwait(). Així s'eliminen d'arrel totes les restriccions del gestor, perquè sigwait() retorna en context normal i aquell fil pot cridar el que vulgui.

Comunicació bloquejant i no bloquejant

Tots els mecanismes anteriors tenen dos modes de funcionament, i triar malament produeix o penjades o consum inútil de CPU.

En el mode bloquejant (el de per defecte), si no hi ha dades a llegir read() adorm el procés fins que n'hi hagi, i si la memòria intermèdia està plena write() dorm fins que hi hagi lloc: el planificador el treu de la cua de llestos i no consumeix gens de CPU. En el mode no bloquejant (O_NONBLOCK) la crida retorna immediatament, i si no hi havia dades retorna -1 amb errno == EAGAIN (equivalent a EWOULDBLOCK).

Bloquejant No bloquejant
Consum de CPU esperant Zero Alt si fas sondeig actiu
Complexitat del codi Baixa: llegeixes i ja està Alta: cal gestionar EAGAIN
Atendre diverses fonts Necessita un fil per font Un sol fil per a milers
Risc Penjada indefinida Bucle de sondeig que crema un nucli
Quan fer-lo servir Un fil dedicat a una font Bucle d'esdeveniments amb epoll

La manera correcta de fer servir el mode no bloquejant no és el sondeig actiu —comprovar en un bucle si hi ha dades crema un nucli sencer— sinó combinar-lo amb una crida de multiplexació que dormi fins que algun dels descriptors estigui llest:

/* L'ingestor vigila 800 sockets alhora amb un sol fil */
int ep = epoll_create1(0);
struct epoll_event ev = { .events = EPOLLIN, .data.fd = socket_estacio };
epoll_ctl(ep, EPOLL_CTL_ADD, socket_estacio, &ev);

struct epoll_event llestos[64];
while (1) {
    int n = epoll_wait(ep, llestos, 64, -1);  /* dorm fins que hi hagi alguna cosa */
    for (int i = 0; i < n; i++)
        processar(llestos[i].data.fd);        /* aquí SÍ que hi ha dades: no bloquejarà */
}

Això és el millor dels dos mons: zero CPU mentre no passa res (el procés dorm a epoll_wait) i un sol fil per a milers de descriptors. És el motor del model asíncron de la lliçó anterior, i la raó que nginx atengui 100.000 connexions amb vuit processos.

El paper de la memòria intermèdia aquí és central: la memòria intermèdia del nucli és el que desacobla l'emissor del receptor. Mentre hi hagi lloc, l'emissor no espera; mentre hi hagi dades, el receptor no espera. Si es queda petita per a les ràfegues del teu trànsit, els dos processos se sincronitzen a la força —o, pitjor, en UDP es descarten dades en silenci—. Dimensionar memòries intermèdies (F_SETPIPE_SZ, SO_RCVBUF/SO_SNDBUF, mq_maxmsg) és una de les palanques d'ajust més efectives i més oblidades.

Comparativa i guia de decisió

Mecanisme Latència (4 KB) Àmbit Sentit Fronteres de missatge Sincronitza Complexitat Quan fer-lo servir
Canonada anònima ~10 µs Processos emparentats Unidireccional No (flux) Molt baixa Encadenar un fill, filtres d'intèrpret d'ordres
FIFO ~10 µs Mateixa màquina, qualsevol Unidireccional No (flux, atòmic ≤4 KB) Baixa Canal simple entre serveis independents
Cua POSIX ~15 µs Mateixa màquina Unidireccional Mitjana Ordres, alertes amb prioritat
Memòria compartida ~0,1 µs Mateixa màquina Bidireccional No NO Alta Grans volums, latència mínima
Socket UNIX ~7 µs Mateixa màquina Bidireccional Sí a SOCK_SEQPACKET Mitjana Client-servidor local, passar descriptors
Socket de xarxa ~50 µs (LAN) Qualsevol màquina Bidireccional Sí a UDP, no a TCP Mitjana Tot el que és distribuït
Senyal ~2 µs Mateixa màquina Unidireccional Sense dades Baixa però traïdora Notificar esdeveniments, recarregar config

I la guia de decisió en forma de preguntes encadenades:

  1. Els processos poden estar en màquines diferents, ara o en el futur?Socket de xarxa. És l'únic que creua la frontera de la màquina, i fer-lo servir des del principi evita una reescriptura quan el sistema creixi.
  2. Només necessites avisar d'un esdeveniment, sense dades?Senyal. Recarregar configuració, demanar una aturada neta, forçar una rotació de registres. És el més barat i el que espera qualsevol administrador de sistemes.
  3. És un fill directe que has creat tu i el flux va en un sol sentit?Canonada anònima. El mecanisme més simple que existeix, i no cal netejar res.
  4. Necessites prioritats, o que els missatges es conservin encara que el receptor no hi sigui?Cua de missatges POSIX, vigilant els límits de mida.
  5. Mous megabytes i la latència és crítica?Memòria compartida, assumint que l'hauràs de sincronitzar amb les primitives de la lliçó següent. És l'opció més ràpida i la que més et pot costar en depuració.
  6. En qualsevol altre casSocket de domini UNIX. Bidireccional, amb control d'accés per permisos, identifica el procés de l'altre costat, funciona amb epoll i és el que menys et sorprendrà. Quan dubtis, aquesta és la resposta.

L'arquitectura de Meteora, ara justificada mecanisme a mecanisme:

Comunicació Mecanisme Per què
Estacions → ingestor Socket UDP de xarxa Són en altres màquines; perdre una lectura és tolerable
ingestoragregador FIFO /run/meteora/lectures.fifo Cabal continu, un sentit, processos sense parentiu
agregadormeteo-api Memòria compartida /dev/shm/meteora-cache 24 KB llegits a cada petició; la latència mana
Alertes de l'ingestor Cua POSIX /meteora-alertes Necessita prioritat: el que és crític, primer
Recàrrega de configuració Senyal SIGHUP Conveni universal, sense dades, cost zero
Clients → meteo-api Socket TCP de xarxa (HTTP) Clients externs, lliurament fiable

Errors Habituals i Consells

No tancar els extrems no utilitzats d'una canonada. Si el procés lector conserva obert el descriptor d'escriptura, no rebrà mai EOF i el seu read() es bloquejarà per sempre. El símptoma és un programa que fa la seva feina però no acaba, i és de les penjades més freqüents amb canonades.

No ignorar SIGPIPE en un servei. Escriure en una canonada o un socket el lector del qual ha tancat mata el procés per defecte, sense missatge. Un servei que mor en silenci quan un client penja és un incident de matinada garantit: signal(SIGPIPE, SIG_IGN) a les primeres línies de main() i comprovar EPIPE.

Cridar printf o malloc des d'un gestor de senyal. Funciona el 99,9 % de les vegades i corromp el munt el 0,1 % restant, produint una fallada aleatòria molt posterior i impossible de relacionar amb la seva causa. El gestor posa una bandera volatile sig_atomic_t i res més.

Suposar que un read() d'una canonada o un socket TCP retorna el missatge complet. Són fluxos de bytes sense fronteres: un read(fd, buf, 24) pot retornar 10. Cal insistir en un bucle fins a completar els bytes esperats, o fer servir SOCK_DGRAM/SOCK_SEQPACKET/cues si necessites fronteres.

Fer servir MAP_PRIVATE on volies MAP_SHARED. El programa funciona, no dona cap error, i les escriptures d'un procés simplement no les veu ningú més, perquè el copy-on-write li va donar a cadascun la seva còpia privada. Relacionat: oblidar -lrt en enllaçar mq_* i shm_open en glibc anterior a la 2.34.

Creure que la memòria compartida «funciona» perquè les proves passen. És l'error més car d'aquesta lliçó. Sense sincronització, les carreres són les de 03-01: probabilitat baixa per operació, certesa a llarg termini. Si comparteixes memòria, necessites la lliçó següent.

Consell: prefereix el mecanisme més simple que resolgui el teu problema. Molt codi fa servir memòria compartida on n'hi hauria hagut prou amb una FIFO, i paga en depuració el que va estalviar en microsegons que ningú no anava a notar. Optimitza l'IPC quan ho hagis mesurat i sigui el coll d'ampolla.

Consell: dimensiona les memòries intermèdies conscientment i vigila'n l'ocupació. El valor per defecte està pensat per al cas general, no per al teu. Una memòria intermèdia que s'omple sovint avisa d'un consumidor que no dona l'abast abans que comenci a perdre dades.

Exercicis

Exercici 1: contrapressió i SIGPIPE

Escriu dos programes en C: un productor que escrigui struct Lectura en una canonada tan ràpid com pugui comptant quantes n'ha escrit, i un consumidor lent que en llegeixi una cada 100 ms. Connecta'ls amb |. Mentre corren, observa l'estat del productor amb ps -o pid,stat,wchan -C productor i explica el que veus. Després mata el consumidor amb Ctrl+C i comprova què li passa al productor; repeteix l'experiment amb signal(SIGPIPE, SIG_IGN) i tracta EPIPE.

Exercici 2: triar el mecanisme

Per a cada necessitat de Meteora, tria el mecanisme d'IPC més adequat i justifica-ho descartant almenys dues alternatives.

  • (a) Una eina nova meteo-ctl ha de poder dir a l'agregador que recalculi les mitjanes del dia actual immediatament.
  • (b) L'agregador publica cada hora un resum de 2 MB que consulten els 4 treballadors de meteo-api a cada petició.
  • (c) S'afegeix un servei d'arxivament en una altra màquina que ha de rebre una còpia de cada fitxer diari tancat.
  • (d) L'ingestor ha d'avisar l'agregador que ha detectat una estació caiguda, amb més urgència que les notificacions rutinàries.

Exercici 3: FIFO amb contrapressió mesurada

Munta el canal ingestoragregador amb una FIFO. L'escriptor ha d'enviar 100.000 struct Lectura; el lector les ha de processar amb un retard artificial configurable. Mesura el temps total amb retards de 0 µs i de 10 µs per lectura, i determina experimentalment la mida de la memòria intermèdia de la FIFO comprovant quants bytes pot escriure el productor abans de bloquejar-se amb un lector que no llegeix res.

Solucions

Solució 1

/* productor.c (fragment) — amb argument, ignora SIGPIPE i tracta EPIPE */
if (argc > 1) signal(SIGPIPE, SIG_IGN);          /* mode "robust" */
struct Lectura l = { .estacio_id = 41, .temperatura = 21.5f };
long n = 0;
while (1) {
    if (write(STDOUT_FILENO, &l, sizeof l) == -1) {
        if (errno == EPIPE) {
            fprintf(stderr, "\n[productor] EPIPE després de %ld lectures. Sortida neta.\n", n);
            return 0;
        }
        perror("write"); return 1;
    }
    if (++n % 1000 == 0) fprintf(stderr, "\r[productor] %ld", n);
}

/* consumidor.c (fragment) — llegeix una lectura cada 100 ms */
struct Lectura l;
while (read(STDIN_FILENO, &l, sizeof l) == sizeof l) usleep(100000);

Observació durant l'execució:

$ ./productor | ./consumidor &
[productor] 2731
$ ps -o pid,stat,wchan,cmd -C productor
  PID STAT WCHAN         CMD
 9142 S    pipe_write    ./productor

Què es veu. El comptador s'atura al voltant de 2.731 i no avança més, amb estat S (adormit interrompible) i WCHAN = pipe_write: el productor està bloquejat dins d'aquesta funció del nucli, esperant espai. El número no és casual: 65.536 bytes de memòria intermèdia / 24 bytes per lectura = 2.730,7. Va omplir la memòria intermèdia exactament i es va adormir; a partir d'aquí avança al ritme del consumidor, 10 lectures per segon. És la contrapressió funcionant: sense una línia de codi dedicada a això, el productor s'ha adaptat al consumidor i la memòria consumida està acotada en 64 KB.

En matar el consumidor:

$ kill %1
[1]+  Terminat (SIGPIPE)   ./productor | ./consumidor        ← sense argument

$ ./productor robust | ./consumidor &  ; kill %1
[productor] EPIPE després de 2985 lectures. Sortida neta.    ← amb SIG_IGN

En el primer cas el productor mor sense imprimir res: SIGPIPE va acabar el procés i el missatge ve de l'intèrpret d'ordres, no del programa. Si això fos l'ingestor en producció, hauria deixat de rebre lectures i al registre no hi hauria ni una línia que ho expliqués. En el segon, el write() retorna -1 amb EPIPE, el programa ho detecta, ho registra i surt ordenadament. La diferència és literalment una línia de codi, i separa un incident diagnosticable d'un que no ho és.

Solució 2

(a) meteo-ctl ordena a l'agregador recalcular.Senyal SIGUSR1. És una notificació puntual sense dades: kill -USR1 $(pidof agregador) no requereix cap infraestructura. Descartades: una FIFO obligaria l'agregador a mantenir un lector obert permanentment i meteo-ctl a gestionar el bloqueig de l'open(), molta maquinària per a un avís; un socket UNIX seria el correcte si meteo-ctl creixés fins a un protocol amb diverses ordres i respostes, però per a una ordre sense resposta és sobreenginyeria.

(b) Resum de 2 MB llegit a cada petició.Memòria compartida POSIX. A 1.200 peticions/s, passar 2 MB per una cua o un socket serien 2,4 GB/s de còpies: impossible. Amb memòria compartida el cost d'accés és zero. Descartades: cua POSIX pel límit de 8 KB per missatge, tres ordres de magnitud per sota; socket UNIX per les dues còpies per petició. Condició imprescindible: cal sincronitzar l'actualització horària de l'agregador amb les lectures contínues dels treballadors, o aquests veuran un resum a mig escriure. El patró adequat és una doble memòria intermèdia amb un índex atòmic, o un bloqueig de lectura/escriptura: exactament el que veurem a Sincronització i Exclusió Mútua i a Problemes Clàssics de Concurrència.

(c) Arxivament en una altra màquina.Socket TCP de xarxa. És l'única família que creua la frontera de la màquina, i TCP en lloc d'UDP perquè un fitxer diari complet ha d'arribar íntegre i en ordre: aquí sí que necessitem les garanties de lliurament que a les lectures individuals no calien. Tots els altres mecanismes són locals per construcció; la memòria compartida no es pot compartir entre màquines, aquesta és precisament la seva frontera.

(d) Avís urgent d'estació caiguda.Cua de missatges POSIX amb prioritat alta. És l'únic mecanisme local amb prioritats incorporades: enviant les alertes amb prioritat 9 i el que és rutinari amb 0, mq_receive() lliura primer el que és urgent encara que hagi arribat després. Descartades: la FIFO és estrictament FIFO —un avís urgent esperaria darrere de tot el que s'hagi acumulat—; una senyal avisaria que «alguna cosa passa» però no de quina estació ni quin problema, i les senyals estàndard no s'encuen: si arriben dos SIGUSR1 abans d'atendre el primer, se'n perd un.

Solució 3

/* fifo_escriptor.c — envia 100.000 lectures i cronometra */
int fd = open("/tmp/meteora.fifo", O_WRONLY);
struct Lectura l = { .estacio_id = 41, .temperatura = 21.5f };
struct timespec t0, t1;
clock_gettime(CLOCK_MONOTONIC, &t0);
for (long i = 0; i < 100000; i++) { l.timestamp = i; write(fd, &l, sizeof l); }
clock_gettime(CLOCK_MONOTONIC, &t1);
fprintf(stderr, "escriptor: %.3f s\n",
        (t1.tv_sec-t0.tv_sec) + (t1.tv_nsec-t0.tv_nsec)/1e9);
close(fd);

/* fifo_lector.c — el retard per lectura, en microsegons, va a argv[1] */
int retard = argc > 1 ? atoi(argv[1]) : 0;
int fd = open("/tmp/meteora.fifo", O_RDONLY);
struct Lectura l; long n = 0;
while (read(fd, &l, sizeof l) == sizeof l) { n++; if (retard) usleep(retard); }
printf("lector: %ld lectures\n", n);
$ mkfifo /tmp/meteora.fifo
$ ./fifo_lector 0  & ./fifo_escriptor     → escriptor: 0.089 s / 100000 lectures
$ ./fifo_lector 10 & ./fifo_escriptor     → escriptor: 6.412 s / 100000 lectures

Anàlisi. Sense retard, 100.000 lectures (2,4 MB) triguen 89 ms: uns 27 MB/s, limitats per les 200.000 crides al sistema, a unes 0,44 µs cadascuna. Amb 10 µs de retard per lectura, l'escriptor triga 6,4 segons, exactament el temps que necessita el lector (100.000 × 10 µs de retard pur, més la sobrecàrrega d'usleep, que arrodoneix a l'alça cada espera). La lliçó és contundent: l'escriptor va exactament al ritme del lector encara que no hi hagi ni una sola línia de sincronització. La memòria intermèdia de 64 KB absorbeix les ràfegues curtes i, quan s'esgota, el nucli adorm l'escriptor.

Mesurar la mida de la memòria intermèdia:

/* mesurar_buffer.c — escriure sense lector actiu fins a bloquejar-se.
   Obrim en lectura perquè l'open d'escriptura no bloquegi,
   però no llegim res d'aquest descriptor. */
int rd = open("/tmp/meteora.fifo", O_RDONLY | O_NONBLOCK);
int wr = open("/tmp/meteora.fifo", O_WRONLY | O_NONBLOCK);
char c = 'x'; long bytes = 0;
while (write(wr, &c, 1) == 1) bytes++;
printf("S'han escrit %ld bytes abans d'EAGAIN (errno=%d)\n", bytes, errno);
close(wr); close(rd);
$ ./mesurar_buffer
S'han escrit 65536 bytes abans d'EAGAIN (errno=11)

$ cat /proc/sys/fs/pipe-max-size
1048576

65.536 bytes exactes, 16 pàgines de 4 KB: el valor per defecte de Linux, que coincideix amb les 2.731 lectures de l'exercici 1 (65.536 / 24 = 2.730,67). Es pot ampliar fins a 1 MB amb fcntl(wr, F_SETPIPE_SZ, 1048576), multiplicant per 16 la capacitat d'absorbir ràfegues: útil si el consumidor té pauses ocasionals llargues, inútil si simplement és més lent de mitjana —allà la memòria intermèdia només retarda el bloqueig, no l'evita—.

Conclusió

Els processos no comparteixen espai d'adreces, i per això el nucli ofereix mecanismes d'IPC. Tots deriven de dos models: el pas de missatges, amb dues còpies per transferència (~5-15 µs per a 4 KB) i sincronització implícita, i la memòria compartida, amb zero còpies (~0,1 µs) i cap sincronització. Aquesta última frase és l'avís central de la lliçó.

Les canonades anònimes són una memòria intermèdia circular de 65.536 bytes al nucli amb dos descriptors; exigeixen pipe() abans de fork(), tancar l'extrem que no s'utilitza —o el lector no veurà mai l'EOF i es penjarà— i assumir que són un flux de bytes sense fronteres de missatge. Les seves dues situacions límit ensenyen més que el cas normal: quan la memòria intermèdia s'omple, write() bloqueja i apareix la contrapressió, que hem vist a WCHAN: pipe_write i mesurat en 2.731 lectures exactes; quan el lector tanca, arriba SIGPIPE i mata el procés en silenci, per la qual cosa tot servei seriós fa signal(SIGPIPE, SIG_IGN) i tracta EPIPE.

Les FIFO donen nom a la canonada al sistema de fitxers, permeten comunicar processos sense parentiu —el canal ingestoragregador de Meteora—, bloquegen l'open() com a mecanisme de trobada i garanteixen escriptures atòmiques de fins a 4 KB, cosa que fa segurs diversos escriptors. Les cues de missatges POSIX conserven les fronteres, persisteixen sense ningú connectat i aporten el que cap altre mecanisme local no té: prioritats, amb les quals una alerta enviada després es lliura abans; a canvi, els seus límits de 10 missatges i 8 KB les reserven per al control, no per al cabal.

La memòria compartida POSIXshm_open + ftruncate + mmap amb MAP_SHARED— és /dev/shm/meteora-cache: un fitxer a tmpfs mapejat per diversos processos, amb persistència de nucli, cost d'accés zero i l'advertència que un n_ultimes++ allà dins és exactament la carrera de 03-01. Els sockets són els més versàtils: els de domini UNIX són 5-10 vegades més ràpids que els de xarxa local, passen descriptors i revelen les credencials del parell; els de xarxa són els únics que creuen de màquina, i amb ells l'ingestor rep les lectures de les 800 estacions per UDP, ampliant SO_RCVBUF a 4 MB per no descartar en silenci. I les senyals notifiquen sense transportar dades, amb la regla d'or que el gestor només posa una bandera volatile sig_atomic_t perquè gairebé res no hi és segur a dins: així es recarrega /etc/meteora/meteora.conf amb SIGHUP. La guia de decisió, en una línia: xarxa si hi pot haver una altra màquina, senyal si és un avís sense dades, canonada si és un fill directe, cua si necessites prioritat, memòria compartida si mous megabytes amb latència crítica, i socket de domini UNIX quan dubtis.

Però l'asterisc continua sent-hi, i ja no admet més ajornament. Hem muntat /dev/shm/meteora-cache i hi hem deixat dins un n_ultimes++ que perd increments i una struct Lectura que es pot llegir a mitges. Tenim el canal; no tenim el protocol que impedeix que dos processos el facin servir alhora. Com es garanteix que només un entri a la secció crítica? Quina instrucció del maquinari fa possible un forrellat, si comptador++ no és atòmic? Per què un mutex no és simplement una bandera, i què fa futex perquè gairebé mai no calgui cridar el nucli?

És el cor del mòdul: Sincronització i Exclusió Mútua.

Fonaments de Sistemes Operatius

Mòdul 1: Introducció als Sistemes Operatius

Mòdul 2: Gestió de Recursos

Mòdul 3: Concurrència

Mòdul 4: Estructures de Fitxers

Mòdul 5: Protecció i Seguretat del Sistema

Mòdul 6: Virtualització i Contenidors

Mòdul 7: Administració i Diagnòstic a la Pràctica

© Copyright 2026. Tots els drets reservats