Vue d’ensemble des communications interprocessus (IPC)

Ce forum est dédié à apprendre le développement de programmes user mode sur Linux

Moderator: Rick

Post Reply
Hydraxx
Site Admin
Posts: 114
Joined: Mon Jan 12, 2026 4:04 pm
Location: France
Contact:

Vue d’ensemble des communications interprocessus (IPC)

Post by Hydraxx »

Vue d’ensemble des communications interprocessus (IPC)

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.
L’ensemble de ces mécanismes est regroupé sous le terme IPC : Interprocess Communication.

On peut regrouper les IPC en trois grandes familles :

Code: Select all

Communication
Synchronisation
Signaux
Les chapitres suivants détaillent chaque mécanisme. Ici, l’objectif est surtout de comprendre quels outils existent, comment ils se comparent et dans quel contexte les utiliser.


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
Synchronisation :

Code: Select all

Sémaphores System V
Sémaphores POSIX
Verrous de fichiers
Mutex
Variables de condition
Signaux :

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
Schéma :

Code: Select all

Processus A
   |
   | write()
   v
[buffer noyau]
   |
   | read()
   v
Processus B
Le noyau sert d’intermédiaire.

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 ----/
Avantage principal : très rapide.

Mais il faut généralement ajouter une synchronisation :

Code: Select all

mutex
sémaphore
etc.
Sinon deux processus peuvent modifier les données simultanément.


4. Byte stream et messages

Certains IPC fonctionnent comme un flux d’octets :

Code: Select all

pipe
FIFO
socket stream
Dans un flux d’octets, les frontières entre les écritures ne représentent pas forcément des messages.

Exemple :

Code: Select all

write("ABC")
write("DEF")
Le lecteur peut récupérer les données comme un seul flux :

Code: Select all

ABCDEF
Il faut donc définir soi-même le framing des messages.

Solutions courantes :
  • délimiteur ;
  • taille en en-tête ;
  • taille fixe.
Les files de messages conservent au contraire les frontières entre messages.


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
Sans synchronisation, deux processus ou threads peuvent provoquer des race conditions.


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()
Les enfants héritent des descripteurs.

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
Persistance noyau

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.
Exemples :

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
Le shell relie la sortie de `ls` à l’entrée de `wc`.

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);
Puis :

Code: Select all

fd[0] = lecture
fd[1] = écriture
Schéma :

Code: Select all

fd[1] ---> [ PIPE ] ---> fd[0]
 écriture              lecture

2. pipe()

Prototype :

Code: Select all

#include <unistd.h>

int pipe(int pipefd[2]);
Retour :

Code: Select all

0  -> succès
-1 -> erreur
Exemple :

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()
Après `fork()`, parent et enfant possèdent les deux extrémités du pipe.

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]);
}
Schéma :

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])
Sinon un `read()` peut rester bloqué alors qu’aucune nouvelle donnée n’arrivera.


6. Sémantique de read()

Si des données sont présentes :

Code: Select all

read() retourne les octets lus
Si le pipe est vide mais qu’un écrivain reste ouvert :

Code: Select all

read() bloque
Si le pipe est vide et que tous les écrivains sont fermés :

Code: Select all

read() retourne 0
Donc :

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
Si le pipe est plein :

Code: Select all

write() peut bloquer
jusqu’à ce que le lecteur libère de la place.


8. SIGPIPE et EPIPE

Si un processus écrit dans un pipe sans aucun lecteur :

Code: Select all

write()
provoque normalement :

Code: Select all

SIGPIPE
Par défaut :

Code: Select all

SIGPIPE -> termine le processus
Si SIGPIPE est ignoré ou intercepté :

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
est atomique vis-à-vis des autres écrivains.

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
les données de plusieurs écrivains peuvent être intercalées.


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
Sous Linux, la capacité peut être interrogée ou ajustée avec :

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
on utilise généralement deux pipes :

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
Le contenu du message peut être secondaire ; le déblocage de `read()` sert de notification.


13. dup() et dup2()

`dup()` duplique un descripteur vers le plus petit descripteur libre :

Code: Select all

int newfd = dup(oldfd);
`dup2()` permet de choisir explicitement la cible :

Code: Select all

dup2(oldfd, STDOUT_FILENO);
C’est particulièrement utile pour connecter stdin/stdout à un pipe.


14. Rediriger stdout vers un pipe

Code: Select all

dup2(fd[1], STDOUT_FILENO);
Ensuite :

Code: Select all

printf(...)
write(STDOUT_FILENO, ...)
partent dans le pipe.

Pour rediriger stdin :

Code: Select all

dup2(fd[0], STDIN_FILENO);

15. Pipeline shell

Pour :

Code: Select all

ls | wc -l
le shell fait conceptuellement :

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()
C’est le modèle fondamental des pipelines UNIX.


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);
Mode lecture :

Code: Select all

FILE *fp = popen("ls -l", "r");

char buf[256];

while (fgets(buf, sizeof(buf), fp) != NULL) {
    printf("%s", buf);
}

pclose(fp);
Mode `"r"` :

Code: Select all

programme <- sortie commande
Mode `"w"` :

Code: Select all

programme -> entrée commande

17. pclose()

Prototype :

Code: Select all

int pclose(FILE *stream);
`pclose()` :
  • ferme le pipe ;
  • attend la terminaison du processus enfant ;
  • retourne son statut.
Il ne faut donc pas remplacer simplement `pclose()` par `fclose()`.


18. Limites de popen()

`popen()` est pratique mais moins flexible qu’un :

Code: Select all

pipe() + fork() + dup2() + exec()
La commande est généralement exécutée via :

Code: Select all

/bin/sh -c ...
Il faut donc faire attention aux caractères interprétés par le shell, surtout si une partie de la commande provient d’une entrée non fiable.


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
Elle permet donc à des processus sans relation parent/enfant de communiquer.


20. mkfifo()

Prototype :

Code: Select all

#include <sys/stat.h>

int mkfifo(const char *pathname, mode_t mode);
Exemple :

Code: Select all

if (mkfifo("/tmp/monfifo", 0600) == -1) {
    perror("mkfifo");
}
Puis la FIFO est utilisée avec :

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);
Par défaut, l’ouverture en lecture peut bloquer en attendant qu’un écrivain ouvre la FIFO.


22. Écriture dans une FIFO

Code: Select all

int fd = open("/tmp/monfifo", O_WRONLY);

write(fd, "hello", 5);

close(fd);
Par défaut, l’ouverture en écriture peut bloquer en attendant un lecteur.


23. Suppression d’une FIFO

Une FIFO est supprimée du système de fichiers avec :

Code: Select all

unlink("/tmp/monfifo");
Les processus possédant déjà un descripteur ouvert peuvent continuer à utiliser l’objet jusqu’à fermeture.


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)
attend généralement un écrivain.

Et :

Code: Select all

open(fifo, O_WRONLY)
attend généralement un lecteur.

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);
ou :

Code: Select all

open("/tmp/monfifo",
     O_WRONLY | O_NONBLOCK);
Cas important :

Code: Select all

O_WRONLY | O_NONBLOCK
sans lecteur :

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);
Activer :

Code: Select all

flags |= O_NONBLOCK;
fcntl(fd, F_SETFL, flags);
Désactiver :

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
ou éventuellement :

Code: Select all

EWOULDBLOCK
Cela signifie :

Code: Select all

aucune donnée disponible maintenant
et non EOF.


29. write() non bloquant

Si le pipe/FIFO n’a pas suffisamment de place :

Code: Select all

write()
peut :
  • é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
Le client peut transmettre :

Code: Select all

PID
nom de sa FIFO
requête
Le serveur ouvre ensuite la FIFO privée du client pour envoyer la réponse.


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
Chaque client reçoit alors son propre flux de réponse.


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
Longueur dans un header

Code: Select all

[taille][payload]
Le lecteur :

Code: Select all

lit la taille
lit exactement taille octets
Messages de taille fixe

Code: Select all

struct request
Chaque message possède la même longueur.


35. read() peut être partiel

Un :

Code: Select all

read(fd, buf, 100);
peut retourner :

Code: Select all

17
Il ne faut donc pas supposer qu’un appel suffit pour recevoir une structure ou un message complet.

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)
peut retourner moins de `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()
peuvent être interrompus par un signal.

Dans ce cas :

Code: Select all

errno = EINTR
Exemple :

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()
Pour les commandes shell :

Code: Select all

popen()
pclose()
Pour les FIFO :

Code: Select all

mkfifo()
open()
read()
write()
close()
unlink()
Pour le non-bloquant :

Code: Select all

fcntl()
F_GETFL
F_SETFL
O_NONBLOCK
Erreurs et événements importants :

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);
}
Carte mentale :

Code: Select all

pipe()
  |
fork()
  |
  +-- parent
  |     close(read)
  |     write()
  |     close(write)
  |
  +-- enfant
        close(write)
        read()
        close(read)
C’est le squelette fondamental à maîtriser avant de passer aux IPC plus avancés.

Who is online

Users browsing this forum: No registered users and 1 guest