Skip to main content

Exercicis de Pipes, FIFOs i dup en C

Comunicació entre processos amb pipes, FIFOs i redireccions (dup/dup2)

Introducció

Els blocs 1 i 2 es resolen amb paper i llapis: per a cada exercici dibuixa la taula de descriptors de cada procés en cada moment important (després de pipe, de fork, de cada close/dup2). A partir del bloc 3 cal escriure codi. Intenta resoldre’l abans d’obrir el raonament; després compila’l i comprova-ho.

Concepte — Recordatori: descriptors, pipe, dup2 i FIFO
  • Cada procés té una taula de descriptors: cada entrada (0, 1, 2, 3…) apunta a una descripció de fitxer oberta del nucli, que guarda l’offset i el mode. open retorna sempre el descriptor lliure més petit.
  • Els descriptors 0, 1 i 2 són STDIN, STDOUT i STDERR: per defecte, el terminal. Són una simple convenció: res no impedeix usar el 3, el 4, etc. per a altres canals (el shell ho permet amb 3< fitxer o 4> fitxer).
  • fork copia la taula: les entrades del fill apunten a les mateixes descripcions que les del pare (comparteixen offset). exec conserva els descriptors oberts.
  • dup(fd) retorna el descriptor lliure més petit que apunta al mateix que fd. dup2(old, new) fa que new apunti al mateix que old, tancant new abans si estava obert. Els dos descriptors comparteixen l’offset.
  • pipe(p) crea un canal al nucli: p[0] és l’extrem de lectura i p[1] el d’escriptura.
  • read d’una pipe buida: es bloqueja mentre hi hagi algun extrem d’escriptura obert, i retorna 0 (EOF) només quan tots els extrems d’escriptura (de qualsevol procés) estan tancats.
  • write en una pipe sense cap lector: el procés rep SIGPIPE (o -1 amb EPIPE si s’ignora). Si la pipe és plena, write es bloqueja.
  • La capacitat d’una pipe a Linux és, per defecte, 64 KiB. Un write de fins a PIPE_BUF bytes (4096 a Linux) és atòmic: no es barreja amb el d’un altre procés.
  • Un FIFO és una pipe amb nom (mkfifo): apareix al sistema de fitxers (tipus p), però les dades no es guarden al disc. open d’un FIFO es bloqueja fins que l’altre extrem també s’obre.
Bona Pràctica

Convencions dels enunciats. Els fragments són trossos de main sense #include; suposen que al començament només estan oberts els descriptors 0, 1 i 2, i que pipe(p) crea p[0] = 3 i p[1] = 4 si no hi ha cap altre fitxer obert. En els fragments s’ometen les comprovacions d’error per simpliciat; en les solucions completes sí que hi són. Quan un exec falla, el fill acaba amb _exit(127).


Bloc 1 · La taula de descriptors

Exercici

Escriu la taula de descriptors del procés després de cada línia, i indica el valor de a, b, c i d.

int a = open("x.txt", O_RDONLY);
int b = open("y.txt", O_WRONLY | O_CREAT | O_TRUNC, 0644);
int c = dup(b);
close(0);
int d = dup(a);
dup2(b, 2);
close(b);
  1. Quins descriptors comparteixen l’offset?
  2. Si ara fem read(0, buf, 10) i després read(3, buf, 10), el segon read llegeix els mateixos 10 bytes que el primer?
  3. Repte: on escriurà ara perror?
Pots veure el raonament
Solució

a = 3, b = 4, c = 5, d = 0. Després de close(0) el descriptor 0 queda lliure i dup(a) agafa el més petit lliure, que és el 0.

Moment 0 1 2 3 4 5
inici STDIN STDOUT STDERR
després dels dos open i dup(b) STDIN STDOUT STDERR x.txt y.txt y.txt
després de close(0) — STDOUT STDERR x.txt y.txt y.txt
després de dup(a) x.txt STDOUT STDERR x.txt y.txt y.txt
després de dup2(b, 2) x.txt STDOUT y.txt x.txt y.txt y.txt
després de close(b) x.txt STDOUT y.txt x.txt — y.txt
  1. Comparteixen offset els que apunten a la mateixa descripció oberta: {0, 3} (x.txt) i {2, 5} (y.txt).
  2. No: el segon read llegeix els 10 bytes següents, perquè 0 i 3 comparteixen l’offset. Si haguéssim fet dos open independents de x.txt, cadascun tindria el seu offset i tots dos llegirien els mateixos 10 bytes.
  3. A y.txt: perror escriu per descriptor 2, i ara el 2 apunta a y.txt. El dup2 va tancar l’STDERR original.

Exercici

El programa següent s’executa amb la sortida estàndard al terminal.

int fd = open("out.txt", O_WRONLY | O_CREAT | O_TRUNC, 0644);
dup2(fd, 1);
close(fd);
printf("A\n");
write(1, "B\n", 2);
printf("C\n");
return 0;
  1. Què apareix per pantalla? Què conté out.txt i en quin ordre?
  2. Repte: què canvia si movem el dup2 (i el close) després de printf("A\n")?
Pots veure el raonament
Solució
  1. Pantalla: res. out.txt conté B, A, C (en aquest ordre).

    Quan printf escriu per primera vegada, stdout ja apunta a un fitxer, i stdio tria buffering complet. A\n i C\n es queden al buffer; el write(1, "B\n", 2) va directe al nucli i surt primer. El buffer es buida en acabar el programa (return 0), i hi surt A\nC\n.

  2. Ara el primer printf("A\n") s’executa amb stdout al terminal, i stdio tria line buffering: A surt per pantalla a l’instant. Aquest mode ja no canvia encara que després el descriptor 1 apunti a un fitxer, així que printf("C\n") també s’escriu de seguida. Pantalla: A. out.txt: B, C.

Error Comú

El mode de buffering de stdout es decideix la primera vegada que s’escriu, mirant on apunta llavors. Per això el mateix codi es comporta diferent amb la sortida a terminal, a fitxer o a una pipe.


Exercici

El shell executa ls /noexisteix /etc/hostname > out.txt 2>&1 de manera diferent que ls /noexisteix /etc/hostname 2>&1 > out.txt. Considera aquests dos fragments, que s’executen en un fill just abans de l’exec de ls:

int fd = open("out.txt", O_WRONLY | O_CREAT | O_TRUNC, 0644);

/* Cas A */                 /* Cas B */
dup2(fd, 1);                dup2(1, 2);
dup2(1, 2);                 dup2(fd, 1);

close(fd);
execlp("ls", "ls", "/noexisteix", "/etc/hostname", NULL);
  1. En cada cas, què surt per pantalla i què hi ha a out.txt?
  2. Quin cas equival a > out.txt 2>&1 i quin a 2>&1 > out.txt?
Pots veure el raonament
Solució

dup2(a, b) es llegeix com b passa a apuntar on apunta a ara mateix, i les redireccions s’apliquen d’esquerra a dreta.

  • Cas A. dup2(fd, 1): el 1 apunta a out.txt. dup2(1, 2): el 2 apunta on apunta el 1 ara, és a dir out.txt. Resultat: pantalla buida, i out.txt conté /etc/hostname i el missatge d’error de ls. Equival a > out.txt 2>&1.
  • Cas B. dup2(1, 2): el 2 apunta al terminal (on apunta el 1 encara). Després dup2(fd, 1): el 1 passa a out.txt, però el 2 no es mou. Resultat: el missatge d’error surt per pantalla i out.txt conté només /etc/hostname. Equival a 2>&1 > out.txt.

L’ordre importa perquè dup2 copia el que hi ha en aquell moment, no crea cap enllaç entre els dos descriptors.


Exercici

Intercanviar STDOUT i STDERR. Un fill té la seva STDOUT connectada a una pipe que llegeix wc -l (és a dir, el pare executa ls ... | wc -l), i la seva STDERR al terminal. Abans de l’exec de ls, el fill fa:

int t = dup(1);
dup2(2, 1);
dup2(t, 2);
close(t);
execlp("ls", "ls", "/noexisteix", "/etc/hostname", NULL);
  1. Quin valor té t? Escriu la taula de descriptors del fill després de cada línia.
  2. Què apareix per pantalla i quin número imprimeix wc -l?
  3. Per què cal la variable t? Què passaria amb dup2(2, 1); dup2(1, 2);?
  4. Repte: escriu l’ordre del shell equivalent amb els descriptors 3>&1 1>&2 2>&3 3>&-. Per a què serveix el 3>&- final?
Pots veure el raonament
Solució
  1. t = 3: el 3 és el primer descriptor lliure.
Moment 0 1 2 3
inici STDIN pipe (escriptura) terminal
després de dup(1) STDIN pipe (escriptura) terminal pipe (escriptura)
després de dup2(2, 1) STDIN terminal terminal pipe (escriptura)
després de dup2(t, 2) STDIN terminal pipe (escriptura) pipe (escriptura)
després de close(t) STDIN terminal pipe (escriptura) —
  1. ls escriu /etc/hostname per STDOUT, que ara és el terminal, i el missatge d’error per STDERR, que ara és la pipe. Per pantalla apareix /etc/hostname, i wc -l imprimeix 1 (la línia d’error).
  2. t és una còpia de seguretat de l’extrem de la pipe: sense ella, el primer dup2(2, 1) sobreescriu el descriptor 1 i ja no queda cap referència a la pipe. Amb dup2(2, 1); dup2(1, 2); els dos descriptors acaben apuntant al terminal, wc -l no rep res i imprimeix 0. És el mateix problema d’intercanviar dues variables: cal una temporal.
  3. ls /noexisteix /etc/hostname 3>&1 1>&2 2>&3 3>&- | wc -l. El 3>&- tanca el 3 perquè ls no el rebi: és el close(t) del programa en C.

Bloc 2 · Semàntica de les pipes

Exercici

Analitza el fragment següent.

int p[2];
char c;
pipe(p);

if (fork() == 0) {
    close(p[1]);
    while (read(p[0], &c, 1) > 0)
        write(1, &c, 1);
    exit(0);
}

write(p[1], "ABC", 3);
close(p[1]);
wait(NULL);
  1. Quins valors tenen p[0] i p[1]? Què imprimeix el programa?
  2. Què passa si traiem el close(p[1]) del pare?
  3. Què passa si traiem el close(p[1]) del fill?
Pots veure el raonament
Solució
  1. p[0] = 3 i p[1] = 4. Imprimeix ABC (sense salt de línia): el fill llegeix byte a byte fins que read retorna 0.
  2. Interbloqueig. El read del fill només retorna 0 quan tots els extrems d’escriptura estan tancats, i el del pare continua obert. El fill es queda bloquejat al read, i el pare al wait esperant el fill.
  3. El mateix interbloqueig, però ara l’extrem obert és el del mateix fill: tot i que el pare l’ha tancat, el fill n’és també escriptor. Un procés que llegeix d’una pipe ha de tancar el seu propi extrem d’escriptura.
Error Comú

L’EOF d’una pipe no depèn de qui ha acabat d’escriure, sinó de quants extrems d’escriptura queden oberts en total, comptant els que hereten els fills en el fork. Si un procés que no escriu es deixa un extrem obert, el lector no veurà mai l’EOF.


Exercici

Analitza el programa següent. Indica l’evolució de la taula de descriptors de cada procés i explica què fa realment. Després, corregeix-lo perquè imprimeixi PARE -> Hola i acabi.

int main(){
    int fd;
    int pipefd[2];
    char msg[200];
    char buffer[200];

    fd = open("fitxer.txt", O_RDONLY);
    pipe(pipefd);

    if(fork() == 0){
        close(1);
        close(pipefd[0]);
        write(4, "Hola", 4);
        while(1);
        exit(0);
    }

    else{
        close(pipefd[1]);
        read(pipefd[0], buffer, 4);
        sprintf(msg, "PARE -> %s\n", buffer);
        write(1, msg, strlen(msg));
        wait(NULL);
        exit(0);
    }
}
Pots veure el raonament
Solució

Tal com està, el programa no imprimeix res i es queda penjat.

Moment Procés 0 1 2 3 4 5
després d’open i pipe Pare STDIN STDOUT STDERR fitxer.txt pipe (lectura) pipe (escriptura)
després de fork Fill STDIN STDOUT STDERR fitxer.txt pipe (lectura) pipe (escriptura)
fill després de close(1) i close(pipefd[0]) Fill STDIN — STDERR fitxer.txt — pipe (escriptura)
pare després de close(pipefd[1]) Pare STDIN STDOUT STDERR fitxer.txt pipe (lectura) —
  • El fill fa write(4, ...), però el 4 és pipefd[0] i ja l’ha tancat: write retorna -1 amb EBADF i no s’escriu res. L’extrem d’escriptura és el 5.
  • Després el fill entra al while(1), sense tancar el 5. El pare fa read d’una pipe buida amb un extrem d’escriptura obert (el del fill): es bloqueja per sempre.
  • Encara que corregíssim el write(4) per write(5), el pare imprimiria Hola, però quedaria penjat al wait(NULL) perquè el fill mai no acaba.
  • Hi ha un segon error: read no escriu un '\0', així que sprintf("%s", buffer) llegeix una cadena sense terminador (comportament indefinit).
  • El close(1) del fill no serveix de res: només tindria efecte si la pipe es creés després, perquè llavors pipe ocuparia el 1.

Versió corregida:

if (fork() == 0) {
    close(pipefd[0]);
    write(pipefd[1], "Hola", 4);
    close(pipefd[1]);
    exit(0);
}

close(pipefd[1]);
ssize_t n = read(pipefd[0], buffer, sizeof buffer - 1);
if (n < 0) n = 0;
buffer[n] = '\0';
sprintf(msg, "PARE -> %s\n", buffer);
write(1, msg, strlen(msg));
wait(NULL);
exit(0);

Exercici

Considera aquest programa i l’ordre del shell següent.

int p[2];
pipe(p);
close(p[0]);
int r = write(p[1], "x", 1);
printf("r=%d\n", r);
  1. Què passa quan s’executa? Quin codi de sortida veu el shell (echo $?)?
  2. Com podem fer que write retorni -1 en lloc de matar el procés? Què val errno?
  3. Per què acaba l’ordre yes | head -n 1? Què passaria si no existís el mecanisme anterior?
Pots veure el raonament
Solució
  1. El procés mor amb SIGPIPE en el write i no arriba al printf. El shell veu el codi 141 (128 + 13, on 13 és el número de SIGPIPE).
  2. Ignorant el senyal amb signal(SIGPIPE, SIG_IGN) abans del write. Aleshores write retorna -1, errno == EPIPE, i s’imprimeix r=-1.
  3. head llegeix una línia i acaba; això tanca l’únic lector. El yes continua escrivint, el següent write rep SIGPIPE i yes mor. Sense SIGPIPE, yes escriuria per sempre a una pipe que ningú no llegeix.

Bloc 3 · Pipelines amb exec

Exercici

Completa el programa que executa ls | wc -l.

int main(void) {
    int p[2];
    pipe(p);

    if (fork() == 0) {                       // fill 1: ls
        dup2(______(1)______, ______(2)______);
        close(p[0]);
        close(p[1]);
        execlp("ls", "ls", NULL);
        perror("execlp");
        _exit(127);
    }

    if (fork() == 0) {                       // fill 2: wc -l
        dup2(______(3)______, ______(4)______);
        close(p[0]);
        close(p[1]);
        execlp("wc", "wc", "-l", NULL);
        perror("execlp");
        _exit(127);
    }

    ______(5)______;
    wait(NULL);
    wait(NULL);
    return 0;
}
  1. Omple els espais.
  2. Per què cada fill tanca p[0] i p[1] després del dup2?
  3. Què passa si el pare s’oblida de (5)? Quants descriptors té oberts wc en el moment de l’exec?
Pots veure el raonament
Solució
  1. p[1]
  2. 1 (o STDOUT_FILENO)
  3. p[0]
  4. 0 (o STDIN_FILENO)
  5. close(p[0]); close(p[1])

Després del dup2, l’original de la pipe és un duplicat que sobra. Si el fill 1 no tanqués p[1] quedarien dos extrems d’escriptura oberts al seu nom (el 1 i el p[1]); si el fill 2 no tanqués p[1], wc seria escriptor de la seva pròpia pipe i mai no veuria l’EOF.

Si el pare no tanca els seus extrems, conserva un extrem d’escriptura obert: ls acaba, però wc es queda esperant perquè encara hi ha un escriptor (el pare). El programa es penja al segon wait. En el moment de l’exec, wc ha de tenir només 3 descriptors: 0 (la pipe), 1 i 2. Es pot comprovar substituint wc per sleep 100 i mirant ls -l /proc/<pid>/fd.

sequenceDiagram
    autonumber
    participant P as Pare
    participant L as Fill ls
    participant W as Fill wc

    P->>P: pipe crea el canal
    P->>L: fork
    L->>L: dup2 de l'escriptura a stdout i tanca la pipe
    L->>L: exec ls
    P->>W: fork
    W->>W: dup2 de la lectura a stdin i tanca la pipe
    W->>W: exec wc
    P->>P: tanca les dues puntes
    L-->>W: ls escriu i acaba
    Note over W: tots els extrems d'escriptura tancats, read retorna 0
    W->>W: wc escriu el recompte i acaba


Exercici

Un filtre amb descriptors personalitzats. Escriu un programa filtre que no usa STDIN ni STDOUT per a les dades: llegeix del descriptor 3, converteix el text a majúscules i l’escriu al descriptor 4. En acabar, imprimeix per STDOUT quants bytes ha processat. S’ha d’executar així:

./filtre 3< entrada.txt 4> sortida.txt
  1. Qui obre entrada.txt i sortida.txt? Per què filtre no necessita cap open?
  2. Què passa si s’executa ./filtre sense les redireccions? Com ho pot detectar el programa?
  3. Repte: què fa ./filtre 3< entrada.txt 4>&1 | cat? En quin ordre surten les dades i el recompte?
Pots veure el raonament
Solució
int main(void) {
    char buf[256];
    ssize_t n;
    long total = 0;

    while ((n = read(3, buf, sizeof buf)) > 0) {
        for (ssize_t i = 0; i < n; i++)
            buf[i] = toupper((unsigned char)buf[i]);
        write(4, buf, n);
        total += n;
    }
    if (n == -1) {
        perror("filtre: read al descriptor 3");
        return 1;
    }
    printf("filtre: %ld bytes processats\n", total);
    return 0;
}
  1. El shell. Per a cada redirecció fa open + dup2(fd, 3) (o 4) en el fill, abans de l’exec, i com que els descriptors sobreviuen a l’exec, filtre troba el 3 i el 4 ja oberts. Per al programa, un descriptor és només un número: no sap (ni li importa) a què apunta.
  2. Els descriptors 3 i 4 no existeixen: read(3, ...) retorna -1 amb EBADF (el programa imprimeix Bad file descriptor i acaba amb codi 1). També es pot comprovar explícitament amb fcntl(3, F_GETFD) == -1.
  3. El 4>&1 fa que el descriptor 4 apunti on apunta el 1, és a dir a la pipe cap a cat. Surt primer el text en majúscules (escrit pel write(4)) i després el recompte, perquè el printf queda al buffer d’stdio fins que el programa acaba.

Exercici

Implementa en C l’ordre cat < /etc/passwd | grep root amb un pare i dos fills. Has d’usar obligatòriament les crides execlp("cat", "cat", NULL) i execlp("grep", "grep", "root", NULL).

  1. Qui és el responsable de la redirecció <? Com ha de fer-ho el teu programa?
  2. Variant: el filtre ha de ser dinàmic: ./programa root. Què canvia? Què passa si l’usuari passa un patró que comença per -?
  3. Repte: fes que el programa retorni el mateix codi de sortida que grep (0 si ha trobat alguna cosa, 1 si no, 2 si hi ha error).
Pots veure el raonament
Solució
  1. En un shell, la redirecció < la fa el shell (open + dup2) abans de l’exec: cat no sap que llegeix d’un fitxer, només llegeix del descriptor 0. Per això aquí el fill de cat ha d’obrir /etc/passwd i fer dup2(fd, 0) abans de l’exec.

Solució completa (ja amb el filtre dinàmic, del punt 2):

int main(int argc, char *argv[]) {
    if (argc != 2) {
        fprintf(stderr, "us: %s filtre\n", argv[0]);
        return 1;
    }

    int p[2];
    if (pipe(p) == -1) { perror("pipe"); exit(1); }

    pid_t pid_cat = fork();
    if (pid_cat == 0) {
        int fd = open("/etc/passwd", O_RDONLY);
        if (fd == -1) { perror("open"); _exit(1); }
        dup2(fd, 0);
        dup2(p[1], 1);
        close(fd);
        close(p[0]);
        close(p[1]);
        execlp("cat", "cat", NULL);
        perror("execlp cat");
        _exit(127);
    }

    pid_t pid_grep = fork();
    if (pid_grep == 0) {
        dup2(p[0], 0);
        close(p[0]);
        close(p[1]);
        execlp("grep", "grep", "--", argv[1], NULL);
        perror("execlp grep");
        _exit(127);
    }

    close(p[0]);
    close(p[1]);

    int st, codi = 1;
    waitpid(pid_cat, NULL, 0);
    waitpid(pid_grep, &st, 0);
    if (WIFEXITED(st))
        codi = WEXITSTATUS(st);
    return codi;
}
  1. Amb el filtre fix, l’últim argument és la cadena literal "root"; amb el dinàmic és argv[1], i cal comprovar argc. Si argv[1] comença per - (per exemple -v), grep l’interpretaria com una opció; el "--" fa que el que ve després es tracti com a patró.
  2. Cal guardar el PID de grep i recollir-lo amb waitpid, que dona l’estat. Un wait(NULL) no serveix, perquè no sabríem quin dels dos fills ha acabat. L’ordre dels dos fork és irrellevant: la pipe sincronitza els processos.

Exercici

Implementa en C head -n 10 < infile | sort -n | tail -n 3 > outfile. Necessites 3 fills i 2 pipes.

  1. Quants extrems de pipe hi ha en total? Quins ha de tancar cada procés?
  2. Quants descriptors ha de tenir obert cada fill en el moment de l’exec?
  3. Repte: generalitza el programa a N ordres passades per línia d’ordres, amb un bucle.
Pots veure el raonament
Solució
  1. 2 pipes × 2 extrems = 4 extrems. Cada fill fa els dup2 que necessita i tanca tots quatre (i el descriptor del fitxer, si n’ha obert un). El pare, que no usa les pipes, també tanca els quatre.
  2. Només 3 (0, 1, 2). Qualsevol altre descriptor és una fuita, i pot impedir un EOF (vegeu l’exercici 5).
static void tanca(int p[2]) {
    close(p[0]);
    close(p[1]);
}

int main(void) {
    int p1[2], p2[2];
    if (pipe(p1) == -1 || pipe(p2) == -1) { perror("pipe"); exit(1); }

    if (fork() == 0) {                                   // head
        int in = open("infile", O_RDONLY);
        if (in == -1) { perror("open infile"); _exit(1); }
        dup2(in, 0);
        dup2(p1[1], 1);
        close(in);
        tanca(p1);
        tanca(p2);
        execlp("head", "head", "-n", "10", NULL);
        perror("execlp head");
        _exit(127);
    }

    if (fork() == 0) {                                   // sort
        dup2(p1[0], 0);
        dup2(p2[1], 1);
        tanca(p1);
        tanca(p2);
        execlp("sort", "sort", "-n", NULL);
        perror("execlp sort");
        _exit(127);
    }

    if (fork() == 0) {                                   // tail
        int out = open("outfile", O_WRONLY | O_CREAT | O_TRUNC, 0644);
        if (out == -1) { perror("open outfile"); _exit(1); }
        dup2(p2[0], 0);
        dup2(out, 1);
        close(out);
        tanca(p1);
        tanca(p2);
        execlp("tail", "tail", "-n", "3", NULL);
        perror("execlp tail");
        _exit(127);
    }

    tanca(p1);
    tanca(p2);
    for (int i = 0; i < 3; i++)
        wait(NULL);
    return 0;
}
  1. Idea: el pare manté una variable prev amb l’extrem de lectura de la pipe anterior (al principi, -1, que vol dir el stdin del programa). A cada iteració crea una pipe nova (excepte a l’última ordre), fa fork, i el fill fa dup2(prev, 0) si prev != -1 i dup2(p[1], 1) si no és l’última. Després de cada fork, el pare tanca prev i p[1], i passa prev = p[0]. Cal vigilar de no tancar el 0 quan prev coincideix amb ell.

Exercici

Comunicació bidireccional sense shell. Escriu un programa pare que llança el ./filtre de l’exercici anterior sense usar el shell: el descriptor 3 del fill ha d’estar connectat a una pipe on escriu el pare, i el 4 a una altra pipe d’on llegeix el pare. El pare envia hola mon\n, mostra la resposta per pantalla i imprimeix el codi de sortida del fill.

  1. Quins números tenen els descriptors de les dues pipes abans del fork?
  2. Per què falla la versió ingènua dup2(pin[0], 3); close(pin[0]); dins del fill?
  3. Quan acaba filtre? Qui ha de tancar què perquè això passi?
  4. Repte: el pare envia 1 MiB de dades abans de començar a llegir. Què passa?
Pots veure el raonament
Solució
int main(void) {
    int pin[2], pout[2];
    if (pipe(pin) == -1 || pipe(pout) == -1) { perror("pipe"); exit(1); }

    pid_t pid = fork();
    if (pid == -1) { perror("fork"); exit(1); }
    if (pid == 0) {
        close(pin[1]);                       // el fill no escriu a pin
        close(pout[0]);                      // ni llegeix de pout
        if (pin[0] != 3)  { dup2(pin[0], 3);  close(pin[0]); }
        if (pout[1] != 4) { dup2(pout[1], 4); close(pout[1]); }
        execl("./filtre", "filtre", NULL);
        perror("execl filtre");
        _exit(127);
    }
    close(pin[0]);
    close(pout[1]);

    const char *msg = "hola mon\n";
    write(pin[1], msg, strlen(msg));
    close(pin[1]);                           // així filtre veurà l'EOF

    char buf[256];
    ssize_t n;
    while ((n = read(pout[0], buf, sizeof buf)) > 0)
        write(1, buf, n);
    close(pout[0]);

    int st;
    waitpid(pid, &st, 0);
    printf("filtre ha acabat amb codi %d\n", WEXITSTATUS(st));
    return 0;
}
  1. pin = {3, 4} i pout = {5, 6}: justament els números que el fill necessita com a 3 i 4, i això fa que sigui delicat.
  2. Amb pin[0] == 3, dup2(pin[0], 3) no fa res i el close(pin[0]) següent tanca el descriptor 3, el mateix que s’acabava de preparar. filtre fa read(3, ...), rep EBADF i acaba, i el pare mor amb SIGPIPE en escriure a una pipe sense lector. Per això només es fa dup2 + close quan el número és diferent. A més, el dup2(pout[1], 4) tanca el 4, que era pin[1]: per això el fill tanca primer pin[1] i pout[0].
  3. filtre acaba quan read(3) retorna 0, és a dir quan tots els extrems d’escriptura de pin estan tancats: el del pare (close(pin[1])) i el que heretava el fill (que tanca abans de l’exec). El pare, al seu torn, deixa de llegir quan filtre acaba i tanca el 4; per això el pare també ha de tancar el seu pout[1].
  4. Interbloqueig. El pare escriu a pin i filtre converteix i escriu a pout; com que el pare no llegeix, pout s’omple (64 KiB) i filtre es bloqueja al write(4); ja no llegeix de pin, que també s’omple, i el pare es bloqueja al write. Cap dels dos no pot avançar. Es resol llegint i escrivint alhora, per exemple amb poll/select, o amb un segon procés (o fil) que s’encarregui d’una de les dues direccions. És el mateix problema de l’exercici 8, ara amb dues pipes.

Bloc 4 · FIFOs

Exercici

Obre dos terminals.

# Terminal A
mkfifo /tmp/f
echo hola > /tmp/f
echo fet

# Terminal B (10 segons després d'haver llançat l'ordre del terminal A)
cat /tmp/f
  1. En quin moment s’imprimeix fet al terminal A?
  2. Què mostra ls -l /tmp/f? Es guarda hola al disc?
  3. Repte: què passa si al terminal A fem exec 3<> /tmp/f en lloc del echo?
Pots veure el raonament
Solució
  1. Quan el terminal B executa cat, uns 10 s després. El shell del terminal A es bloqueja a l’open de la redirecció > /tmp/f, perquè un open per escriure en un FIFO espera que algú l’obri per llegir. Si ningú no llegeix, el terminal A es queda bloquejat indefinidament (Ctrl-C per sortir). Si invertim l’ordre i cat va primer, és cat qui espera l’escriptor.
  2. El tipus del fitxer és p (prw-------…) i la mida és 0. hola passa pel buffer del nucli, igual que en una pipe anònima; el fitxer és només un nom que permet que processos no emparentats trobin la mateixa pipe.
  3. En Linux, obrir el FIFO en mode lectura i escriptura (<>) no es bloqueja, perquè el mateix procés fa de lector i d’escriptor. Aquest comportament no està especificat per POSIX, així que no s’ha de fer servir en codi portable.

Exercici

Escriu dos programes independents: escriptor llegeix línies de stdin i les envia a un FIFO /tmp/so_fifo; lector les llegeix del FIFO i les mostra per pantalla. Cap dels dos no és fill de l’altre.

  1. Què passa si s’executa primer el lector? I si s’executa primer l’escriptor?
  2. Quan acaba el lector?
  3. Dos escriptors alhora envien línies de com a molt 255 bytes. Es poden barrejar els bytes de dues línies?
  4. Qui hauria d’eliminar el FIFO?
Pots veure el raonament
Solució

escriptor.c:

#define FIFO "/tmp/so_fifo"

int main(void) {
    if (mkfifo(FIFO, 0600) == -1 && errno != EEXIST) { perror("mkfifo"); exit(1); }

    int fd = open(FIFO, O_WRONLY);
    if (fd == -1) { perror("open"); exit(1); }

    char linia[256];
    while (fgets(linia, sizeof linia, stdin) != NULL)
        write(fd, linia, strlen(linia));

    close(fd);
    return 0;
}

lector.c:

#define FIFO "/tmp/so_fifo"

int main(void) {
    if (mkfifo(FIFO, 0600) == -1 && errno != EEXIST) { perror("mkfifo"); exit(1); }

    int fd = open(FIFO, O_RDONLY);
    if (fd == -1) { perror("open"); exit(1); }

    char buf[256];
    ssize_t n;
    while ((n = read(fd, buf, sizeof buf)) > 0)
        write(1, buf, n);

    close(fd);
    unlink(FIFO);
    return 0;
}
  1. Els dos programes fan mkfifo tolerant EEXIST, així que l’ordre d’arrencada és indiferent: el primer que arriba el crea i es bloqueja a l’open fins que arriba l’altre. Si només l’escriptor el creés, un lector arrencat primer fallaria amb ENOENT.
  2. Quan read retorna 0, és a dir quan tots els escriptors han tancat el FIFO (per exemple, amb Ctrl-D a l’escriptor).
  3. No, en les línies de com a molt PIPE_BUF bytes (4096 a Linux), perquè cada write és atòmic. L’ordre en què arriben les línies sí que és arbitrari. Amb línies més llargues, els trossos de cada write s’intercalarien.
  4. El FIFO persisteix al sistema de fitxers encara que acabin tots els processos. Algú l’ha d’eliminar amb unlink; aquí ho fa el lector en acabar. Si l’escriptor es torna a executar després, tornarà a crear-lo.

Bloc 5 · Integració

Exercici

Multiplicació de matrius amb processos. Volem calcular C = A × B amb matrius quadrades N × N. El pare crea un fill per cada fila de C. Cada fill calcula la seva fila i l’envia al pare per una pipe; el pare recull les files i imprimeix C.

Per comprovar-ho, usa N = 4, A[i][j] = i + j i B la matriu identitat: el resultat ha de ser igual a A.

  1. Els fills necessiten que el pare els enviï A i B? Per què?
  2. Quin ha de ser el contingut de cada missatge? Fins a quin N són atòmics els write?
  3. Una pipe per fill o una de compartida? Compara’ls.
  4. Repte: el pare fa wait de tots els fills abans de llegir. Per a quin N deixa de funcionar?
  5. Repte: si s’usés un FIFO en comptes d’una pipe anònima, què canviaria, i quin problema nou apareixeria amb l’EOF?
Pots veure el raonament
Solució
  1. No. Les matrius s’omplen abans del fork, i el fill hereta una còpia de tota la memòria (copy-on-write). Només s’ha de comunicar el resultat. Llegir les matrius d’un fitxer és una feina independent (fscanf), i s’ha de fer abans del fork.
  2. Cada missatge porta l’índex de la fila i els N valors, perquè les files arriben desordenades. Amb int, el missatge fa 4 + 4N bytes, i és atòmic mentre 4 + 4N ≤ PIPE_BUF = 4096, és a dir fins a N = 1023.
#define N 4

typedef struct {
    int fila;
    int valors[N];
} fila_t;

int A[N][N], B[N][N], C[N][N];

int main(void) {
    for (int i = 0; i < N; i++)
        for (int j = 0; j < N; j++) {
            A[i][j] = i + j;
            B[i][j] = (i == j) ? 1 : 0;
        }

    int p[2];
    if (pipe(p) == -1) { perror("pipe"); exit(1); }

    for (int i = 0; i < N; i++) {
        pid_t pid = fork();
        if (pid == -1) { perror("fork"); exit(1); }
        if (pid == 0) {
            close(p[0]);
            fila_t r;
            r.fila = i;
            for (int j = 0; j < N; j++) {
                r.valors[j] = 0;
                for (int k = 0; k < N; k++)
                    r.valors[j] += A[i][k] * B[k][j];
            }
            write(p[1], &r, sizeof r);
            close(p[1]);
            exit(0);
        }
    }
    close(p[1]);

    fila_t r;
    while (read(p[0], &r, sizeof r) == (ssize_t)sizeof r)
        memcpy(C[r.fila], r.valors, sizeof r.valors);
    close(p[0]);
    while (wait(NULL) > 0)
        ;

    for (int i = 0; i < N; i++) {
        for (int j = 0; j < N; j++)
            printf("%3d", C[i][j]);
        printf("\n");
    }
    return 0;
}
  1. Una pipe compartida: només 2 descriptors, sense límit pràctic de fills, i el pare fa un únic bucle de read; cal l’índex de fila. Una pipe per fill: el pare sap qui li envia cada fila (no cal índex), però necessita 2N descriptors, i el límit típic és ulimit -n = 1024, així que fallaria cap a N ≈ 500; a més, cal llegir-les en algun ordre i un fill lent pot fer esperar els altres.
  2. Interbloqueig quan el total de dades, N × (4 + 4N) bytes aproximadament, supera la capacitat de la pipe (64 KiB): a partir de N = 128, algun fill es bloqueja al write i el pare és al wait (vegeu l’exercici 8). Per això el programa llegeix fins a l’EOF i fa wait després.
  3. Amb un FIFO, cada fill hauria de fer open(ruta, O_WRONLY) pel seu compte, i el pare open(ruta, O_RDONLY), que es bloqueja fins que arriba el primer escriptor. El problema nou: el pare veu l’EOF quan l’últim escriptor tanca, i si el primer fill acaba abans que el segon hagi obert el FIFO, el pare veu un EOF prematur i perd files. La solució és comptar els missatges (N files) en lloc de dependre de l’EOF. En definitiva, un FIFO no aporta res aquí, perquè els processos ja estan emparentats.