Objectif
Sous UNIX/Linux, plusieurs mécanismes permettent aux processus et aux threads :
- d’échanger des données ;
- de synchroniser leurs actions ;
- de se notifier des événements.
On peut regrouper les IPC en trois grandes familles :
Code: Select all
Communication
Synchronisation
Signaux
1. Taxonomie générale des IPC
Les mécanismes IPC principaux sous UNIX/Linux comprennent :
Communication :
Code: Select all
Pipes
FIFO
Sockets UNIX
Sockets réseau
Files de messages System V
Files de messages POSIX
Mémoire partagée System V
Mémoire partagée POSIX
Memory mapping
Code: Select all
Sémaphores System V
Sémaphores POSIX
Verrous de fichiers
Mutex
Variables de condition
Code: Select all
Signaux standards
Signaux temps réel
2. Communication par transfert de données
Certains mécanismes IPC déplacent explicitement les données d’un processus vers un autre.
Exemples :
Code: Select all
pipe
FIFO
socket
message queue
Code: Select all
Processus A
|
| write()
v
[buffer noyau]
|
| read()
v
Processus B
Cela implique généralement :
- un appel système pour écrire ;
- une copie depuis l’espace utilisateur vers le noyau ;
- un appel système pour lire ;
- une copie du noyau vers l’espace utilisateur du lecteur.
3. Communication par mémoire partagée
Avec la mémoire partagée, les deux processus accèdent directement à une même zone mémoire.
Code: Select all
Processus A ----\
> [Mémoire partagée]
Processus B ----/
Mais il faut généralement ajouter une synchronisation :
Code: Select all
mutex
sémaphore
etc.
4. Byte stream et messages
Certains IPC fonctionnent comme un flux d’octets :
Code: Select all
pipe
FIFO
socket stream
Exemple :
Code: Select all
write("ABC")
write("DEF")
Code: Select all
ABCDEF
Solutions courantes :
- délimiteur ;
- taille en en-tête ;
- taille fixe.
5. Synchronisation
La synchronisation sert à contrôler l’ordre d’exécution et l’accès concurrent aux ressources.
Exemples :
Code: Select all
sémaphore
mutex
verrou de fichier
condition variable
6. Identifiants et handles IPC
Selon le mécanisme utilisé, les objets IPC sont identifiés différemment :
Code: Select all
pipe
-> file descriptor
FIFO
-> pathname + file descriptor
socket UNIX
-> pathname + file descriptor
socket réseau
-> IP + port + file descriptor
System V IPC
-> identifiant numérique
POSIX message queue
-> nom POSIX
POSIX semaphore
-> nom ou sem_t *
shared memory
-> identifiant ou nom
7. Accessibilité
Un pipe anonyme est généralement utilisé entre processus liés :
Code: Select all
pipe()
fork()
D’autres IPC peuvent être retrouvés par nom et utilisés par des processus indépendants :
Code: Select all
FIFO
socket UNIX
message queue
shared memory
8. Persistance des IPC
Persistance processus
L’objet existe tant qu’il reste référencé par des processus.
Exemple :
Code: Select all
pipe
L’objet peut survivre à la fin du processus créateur jusqu’à suppression explicite ou redémarrage.
Persistance filesystem
L’objet continue d’exister comme entrée du système de fichiers.
Exemples :
Code: Select all
FIFO
fichier utilisé avec mmap()
9. Choisir un IPC
Le choix dépend notamment de :
- processus liés ou indépendants ;
- communication locale ou réseau ;
- flux ou messages ;
- volume de données ;
- persistance ;
- synchronisation ;
- portabilité ;
- performance.
Code: Select all
Parent -> enfant
-> pipe
Processus locaux indépendants
-> FIFO ou socket UNIX
Très gros volume
-> mémoire partagée
Messages structurés
-> message queue
Réseau
-> sockets
Chapitre 44 — Pipes et FIFO
Les pipes sont parmi les plus anciens mécanismes IPC UNIX.
Exemple shell :
Code: Select all
ls | wc -l
Code: Select all
ls
|
stdout
|
v
[ PIPE ]
|
stdin
|
v
wc -l
1. Propriétés principales d’un pipe
Un pipe classique :
- est un flux d’octets ;
- est unidirectionnel ;
- possède une capacité limitée ;
- est souvent utilisé entre processus liés ;
- est représenté par deux file descriptors.
Code: Select all
int fd[2];
pipe(fd);
Code: Select all
fd[0] = lecture
fd[1] = écriture
Code: Select all
fd[1] ---> [ PIPE ] ---> fd[0]
écriture lecture
2. pipe()
Prototype :
Code: Select all
#include <unistd.h>
int pipe(int pipefd[2]);
Code: Select all
0 -> succès
-1 -> erreur
Code: Select all
int fd[2];
if (pipe(fd) == -1) {
perror("pipe");
exit(EXIT_FAILURE);
}
3. Pipe + fork()
Cas classique :
Code: Select all
pipe()
fork()
Chaque processus doit fermer celle qu’il n’utilise pas.
4. Parent -> enfant
Code: Select all
int fd[2];
pipe(fd);
pid_t pid = fork();
if (pid == 0) {
close(fd[1]);
char buf[100];
ssize_t n = read(fd[0], buf, sizeof(buf));
close(fd[0]);
} else {
close(fd[0]);
write(fd[1], "hello", 5);
close(fd[1]);
}
Code: Select all
Parent fd[1]
|
v
PIPE
|
v
Enfant fd[0]
5. Fermer les extrémités inutilisées
C’est un point fondamental.
Le lecteur ne reçoit EOF que lorsque toutes les extrémités d’écriture du pipe sont fermées.
Règle :
Code: Select all
lecteur -> close(fd[1])
écrivain -> close(fd[0])
6. Sémantique de read()
Si des données sont présentes :
Code: Select all
read() retourne les octets lus
Code: Select all
read() bloque
Code: Select all
read() retourne 0
Code: Select all
0 = EOF
7. Sémantique de write()
Si le pipe a de la place :
Code: Select all
write() écrit les données
Code: Select all
write() peut bloquer
8. SIGPIPE et EPIPE
Si un processus écrit dans un pipe sans aucun lecteur :
Code: Select all
write()
Code: Select all
SIGPIPE
Code: Select all
SIGPIPE -> termine le processus
Code: Select all
write() retourne -1
errno = EPIPE
9. PIPE_BUF et atomicité
POSIX garantit qu’une écriture de taille :
Code: Select all
<= PIPE_BUF
Cela signifie qu’une écriture ne sera pas mélangée au milieu avec celle d’un autre processus.
Pour :
Code: Select all
> PIPE_BUF
10. Capacité du pipe
Le pipe possède un buffer noyau de taille limitée.
Ne pas confondre :
Code: Select all
PIPE_BUF
-> limite liée à l’atomicité
capacité du pipe
-> quantité totale pouvant être tamponnée
Code: Select all
fcntl(fd, F_GETPIPE_SZ)
fcntl(fd, F_SETPIPE_SZ, taille)
11. Communication bidirectionnelle
Un pipe classique est unidirectionnel.
Pour obtenir :
Code: Select all
Parent <-> Enfant
Code: Select all
pipe1 : parent -> enfant
pipe2 : enfant -> parent
12. Pipe comme mécanisme de synchronisation
Un pipe peut aussi servir uniquement à notifier un événement.
Code: Select all
Enfant travaille
|
| write(pipe)
v
Parent bloqué sur read()
|
v
Parent reprend
13. dup() et dup2()
`dup()` duplique un descripteur vers le plus petit descripteur libre :
Code: Select all
int newfd = dup(oldfd);
Code: Select all
dup2(oldfd, STDOUT_FILENO);
14. Rediriger stdout vers un pipe
Code: Select all
dup2(fd[1], STDOUT_FILENO);
Code: Select all
printf(...)
write(STDOUT_FILENO, ...)
Pour rediriger stdin :
Code: Select all
dup2(fd[0], STDIN_FILENO);
15. Pipeline shell
Pour :
Code: Select all
ls | wc -l
Code: Select all
pipe()
fork() -> ls
dup2(pipe_write, STDOUT_FILENO)
exec(...)
fork() -> wc
dup2(pipe_read, STDIN_FILENO)
exec(...)
parent
ferme les descripteurs inutiles
wait()
16. popen()
`popen()` permet de lancer une commande shell connectée à un pipe.
Prototype :
Code: Select all
#include <stdio.h>
FILE *popen(const char *command, const char *type);
Code: Select all
FILE *fp = popen("ls -l", "r");
char buf[256];
while (fgets(buf, sizeof(buf), fp) != NULL) {
printf("%s", buf);
}
pclose(fp);
Code: Select all
programme <- sortie commande
Code: Select all
programme -> entrée commande
17. pclose()
Prototype :
Code: Select all
int pclose(FILE *stream);
- ferme le pipe ;
- attend la terminaison du processus enfant ;
- retourne son statut.
18. Limites de popen()
`popen()` est pratique mais moins flexible qu’un :
Code: Select all
pipe() + fork() + dup2() + exec()
Code: Select all
/bin/sh -c ...
19. FIFO — named pipe
Une FIFO est un pipe nommé.
Elle possède une entrée dans le système de fichiers :
Code: Select all
/tmp/monfifo
20. mkfifo()
Prototype :
Code: Select all
#include <sys/stat.h>
int mkfifo(const char *pathname, mode_t mode);
Code: Select all
if (mkfifo("/tmp/monfifo", 0600) == -1) {
perror("mkfifo");
}
Code: Select all
open()
read()
write()
close()
21. Lecture d’une FIFO
Code: Select all
int fd = open("/tmp/monfifo", O_RDONLY);
char buf[256];
read(fd, buf, sizeof(buf));
close(fd);
22. Écriture dans une FIFO
Code: Select all
int fd = open("/tmp/monfifo", O_WRONLY);
write(fd, "hello", 5);
close(fd);
23. Suppression d’une FIFO
Une FIFO est supprimée du système de fichiers avec :
Code: Select all
unlink("/tmp/monfifo");
24. Pipe anonyme vs FIFO
Code: Select all
PIPE
- pas de nom
- souvent parent/enfant
- pipe()
- disparaît quand les références disparaissent
FIFO
- nom dans le filesystem
- processus indépendants possibles
- mkfifo()
- open()
- persiste jusqu'à unlink()
25. Blocage de open() sur FIFO
En mode bloquant :
Code: Select all
open(fifo, O_RDONLY)
Et :
Code: Select all
open(fifo, O_WRONLY)
Deux processus utilisant plusieurs FIFO peuvent donc créer un deadlock s’ils les ouvrent dans un mauvais ordre.
26. O_NONBLOCK
On peut ouvrir une FIFO en mode non bloquant :
Code: Select all
open("/tmp/monfifo",
O_RDONLY | O_NONBLOCK);
Code: Select all
open("/tmp/monfifo",
O_WRONLY | O_NONBLOCK);
Code: Select all
O_WRONLY | O_NONBLOCK
Code: Select all
open() -> -1
errno = ENXIO
27. Modifier O_NONBLOCK avec fcntl()
Lire les flags :
Code: Select all
int flags = fcntl(fd, F_GETFL);
Code: Select all
flags |= O_NONBLOCK;
fcntl(fd, F_SETFL, flags);
Code: Select all
flags &= ~O_NONBLOCK;
fcntl(fd, F_SETFL, flags);
28. read() non bloquant
Si aucune donnée n’est disponible mais qu’un écrivain existe :
Code: Select all
read()
-> -1
errno = EAGAIN
Code: Select all
EWOULDBLOCK
Code: Select all
aucune donnée disponible maintenant
29. write() non bloquant
Si le pipe/FIFO n’a pas suffisamment de place :
Code: Select all
write()
- échouer avec `EAGAIN` ;
- effectuer une écriture partielle selon la taille et les conditions.
30. Résumé de read()
Code: Select all
Données présentes
-> retourne des octets
Vide + écrivain présent
-> bloquant : attend
-> non bloquant : EAGAIN
Vide + aucun écrivain
-> retourne 0
-> EOF
31. Résumé de write()
Code: Select all
Lecteur présent + place disponible
-> succès
Lecteur présent + pipe plein
-> bloquant : attend
-> non bloquant : EAGAIN / écriture partielle
Aucun lecteur
-> SIGPIPE
-> EPIPE si signal ignoré/géré
32. FIFO et client/serveur
Architecture classique :
- une FIFO connue du serveur ;
- une FIFO privée par client.
Code: Select all
Client A ----\
Client B -----+--> FIFO serveur --> Serveur
Client C ----/
Serveur --> FIFO client A --> Client A
Serveur --> FIFO client B --> Client B
Serveur --> FIFO client C --> Client C
Code: Select all
PID
nom de sa FIFO
requête
33. Pourquoi une FIFO par client ?
Avec une FIFO de réponse unique, un client pourrait lire la réponse destinée à un autre.
Exemple :
Code: Select all
client 123 -> /tmp/client.123
client 456 -> /tmp/client.456
34. Framing des messages
Les pipes et FIFO sont des byte streams.
Ils ne conservent pas automatiquement les frontières logiques entre messages.
Trois solutions classiques :
Délimiteur
Code: Select all
MESSAGE1\n
MESSAGE2\n
Code: Select all
[taille][payload]
Code: Select all
lit la taille
lit exactement taille octets
Code: Select all
struct request
35. read() peut être partiel
Un :
Code: Select all
read(fd, buf, 100);
Code: Select all
17
Pour lire exactement une taille donnée, il faut boucler.
36. write() peut être partiel
Même principe :
Code: Select all
write(fd, buf, len)
Pattern :
Code: Select all
size_t total = 0;
while (total < len) {
ssize_t n = write(fd,
buf + total,
len - total);
if (n == -1) {
// gérer erreur
break;
}
total += n;
}
37. EINTR
Des appels comme :
Code: Select all
open()
read()
write()
Dans ce cas :
Code: Select all
errno = EINTR
Code: Select all
do {
n = read(fd, buf, size);
} while (n == -1 && errno == EINTR);
38. APIs essentielles
Pour les pipes :
Code: Select all
pipe()
fork()
read()
write()
close()
dup()
dup2()
exec()
wait()
Code: Select all
popen()
pclose()
Code: Select all
mkfifo()
open()
read()
write()
close()
unlink()
Code: Select all
fcntl()
F_GETFL
F_SETFL
O_NONBLOCK
Code: Select all
EAGAIN
EWOULDBLOCK
EPIPE
ENXIO
EINTR
SIGPIPE
39. Carte mentale
Code: Select all
IPC
|
+-- Communication
| |
| +-- pipe
| | |
| | +-- byte stream
| | +-- fd[0] lecture
| | +-- fd[1] écriture
| | +-- parent/enfant
| |
| +-- FIFO
| |
| +-- named pipe
| +-- mkfifo()
| +-- open()
| +-- processus indépendants
|
+-- Redirection
| |
| +-- dup()
| +-- dup2()
|
+-- Shell
| |
| +-- popen()
| +-- pclose()
|
+-- Comportement
| |
| +-- buffer limité
| +-- PIPE_BUF
| +-- EOF
| +-- SIGPIPE / EPIPE
| +-- O_NONBLOCK
| +-- EAGAIN
|
+-- Framing
|
+-- délimiteur
+-- longueur
+-- taille fixe
40. À retenir absolument
- pipe(fd) crée `fd[0]` pour lire et `fd[1]` pour écrire.
- Un pipe est un flux d’octets, pas une file de messages.
- Un pipe classique est unidirectionnel.
- Après `fork()`, les descripteurs du pipe sont hérités.
- Il faut fermer immédiatement les extrémités inutilisées.
- `read()` retourne `0` lorsque tous les écrivains sont fermés et qu’il ne reste plus de données.
- Écrire sans lecteur provoque `SIGPIPE`, ou `EPIPE` si le signal est ignoré/géré.
- Les écritures `<= PIPE_BUF` sont atomiques.
- `PIPE_BUF` n’est pas la capacité totale du pipe.
- `dup2()` permet de brancher stdin/stdout sur un pipe.
- Les pipelines shell utilisent essentiellement `pipe + fork + dup2 + exec`.
- `popen()` encapsule pipe + shell.
- `pclose()` attend aussi la fin du processus enfant.
- Une FIFO est un pipe nommé créé avec `mkfifo()`.
- Une FIFO peut relier des processus indépendants.
- Les ouvertures de FIFO peuvent bloquer en attendant l’autre côté.
- `O_NONBLOCK` change la sémantique de `open`, `read` et `write`.
- `EAGAIN` signifie qu’une opération ne peut pas être satisfaite immédiatement.
- Les pipes/FIFO ne conservent pas les frontières des messages.
- Il faut prévoir les lectures/écritures partielles et `EINTR`.
41. Pattern minimal à connaître par cœur
Code: Select all
int fd[2];
pipe(fd);
pid_t pid = fork();
if (pid == 0) {
close(fd[1]);
char buf[128];
ssize_t n = read(fd[0], buf, sizeof(buf));
close(fd[0]);
} else {
close(fd[0]);
const char *msg = "hello";
write(fd[1], msg, 5);
close(fd[1]);
wait(NULL);
}
Code: Select all
pipe()
|
fork()
|
+-- parent
| close(read)
| write()
| close(write)
|
+-- enfant
close(write)
read()
close(read)
