Comunication Interprocessus (IPC) sous Linux
La Communication Interprocessus (IPC) en Linux représente l'ensemble des mécanismes permettant à différents processus d'échanger des informations et de coordonner leurs activités. Comprendre ces mécanismes est essentiel pour développer des applications robustes et efficaces sur les systèmes d'exploitation multi-tâches.
Fondements de l'IPC
- Isolation des espaces mémoire:
- Chaque processus dans un système d'exploitation moderne bénéficie d'un espace mémoire protégé et indépendant.
- Cette isolation assure la sécurité et la stabilité du système, mais empêche directement l'accès aux ressources d'un autre processus.
- Nécessité de collaboration:
- Les applications modernes nécessitent souvent l'échange de données entre processus distincts.
- Les ressources doivent être partagées de manière coordonnée pour éviter les conflits et les conditions de concurrence.
Principales méthodes d'IPC
- Tubes (Pipes):
- Communication unidirectionnelle entre processus
- Deux types : tubes anonymes et tubes nommés (FIFO)
- Files de messages:
- Permettent l'échange de données structurées
- Supportent la communication asynchrone entre processus
- Mémoire partagée:
- Méthode la plus efficace pour l'échange de grandes quantités de données
- Nécessite des mécanismes de synchronisation associés
- Signaux:
- Mécanisme de notification simple entre processus
- Utile pour la gestion d'événements asynchrones
- Sockets:
- Permettent la communication entre processus sur différents hôtes
- Fondamentaux pour les applications réseau
Tubes anonymes (Pipes)
Les tubes anonymes représentent un mécanisme de communication unidirectionnel principalement utilisé entre processus ayant un lien de parenté.
Caractéristiques des tubes:
- Mode de fonctionnement half-duplex (un seul sens de communication à la fois)
- Les tubes sont traités comme des fichiers spéciaux sans support des opérations de positionnement
- L'accès se fait via les appels système de lecture/écriture de fichiers
Points importants:
- Si le processus de lecture est actif mais lent, l'écriture se bloque lorsque la capacité du tube (64 Ko) est atteinte
- Si le processus d'écriture est actif mais le tube est vide, la lecture se bloque
- La rupture d'un tube (fermeture du côté lecture lors d'une écriture) provoque une erreur SIGPIPE
- Lorsque le côté écriture est fermé, une lecture sur un tube vide retourne 0
Exemple d'implémentation:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
int main(int argc, char *argv[]) {
int descripteurs[2] = {0};
int resultat = pipe(descripteurs);
if (resultat == -1) {
perror("Échec de la création du tube");
exit(1);
}
pid_t identifiant = fork();
if (identifiant > 0) { // Processus parent
char message[] = "Bonjour depuis le parent";
close(descripteurs[0]); // Fermer le côté lecture
sleep(2);
write(descripteurs[1], message, strlen(message));
close(descripteurs[1]);
}
else if (identifiant == 0) { // Processus enfant
close(descripteurs[1]); // Fermer le côté écriture
char tampon[100] = {0};
read(descripteurs[0], tampon, sizeof(tampon));
printf("Reçu dans l'enfant: %s\n", tampon);
close(descripteurs[0]);
}
else {
perror("Échec du fork");
exit(1);
}
return 0;
}
Cas particuliers:
- Dépassement de capacité du tube:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
int main(int argc, char *argv[]) {
int descripteurs[2] = {0};
int resultat = pipe(descripteurs);
if (resultat == -1) {
perror("Échec de la création du tube");
exit(1);
}
pid_t identifiant = fork();
if (identifiant > 0) {
char tampon[1024] = {0};
close(descripteurs[0]);
memset(tampon, 'z', 1024);
int i = 0;
for (i = 0; i < 65; i++) {
write(descripteurs[1], tampon, sizeof(tampon));
printf("Écriture numéro %d\n", i);
}
}
else if (identifiant == 0) {
close(descripteurs[1]);
// Lecture lente ou nulle
while(1) sleep(1);
}
else {
perror("Échec du fork");
exit(1);
}
return 0;
}
- Rupture de tube:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
int main(int argc, char *argv[]) {
int descripteurs[2] = {0};
int resultat = pipe(descripteurs);
if (resultat == -1) {
perror("Échec de la création du tube");
exit(1);
}
pid_t identifiant = fork();
if (identifiant > 0) {
char message[] = "Test de rupture";
close(descripteurs[0]);
sleep(3);
write(descripteurs[1], message, strlen(message));
printf("Message envoyé\n");
}
else if (identifiant == 0) {
close(descripteurs[1]);
close(descripteurs[0]); // Fermeture du côté lecture
}
else {
perror("Échec du fork");
exit(1);
}
return 0;
}
Utilisation pratique: Copie de fichier via tube:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <fcntl.h>
int main(int argc, char *argv[]) {
int descripteurs[2] = {0};
int resultat = pipe(descripteurs);
if (resultat == -1) {
perror("Échec de la création du tube");
exit(1);
}
pid_t identifiant = fork();
if (identifiant > 0) { // Processus parent - lecture du fichier source
close(descripteurs[0]);
int source = open("/chemin/vers/source.txt", O_RDONLY);
if (source == -1) {
perror("Échec de l'ouverture du fichier source");
exit(1);
}
char tampon[4096] = {0};
ssize_t bytes_lus;
while ((bytes_lus = read(source, tampon, sizeof(tampon))) > 0) {
write(descripteurs[1], tampon, bytes_lus);
}
close(source);
close(descripteurs[1]);
}
else if (identifiant == 0) { // Processus enfant - écriture du fichier destination
close(descripteurs[1]);
int destination = open("/chemin/vers/destination.txt",
O_WRONLY | O_CREAT | O_TRUNC, 0666);
if (destination == -1) {
perror("Échec de la création du fichier destination");
exit(1);
}
char tampon[1024] = {0};
ssize_t bytes_lus;
while ((bytes_lus = read(descripteurs[0], tampon, sizeof(tampon))) > 0) {
write(destination, tampon, bytes_lus);
}
close(destination);
close(descripteurs[0]);
}
else {
perror("Échec du fork");
exit(1);
}
return 0;
}
Tubes nommés (FIFO)
Les tubes nommés, ou FIFO, permettent la communication entre processus quelconques sur la même machine, sans nécessiter de lien de parenté. Contrairement aux tubes anoynmes, les FIFO existent comme des fichiers dans le système de fichiers.
Cadre d'utilisation:
- Création du FIFO
- Ouverture du FIFO
- Lecture/écriture dans le FIFO
- Fermeture du FIFO
- Suppression du FIFO
Implémentation:
- Création:
#include <sys/types.h>
#include <sys/stat.h>
int mkfifo(const char *chemin, mode_t mode);
chemin: chemin complet du FIFO à créermode: permissions du fichier (en octal)- Retourne 0 en cas de succès, -1 en cas d'erreur
- Ouverture:
int fifo_lecture = open("mon_fifo", O_RDONLY); // Mode lecture
int fifo_ecriture = open("mon_fifo", O_WRONLY); // Mode écriture
- L'ouverture se bloque jusqu'à ce que l'autre côté soit ouvert
- L'accès en lecture/écriture simultanée (O_RDWR) n'est pas possible
- Lecture/écriture:
// Lecture
char tampon[256];
read(fifo_lecture, tampon, sizeof(tampon));
// Écriture
char message[] = "Test FIFO";
write(fifo_ecriture, message, strlen(message));
- Fermeture:
close(fifo_lecture);
close(fifo_ecriture);
- Suppression:
#include <unistd.h>
unlink("mon_fifo");
Exemple complet:
// Écrivain (writer.c)
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <errno.h>
int main(int argc, char *argv[]) {
// Création du FIFO si nécessaire
int resultat = mkfifo("mon_fifo", 0666);
if (resultat == -1 && errno != EEXIST) {
perror("Échec de la création du FIFO");
exit(1);
}
// Ouverture en écriture (bloque jusqu'à l'ouverture en lecture)
int descripteur = open("mon_fifo", O_WRONLY);
if (descripteur == -1) {
perror("Échec de l'ouverture du FIFO");
exit(1);
}
char message[256] = "Message depuis l'écrivain";
write(descripteur, message, strlen(message));
close(descripteur);
return 0;
}
// Lecteur (reader.c)
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <errno.h>
int main(int argc, char *argv[]) {
// Création du FIFO si nécessaire
int resultat = mkfifo("mon_fifo", 0666);
if (resultat == -1 && errno != EEXIST) {
perror("Échec de la création du FIFO");
exit(1);
}
// Ouverture en lecture (bloque jusqu'à l'ouverture en écriture)
int descripteur = open("mon_fifo", O_RDONLY);
if (descripteur == -1) {
perror("Échec de l'ouverture du FIFO");
exit(1);
}
char tampon[256] = {0};
read(descripteur, tampon, sizeof(tampon));
printf("Reçu: %s\n", tampon);
close(descripteur);
//unlink("mon_fifo"); // Supprimer le FIFO optionnellement
return 0;
}
Considérations importantes:
- Les FIFO nécessitent la présence simultanée d'un lecteur et d'un écrivain
- L'ouverture d'un FIFO se bloque si l'autre extrémité n'est pas encore ouverte
- Les FIFO peuvent être utilisés entre processus avec ou sans lien de parenté
- Les FIFO peuvent être manipulés directement avec des commandes shell:
- Lecture:
cat mon_fifo - Écriture:
echo "message" > mon_fifo
Exercice d'application: Développer un programme de communication entre deux processus sans lien de parenté, où:
- Le processus A envoie différents messages au processus B
- Le processus B affiche les messages reçus
- Les deux processus se terminent lorsque "quit" est reçu
Conclusion: Les mécanismes de communication interprocessus sous Linux, notamment les tubes et FIFO, fournissent des solutions efficaces pour l'échange de données entre processus. Le choix entre tube anonyme et FIFO dépend des besoins spécifiques de l'application, notamment concernant la nécessité d'un lien de pareneté et la persistance de la méthode de communication.