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
Exercicis de Pipes, FIFOs i dup en C
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.
- 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.
openretorna sempre el descriptor lliure més petit. - Els descriptors 0, 1 i 2 són
STDIN,STDOUTiSTDERR: 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 amb3< fitxero4> fitxer). forkcopia la taula: les entrades del fill apunten a les mateixes descripcions que les del pare (comparteixen offset).execconserva els descriptors oberts.dup(fd)retorna el descriptor lliure més petit que apunta al mateix quefd.dup2(old, new)fa quenewapunti al mateix queold, tancantnewabans si estava obert. Els dos descriptors comparteixen l’offset.pipe(p)crea un canal al nucli:p[0]és l’extrem de lectura ip[1]el d’escriptura.readd’una pipe buida: es bloqueja mentre hi hagi algun extrem d’escriptura obert, i retorna0(EOF) només quan tots els extrems d’escriptura (de qualsevol procés) estan tancats.writeen una pipe sense cap lector: el procés repSIGPIPE(o-1ambEPIPEsi s’ignora). Si la pipe és plena,writees bloqueja.- La capacitat d’una pipe a Linux és, per defecte, 64 KiB. Un
writede fins aPIPE_BUFbytes (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 (tipusp), però les dades no es guarden al disc.opend’un FIFO es bloqueja fins que l’altre extrem també s’obre.
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
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);- Quins descriptors comparteixen l’offset?
- Si ara fem
read(0, buf, 10)i desprésread(3, buf, 10), el segonreadllegeix els mateixos 10 bytes que el primer? - Repte: on escriurà ara
perror?
Pots veure el raonament
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 |
- Comparteixen offset els que apunten a la mateixa descripció oberta: {0, 3} (
x.txt) i {2, 5} (y.txt). - No: el segon
readllegeix els 10 bytes següents, perquè0i3comparteixen l’offset. Si haguéssim fet dosopenindependents dex.txt, cadascun tindria el seu offset i tots dos llegirien els mateixos 10 bytes. - A
y.txt:perrorescriu per descriptor2, i ara el2apunta ay.txt. Eldup2va tancar l’STDERRoriginal.
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;- Què apareix per pantalla? Què conté
out.txti en quin ordre? - Repte: què canvia si movem el
dup2(i elclose) després deprintf("A\n")?
Pots veure el raonament
Pantalla: res.
out.txtcontéB,A,C(en aquest ordre).Quan
printfescriu per primera vegada,stdoutja apunta a un fitxer, istdiotria buffering complet.A\niC\nes queden al buffer; elwrite(1, "B\n", 2)va directe al nucli i surt primer. El buffer es buida en acabar el programa (return 0), i hi surtA\nC\n.Ara el primer
printf("A\n")s’executa ambstdoutal terminal, istdiotria line buffering:Asurt per pantalla a l’instant. Aquest mode ja no canvia encara que després el descriptor 1 apunti a un fitxer, així queprintf("C\n")també s’escriu de seguida. Pantalla:A.out.txt:B,C.
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.
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);- En cada cas, què surt per pantalla i què hi ha a
out.txt? - Quin cas equival a
> out.txt 2>&1i quin a2>&1 > out.txt?
Pots veure el raonament
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): el1apunta aout.txt.dup2(1, 2): el2apunta on apunta el1ara, és a dirout.txt. Resultat: pantalla buida, iout.txtconté/etc/hostnamei el missatge d’error dels. Equival a> out.txt 2>&1. - Cas B.
dup2(1, 2): el2apunta al terminal (on apunta el1encara). Desprésdup2(fd, 1): el1passa aout.txt, però el2no es mou. Resultat: el missatge d’error surt per pantalla iout.txtconté només/etc/hostname. Equival a2>&1 > out.txt.
L’ordre importa perquè dup2 copia el que hi ha en aquell moment, no crea cap enllaç entre els dos descriptors.
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);- Quin valor té
t? Escriu la taula de descriptors del fill després de cada línia. - Què apareix per pantalla i quin número imprimeix
wc -l? - Per què cal la variable
t? Què passaria ambdup2(2, 1); dup2(1, 2);? - Repte: escriu l’ordre del shell equivalent amb els descriptors
3>&1 1>&2 2>&3 3>&-. Per a què serveix el3>&-final?
Pots veure el raonament
t = 3: el3é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) | — |
lsescriu/etc/hostnameperSTDOUT, que ara és el terminal, i el missatge d’error perSTDERR, que ara és la pipe. Per pantalla apareix/etc/hostname, iwc -limprimeix1(la línia d’error).tés una còpia de seguretat de l’extrem de la pipe: sense ella, el primerdup2(2, 1)sobreescriu el descriptor 1 i ja no queda cap referència a la pipe. Ambdup2(2, 1); dup2(1, 2);els dos descriptors acaben apuntant al terminal,wc -lno rep res i imprimeix0. És el mateix problema d’intercanviar dues variables: cal una temporal.ls /noexisteix /etc/hostname 3>&1 1>&2 2>&3 3>&- | wc -l. El3>&-tanca el3perquèlsno el rebi: és elclose(t)del programa en C.
Bloc 2 · Semàntica de les pipes
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);- Quins valors tenen
p[0]ip[1]? Què imprimeix el programa? - Què passa si traiem el
close(p[1])del pare? - Què passa si traiem el
close(p[1])del fill?
Pots veure el raonament
p[0] = 3ip[1] = 4. ImprimeixABC(sense salt de línia): el fill llegeix byte a byte fins quereadretorna0.- Interbloqueig. El
readdel fill només retorna0quan tots els extrems d’escriptura estan tancats, i el del pare continua obert. El fill es queda bloquejat alread, i el pare alwaitesperant el fill. - 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.
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.
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
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ò el4éspipefd[0]i ja l’ha tancat:writeretorna-1ambEBADFi no s’escriu res. L’extrem d’escriptura és el5. - Després el fill entra al
while(1), sense tancar el5. El pare fareadd’una pipe buida amb un extrem d’escriptura obert (el del fill): es bloqueja per sempre. - Encara que corregíssim el
write(4)perwrite(5), el pare imprimiriaHola, però quedaria penjat alwait(NULL)perquè el fill mai no acaba. - Hi ha un segon error:
readno escriu un'\0', així quesprintf("%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è llavorspipeocuparia el1.
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);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);- Què passa quan s’executa? Quin codi de sortida veu el shell (
echo $?)? - Com podem fer que
writeretorni-1en lloc de matar el procés? Què valerrno? - Per què acaba l’ordre
yes | head -n 1? Què passaria si no existís el mecanisme anterior?
Pots veure el raonament
- El procés mor amb
SIGPIPEen elwritei no arriba alprintf. El shell veu el codi 141 (128 + 13, on 13 és el número deSIGPIPE). - Ignorant el senyal amb
signal(SIGPIPE, SIG_IGN)abans delwrite. Aleshoreswriteretorna-1,errno == EPIPE, i s’imprimeixr=-1. headllegeix una línia i acaba; això tanca l’únic lector. Elyescontinua escrivint, el següentwriterepSIGPIPEiyesmor. SenseSIGPIPE,yesescriuria per sempre a una pipe que ningú no llegeix.
Bloc 3 · Pipelines amb exec
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;
}- Omple els espais.
- Per què cada fill tanca
p[0]ip[1]després deldup2? - Què passa si el pare s’oblida de (5)? Quants descriptors té oberts
wcen el moment de l’exec?
Pots veure el raonament
p[1]1(oSTDOUT_FILENO)p[0]0(oSTDIN_FILENO)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.
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- Qui obre
entrada.txtisortida.txt? Per quèfiltreno necessita capopen? - Què passa si s’executa
./filtresense les redireccions? Com ho pot detectar el programa? - Repte: què fa
./filtre 3< entrada.txt 4>&1 | cat? En quin ordre surten les dades i el recompte?
Pots veure el raonament
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;
}- El shell. Per a cada redirecció fa
open+dup2(fd, 3)(o4) en el fill, abans de l’exec, i com que els descriptors sobreviuen a l’exec,filtretroba el3i el4ja oberts. Per al programa, un descriptor és només un número: no sap (ni li importa) a què apunta. - Els descriptors
3i4no existeixen:read(3, ...)retorna-1ambEBADF(el programa imprimeixBad file descriptori acaba amb codi 1). També es pot comprovar explícitament ambfcntl(3, F_GETFD) == -1. - El
4>&1fa que el descriptor4apunti on apunta el1, és a dir a la pipe cap acat. Surt primer el text en majúscules (escrit pelwrite(4)) i després el recompte, perquè elprintfqueda al buffer d’stdiofins que el programa acaba.
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).
- Qui és el responsable de la redirecció
<? Com ha de fer-ho el teu programa? - Variant: el filtre ha de ser dinàmic:
./programa root. Què canvia? Què passa si l’usuari passa un patró que comença per-? - 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
- En un shell, la redirecció
<la fa el shell (open+dup2) abans de l’exec:catno sap que llegeix d’un fitxer, només llegeix del descriptor 0. Per això aquí el fill decatha d’obrir/etc/passwdi ferdup2(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;
}- Amb el filtre fix, l’últim argument és la cadena literal
"root"; amb el dinàmic ésargv[1], i cal comprovarargc. Siargv[1]comença per-(per exemple-v),grepl’interpretaria com una opció; el"--"fa que el que ve després es tracti com a patró. - Cal guardar el PID de
grepi recollir-lo ambwaitpid, que dona l’estat. Unwait(NULL)no serveix, perquè no sabríem quin dels dos fills ha acabat. L’ordre dels dosforkés irrellevant: la pipe sincronitza els processos.
Implementa en C head -n 10 < infile | sort -n | tail -n 3 > outfile. Necessites 3 fills i 2 pipes.
- Quants extrems de pipe hi ha en total? Quins ha de tancar cada procés?
- Quants descriptors ha de tenir obert cada fill en el moment de l’
exec? - Repte: generalitza el programa a N ordres passades per línia d’ordres, amb un bucle.
Pots veure el raonament
- 2 pipes × 2 extrems = 4 extrems. Cada fill fa els
dup2que 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. - 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;
}- Idea: el pare manté una variable
prevamb l’extrem de lectura de la pipe anterior (al principi,-1, que vol dir elstdindel programa). A cada iteració crea una pipe nova (excepte a l’última ordre), fafork, i el fill fadup2(prev, 0)siprev != -1idup2(p[1], 1)si no és l’última. Després de cadafork, el pare tancaprevip[1], i passaprev = p[0]. Cal vigilar de no tancar el0quanprevcoincideix amb ell.
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.
- Quins números tenen els descriptors de les dues pipes abans del
fork? - Per què falla la versió ingènua
dup2(pin[0], 3); close(pin[0]);dins del fill? - Quan acaba
filtre? Qui ha de tancar què perquè això passi? - Repte: el pare envia 1 MiB de dades abans de començar a llegir. Què passa?
Pots veure el raonament
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;
}pin = {3, 4}ipout = {5, 6}: justament els números que el fill necessita com a3i4, i això fa que sigui delicat.- Amb
pin[0] == 3,dup2(pin[0], 3)no fa res i elclose(pin[0])següent tanca el descriptor 3, el mateix que s’acabava de preparar.filtrefaread(3, ...), repEBADFi acaba, i el pare mor ambSIGPIPEen escriure a una pipe sense lector. Per això només es fadup2+closequan el número és diferent. A més, eldup2(pout[1], 4)tanca el 4, que erapin[1]: per això el fill tanca primerpin[1]ipout[0]. filtreacaba quanread(3)retorna0, és a dir quan tots els extrems d’escriptura depinestan 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 quanfiltreacaba i tanca el4; per això el pare també ha de tancar el seupout[1].- Interbloqueig. El pare escriu a
pinifiltreconverteix i escriu apout; com que el pare no llegeix,pouts’omple (64 KiB) ifiltrees bloqueja alwrite(4); ja no llegeix depin, que també s’omple, i el pare es bloqueja alwrite. Cap dels dos no pot avançar. Es resol llegint i escrivint alhora, per exemple ambpoll/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
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- En quin moment s’imprimeix
fetal terminal A? - Què mostra
ls -l /tmp/f? Es guardaholaal disc? - Repte: què passa si al terminal A fem
exec 3<> /tmp/fen lloc delecho?
Pots veure el raonament
- Quan el terminal B executa
cat, uns 10 s després. El shell del terminal A es bloqueja a l’opende la redirecció> /tmp/f, perquè unopenper escriure en un FIFO espera que algú l’obri per llegir. Si ningú no llegeix, el terminal A es queda bloquejat indefinidament (Ctrl-Cper sortir). Si invertim l’ordre icatva primer, éscatqui espera l’escriptor. - El tipus del fitxer és
p(prw-------…) i la mida és 0.holapassa 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. - 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.
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.
- Què passa si s’executa primer el lector? I si s’executa primer l’escriptor?
- Quan acaba el lector?
- Dos escriptors alhora envien línies de com a molt 255 bytes. Es poden barrejar els bytes de dues línies?
- Qui hauria d’eliminar el FIFO?
Pots veure el raonament
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;
}- Els dos programes fan
mkfifotolerantEEXIST, així que l’ordre d’arrencada és indiferent: el primer que arriba el crea i es bloqueja a l’openfins que arriba l’altre. Si només l’escriptor el creés, un lector arrencat primer fallaria ambENOENT. - Quan
readretorna0, és a dir quan tots els escriptors han tancat el FIFO (per exemple, ambCtrl-Da l’escriptor). - No, en les línies de com a molt
PIPE_BUFbytes (4096 a Linux), perquè cadawriteés atòmic. L’ordre en què arriben les línies sí que és arbitrari. Amb línies més llargues, els trossos de cadawrites’intercalarien. - 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ó
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.
- Els fills necessiten que el pare els enviï
AiB? Per què? - Quin ha de ser el contingut de cada missatge? Fins a quin
Nsón atòmics elswrite? - Una pipe per fill o una de compartida? Compara’ls.
- Repte: el pare fa
waitde tots els fills abans de llegir. Per a quinNdeixa de funcionar? - 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
- 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 delfork. - Cada missatge porta l’índex de la fila i els
Nvalors, perquè les files arriben desordenades. Ambint, el missatge fa4 + 4Nbytes, i és atòmic mentre4 + 4N ≤ PIPE_BUF = 4096, és a dir fins aN = 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;
}- 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ò necessita2Ndescriptors, i el límit típic ésulimit -n = 1024, així que fallaria cap aN ≈ 500; a més, cal llegir-les en algun ordre i un fill lent pot fer esperar els altres. - Interbloqueig quan el total de dades,
N × (4 + 4N)bytes aproximadament, supera la capacitat de la pipe (64 KiB): a partir deN = 128, algun fill es bloqueja alwritei el pare és alwait(vegeu l’exercici 8). Per això el programa llegeix fins a l’EOF i fawaitdesprés. - Amb un FIFO, cada fill hauria de fer
open(ruta, O_WRONLY)pel seu compte, i el pareopen(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 (Nfiles) en lloc de dependre de l’EOF. En definitiva, un FIFO no aporta res aquí, perquè els processos ja estan emparentats.