Support de Cours
Programmation Système avec Linux et C
Pour les Étudiants de Génie Logiciel - Niveau 4
École Nationale Supérieure Polytechnique de Douala
Université de Douala
Année Académique : 2024-2025
Programmation Système avec Linux et C Polytechnique Douala - Génie Logiciel N4
2
Préface
Bienvenue dans ce cours de Programmation Système avec Linux et C, conçu spécifique-
ment pour les étudiants de Génie Logiciel de Niveau 4 à l’École Nationale Supérieure Poly-
technique de Douala. À ce stade de votre formation, vous avez déjà acquis des bases solides en
programmation et en algorithmique. Ce cours a pour ambition de vous faire plonger au cœur
du système d’exploitation, en vous permettant de comprendre et d’interagir directement avec
ses fonctionnalités les plus fondamentales.
La maîtrise de la programmation système est une compétence essentielle pour tout in-
génieur logiciel. Elle ouvre les portes à la conception de systèmes robustes, performants et
sécurisés, qu’il s’agisse de développer des applications embarquées, des serveurs réseau, des
outils de gestion de processus, ou d’optimiser le code pour des performances maximales.
Le choix de Linux comme environnement de développement et du langage C comme outil
principal n’est pas anodin : Linux est un système d’exploitation omniprésent dans le monde
professionnel, des serveurs aux systèmes embarqués, et le C reste le langage de prédilection
pour l’interaction de bas niveau avec le matériel et le système, offrant un contrôle inégalé.
Ce support de cours est structuré pour vous guider pas à pas, des concepts introductifs
aux mécanismes complexes de communication inter-processus et de programmation réseau.
Chaque chapitre comprendra des explications détaillées, des exemples de code pratiques utili-
sant l’environnement verbatim, et des exercices pour renforcer votre compréhension. L’accent
sera mis sur la pratique et l’expérimentation, car c’est en codant que vous développerez une
véritable intuition des principes de la programmologie système.
Nous espérons que ce cours éveillera votre curiosité et vous fournira les outils nécessaires
pour exceller dans le domaine passionnant du génie logiciel.
L’Équipe Pédagogique
Polytechnique Douala
i
Programmation Système avec Linux et C Polytechnique Douala - Génie Logiciel N4
ii
Objectifs du Cours
Ce cours vise à fournir aux étudiants les connaissances et les compétences pratiques
nécessaires pour développer des applications qui interagissent directement avec le système
d’exploitation Linux, en utilisant le langage C.
0.1. Objectifs Généraux
À la fin de ce cours, l’étudiant sera capable de :
— Comprendre les principes fondamentaux du fonctionnement d’un système d’exploita-
tion de type UNIX/Linux.
— Développer des applications C qui interagissent directement avec le noyau Linux via
les appels système.
— Acquérir une expertise dans la conception et l’implémentation de solutions logicielles
de bas niveau.
— Maîtriser les outils de développement et de débogage sous environnement Linux.
0.2. Objectifs Spécifiques
À la fin de ce cours, l’étudiant sera capable de :
— Manipuler les fichiers et les répertoires au niveau système (appels système de E/S).
— Gérer les processus : création, exécution, terminaison, communication.
— Utiliser les mécanismes de communication inter-processus (IPC) standard de Linux.
— Gérer les signaux et les handlers de signaux.
— Implémenter des applications multi-threadées et gérer la synchronisation des threads.
— Développer des applications réseau basées sur les sockets TCP/IP.
— Gérer la mémoire au niveau système (allocation dynamique, mapping mémoire).
iii
Programmation Système avec Linux et C Polytechnique Douala - Génie Logiciel N4
iv
Table des matières
Préface i
Objectifs du Cours iii
0.1 Objectifs Généraux . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . iii
0.2 Objectifs Spécifiques . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . iii
1 Introduction à la Programmation Système et à l’Environnement Linux 1
1.1 Qu’est-ce que la Programmation Système ? . . . . . . . . . . . . . . . . . . . 1
1.1.1 Définition et Rôle en Génie Logiciel . . . . . . . . . . . . . . . . . . . 1
1.1.2 Interface Programme-Système : Appels Système et Librairies Standard 2
1.2 L’Environnement de Développement Linux . . . . . . . . . . . . . . . . . . . 4
1.2.1 Le Système d’Exploitation Linux . . . . . . . . . . . . . . . . . . . . 4
1.2.2 La Ligne de Commande et le Shell (Bash) . . . . . . . . . . . . . . . 6
1.2.3 Outils de Développement . . . . . . . . . . . . . . . . . . . . . . . . . 8
1.3 Rappel du Langage C pour la Programmation Système . . . . . . . . . . . . 12
1.3.1 Pointeurs Avancés et Arithmétique de Pointeurs . . . . . . . . . . . . 12
1.3.2 Gestion de la Mémoire Dynamique (malloc, free) . . . . . . . . . . 14
1.3.3 Structures de Données et Unions . . . . . . . . . . . . . . . . . . . . 15
1.3.4 Les Fichiers d’En-tête et les Librairies . . . . . . . . . . . . . . . . . 16
2 Gestion des Fichiers et Entrées/Sorties de Bas Niveau 19
2.1 Concepts Fondamentaux des Fichiers sous Linux . . . . . . . . . . . . . . . . 19
2.1.1 Descripteurs de Fichiers (File Descriptors) . . . . . . . . . . . . . . . 19
2.1.2 Types de Fichiers . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 20
2.1.3 Permissions de Fichiers . . . . . . . . . . . . . . . . . . . . . . . . . . 20
2.2 Opérations de Fichier de Bas Niveau . . . . . . . . . . . . . . . . . . . . . . 21
2.2.1 Ouverture et Fermeture de Fichiers . . . . . . . . . . . . . . . . . . . 21
2.2.2 Lecture et Écriture dans les Fichiers . . . . . . . . . . . . . . . . . . 24
2.2.3 Positionnement dans les Fichiers (lseek()) . . . . . . . . . . . . . . 26
2.2.4 Duplication des Descripteurs de Fichiers (dup(), dup2()) . . . . . . . 28
2.2.5 Informations et Attributs des Fichiers . . . . . . . . . . . . . . . . . . 32
2.2.6 stat(), fstat(), lstat() . . . . . . . . . . . . . . . . . . . . . . . . 32
2.2.7 Changement des Permissions et Propriétaire (chmod(), chown()) . . . 34
2.3 Opérations sur les Répertoires . . . . . . . . . . . . . . . . . . . . . . . . . . 37
2.3.1 Lecture de Répertoires (opendir(), readdir(), closedir()) . . . . 37
v
Programmation Système avec Linux et C Polytechnique Douala - Génie Logiciel N4
2.3.2 Création et Suppression de Répertoires (mkdir(), rmdir()) . . . . . 39
3 Gestion des Processus 43
3.1 Concepts de Processus . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 43
3.1.1 Processus vs. Programme . . . . . . . . . . . . . . . . . . . . . . . . 43
3.1.2 Structure d’un Processus (PID, PPID) . . . . . . . . . . . . . . . . . 44
3.1.3 Cycle de Vie d’un Processus . . . . . . . . . . . . . . . . . . . . . . . 44
3.2 Création de Processus . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 45
3.2.1 fork() . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 45
3.2.2 exec() . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 46
3.2.3 wait() et waitpid() . . . . . . . . . . . . . . . . . . . . . . . . . . . 48
3.2.4 Processus Zombies et Orphelins . . . . . . . . . . . . . . . . . . . . . 51
3.3 Terminaison de Processus . . . . . . . . . . . . . . . . . . . . . . . . . . . . 53
3.3.1 exit() et _exit() . . . . . . . . . . . . . . . . . . . . . . . . . . . . 53
3.3.2 abort() . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 55
3.4 Processus Avancés . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 55
3.4.1 Groupes de Processus et Sessions . . . . . . . . . . . . . . . . . . . . 55
3.4.2 Daemons . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 56
4 Communication Inter-Processus (IPC) 61
4.1 Concepts d’IPC . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 61
4.1.1 Pourquoi l’IPC ? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 61
4.1.2 Mécanismes d’IPC . . . . . . . . . . . . . . . . . . . . . . . . . . . . 62
4.2 Pipes (Tuyaux) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 62
4.2.1 Pipes Anonymes (pipe()) . . . . . . . . . . . . . . . . . . . . . . . . 62
4.2.2 Pipes Nommés (FIFOs) (mkfifo()) . . . . . . . . . . . . . . . . . . . 65
4.3 Mémoire Partagée (System V) . . . . . . . . . . . . . . . . . . . . . . . . . . 68
4.3.1 shmget(), shmat(), shmdt(), shmctl() . . . . . . . . . . . . . . . . 68
4.3.2 Synchronisation de la Mémoire Partagée (avec Sémaphores ou Mutex) 73
4.4 Queues de Messages (System V) . . . . . . . . . . . . . . . . . . . . . . . . . 74
4.4.1 msgget(), msgsnd(), msgrcv(), msgctl() . . . . . . . . . . . . . . . 74
4.5 Sémaphores (System V) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 79
4.5.1 semget(), semop(), semctl() . . . . . . . . . . . . . . . . . . . . . . 80
4.6 IPC POSIX (Vue d’Ensemble) . . . . . . . . . . . . . . . . . . . . . . . . . . 85
4.6.1 Files de messages POSIX, Sémaphores POSIX, Mémoire partagée POSIX 86
5 Gestion des Signaux 87
5.1 Concepts de Signaux . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 87
5.1.1 Qu’est-ce qu’un Signal ? . . . . . . . . . . . . . . . . . . . . . . . . . 87
5.1.2 Types de Signaux Courants (SIGINT, SIGTERM, SIGKILL, SIGCHLD) 88
5.1.3 Actions par Défaut des Signaux . . . . . . . . . . . . . . . . . . . . . 90
5.2 Envoi de Signaux . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 90
5.2.1 kill() . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 90
5.2.2 raise() . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 92
5.3 Capture et Gestion des Signaux . . . . . . . . . . . . . . . . . . . . . . . . . 93
vi
Polytechnique Douala - Génie Logiciel N4 Programmation Système avec Linux et C
5.3.1 signal() (API obsolète mais simple) . . . . . . . . . . . . . . . . . . 94
5.3.2 sigaction() (API moderne et fiable) . . . . . . . . . . . . . . . . . . 94
5.3.3 Bloquer et Débloquer des Signaux (sigprocmask()) . . . . . . . . . . 97
5.3.4 Signaux Fiables et non Fiables . . . . . . . . . . . . . . . . . . . . . . 99
5.4 Temporisateurs (Timers) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 100
5.4.1 alarm() . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 100
5.4.2 setitimer() . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 102
6 Threads et Synchronisation 105
6.1 Concepts de Threads . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 105
6.1.1 Threads vs. Processus . . . . . . . . . . . . . . . . . . . . . . . . . . 105
6.1.2 Avantages et Inconvénients des Threads . . . . . . . . . . . . . . . . . 106
6.1.3 Modèle de Mémoire des Threads . . . . . . . . . . . . . . . . . . . . . 107
6.2 Création et Gestion de Threads (POSIX Threads - pthreads) . . . . . . . . . 108
6.2.1 pthread_create() . . . . . . . . . . . . . . . . . . . . . . . . . . . . 108
6.2.2 pthread_join() . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 110
6.2.3 pthread_exit() . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 112
6.2.4 pthread_self(), pthread_equal() . . . . . . . . . . . . . . . . . . . 112
6.3 Problèmes de Concurrence . . . . . . . . . . . . . . . . . . . . . . . . . . . . 115
6.3.1 Conditions de Course (Race Conditions) . . . . . . . . . . . . . . . . 116
6.3.2 Interblocages (Deadlocks) . . . . . . . . . . . . . . . . . . . . . . . . 117
6.4 Synchronisation des Threads . . . . . . . . . . . . . . . . . . . . . . . . . . . 119
6.4.1 Mutex (Exclusion Mutuelle) . . . . . . . . . . . . . . . . . . . . . . . 119
6.4.2 Variables de Condition . . . . . . . . . . . . . . . . . . . . . . . . . . 122
6.4.3 Sémaphores POSIX . . . . . . . . . . . . . . . . . . . . . . . . . . . . 125
7 Programmation Réseau avec Sockets 131
7.1 Concepts Fondamentaux du Réseau . . . . . . . . . . . . . . . . . . . . . . . 131
7.1.1 Modèle Client-Serveur . . . . . . . . . . . . . . . . . . . . . . . . . . 131
7.1.2 Adresses IP et Noms de Domaine . . . . . . . . . . . . . . . . . . . . 132
7.1.3 Ports . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 132
7.1.4 Protocoles TCP et UDP . . . . . . . . . . . . . . . . . . . . . . . . . 132
7.2 API Sockets (Berkeley Sockets) . . . . . . . . . . . . . . . . . . . . . . . . . 133
7.2.1 Descripteurs de Sockets . . . . . . . . . . . . . . . . . . . . . . . . . . 133
7.2.2 Types de Sockets (SOCK_STREAM, SOCK_DGRAM) . . . . . . . . . . . . . 133
7.3 Socket TCP (Mode Connecté) . . . . . . . . . . . . . . . . . . . . . . . . . . 134
7.3.1 Création d’un Socket (socket()) . . . . . . . . . . . . . . . . . . . . 134
7.3.2 Liaison à une Adresse (bind()) . . . . . . . . . . . . . . . . . . . . . 134
7.3.3 Mise en Écoute (listen()) . . . . . . . . . . . . . . . . . . . . . . . 135
7.3.4 Acceptation des Connexions (accept()) . . . . . . . . . . . . . . . . 135
7.3.5 Connexion à un Serveur (connect()) . . . . . . . . . . . . . . . . . . 136
7.3.6 Envoi et Réception de Données (send(), recv()) . . . . . . . . . . . 136
7.3.7 Fermeture de Connexions (shutdown(), close()) . . . . . . . . . . . 141
7.4 Socket UDP (Mode Non Connecté) . . . . . . . . . . . . . . . . . . . . . . . 142
7.4.1 Envoi et Réception de Datagrammes (sendto(), recvfrom()) . . . . 142
vii
Programmation Système avec Linux et C Polytechnique Douala - Génie Logiciel N4
7.5 Gestion de Plusieurs Clients Concurremment . . . . . . . . . . . . . . . . . . 147
7.5.1 Serveurs Forking (multiprocessus) . . . . . . . . . . . . . . . . . . . . 147
7.5.2 Serveurs Threading (multithreaded) . . . . . . . . . . . . . . . . . . . 150
7.5.3 Multiplexage des E/S (select(), poll(), epoll()) . . . . . . . . . . 153
viii
Chapitre 1
Introduction à la Programmation
Système et à l’Environnement Linux
Ce chapitre pose les bases de la programmation système en définissant ses concepts clés
et en la distinguant de la programmation applicative. Nous explorerons l’interface entre les
programmes et le système d’exploitation, en nous concentrant sur les appels système et les
fonctions de la librairie standard C. Une part importante sera dédiée à la familiarisation avec
l’environnement de développement Linux, y compris la ligne de commande, le compilateur
GCC, Makefiles, et les outils de débogage, ainsi qu’un rappel sur les aspects avancés du
langage C essentiels pour ce cours.
1.1. Qu’est-ce que la Programmation Système ?
1.1.1. Définition et Rôle en Génie Logiciel
Définition
La programmation système est une branche de la programmation informatique qui se
concentre sur le développement de logiciels qui interagissent directement avec le matériel d’un
ordinateur et le système d’exploitation (OS). Contrairement à la programmation applicative,
qui vise à créer des applications pour l’utilisateur final (traitement de texte, jeux, naviga-
teurs web, etc.), la programmation système s’intéresse aux logiciels qui gèrent les ressources
système, permettent aux applications de fonctionner, ou fournissent des services au niveau
du système d’exploitation.
Les programmes système sont souvent écrits dans des langages de bas niveau comme le
C, le C++ ou l’assembleur, car ils offrent un contrôle précis sur la mémoire, les processus,
les fichiers et les périphériques. Ils sont essentiels pour le développement de :
— Systèmes d’exploitation (noyaux, shells)
— Pilotes de périphériques (drivers)
— Compilateurs et interpréteurs
— Systèmes embarqués
— Outils de gestion de ressources (moniteurs de processus, outils de surveillance)
— Serveurs réseau et infrastructures de communication
1
Programmation Système avec Linux et C Polytechnique Douala - Génie Logiciel N4
Le rôle du génie logiciel dans ce domaine est de concevoir et d’implémenter des solutions logi-
cielles qui soient à la fois performantes, fiables, sécurisées et optimisées pour l’environnement
matériel donné.
Distinction avec la Programmation Applicative
Pour mieux comprendre la programmation système, il est utile de la comparer à la pro-
grammation applicative :
Programmation Système Programmation Applicative
Niveau d’Abstraction : Bas niveau, Niveau d’Abstraction : Haut niveau,
proche du matériel et du noyau du sys- utilise des API et des bibliothèques pour
tème d’exploitation. s’abstraire du matériel.
Objectif : Gérer les ressources, four- Objectif : Résoudre des problèmes spé-
nir des services au système, optimiser les cifiques de l’utilisateur, fournir des fonc-
performances, interagir avec les périphé- tionnalités directes à l’utilisateur final.
riques.
Langages Typiques : C, C++, Assem- Langages Typiques : Python, Java, Ja-
bleur. vaScript, C#, PHP, etc.
Focus : Efficacité, performance, gestion Focus : Logique métier, interface utilisa-
directe de la mémoire, gestion des proces- teur, interopérabilité, rapidité de dévelop-
sus, des threads, E/S de bas niveau, ges- pement, portabilité.
tion des erreurs système.
Exemples : Noyau Linux, gestionnaire de Exemples : Navigateur web (Firefox,
démarrage (bootloader), pilote de carte Chrome), traitement de texte (Word), ap-
réseau, serveur web (Apache, Nginx), shell plication mobile (WhatsApp), jeux vidéo,
(Bash). site web de e-commerce.
1.1.2. Interface Programme-Système : Appels Système et Librairies
Standard
Un programme interagit avec le système d’exploitation via deux mécanismes principaux :
les appels système et les fonctions de la librairie standard.
Appels Système (System Calls)
Les appels système sont la méthode fondamentale par laquelle un programme informa-
tique demande un service au noyau du système d’exploitation. Le noyau est la partie centrale
de l’OS qui gère les ressources matérielles et logicielles.
Lorsqu’un programme d’application a besoin d’effectuer une opération privilégiée (comme
lire/écrire sur un disque, créer un nouveau processus, allouer de la mémoire système, ou
interagir avec un périphérique), il ne peut pas le faire directement. Il doit demander au
noyau de le faire en son nom. C’est le rôle des appels système.
— Mécanisme : Un appel système implique une transition du mode utilisateur (où
s’exécute le programme) au mode noyau (où s’exécute le système d’exploitation).
2
Polytechnique Douala - Génie Logiciel N4 Programmation Système avec Linux et C
Cette transition est coûteuse en temps et nécessite un mécanisme de sécurité pour
éviter que les programmes ne compromettent l’intégrité du système.
— Exemples Courants :
— open(), read(), write(), close() : Pour la gestion des fichiers.
— fork(), execve(), wait() : Pour la gestion des processus.
— exit() : Pour la terminaison du processus.
— mmap() : Pour la gestion de la mémoire.
— socket(), bind(), connect(), send(), recv() : Pour la programmation réseau.
— Documentation : Les appels système sont documentés dans la section 2 des pages
de manuel (man pages) de Linux. Par exemple, man 2 open vous donnera la do-
cumentation de l’appel système open().
Fonctions de la Librairie Standard C
La librairie standard C (libc, souvent implémentée par glibc sur les systèmes GNU/Linux)
est une collection de fonctions pré-écrites qui fournissent des services couramment utilisés par
les programmes C.
— Rôle : Beaucoup de ces fonctions sont des "wrappers" (enveloppes) autour des appels
système. Elles fournissent une interface plus conviviale et portable pour accéder aux
services du noyau, gèrent des détails de bas niveau et peuvent ajouter des fonctionna-
lités supplémentaires (comme le buffering pour les E/S).
— Exemples Courants :
— fopen(), fread(), fwrite(), fclose() : Fonctions d’E/S de haut niveau (gèrent
le buffering).
— printf(), scanf() : Fonctions d’E/S formatées.
— malloc(), free() : Fonctions d’allocation dynamique de mémoire (peuvent utiliser
des appels système comme brk() ou mmap() en interne).
— strcpy(), strlen(), strcmp() : Fonctions de manipulation de chaînes de carac-
tères.
— Relation avec les Appels Système :
— Une fonction de la librairie standard peut encapsuler un ou plusieurs appels sys-
tème. Par exemple, fopen() utilise en interne open().
— Certaines fonctions de la librairie standard ne sont pas directement liées à des
appels système et effectuent des opérations purement logicielles (ex : strlen()).
— L’utilisation des fonctions de la librairie standard est généralement plus simple et
plus portable que l’utilisation directe des appels système. Cependant, la program-
mation système implique souvent l’utilisation directe des appels système pour un
contrôle plus fin et des performances accrues.
— Documentation : Les fonctions de la librairie standard C sont documentées dans
la section 3 des pages de manuel (man pages) de Linux. Par exemple, man 3
printf vous donnera la documentation de la fonction printf().
Exercice 1.1 :
Appels Système vs. Librairie Standard
[label=()]Expliquez en vos propres mots la différence fondamentale entre read() et
fread(). Quand utiliseriez-vous l’une plutôt que l’autre dans un contexte de program-
3
Programmation Système avec Linux et C Polytechnique Douala - Génie Logiciel N4
mation système ? Citez deux exemples d’opérations courantes qui nécessitent un appel
système, et deux exemples d’opérations qui peuvent être réalisées uniquement par une
fonction de la librairie standard C sans passer par le noyau.
1.2. L’Environnement de Développement Linux
Travailler en programmation système sous Linux exige une bonne connaissance de l’envi-
ronnement du système d’exploitation.
1.2.1. Le Système d’Exploitation Linux
Architecture Générale (Noyau, Shell, Utilities)
Linux est un système d’exploitation de type UNIX. Son architecture peut être concep-
tualisée en plusieurs couches :
—2. Matériel (Hardware) : La couche la plus basse, composée des composants physiques
1.
de l’ordinateur (CPU, mémoire, disque dur, périphériques réseau, etc.).
— Noyau (Kernel) : Le cœur du système d’exploitation. Le noyau est responsable de
la gestion des ressources matérielles, de l’ordonnancement des processus, de la gestion
de la mémoire, du système de fichiers et de la communication avec les périphériques
via les pilotes (drivers). C’est le pont entre le matériel et les logiciels.
— Shell : Une interface utilisateur en ligne de commande qui permet aux utilisateurs
d’interagir avec le noyau et d’exécuter des programmes. Le shell interprète les com-
mandes tapées par l’utilisateur (comme ls, cd, gcc) et les traduit en appels système
pour que le noyau les exécute. Bash (Bourne Again SHell ) est le shell le plus couram-
ment utilisé sous Linux.
— Utilitaires / Programmes Utilisateur : Ce sont les applications et les outils qui
s’exécutent en mode utilisateur et fournissent des fonctionnalités aux utilisateurs. Cela
inclut tout, des commandes système de base (comme ls, cat, grep) aux applications
graphiques complexes (navigateurs web, suites bureautiques, etc.).
Architecture Simplifiée de Linux
Utilisateurs / Applications
↑
Utilitaires / Programmes Utilisateur (Shell, Commandes, Applications Graphiques)
↑
Librairies Standard (ex : Glibc)
↑
Appels Système (Interface Utilisateur/Noyau)
↑
Noyau (Gestion des Processus, Mémoire, Fichiers, Pilotes)
↑
Matériel (CPU, RAM, Disque, Périphériques)
4
Polytechnique Douala - Génie Logiciel N4 Programmation Système avec Linux et C
Arborescence du Système de Fichiers (FHS)
Le Filesystem Hierarchy Standard (FHS) définit la structure de l’arborescence des
répertoires sous Linux et les systèmes de type UNIX. Comprendre cette structure est crucial
pour savoir où trouver les exécutables, les bibliothèques, les fichiers de configuration, les
données utilisateur, etc.
Voici quelques-uns des répertoires les plus importants :
— /bin : Binaires essentiels pour le démarrage du système et pour tous les utilisateurs
(ls, cp, mv, bash).
— /boot : Fichiers nécessaires au démarrage du système (noyau Linux, GRUB).
— /dev : Fichiers de périphériques (disques, ports série, terminaux). Chaque périphérique
est représenté par un fichier.
— /etc : Fichiers de configuration du système (mots de passe, réseau, services).
— /home : Répertoires personnels des utilisateurs. Chaque utilisateur a son propre sous-
répertoire (/home/utilisateur).
— /lib : Bibliothèques partagées essentielles au démarrage du système et aux com-
mandes dans /bin.
— /media : Points de montage pour les périphériques amovibles (CD-ROM, clés USB)
montés automatiquement.
— /mnt : Point de montage temporaire pour les systèmes de fichiers montés manuelle-
ment.
— /opt : Logiciels tiers optionnels.
— /proc : Pseudo-système de fichiers contenant des informations sur les processus en
cours et d’autres informations du noyau. C’est une interface dynamique avec le noyau.
— /root : Répertoire personnel de l’utilisateur root.
— /run : Fichiers d’état d’exécution (runtime data) du système.
— /sbin : Binaires système essentiels pour l’administration (réservés à l’administrateur
système).
— /srv : Données pour les services fournis par le système (ex : données de serveurs web,
FTP).
— /sys : Pseudo-système de fichiers fournissant une interface vers les périphériques et
les paramètres du noyau.
— /tmp : Fichiers temporaires (supprimés au redémarrage).
— /usr : Hiérarchie secondaire pour les données utilisateur ; contient la plupart des
binaires non essentiels, des bibliothèques, de la documentation, etc.
— /usr/bin : Commandes non essentielles à l’amorçage.
— /usr/lib : Bibliothèques partagées des programmes dans /usr/bin.
— /usr/local : Logiciels installés localement par l’administrateur.
— /usr/include : Fichiers d’en-tête (headers) des bibliothèques C.
— /var : Fichiers de données variables, dont la taille peut changer (logs, spool de courrier,
fichiers temporaires).
5
Programmation Système avec Linux et C Polytechnique Douala - Génie Logiciel N4
1.2.2. La Ligne de Commande et le Shell (Bash)
La ligne de commande est l’outil principal pour interagir avec un système Linux dans le
cadre de la programmation système. Le shell (Bash) vous permet d’exécuter des commandes,
de compiler des programmes, de gérer des fichiers et de déboguer.
Commandes Essentielles pour le Développement
— ls : Liste le contenu d’un répertoire.
ls -l # Affiche une liste détaillée
ls -a # Affiche tous les fichiers, y compris les cachés
— cd : Change de répertoire.
cd /home/utilisateur/projet # Aller a un chemin specifique
cd .. # Remonter d’un repertoire
— pwd : Affiche le chemin du répertoire de travail actuel.
pwd
— mkdir : Crée un nouveau répertoire.
mkdir nouveau_dossier
mkdir -p parent/enfant # Cree les repertoires parents si necessaire
— rm : Supprime des fichiers ou des répertoires.
rm fichier.txt # Supprime un fichier
rm -r mon_dossier # Supprime un repertoire et son contenu (recursif)
rm -rf mon_dossier # Supprime recursivement sans demander confirmation (atte
— cp : Copie des fichiers ou des répertoires.
cp source.txt destination.txt # Copie un fichier
cp -r dossier_source dossier_destination # Copie un repertoire recursif
— mv : Déplace ou renomme des fichiers/répertoires.
mv ancien_nom.txt nouveau_nom.txt # Renomme un fichier
mv fichier.txt /chemin/vers/dossier/ # Deplace un fichier
— cat : Affiche le contenu d’un fichier.
cat mon_fichier.txt
— grep : Recherche des motifs dans les fichiers.
grep "recherche" mon_fichier.txt # Cherche "recherche" dans le fichier
grep -r "erreur" . # Cherche "erreur" recursivement dans le reper
— find : Recherche des fichiers et répertoires.
find . -name "*.c" # Cherche tous les fichiers .c dans le repertoir
6
Polytechnique Douala - Génie Logiciel N4 Programmation Système avec Linux et C
find /home -type d -name "projet*" # Cherche les repertoires dont le nom commen
— man : Affiche les pages de manuel (documentation) des commandes et appels système.
man ls # Documentation de la commande ls
man 2 open # Documentation de l’appel systeme open()
man 3 printf # Documentation de la fonction printf()
— ps : Affiche les processus en cours.
ps aux # Affiche tous les processus pour tous les utilisateurs
ps -ef # Une autre vue detaillee des processus
— kill : Envoie un signal à un processus.
kill <PID> # Envoie SIGTERM (signal par defaut pour terminer proprement)
kill -9 <PID> # Envoie SIGKILL (force la terminaison, ne peut pas etre interce
Redirections et Pipes
Ces mécanismes permettent de contrôler les entrées et sorties des commandes et de chaîner
des commandes ensemble.
— Redirection de sortie :
— > : Redirige la sortie standard (stdout) d’une commande vers un fichier, écrasant
le contenu existant.
ls -l > liste_fichiers.txt # La sortie de ’ls -l’ va dans le fichier
— » : Redirige la sortie standard vers un fichier, en ajoutant le contenu à la fin du
fichier.
echo "Nouvelle ligne" >> log.txt # Ajoute une ligne au fichier log.txt
— 2> : Redirige la sortie d’erreur standard (stderr) vers un fichier.
commande_qui_echoue 2> erreurs.log # Les erreurs de la commande vont dan
— > ou > : Redirige stdout et stderr vers un même fichier.
commande_complexe &> sortie_complete.log
— Redirection d’entrée :
— < : Redirige l’entrée standard (stdin) d’une commande depuis un fichier.
sort < donnees.txt # La commande sort lit les donnees depuis donnees.txt
— Pipes :
— | : Le pipe (|) permet de connecter la sortie standard d’une commande à l’entrée
standard d’une autre commande.
ls -l | grep ".c" # Liste les fichiers, puis filtre pour ne montrer que
ps aux | grep "apache" | wc -l # Compte le nombre de processus apache
7
Programmation Système avec Linux et C Polytechnique Douala - Génie Logiciel N4
1.2.3. Outils de Développement
Le Compilateur GCC (GNU Compiler Collection)
GCC est le compilateur standard pour le langage C (et d’autres langages comme C++,
Fortran) sous Linux. Il convertit votre code source en un programme exécutable. Le processus
de compilation se déroule en plusieurs étapes :
1. Pré-traitement (-E) : Le pré-processeur gère les directives comme #include (inclu-
sion de fichiers d’en-tête) et #define (définition de macros). Le résultat est un fichier
.i.
2. Compilation (-S) : Le compilateur transforme le code C pré-traité en code assem-
bleur. Le résultat est un fichier .s.
3. Assemblage (-c) : L’assembleur convertit le code assembleur en code machine binaire
(code objet). Le résultat est un fichier .o.
4. Édition de liens (Linking) : L’éditeur de liens combine les fichiers objet avec les
bibliothèques nécessaires (standard C, etc.) pour créer l’exécutable final.
— Compilation simple : Pour un fichier source unique, GCC gère toutes les étapes auto-
matiquement.
gcc -o mon_programme mon_source.c
— Options de compilation utiles :
— -Wall : Active la plupart des avertissements de compilation utiles. Toujours l’uti-
liser !
— -g : Inclut les informations de débogage dans l’exécutable, essentielles pour utiliser
GDB.
— -std=c11 ou -std=c99 : Spécifie la norme C à utiliser. C11 est la norme la plus
récente et recommandée.
— -O<niveau> : Active les optimisations de code (ex : -O2, -O3). À utiliser pour les
versions de production, mais peut rendre le débogage plus difficile.
— -c : Compile uniquement le fichier source en fichier objet, sans l’édition de liens.
Utile pour les projets multi-fichiers.
gcc -c mon_source.c -o mon_source.o
— -L<chemin> : Ajoute un répertoire où chercher les bibliothèques.
— -l<lib> : Lie avec une bibliothèque spécifique (ex : -lpthread pour la bibliothèque
POSIX threads).
gcc -Wall -g -std=c11 -o mon_programme mon_source.c
L’Utilitaire Make et les Makefiles
Pour des projets C complexes avec de nombreux fichiers sources, compiler manuellement
avec GCC devient fastidieux. Make est un utilitaire qui automatise le processus de compilation
et de gestion des dépendances. Il lit un fichier nommé Makefile (ou makefile) qui contient
des règles pour construire le projet.
8
Polytechnique Douala - Génie Logiciel N4 Programmation Système avec Linux et C
— Pourquoi Make ?
— Automatisation : Exécute les bonnes commandes de compilation dans le bon
ordre.
— Dépendances : Re-compile uniquement les fichiers qui ont été modifiés ou dont
les dépendances ont été modifiées, ce qui accélère la compilation.
— Nettoyage : Facilite les opérations courantes comme le nettoyage des fichiers
intermédiaires.
— Structure d’un Makefile : Un Makefile est composé de règles. Chaque règle a la
forme :
cible: dépendances
[TAB] commande1
[TAB] commande2
...
— cible : Le nom du fichier ou de l’action à produire (ex : mon_programme, clean).
— dépendances : Les fichiers ou cibles qui doivent exister ou être mis à jour avant
que la cible ne puisse être construite.
— commande : Les commandes shell à exécuter pour construire la cible. Attention :
chaque ligne de commande doit commencer par une tabulation (TAB),
pas des espaces !
— Exemple de Makefile simple :
# Variables pour le compilateur et les options
CC = gcc
CFLAGS = -Wall -g -std=c11
# Cible par defaut (execute quand on tape ’make’ sans argument)
all: mon_programme
# Regle pour construire l’executable ’mon_programme’
# Depend de mon_source.o
mon_programme: mon_source.o
$(CC) $(CFLAGS) -o mon_programme mon_source.o
# Regle pour compiler mon_source.c en mon_source.o
# Depend de mon_source.c
mon_source.o: mon_source.c
$(CC) $(CFLAGS) -c mon_source.c
# Cible pour nettoyer les fichiers generes
clean:
rm -f *.o mon_programme
Pour utiliser ce Makefile, naviguez dans le répertoire contenant le Makefile et tapez :
— make : Pour compiler le programme (mon_programme sera la cible par défaut).
— make clean : Pour supprimer les fichiers objets et l’exécutable.
9
Programmation Système avec Linux et C Polytechnique Douala - Génie Logiciel N4
Le Débogueur GDB (GNU Debugger)
GDB est un outil puissant pour déboguer les programmes C/C++ sous Linux. Il vous
permet d’exécuter votre programme étape par étape, de définir des points d’arrêt, d’inspecter
les valeurs des variables, et de suivre le flux d’exécution.
— Utilisation : Pour déboguer un programme avec GDB, vous devez le compiler avec
l’option -g de GCC.
gcc -g -o mon_programme mon_source.c
— Commandes GDB essentielles :
gdb mon_programme # Lance GDB avec votre executable
(gdb) run # Execute le programme
(gdb) break main # Definit un point d’arret au debut de la fonction main
(gdb) break mon_source.c:10 # Definit un point d’arret a la ligne 10 du fichier
(gdb) next # Execute la ligne de code courante et s’arrete a la suivan
(gdb) step # Execute la ligne de code courante et entre dans les appel
(gdb) print ma_variable # Affiche la valeur de ma_variable
(gdb) display autre_variable # Affiche la valeur a chaque arret
(gdb) continue # Continue l’execution jusqu’au prochain point d’arret ou l
(gdb) list # Affiche le code source autour du point d’arret
(gdb) info locals # Affiche les variables locales
(gdb) quit # Quitte GDB
Outils d’Analyse de Performance (valgrind)
Valgrind est une suite d’outils d’instrumentation dynamique qui aide à détecter les erreurs
de gestion de mémoire (fuites, accès invalides) et à profiler les performances d’un programme.
— Detection d’erreurs mémoire (Memcheck) : C’est l’outil le plus couramment
utilisé de Valgrind. Il détecte :
— Accès en dehors des limites (lecture/écriture avant ou après un bloc alloué).
— Utilisation de mémoire non initialisée.
— Libération de mémoire non allouée ou déjà libérée (double free).
— Fuites de mémoire (blocs alloués qui ne sont jamais libérés).
— Utilisation :
valgrind --leak-check=full ./mon_programme
L’option –leak-check=full demande une analyse complète des fuites de mémoire.
— Exemple d’utilisation de Valgrind : Considérons le code C suivant (meml eak.c) :
#include <stdlib.h>
#include <stdio.h>
void generate_leak() {
int *data = (int *)malloc(10 * sizeof(int)); // Allocation de 10 entiers
if (data == NULL) {
perror("malloc failed");
10
Polytechnique Douala - Génie Logiciel N4 Programmation Système avec Linux et C
return;
}
// Pas de free(data); => Fuite de memoire
printf("Mémoire allouée et fuite volontaire.\n");
}
int main() {
generate_leak();
printf("Programme terminé.\n");
return 0;
}
\end{verbatim}
Compilation et exécution avec Valgrind :
\begin{verbatim}
gcc -g mem_leak.c -o mem_leak
valgrind --leak-check=full ./mem_leak
\end{verbatim}
La sortie de Valgrind indiquera clairement la fuite de mémoire, l’endroit où el
Exercice 1.2 :
Environnement de Développement
[label=()]Créez un fichier C simple (hello.c) qui affiche "Bonjour, Polytechnique
Douala !". Compilez ce fichier en utilisant GCC avec les options -Wall et -g. Nommez
l’exécutable bonjour. Exécutez le programme bonjour. Utilisez GDB pour définir un
point d’arrêt dans la fonction main(), exécutez le programme, affichez le code source,
puis quittez GDB. Modifiez hello.c pour qu’il alloue dynamiquement une chaîne
de caractères sans la libérer (fuite de mémoire). Compilez et utilisez Valgrind pour
détecter la fuite. Corrigez ensuite la fuite et vérifiez avec Valgrind.
Correction Exercice 1.2, partie (e) - Détection et correction de fuite mémoire
[label=()]Code initial (hello.c) avec fuite :
5.
3.
1.
4.
2. #include <stdio.h>
#include <stdlib.h> // Pour malloc, free
#include <string.h> // Pour strcpy
int main() {
char *message = (char *)malloc(30 * sizeof(char));
if (message == NULL) {
perror("malloc failed");
return 1;
}
strcpy(message, "Bonjour, Polytechnique Douala!");
printf("%s\n", message);
// Fuite de memoire: ’message’ n’est pas libere avec free()
11
Programmation Système avec Linux et C Polytechnique Douala - Génie Logiciel N4
return 0;
}
2. Compilation et exécution avec Valgrind (montrant la fuite) :
gcc -g hello.c -o bonjour
valgrind --leak-check=full ./bonjour
La sortie de Valgrind affichera des informations sur la fuite de 30 octets.
3. Code corrigé (hello.c) sans fuite :
#include <stdio.h>
#include <stdlib.h> // Pour malloc, free
#include <string.h> // Pour strcpy
int main() {
char *message = (char *)malloc(30 * sizeof(char));
if (message == NULL) {
perror("malloc failed");
return 1;
}
strcpy(message, "Bonjour, Polytechnique Douala!");
printf("%s\n", message);
free(message); // Correction: liberation de la memoire allouee
message = NULL; // Bonne pratique: eviter les pointeurs suspendus
return 0;
}
4. Vérification avec Valgrind (ne montrant plus de fuite) :
gcc -g hello.c -o bonjour
valgrind --leak-check=full ./bonjour
Valgrind indiquera "All heap blocks were freed – no leaks are possible".
1.3. Rappel du Langage C pour la Programmation
Système
La programmation système en C s’appuie fortement sur des concepts avancés du langage.
Ce rappel vise à consolider ces bases.
1.3.1. Pointeurs Avancés et Arithmétique de Pointeurs
Les pointeurs sont fondamentaux en C, et encore plus en programmation système, car ils
permettent une manipulation directe de la mémoire.
12
Polytechnique Douala - Génie Logiciel N4 Programmation Système avec Linux et C
— Rappel des bases :
— ‘‘ (opérateur d’adresse de) : Renvoie l’adresse mémoire d’une variable.
— ‘*‘ (opérateur de déréférencement) : Accède à la valeur stockée à l’adresse pointée
par le pointeur.
— Arithmétique de pointeurs : Lorsque vous effectuez des opérations arithmétiques
sur un pointeur, l’unité de déplacement est la taille du type de données auquel il
pointe.
int arr[] = {10, 20, 30, 40, 50};
int *ptr = arr; // ptr pointe vers arr[0]
printf("ptr pointe vers %d\n", *ptr); // Affiche 10
ptr++; // ptr pointe maintenant vers arr[1] (décalé de sizeof(int) octets)
printf("ptr++ pointe vers %d\n", *ptr); // Affiche 20
printf("arr[2] via pointeur: %d\n", *(ptr + 1)); // Equivalent a arr[2] (20 + 1
— Pointeurs vers pointeurs : Utilisés par exemple pour modifier un pointeur dans
une fonction, ou pour des tableaux dynamiques de chaînes de caractères.
int x = 10;
int *ptr_x = &x;
int **ptr_ptr_x = &ptr_x;
printf("Valeur de x: %d\n", x);
printf("Valeur pointee par ptr_x: %d\n", *ptr_x);
printf("Valeur pointee par ptr_ptr_x: %d\n", **ptr_ptr_x);
— Pointeurs et tableaux : En C, le nom d’un tableau peut souvent être traité comme
un pointeur vers son premier élément.
int tableau[5] = {1, 2, 3, 4, 5};
int *p = tableau; // ’p’ pointe vers tableau[0]
printf("%d %d\n", tableau[0], *p); // Affiche 1 1
printf("%d %d\n", tableau[2], *(p + 2)); // Affiche 3 3
— Pointeurs de fonctions : Permettent de passer une fonction en argument à une autre
fonction, ou de stocker des adresses de fonctions. Essentiel pour les rappels (callbacks)
comme les gestionnaires de signaux.
#include <stdio.h>
int addition(int a, int b) {
return a + b;
}
int soustraction(int a, int b) {
return a - b;
13
Programmation Système avec Linux et C Polytechnique Douala - Génie Logiciel N4
// Fonction qui prend un pointeur de fonction en argument
void executer_operation(int (*operation)(int, int), int x, int y) {
printf("Resultat: %d\n", operation(x, y));
}
int main() {
executer_operation(addition, 10, 5); // Appelle addition
executer_operation(soustraction, 10, 5); // Appelle soustraction
return 0;
}
1.3.2. Gestion de la Mémoire Dynamique (malloc, free)
L’allocation dynamique de mémoire est cruciale lorsque la taille des données n’est pas
connue à la compilation, ou lorsqu’il faut gérer des structures de données dont la durée de
vie est variable.
— malloc() : Alloue un bloc de mémoire de la taille spécifiée en octets et renvoie un
pointeur ‘void *‘ vers le début du bloc. Le contenu de la mémoire n’est pas initialisé.
#include <stdlib.h> // Pour malloc, free
#include <stdio.h>
int main() {
int *tableau = (int *)malloc(5 * sizeof(int)); // Alloue 5 entiers
if (tableau == NULL) {
perror("malloc");
return 1;
}
for (int i = 0; i < 5; i++) {
tableau[i] = i * 10;
printf("%d ", tableau[i]);
}
printf("\n");
free(tableau); // Libere la memoire
return 0;
}
— calloc() : Alloue un bloc de mémoire pour un nombre spécifié d’éléments de taille
donnée, et initialise tous les bits à zéro.
int *tableau = (int *)calloc(5, sizeof(int)); // Alloue 5 entiers, initialise a
— realloc() : Modifie la taille d’un bloc de mémoire alloué précédemment. Peut déplacer
le bloc en mémoire si nécessaire.
14
Polytechnique Douala - Génie Logiciel N4 Programmation Système avec Linux et C
int *tableau = (int *)malloc(5 * sizeof(int));
// ...
tableau = (int *)realloc(tableau, 10 * sizeof(int)); // Resize a 10 entiers
— free() : Libère un bloc de mémoire alloué dynamiquement, le rendant disponible pour
d’autres allocations. Ne pas libérer la mémoire conduit à des fuites de mémoire.
Libérer une mémoire déjà libérée (double free) ou accéder à une mémoire libérée
(use-after-free) sont des erreurs graves.
1.3.3. Structures de Données et Unions
Les structures et les unions sont des types de données composés qui permettent de re-
grouper des données de types différents.
— Structures (struct) : Une structure permet de regrouper plusieurs variables de types
différents sous un seul nom. Chaque membre de la structure occupe sa propre place
en mémoire.
struct Point {
int x;
int y;
};
struct Point p1;
p1.x = 10;
p1.y = 20;
// Pointeur vers une structure
struct Point *ptr_p1 = &p1;
printf("Point: (%d, %d)\n", ptr_p1->x, ptr_p1->y); // Utiliser ’->’ pour les po
En programmation système, les structures sont omniprésentes pour représenter des
entités du système (informations sur les fichiers struct stat, descripteurs de sockets,
etc.).
— Unions (union) : Une union permet de stocker différentes variables dans la même zone
mémoire. Les membres partagent la même adresse, et seule la dernière valeur écrite est
valide. La taille de l’union est celle de son membre le plus grand. Utile pour économiser
de la mémoire lorsque différentes données ne sont pas utilisées simultanément, ou pour
interpréter les mêmes bits de mémoire de différentes manières.
union Data {
int i;
float f;
char s[20];
};
union Data data;
data.i = 10;
15
Programmation Système avec Linux et C Polytechnique Douala - Génie Logiciel N4
printf("data.i: %d\n", data.i);
data.f = 220.5;
printf("data.f: %f\n", data.f); // data.i est maintenant corrompu
printf("data.i (corrompu): %d\n", data.i);
— typedef : Utilisé pour créer des alias pour des types de données existants, ce qui rend
le code plus lisible.
typedef struct Point Point_t; // Maintenant, on peut utiliser Point_t au lieu d
Point_t p2;
p2.x = 5;
p2.y = 7;
1.3.4. Les Fichiers d’En-tête et les Librairies
— Fichiers d’en-tête (.h) : Les fichiers d’en-tête contiennent les déclarations des fonc-
tions, les définitions des macros, les types personnalisés (struct, union, typedef) et
les variables externes. Ils ne contiennent pas l’implémentation du code. L’inclusion
d’un fichier d’en-tête avec #include permet au compilateur de connaître les signa-
tures des fonctions et les structures de données utilisées dans votre code. Pour les ap-
pels système et les structures système, vous inclurez des fichiers comme <unistd.h>,
<fcntl.h>, <sys/stat.h>, etc.
— Librairies (.a ou .so) : Les librairies contiennent le code compilé (les implémenta-
tions des fonctions) qui peut être lié à votre programme.
— Librairies statiques (.a) : Le code de la librairie est copié directement dans
votre exécutable final. L’exécutable est plus grand, mais autonome.
— Librairies dynamiques / partagées (.so) : Le code de la librairie est chargé
en mémoire au moment de l’exécution du programme. L’exécutable est plus petit,
et plusieurs programmes peuvent partager la même instance de la librairie en
mémoire, économisant des ressources.
— Importance des pages de manuel (man pages) : Les pages de manuel sont
votre meilleure amie en programmation système. Elles fournissent une documentation
complète sur les appels système, les fonctions de librairie, les formats de fichiers et
d’autres outils.
— man 2 <appel_systeme> : Pour les appels système (ex : man 2 open).
— man 3 <fonction_librairie> : Pour les fonctions de la librairie standard (ex :
man 3 printf).
— man <commande> : Pour les commandes shell (ex : man ls).
Exercice 1.3 :
Rappel C pour la Programmation Système
[label=()]Écrivez un programme C qui alloue dynamiquement un tableau de 10 entiers, le
remplit avec des valeurs (ex : 0 à 9), puis affiche son contenu en utilisant l’arithmétique de
pointeurs. N’oubliez pas de libérer la mémoire. Définissez une structure ‘Personne‘
16
Polytechnique Douala - Génie Logiciel N4 Programmation Système avec Linux et C
contenant un nom (tableau de caractères), un âge et un ID. Créez un pointeur vers une
instance de cette structure, allouez-la dynamiquement, remplissez ses membres, puis
affichez-les en utilisant le pointeur. Écrivez une fonction ‘void traiterd onnees(void ∗
data, sizet size, void(∗processf unc)(void∗))‘quiprendunpointeurgnrique‘data‘, sataille, etunpointeurversu
‘enargumentettraiterauneportiondesdonnes.Crezdeuximplmentationsde‘processf unc‘(parexemple, unep
Correction Exercice 1.3, partie (b) - Structures et allocation dynamique
3.
1.
2. #include <stdio.h>
#include <stdlib.h> // Pour malloc, free
#include <string.h> // Pour strcpy
// Definition de la structure Personne
typedef struct {
char nom[50];
int age;
int id;
} Personne; // Utilisation de typedef pour simplifier le nom
int main() {
// 1. Creation d’un pointeur vers une instance de Personne
Personne *individu;
// 2. Allocation dynamique de la memoire pour la structure
individu = (Personne *)malloc(sizeof(Personne));
// Verifier si l’allocation a reussi
if (individu == NULL) {
perror("Erreur d’allocation memoire pour Personne");
return EXIT_FAILURE;
}
// 3. Remplir les membres de la structure en utilisant le pointeur
// Utiliser -> pour acceder aux membres via un pointeur de structure
strcpy(individu->nom, "Jean Dupont");
individu->age = 30;
individu->id = 12345;
// 4. Afficher les membres en utilisant le pointeur
printf("Informations de la personne:\n");
printf(" Nom: %s\n", individu->nom);
printf(" Age: %d ans\n", individu->age);
printf(" ID : %d\n", individu->id);
// 5. Liberer la memoire allouee
17
Programmation Système avec Linux et C Polytechnique Douala - Génie Logiciel N4
free(individu);
individu = NULL; // Bonne pratique: eviter les pointeurs suspendus
return EXIT_SUCCESS;
}
18
Chapitre 2
Gestion des Fichiers et Entrées/Sorties
de Bas Niveau
Ce chapitre plonge dans l’interaction de bas niveau avec le système de fichiers Linux. Nous
explorerons les concepts fondamentaux des fichiers, y compris les descripteurs de fichiers, les
types et les permissions. Le cœur du chapitre sera dédié aux appels système pour l’ouverture,
la fermeture, la lecture, l’écriture et le positionnement dans les fichiers. Enfin, nous verrons
comment manipuler les répertoires et obtenir des informations détaillées sur les attributs des
fichiers. La compréhension de ces mécanismes est essentielle pour développer des applications
C robustes qui gèrent efficacement les données sur disque.
2.1. Concepts Fondamentaux des Fichiers sous Linux
Sous Linux, presque tout est considéré comme un fichier. Cette abstraction simplifie de
nombreuses opérations, mais nécessite une compréhension des mécanismes sous-jacents.
2.1.1. Descripteurs de Fichiers (File Descriptors)
Un descripteur de fichier (File Descriptor, FD) est un entier non-négatif utilisé par le
noyau Linux pour identifier un fichier ou une ressource d’E/S ouverte par un processus. Lors-
qu’un programme ouvre un fichier, le système d’exploitation renvoie un descripteur de fichier
qui est ensuite utilisé pour toutes les opérations subséquentes sur ce fichier (lecture, écriture,
fermeture, etc.). Les descripteurs de fichiers sont des indices dans une table maintenue par
le noyau pour chaque processus.
Par défaut, chaque processus hérite de trois descripteurs de fichiers standard :
— 0 : Standard Input (STDIN_FILENO), généralement associé au clavier.
— 1 : Standard Output (STDOUT_FILENO), généralement associé à l’écran (console).
— 2 : Standard Error (STDERR_FILENO), également associé à l’écran par défaut, mais
spécifiquement utilisé pour les messages d’erreur.
Ces descripteurs standard sont automatiquement ouverts au démarrage de chaque programme.
19
Programmation Système avec Linux et C Polytechnique Douala - Génie Logiciel N4
2.1.2. Types de Fichiers
Linux supporte plusieurs types de fichiers, chacun ayant un comportement spécifique et
étant reconnaissable par un caractère initial dans la sortie de la commande ls -l :
— Fichier régulier (-) : Contient des données (texte, exécutables, images, etc.). C’est
le type de fichier le plus courant.
— Répertoire (d) : Contient une liste d’autres fichiers et répertoires.
— Lien symbolique (symlink) (l) : Un pointeur vers un autre fichier ou répertoire.
Similaire à un raccourci Windows. La suppression du lien symbolique n’affecte pas le
fichier cible.
— Lien physique (hard link) : Une entrée de répertoire supplémentaire pour un fichier
existant. Le fichier existe tant qu’au moins un lien physique pointe vers lui. Tous les
liens physiques sont des entrées de répertoire égales pointant vers le même inode.
— Fichier périphérique (device file) : Représente un périphérique matériel (disque
dur, imprimante, etc.) ou une pseudo-périphérique. Il existe deux types principaux :
— Fichier bloc (b) : Pour les périphériques qui transfèrent des données par blocs de
taille fixe (ex : disques durs, /dev/sda1).
— Fichier caractère (c) : Pour les périphériques qui transfèrent des données carac-
tère par caractère (ex : terminal, /dev/tty).
— FIFO (Named Pipe) (p) : Un tube nommé, utilisé pour la communication unidi-
rectionnelle entre processus. Il est représenté comme un fichier dans le système de
fichiers, ce qui permet à des processus non liés de communiquer.
— Socket (s) : Utilisé pour la communication réseau entre processus, y compris sur
la même machine (sockets de domaine UNIX) ou sur des réseaux distants (sockets
Internet). Un socket de domaine UNIX est représenté comme un fichier dans le système
de fichiers.
2.1.3. Permissions de Fichiers
Les permissions de fichiers sous Linux déterminent qui peut accéder à un fichier et de
quelle manière. Elles sont organisées en trois catégories d’utilisateurs et trois types de droits,
représentées sous forme symbolique ou octale.
— Catégories d’utilisateurs :
— u : User (Propriétaire du fichier)
— g : Group (Membres du groupe propriétaire du fichier)
— o : Others (Tous les autres utilisateurs du système qui ne sont ni le propriétaire,
ni membres du groupe)
— Types de droits :
— r : Read (Lecture) : Permet de lire le contenu d’un fichier, ou de lister le contenu
d’un répertoire.
— w : Write (Écriture) : Permet de modifier le contenu d’un fichier, ou de créer/supprimer
des fichiers dans un répertoire.
— x : Execute (Exécution) : Permet d’exécuter un fichier (s’il est un programme),
ou de traverser un répertoire (y accéder).
Ces droits sont souvent représentés par une suite de 9 caractères (trois blocs de rwx) ou
20
Polytechnique Douala - Génie Logiciel N4 Programmation Système avec Linux et C
par une valeur octale.
— Valeurs octales des droits :
— r=4
— w=2
— x=1
— - = 0 (aucun droit)
Une permission octale est la somme des droits pour chaque catégorie (propriétaire, groupe,
autres). Par exemple, 764 signifie :
— Propriétaire : 4 + 2 + 1 = 7 (rwx - lecture, écriture, exécution)
— Groupe : 4 + 2 + 0 = 6 (rw- - lecture, écriture seulement)
— Autres : 4 + 0 + 0 = 4 (r– - lecture seulement)
Un fichier typique peut avoir des permissions 644 (rw-r–r–), tandis qu’un exécutable aurait
755 (rwxr-xr-x).
Le umask est un masque de création de fichier qui spécifie les permissions qui ne doivent
pas être définies par défaut lors de la création d’un nouveau fichier ou répertoire. Les per-
missions finales d’un nouveau fichier sont (mode_demandé AND NOT umask). Par exemple, si
le umask est 0022 (par défaut sur de nombreux systèmes) et que vous demandez 0666 pour
un fichier, les permissions finales seront 0644.
2.2. Opérations de Fichier de Bas Niveau
Les opérations de fichier de bas niveau sont réalisées via des appels système qui inter-
agissent directement avec le noyau. Ces appels sont plus granulaires et offrent un contrôle
plus fin que les fonctions de la librairie standard (comme fopen, fread, fwrite).
2.2.1. Ouverture et Fermeture de Fichiers
open()
La fonction open() est l’appel système utilisé pour ouvrir un fichier existant ou en créer
un nouveau. Elle renvoie un descripteur de fichier qui est un entier non-négatif que le noyau
utilise pour identifier le fichier pour toutes les opérations d’E/S subséquentes.
— Syntaxe :
#include <sys/types.h> // Souvent nécessaire pour des types comme mode_t
#include <sys/stat.h> // Pour les flags O_CREAT (S_IRWXU, ...)
#include <fcntl.h> // Pour les flags d’ouverture (O_RDONLY, O_WRONLY, ...)
int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode); // Utilisé avec O_CREAT
— pathname : Une chaîne de caractères C (const char *) spécifiant le chemin d’accès
au fichier à ouvrir ou créer.
— flags : Un ou plusieurs drapeaux (flags) combinés avec l’opérateur OR bit-à-bit (|)
qui spécifient le mode d’accès au fichier et le comportement de open().
21
Programmation Système avec Linux et C Polytechnique Douala - Génie Logiciel N4
— Modes d’accès (obligatoire, un seul doit être spécifié) :
— O_RDONLY : Ouvrir en lecture seule.
— O_WRONLY : Ouvrir en écriture seule.
— O_RDWR : Ouvrir en lecture et écriture.
— Drapeaux de création/comportement (optionnels) :
— O_CREAT : Si le fichier n’existe pas, il est créé. Si le fichier existe, cet attribut
n’a aucun effet (sauf si O_EXCL est aussi spécifié). Lorsque O_CREAT est utilisé,
le troisième argument mode est obligatoire pour spécifier les permissions du
nouveau fichier.
— O_EXCL : Utilisé en conjonction avec O_CREAT. Si O_CREAT et O_EXCL sont tous
deux spécifiés, open() échoue avec l’erreur EEXIST si le fichier existe déjà. Cela
est utile pour implémenter des verrous de fichier simples ou pour s’assurer qu’un
processus est le seul à créer un fichier.
— O_TRUNC : Si le fichier existe et qu’il est ouvert en mode écriture (O_WRONLY ou
O_RDWR), son contenu est tronqué à une taille de 0 octet.
— O_APPEND : Toutes les écritures sur le fichier se feront à la fin du fichier, quelle
que soit la position actuelle du pointeur de fichier.
— O_NONBLOCK : Ouvre le fichier en mode non bloquant (pour les périphériques
ou FIFOs).
— O_DSYNC, O_RSYNC, O_SYNC : Drapeaux pour la synchronisation des écritures
sur le disque.
— mode : (Type mode_t, utilisé uniquement si O_CREAT est spécifié) Il s’agit d’une
valeur octale (par exemple, 0644) qui spécifie les permissions du fichier créé. Ces
permissions sont ensuite modifiées par le umask du processus.
— Valeur de retour :
— Succès : Un entier non négatif qui est le nouveau descripteur de fichier. Il est
garanti que ce sera le plus petit descripteur de fichier non utilisé disponible.
— Échec : -1. Dans ce cas, la variable globale errno est définie pour indiquer la
cause exacte de l’erreur (perror() peut être utilisée pour afficher un message
d’erreur lisible).
— Exemples d’utilisation de open() :
#include <fcntl.h> // Pour O_WRONLY, O_CREAT, O_TRUNC, O_RDONLY, O_EXCL
#include <stdio.h> // Pour printf, perror
#include <stdlib.h> // Pour EXIT_FAILURE, EXIT_SUCCESS
#include <unistd.h> // Pour close
int main() {
int fd_lecture, fd_ecriture, fd_excl;
// --- Exemple 1: Ouvrir un fichier en lecture seule ---
// Supposons que ’donnees.txt’ existe
fd_lecture = open("donnees.txt", O_RDONLY);
if (fd_lecture == -1) {
perror("Erreur lors de l’ouverture de ’donnees.txt’ en lecture");
// Le programme peut continuer ou sortir selon la logique
22
Polytechnique Douala - Génie Logiciel N4 Programmation Système avec Linux et C
} else {
printf("Fichier ’donnees.txt’ ouvert avec FD : %d\n", fd_lecture);
// ... operations de lecture ...
close(fd_lecture);
printf("Fichier ’donnees.txt’ ferme.\n\n");
}
// --- Exemple 2: Creer ou ecraser un fichier pour l’ecriture ---
// O_WRONLY: ecriture seule
// O_CREAT: cree le fichier s’il n’existe pas
// O_TRUNC: tronque le fichier a taille 0 s’il existe et qu’il est ouvert e
// 0644: permissions (rw-r--r--) pour le nouveau fichier
fd_ecriture = open("fichier_sortie.txt", O_WRONLY | O_CREAT | O_TRUNC, 0644
if (fd_ecriture == -1) {
perror("Erreur lors de l’ouverture/creation de ’fichier_sortie.txt’");
return EXIT_FAILURE; // Erreur critique, le programme sort
}
printf("Fichier ’fichier_sortie.txt’ ouvert/cree avec FD : %d\n", fd_ecritu
// ... operations d’ecriture ...
close(fd_ecriture);
printf("Fichier ’fichier_sortie.txt’ ferme.\n\n");
// --- Exemple 3: Creer un fichier de maniere exclusive ---
// O_CREAT | O_EXCL: cree le fichier UNIQUEMENT s’il n’existe pas.
// Si ’fichier_exclusif.txt’ existe deja, open() echouera avec EEXIST.
fd_excl = open("fichier_exclusif.txt", O_WRONLY | O_CREAT | O_EXCL, 0600);
if (fd_excl == -1) {
perror("Tentative de creation exclusive de ’fichier_exclusif.txt’");
// Ici, c’est normal d’avoir une erreur si vous l’exécutez plusieurs fo
} else {
printf("Fichier ’fichier_exclusif.txt’ créé exclusivement avec FD : %d\
close(fd_excl);
}
printf("\n");
return EXIT_SUCCESS;
}
close()
La fonction close() est utilisée pour fermer un descripteur de fichier, libérant ainsi la
ressource associée au système d’exploitation. C’est une étape cruciale pour éviter les fuites
de descripteurs de fichiers, garantir que toutes les données mises en tampon sont écrites sur
le disque et permettre à d’autres processus d’accéder au fichier (si des verrous sont en place).
23
Programmation Système avec Linux et C Polytechnique Douala - Génie Logiciel N4
— Syntaxe :
#include <unistd.h> // Pour close
int close(int fd);
— fd : L’entier représentant le descripteur de fichier à fermer.
— Valeur de retour :
— Succès : 0.
— Échec : -1, et la variable globale errno est définie pour indiquer l’erreur (par
exemple, EBADF si fd n’est pas un descripteur valide).
Il est de bonne pratique de toujours vérifier la valeur de retour de close() en production,
bien que dans les exemples simples, cela soit parfois omis pour la clarté. Un échec de close()
peut indiquer que des données n’ont pas été correctement écrites sur le disque.
2.2.2. Lecture et Écriture dans les Fichiers
Une fois un fichier ouvert, read() et write() sont utilisées pour transférer des données.
Ces fonctions opèrent sur des blocs d’octets bruts.
read()
La fonction read() tente de lire un certain nombre d’octets à partir du descripteur de
fichier spécifié et de les placer dans un buffer mémoire.
— Syntaxe :
#include <unistd.h> // Pour read
ssize_t read(int fd, void *buf, size_t count);
— fd : Le descripteur de fichier à partir duquel les données doivent être lues.
— buf : Un pointeur vers le buffer (zone mémoire) où les données lues seront stockées.
C’est généralement un tableau de caractères ou une structure.
— count : Le nombre maximal d’octets que read() doit essayer de lire.
— Valeur de retour :
— Succès : Le nombre d’octets réellement lus. Ce nombre peut être inférieur à count
dans plusieurs cas :
— La fin du fichier (EOF) a été atteinte (retourne 0).
— Moins d’octets que demandé étaient disponibles (ex : lecture depuis un pipe,
un terminal).
— Une interruption par un signal s’est produite.
— Échec : -1, et errno est définie pour indiquer la cause de l’erreur (ex : EAGAIN
pour une opération non bloquante, EBADF pour un descripteur invalide).
write()
La fonction write() tente d’écrire un certain nombre d’octets depuis un buffer mémoire
vers le descripteur de fichier spécifié.
— Syntaxe :
24
Polytechnique Douala - Génie Logiciel N4 Programmation Système avec Linux et C
#include <unistd.h> // Pour write
ssize_t write(int fd, const void *buf, size_t count);
— fd : Le descripteur de fichier vers lequel les données doivent être écrites.
— buf : Un pointeur vers le buffer (zone mémoire) contenant les données à écrire.
— count : Le nombre d’octets à écrire.
— Valeur de retour :
— Succès : Le nombre d’octets réellement écrits. Ce nombre peut être inférieur à count
si, par exemple, le disque est plein, une erreur s’est produite ou le descripteur est
en mode non bloquant.
— Échec : -1, et errno est définie.
— Exemple de lecture/écriture : Ce programme crée un fichier, y écrit une chaîne de
caractères, puis relit le contenu de ce même fichier et l’affiche à l’écran.
#include <unistd.h> // Pour read, write, close
#include <fcntl.h> // Pour open, O_WRONLY, O_CREAT, O_TRUNC, O_RDONLY
#include <stdio.h> // Pour printf, perror
#include <stdlib.h> // Pour EXIT_FAILURE, EXIT_SUCCESS
#include <string.h> // Pour strlen
#define BUFFER_SIZE 100 // Taille du buffer de lecture
int main() {
int fd_fichier;
char buffer_ecriture[] = "Ceci est un message d’exemple pour le fichier.\n"
char buffer_lecture[BUFFER_SIZE];
ssize_t bytes_transferred; // Pour stocker le nombre d’octets lus/ecrits
// --- PARTIE 1: Ecriture dans un fichier ---
// Ouvrir (ou creer) le fichier en mode ecriture seule,
// le tronquer s’il existe, avec les permissions rw-r--r-- (0644)
fd_fichier = open("output_bas_niveau.txt", O_WRONLY | O_CREAT | O_TRUNC, 06
if (fd_fichier == -1) {
perror("Erreur lors de l’ouverture du fichier pour ecriture");
return EXIT_FAILURE;
}
// Ecrire le contenu du buffer_ecriture dans le fichier
bytes_transferred = write(fd_fichier, buffer_ecriture, strlen(buffer_ecritu
if (bytes_transferred == -1) {
perror("Erreur lors de l’ecriture dans le fichier");
close(fd_fichier);
return EXIT_FAILURE;
}
printf("Ecrit %zd octets dans ’output_bas_niveau.txt’.\n", bytes_transferre
close(fd_fichier); // Fermer le descripteur d’ecriture
25
Programmation Système avec Linux et C Polytechnique Douala - Génie Logiciel N4
// --- PARTIE 2: Lecture depuis le meme fichier ---
// Ouvrir le fichier en mode lecture seule
fd_fichier = open("output_bas_niveau.txt", O_RDONLY);
if (fd_fichier == -1) {
perror("Erreur lors de l’ouverture du fichier pour lecture");
return EXIT_FAILURE;
}
// Lire le contenu du fichier dans buffer_lecture
// On lit au maximum BUFFER_SIZE - 1 octets pour reserver de la place pour
bytes_transferred = read(fd_fichier, buffer_lecture, sizeof(buffer_lecture)
if (bytes_transferred == -1) {
perror("Erreur lors de la lecture du fichier");
close(fd_fichier);
return EXIT_FAILURE;
}
// Ajouter le caractere de fin de chaine pour s’assurer que c’est une chain
buffer_lecture[bytes_transferred] = ’\0’;
printf("Lu %zd octets depuis ’output_bas_niveau.txt’:\n%s", bytes_transferr
close(fd_fichier); // Fermer le descripteur de lecture
return EXIT_SUCCESS;
}
2.2.3. Positionnement dans les Fichiers (lseek())
Normalement, chaque opération de lecture ou d’écriture avance un "pointeur de fichier"
interne maintenu par le noyau pour ce descripteur de fichier. La fonction lseek() permet de
modifier explicitement la position de ce pointeur, ce qui est essentiel pour l’accès aléatoire
aux données dans un fichier.
— Syntaxe :
#include <unistd.h> // Pour lseek, off_t
off_t lseek(int fd, off_t offset, int whence);
— fd : Le descripteur de fichier sur lequel opérer.
— offset : La valeur de décalage (en octets) à appliquer par rapport au point de référence
spécifié par whence. Il peut être positif (vers la fin), négatif (vers le début) ou nul.
— whence : Spécifie le point de référence pour le décalage :
— SEEK_SET : L’offset est relatif au début du fichier (position absolue). La nouvelle
position sera offset.
— SEEK_CUR : L’offset est relatif à la position courante du pointeur de fichier. La
nouvelle position sera position_courante + offset.
— SEEK_END : L’offset est relatif à la fin du fichier. La nouvelle position sera taille_du_fichier
26
Polytechnique Douala - Génie Logiciel N4 Programmation Système avec Linux et C
+ offset.
— Valeur de retour :
— Succès : La nouvelle position du pointeur de fichier, mesurée en octets depuis le
début du fichier.
— Échec : (off_t)-1, et errno est définie (ex : EINVAL si whence est invalide ou si
l’offset est hors des limites valides).
— Exemple de lseek() : Ce programme crée un fichier, y écrit des caractères, puis utilise
lseek() pour se positionner à différents endroits et lire des sous-parties du contenu.
#include <unistd.h> // Pour lseek, read, write, close
#include <fcntl.h> // Pour open, O_RDWR, O_CREAT, O_TRUNC
#include <stdio.h> // Pour printf, perror
#include <stdlib.h> // Pour EXIT_FAILURE, EXIT_SUCCESS
#include <string.h> // Pour strlen
int main() {
int fd;
char buffer[20]; // Buffer pour la lecture
off_t current_pos; // Variable pour stocker la position du pointeur
// Ouvrir/Creer le fichier en lecture/ecriture, tronquer s’il existe
fd = open("lseek_test.txt", O_RDWR | O_CREAT | O_TRUNC, 0644);
if (fd == -1) {
perror("Erreur lors de l’ouverture du fichier");
return EXIT_FAILURE;
}
// Ecrire une sequence de caracteres dans le fichier
if (write(fd, "0123456789ABCDEF", 16) == -1) {
perror("Erreur lors de l’ecriture initiale");
close(fd);
return EXIT_FAILURE;
}
// 1. Obtenir la position courante du pointeur
// apres l’ecriture, elle devrait etre 16 (fin du texte ecrit)
current_pos = lseek(fd, 0, SEEK_CUR);
printf("1. Position actuelle apres ecriture: %ld octets\n", (long)current_p
// 2. Positionner le pointeur au debut du fichier (offset 0)
lseek(fd, 0, SEEK_SET);
printf("2. Pointeur repositionne au debut (0 octets).\n");
// 3. Lire 5 octets depuis le debut
if (read(fd, buffer, 5) == -1) { perror("read 1"); close(fd); return EXIT_F
buffer[5] = ’\0’; // Nul-terminate le buffer
27
Programmation Système avec Linux et C Polytechnique Douala - Génie Logiciel N4
printf("3. Lu 5 octets depuis le debut: ’%s’\n", buffer);
// Position actuelle du pointeur devrait etre 5
// 4. Obtenir la nouvelle position courante (devrait etre 5)
current_pos = lseek(fd, 0, SEEK_CUR);
printf("4. Position actuelle apres lecture: %ld octets\n", (long)current_po
// 5. Positionner le pointeur a 5 octets de la fin du fichier
// Le fichier a 16 octets. Nouvelle position = 16 - 5 = 11.
lseek(fd, -5, SEEK_END);
printf("5. Pointeur repositionne a 5 octets de la fin (%ld octets).\n", (lo
// 6. Lire 5 octets depuis cette nouvelle position
if (read(fd, buffer, 5) == -1) { perror("read 2"); close(fd); return EXIT_F
buffer[5] = ’\0’; // Nul-terminate le buffer
printf("6. Lu 5 octets depuis cette position: ’%s’\n", buffer); // Devrait
close(fd);
printf("\nFichier ’lseek_test.txt’ ferme.\n");
return EXIT_SUCCESS;
}
2.2.4. Duplication des Descripteurs de Fichiers (dup(), dup2())
Les fonctions dup() et dup2() sont utilisées pour dupliquer des descripteurs de fichiers.
Cela signifie qu’un nouveau descripteur de fichier est créé, mais qu’il pointe vers la même
entrée de la table des fichiers ouverts du processus que le descripteur original. Les deux des-
cripteurs partagent donc la même position de lecture/écriture, les mêmes drapeaux d’accès,
et la même information d’offset.
dup()
— Syntaxe :
#include <unistd.h> // Pour dup
int dup(int oldfd);
— La fonction dup() crée une copie de oldfd en utilisant le plus petit descripteur de
fichier disponible qui n’est pas encore utilisé par le processus.
— Valeur de retour :
— Succès : Le nouveau descripteur de fichier.
— Échec : -1, et errno est définie.
28
Polytechnique Douala - Génie Logiciel N4 Programmation Système avec Linux et C
dup2()
— Syntaxe :
#include <unistd.h> // Pour dup2
int dup2(int oldfd, int newfd);
— La fonction dup2() crée une copie de oldfd dans un descripteur de fichier spécifique
désigné par newfd.
— Si newfd est déjà un descripteur de fichier ouvert, il est fermé avant que la copie ne
soit effectuée.
— Si oldfd est un descripteur non valide, dup2() échoue sans fermer newfd.
— Si oldfd est égal à newfd, dup2() ne fait rien et retourne newfd (sans le fermer).
— Cette fonction est particulièrement utile pour la redirection des entrées/sorties
standard, par exemple pour rediriger stdout (descripteur 1) vers un fichier au lieu
de la console.
— Valeur de retour :
— Succès : newfd.
— Échec : -1, et errno est définie.
— Exemple de dup2() (Redirection de stdout) : Ce programme redirige tout ce qui est
normalement imprimé sur la sortie standard (stdout) vers un fichier, tout en laissant
les erreurs (stderr) aller à la console.
#include <unistd.h> // Pour dup2, close, STDOUT_FILENO, STDERR_FILENO
#include <fcntl.h> // Pour open, O_WRONLY, O_CREAT, O_TRUNC
#include <stdio.h> // Pour printf, fprintf, perror
#include <stdlib.h> // Pour EXIT_FAILURE, EXIT_SUCCESS
int main() {
int fd_fichier_log;
// Ouvrir un fichier où la sortie standard sera redirigée
fd_fichier_log = open("journal_redirection.log", O_WRONLY | O_CREAT | O_TRU
if (fd_fichier_log == -1) {
perror("Erreur lors de l’ouverture du fichier journal");
return EXIT_FAILURE;
}
// Rediriger STDOUT_FILENO (normalement la console, FD 1) vers fd_fichier_l
// Tout ce qui est ecrit sur stdout (par exemple par printf) ira maintenant
if (dup2(fd_fichier_log, STDOUT_FILENO) == -1) {
perror("Erreur lors de la redirection de stdout avec dup2");
close(fd_fichier_log); // Fermer le descripteur original
return EXIT_FAILURE;
}
// Il est crucial de fermer le descripteur original maintenant.
// STDOUT_FILENO est maintenant une copie et pointe vers le fichier.
29
Programmation Système avec Linux et C Polytechnique Douala - Génie Logiciel N4
close(fd_fichier_log);
// Ces printf iront dans le fichier ’journal_redirection.log’
printf("Ceci est la premiere ligne ecrite dans le fichier.\n");
printf("Une autre ligne de texte.\n");
// fprintf(stderr, ...) va toujours a la console (STDERR_FILENO, FD 2)
fprintf(stderr, "Ceci est un message d’erreur qui apparaitra sur la console
fprintf(stderr, "Le stderr n’est pas redirige ici.\n");
printf("Et cette ligne est egalement dans le fichier.\n");
return EXIT_SUCCESS;
}
Exercice 2.2 :
Lecture/Écriture et Redirection
[]Écrivez un programme C qui ouvre un fichier nommé "source.txt" en lecture, lit son
contenu bloc par bloc (par exemple, des blocs de 64 octets), et écrit ce contenu dans un
nouveau fichier appelé "destination.txt". Assurez-vous de gérer les erreurs pour toutes
les opérations de fichier. Modifiez le programme de la question (a) pour qu’il redirige
sa propre sortie standard (stdout) vers un fichier nommé "copie_log.txt" juste avant
de commencer la copie de fichier. Le programme ne doit rien afficher sur la console,
sauf d’éventuels messages d’erreur sur stderr. Écrivez un programme qui prend un
nombre entier en argument, puis utilise lseek() pour créer un fichier "sparse" (fichier
avec des "trous") d’une taille spécifiée par cet argument (en octets). Un fichier sparse
est un fichier qui ne consomme pas d’espace disque pour les blocs de zéros. Vous pouvez
le vérifier avec la commande du -h <nom_fichier> et ls -lh <nom_fichier>.
Correction Exercice 2.2, partie (a) - Copie de fichier de bas niveau
3.
1.
2. #include <unistd.h> // Pour read, write, close
#include <fcntl.h> // Pour open, O_RDONLY, O_WRONLY, O_CREAT, O_TRUNC
#include <stdio.h> // Pour printf, perror
#include <stdlib.h> // Pour EXIT_FAILURE, EXIT_SUCCESS
#define BUFFER_SIZE 64 // Taille du bloc de lecture/ecriture
int main() {
int fd_source, fd_destination;
char buffer[BUFFER_SIZE];
ssize_t bytes_read;
30
Polytechnique Douala - Génie Logiciel N4 Programmation Système avec Linux et C
// 1. Ouvrir le fichier source en lecture seule
fd_source = open("source.txt", O_RDONLY);
if (fd_source == -1) {
perror("Erreur: Impossible d’ouvrir le fichier source.txt");
return EXIT_FAILURE;
}
// 2. Ouvrir (ou creer) le fichier destination en ecriture seule,
// le tronquer s’il existe, avec les permissions 0644
fd_destination = open("destination.txt", O_WRONLY | O_CREAT | O_TRUNC, 0644);
if (fd_destination == -1) {
perror("Erreur: Impossible d’ouvrir/creer le fichier destination.txt");
close(fd_source); // Fermer le fichier source avant de sortir
return EXIT_FAILURE;
}
printf("Copie de ’source.txt’ vers ’destination.txt’...\n");
// 3. Lire bloc par bloc du fichier source et ecrire dans le fichier destination
while ((bytes_read = read(fd_source, buffer, BUFFER_SIZE)) > 0) {
if (write(fd_destination, buffer, bytes_read) == -1) {
perror("Erreur: ecriture dans le fichier destination.txt");
close(fd_source);
close(fd_destination);
return EXIT_FAILURE;
}
}
// Gerer les erreurs de lecture (si bytes_read est -1)
if (bytes_read == -1) {
perror("Erreur: lecture du fichier source.txt");
close(fd_source);
close(fd_destination);
return EXIT_FAILURE;
}
printf("Copie terminee avec succes.\n");
// 4. Fermer les descripteurs de fichiers
close(fd_source);
close(fd_destination);
return EXIT_SUCCESS;
}
31
Programmation Système avec Linux et C Polytechnique Douala - Génie Logiciel N4
Pour tester ce code, créez d’abord un fichier ‘source.txt‘ avec du contenu (par exemple, ‘echo
"Ceci est un test." > source.txt‘ ou plus de texte pour voir les blocs).
2.2.5. Informations et Attributs des Fichiers
Il est souvent nécessaire d’obtenir des informations sur un fichier ou un répertoire sans
en lire le contenu.
2.2.6. stat(), fstat(), lstat()
Ces fonctions permettent d’obtenir des informations détaillées sur un fichier ou un réper-
toire et les stockent dans une structure struct stat.
— Syntaxe :
#include <sys/stat.h> // Pour stat, fstat, lstat, struct stat
#include <sys/types.h> // Pour mode_t, off_t
int stat(const char *pathname, struct stat *buf);
int fstat(int fd, struct stat *buf);
int lstat(const char *pathname, struct stat *buf);
— stat() : Obtient les informations sur le fichier spécifié par son chemin. Si le chemin
est un lien symbolique, il suit le lien.
— fstat() : Obtient les informations sur le fichier spécifié par son descripteur de fichier.
— lstat() : Similaire à stat(), mais si le chemin est un lien symbolique, il retourne les
informations sur le lien lui-même, pas sur la cible du lien.
— La structure struct stat contient des champs comme :
— st_mode : Type de fichier et permissions.
— st_ino : Numéro d’inode.
— st_dev : ID du périphérique sur lequel le fichier réside.
— st_nlink : Nombre de liens physiques.
— st_uid : ID utilisateur du propriétaire.
— st_gid : ID groupe du propriétaire.
— st_size : Taille totale du fichier en octets.
— st_blksize : Taille de bloc d’E/S préférée pour le système de fichiers.
— st_blocks : Nombre de blocs alloués pour le fichier.
— st_atime : Temps du dernier accès.
— st_mtime : Temps de la dernière modification (contenu).
— st_ctime : Temps du dernier changement de statut (inode, permissions).
— Des macros comme S_ISREG(), S_ISDIR(), S_ISLNK() peuvent être utilisées avec
st_mode pour déterminer le type de fichier.
— Exemple de stat() : Ce programme prend un chemin de fichier en argument et affiche
diverses informations sur ce fichier.
#include <sys/stat.h> // Pour stat, struct stat, S_ISREG, S_ISDIR, S_ISLNK
#include <stdio.h> // Pour printf, perror, fprintf
#include <stdlib.h> // Pour EXIT_FAILURE, EXIT_SUCCESS
#include <time.h> // Pour ctime
32
Polytechnique Douala - Génie Logiciel N4 Programmation Système avec Linux et C
int main(int argc, char *argv[]) {
// Verifier que le chemin du fichier est fourni en argument
if (argc != 2) {
fprintf(stderr, "Usage: %s <chemin_fichier>\n", argv[0]);
return EXIT_FAILURE;
}
struct stat file_stat; // Declare une structure pour stocker les informatio
// Appelle stat() pour obtenir les informations sur le fichier
// argv[1] est le chemin du fichier fourni en argument
if (stat(argv[1], &file_stat) == -1) {
perror("Erreur lors de l’appel stat"); // Afficher l’erreur systeme si
return EXIT_FAILURE;
}
printf("Informations sur le fichier ’%s’:\n", argv[1]);
printf(" Taille: %lld octets\n", (long long)file_stat.st_size); // Taille
printf(" Permissions: %o (octal)\n", file_stat.st_mode & 0777); // Permiss
printf(" UID du proprietaire: %d\n", file_stat.st_uid); // User ID du
printf(" GID du groupe: %d\n", file_stat.st_gid); // Group ID du gro
printf(" Nombre de liens physiques: %ld\n", (long)file_stat.st_nlink); //
printf(" Numero d’inode: %ld\n", (long)file_stat.st_ino); // Numero d’in
// Determiner et afficher le type de fichier en utilisant les macros S_IS*
printf(" Type de fichier: ");
if (S_ISREG(file_stat.st_mode)) {
printf("Fichier regulier\n");
} else if (S_ISDIR(file_stat.st_mode)) {
printf("Repertoire\n");
} else if (S_ISLNK(file_stat.st_mode)) {
printf("Lien symbolique\n");
} else if (S_ISCHR(file_stat.st_mode)) {
printf("Fichier de peripherique caractere\n");
} else if (S_ISBLK(file_stat.st_mode)) {
printf("Fichier de peripherique bloc\n");
} else if (S_ISFIFO(file_stat.st_mode)) {
printf("FIFO (tube nomme)\n");
} else if (S_ISSOCK(file_stat.st_mode)) {
printf("Socket\n");
} else {
printf("Inconnu\n");
}
33
Programmation Système avec Linux et C Polytechnique Douala - Génie Logiciel N4
// Afficher les horodatages (temps d’acces, modification, changement de sta
printf(" Dernier acces: %s", ctime(&file_stat.st_atime)); // ctime renvoie
printf(" Derniere modification: %s", ctime(&file_stat.st_mtime));
printf(" Dernier changement de statut: %s", ctime(&file_stat.st_ctime));
return EXIT_SUCCESS;
}
2.2.7. Changement des Permissions et Propriétaire (chmod(), chown())
Ces appels système permettent de modifier les attributs d’un fichier liés à la sécurité, à
savoir ses permissions et son propriétaire/groupe.
chmod()
La fonction chmod() modifie les bits de permission d’un fichier spécifié par son chemin.
— Syntaxe :
#include <sys/stat.h> // Pour chmod, mode_t
#include <sys/types.h> // Pour mode_t (parfois necessaire)
int chmod(const char *pathname, mode_t mode);
— pathname : Le chemin d’accès au fichier dont les permissions doivent être modifiées.
— mode : Une valeur octale (ex : 0755) ou une combinaison de drapeaux (ex : S_IRWXU |
S_IRGRP | S_IXGRP | S_IROTH | S_IXOTH) représentant les nouvelles permissions.
— S_IRUSR : lecture pour le propriétaire
— S_IWUSR : écriture pour le propriétaire
— S_IXUSR : exécution pour le propriétaire
— S_IRWXU : lecture/écriture/exécution pour le propriétaire
— S_IRGRP, S_IWGRP, S_IXGRP, S_IRWXG : pour le groupe
— S_IROTH, S_IWOTH, S_IXOTH, S_IRWXO : pour les autres
— Valeur de retour :
— Succès : 0.
— Échec : -1, et errno est définie (ex : EPERM si le processus n’a pas les permissions
suffisantes, ENOENT si le fichier n’existe pas).
— Exemple de chmod() : Ce programme crée un fichier, puis change ses permissions pour
le rendre exécutable par le propriétaire et lisible/exécutable par le groupe et les autres.
#include <sys/stat.h> // Pour chmod, S_IRWXU, S_IRGRP, S_IXGRP, S_IROTH, S_IXOT
#include <stdio.h> // Pour printf, perror
#include <stdlib.h> // Pour EXIT_FAILURE, EXIT_SUCCESS
#include <fcntl.h> // Pour open, O_CREAT, O_TRUNC, O_WRONLY
#include <unistd.h> // Pour close
int main() {
const char *filename = "mon_exec_file.txt";
34
Polytechnique Douala - Génie Logiciel N4 Programmation Système avec Linux et C
int fd;
mode_t initial_permissions = 0644; // rw-r--r--
mode_t new_permissions = 0755; // rwxr-xr-x
// 1. Creer un fichier avec des permissions initiales (par exemple 0644)
fd = open(filename, O_CREAT | O_TRUNC | O_WRONLY, initial_permissions);
if (fd == -1) {
perror("open");
return EXIT_FAILURE;
}
close(fd);
printf("Fichier ’%s’ cree avec les permissions 0%o.\n", filename, initial_p
// 2. Changer les permissions du fichier a 0755 (rwxr-xr-x)
if (chmod(filename, new_permissions) == -1) {
perror("chmod");
return EXIT_FAILURE;
}
printf("Permissions de ’%s’ changees en 0%o.\n", filename, new_permissions)
// Vous pouvez verifier depuis la ligne de commande avec ’ls -l mon_exec_fi
// Expected output: -rwxr-xr-x 1 user group 0 date time mon_exec_file.txt
return EXIT_SUCCESS;
}
chown()
La fonction chown() modifie le propriétaire utilisateur et/ou le groupe propriétaire d’un
fichier. Cet appel système est généralement restreint : seul le super-utilisateur (root) ou le
propriétaire actuel du fichier (pour changer seulement le groupe vers un groupe dont il est
membre) peut exécuter chown().
— Syntaxe :
#include <unistd.h> // Pour chown, uid_t, gid_t
int chown(const char *pathname, uid_t owner, gid_t group);
\end{verbatim>
\item \texttt{pathname} : Le chemin d’accès au fichier dont le propriétaire ou
\item \texttt{owner} : Le nouvel ID utilisateur du propriétaire. Si \texttt{-1}
\item \texttt{group} : Le nouvel ID de groupe. Si \texttt{-1}, le groupe n’est
\item \textbf{Valeur de retour} :
\begin{itemize>
\item Succès : \texttt{0}.
\item Échec : \texttt{-1}, et \texttt{errno} est définie (ex: \texttt{EPERM
\end{itemize>
\end{itemize>
35
Programmation Système avec Linux et C Polytechnique Douala - Génie Logiciel N4
\textbf{Note} : Exécuter le programme d’exemple suivant en tant qu’utilisateur norm
\begin{itemize}
\item Exemple de \texttt{chown()} :
Ce programme tente de changer le propriétaire et le groupe d’un fichier.
\begin{verbatim}
#include <unistd.h> // Pour chown, getuid, getgid
#include <stdio.h> // Pour printf, perror
#include <stdlib.h> // Pour EXIT_FAILURE, EXIT_SUCCESS
#include <fcntl.h> // Pour open, O_CREAT, O_TRUNC, O_WRONLY
#include <sys/types.h> // Pour uid_t, gid_t
int main() {
const char *filename = "fichier_proprietaire.txt";
int fd;
uid_t new_uid; // Nouvel ID utilisateur
gid_t new_gid; // Nouvel ID de groupe
// Creer un fichier test
fd = open(filename, O_CREAT | O_TRUNC | O_WRONLY, 0644);
if (fd == -1) {
perror("open");
return EXIT_FAILURE;
}
close(fd);
printf("Fichier ’%s’ cree.\n", filename);
// Obtenir l’UID et GID de l’utilisateur courant (pour l’exemple)
// new_uid = getuid(); // Pour le laisser a l’utilisateur courant
// new_gid = getgid(); // Pour le laisser au groupe courant
// Pour changer vraiment le proprietaire, vous auriez besoin d’un UID/GID d
// Par exemple, pour changer pour l’utilisateur ’nobody’ (souvent uid 65534
new_uid = 65534; // UID pour ’nobody’ (peut varier, verifier /etc/passwd)
new_gid = 65534; // GID pour ’nogroup’ ou ’nobody’ (peut varier, verifier /
printf("Tentative de changer proprietaire de ’%s’ en UID %d, GID %d...\n",
// Changer le proprietaire et le groupe du fichier
if (chown(filename, new_uid, new_gid) == -1) {
perror("chown");
// Ce message est attendu si le programme n’est pas execute en tant que
fprintf(stderr, "Probablement besoin d’etre root pour chowner (ex: sudo
return EXIT_FAILURE;
}
36
Polytechnique Douala - Génie Logiciel N4 Programmation Système avec Linux et C
printf("Proprietaire et groupe de ’%s’ changes avec succes.\n", filename);
// Verifiez avec ’ls -l fichier_proprietaire.txt’
return EXIT_SUCCESS;
}
2.3. Opérations sur les Répertoires
Sous Linux, les répertoires sont gérés comme des fichiers spéciaux, mais nécessitent un
ensemble d’appels système différents pour leur manipulation de contenu.
2.3.1. Lecture de Répertoires (opendir(), readdir(), closedir())
Pour parcourir le contenu d’un répertoire (c’est-à-dire lister les fichiers et sous-répertoires
qu’il contient), vous devez l’ouvrir comme un "flux de répertoire", lire ses entrées une par
une, puis le fermer.
— opendir() : Ouvre un flux de répertoire et renvoie un pointeur de type DIR * vers ce
flux.
#include <sys/types.h> // Pour DIR (necessaire par opendir, readdir)
#include <dirent.h> // Pour opendir
DIR *opendir(const char *name);
\end{verbatim>
\item \texttt{name} : Le chemin d’accès au répertoire à ouvrir.
\item \textbf{Valeur de retour} : Un pointeur \texttt{DIR *} en cas de succès,
\end{itemize>
\begin{itemize>
\item \texttt{readdir()} : Lit l’entrée suivante du flux de répertoire ouvert p
\begin{verbatim}
#include <dirent.h> // Pour readdir, struct dirent
struct dirent *readdir(DIR *dirp);
— dirp : Le pointeur DIR * retourné par opendir().
— La fonction renvoie un pointeur vers une structure struct dirent qui contient des
informations sur l’entrée lue, notamment :
— d_name : Un tableau de caractères (char d_name[]) contenant le nom du fichier
ou du sous-répertoire.
— d_type : (si supporté par le système de fichiers) Un entier indiquant le type de
l’entrée (DT_REG pour fichier régulier, DT_DIR pour répertoire, DT_LNK pour lien
symbolique, etc.). Il est important de noter que d_type n’est pas toujours sup-
porté par tous les systèmes de fichiers ou toutes les versions de noyau, et peut
retourner DT_UNKNOWN. Dans ce cas, une fonction comme stat() serait nécessaire
pour déterminer le type réel.
37
Programmation Système avec Linux et C Polytechnique Douala - Génie Logiciel N4
— Valeur de retour : Un pointeur vers la prochaine struct dirent en cas de succès,
NULL si la fin du répertoire a été atteinte ou en cas d’erreur.
— closedir() : Ferme le flux de répertoire ouvert par opendir().
#include <dirent.h> // Pour closedir
int closedir(DIR *dirp);
\end{verbatim>
\item \texttt{dirp} : Le pointeur \texttt{DIR *} à fermer.
\item \textbf{Valeur de retour} : \texttt{0} en cas de succès, \texttt{-1} en c
\end{itemize>
\begin{itemize}
\item Exemple de lecture de répertoire :
Ce programme liste le contenu du répertoire courant ou d’un répertoire spécifié
\begin{verbatim}
#include <dirent.h> // Pour DIR, dirent, opendir, readdir, closedir, DT_REG,
#include <stdio.h> // Pour printf, perror
#include <stdlib.h> // Pour EXIT_FAILURE, EXIT_SUCCESS
#include <string.h> // Pour strcmp
int main(int argc, char *argv[]) {
DIR *dirp; // Pointeur vers le flux de repertoire
struct dirent *dp; // Pointeur vers la structure d’entree de repertoire
const char *dirname; // Nom du repertoire a lister
// Determiner le repertoire a lister: argument ou repertoire courant
if (argc < 2) {
dirname = "."; // Repertoire courant
} else {
dirname = argv[1]; // Repertoire specifie par l’utilisateur
}
// 1. Ouvrir le repertoire
dirp = opendir(dirname);
if (dirp == NULL) {
perror("Erreur lors de l’ouverture du repertoire");
return EXIT_FAILURE;
}
printf("Contenu du repertoire ’%s’:\n", dirname);
// 2. Lire chaque entree du repertoire jusqu’a la fin
while ((dp = readdir(dirp)) != NULL) {
// Ignorer les entrees "." (repertoire courant) et ".." (repertoire par
if (strcmp(dp->d_name, ".") != 0 && strcmp(dp->d_name, "..") != 0) {
printf("- %s", dp->d_name);
38
Polytechnique Douala - Génie Logiciel N4 Programmation Système avec Linux et C
// 3. Afficher le type de fichier si d_type est supporte par le sys
// Cette partie du code est conditionnelle car d_type n’est pas uni
#ifdef _DIRENT_HAVE_D_TYPE
if (dp->d_type == DT_REG) {
printf(" (Fichier)");
} else if (dp->d_type == DT_DIR) {
printf(" (Repertoire)");
} else if (dp->d_type == DT_LNK) {
printf(" (Lien symbolique)");
} else if (dp->d_type == DT_UNKNOWN) {
printf(" (Type inconnu)");
}
#endif
printf("\n");
}
}
// 4. Fermer le repertoire
if (closedir(dirp) == -1) {
perror("Erreur lors de la fermeture du repertoire");
return EXIT_FAILURE;
}
return EXIT_SUCCESS;
}
2.3.2. Création et Suppression de Répertoires (mkdir(), rmdir())
mkdir()
La fonction mkdir() est utilisée pour créer un nouveau répertoire.
— Syntaxe :
#include <sys/stat.h> // Pour mkdir, mode_t
#include <sys/types.h> // Pour mode_t (parfois necessaire pour les anciennes no
int mkdir(const char *pathname, mode_t mode);
\end{verbatim>
\item \texttt{pathname} : Le chemin du nouveau répertoire à créer.
\item \texttt{mode} : Les permissions du nouveau répertoire. C’est une valeur o
\item \textbf{Valeur de retour} :
\begin{itemize>
\item Succès : \texttt{0}.
\item Échec : \texttt{-1}, et \texttt{errno} est définie (ex: \texttt{EEXIS
\end{itemize>
\end{itemize>
39
Programmation Système avec Linux et C Polytechnique Douala - Génie Logiciel N4
\subsubsection{\texttt{rmdir()}}
La fonction \texttt{rmdir()} est utilisée pour supprimer un répertoire existant.
\begin{itemize>
\item Syntaxe :
\begin{verbatim}
#include <unistd.h> // Pour rmdir
int rmdir(const char *pathname);
\end{verbatim>
\item \texttt{pathname} : Le chemin du répertoire à supprimer.
\item \textbf{Important} : \texttt{rmdir()} ne peut supprimer qu’un répertoire
\item \textbf{Valeur de retour} :
\begin{itemize>
\item Succès : \texttt{0}.
\item Échec : \texttt{-1}, et \texttt{errno} est définie (ex: \texttt{ENOTE
\end{itemize>
\end{itemize>
\begin{itemize>
\item Exemple de création et suppression de répertoire :
Ce programme crée un répertoire, puis le supprime. Il inclut également une tent
\begin{verbatim}
#include <sys/stat.h> // Pour mkdir
#include <unistd.h> // Pour rmdir, close
#include <stdio.h> // Pour printf, perror
#include <stdlib.h> // Pour EXIT_FAILURE, EXIT_SUCCESS
#include <fcntl.h> // Pour open, O_CREAT, O_WRONLY
int main() {
const char *dir_name = "mon_nouveau_repertoire";
const char *nested_dir_name = "mon_nouveau_repertoire/sous_repertoire";
const char *file_in_dir = "mon_nouveau_repertoire/fichier.txt";
int fd;
// 1. Creation du repertoire parent avec permissions 0755
// (rwxr-xr-x: proprietaire peut tout faire, groupe et autres peuvent lire
if (mkdir(dir_name, 0755) == -1) {
perror("Erreur lors de la creation du repertoire parent");
return EXIT_FAILURE;
}
printf("Repertoire ’%s’ cree avec succes.\n", dir_name);
// 2. Tentative de supprimer le repertoire (devrait reussir car vide)
if (rmdir(dir_name) == -1) {
perror("Erreur lors de la suppression du repertoire vide");
40
Polytechnique Douala - Génie Logiciel N4 Programmation Système avec Linux et C
return EXIT_FAILURE;
}
printf("Repertoire ’%s’ supprime avec succes car il etait vide.\n\n", dir_n
// --- Demo de rmdir sur repertoire non vide ---
// Re-creation du repertoire parent
if (mkdir(dir_name, 0755) == -1) {
perror("Erreur lors de la re-creation du repertoire parent");
return EXIT_FAILURE;
}
printf("Repertoire ’%s’ re-cree.\n", dir_name);
// Creation d’un sous-repertoire et d’un fichier a l’interieur
if (mkdir(nested_dir_name, 0755) == -1) {
perror("Erreur lors de la creation du sous-repertoire");
rmdir(dir_name); return EXIT_FAILURE; // Nettoyer
}
printf("Sous-repertoire ’%s’ cree.\n", nested_dir_name);
fd = open(file_in_dir, O_CREAT | O_WRONLY, 0644);
if (fd == -1) {
perror("Erreur lors de la creation du fichier dans le repertoire");
rmdir(nested_dir_name); rmdir(dir_name); return EXIT_FAILURE; // Nettoy
}
close(fd);
printf("Fichier ’%s’ cree a l’interieur.\n", file_in_dir);
// Tentative de supprimer le repertoire parent (devrait echouer car non vid
printf("\nTentative de suppression de ’%s’ (non vide)...\n", dir_name);
if (rmdir(dir_name) == -1) {
perror("Attendu: Erreur lors de la suppression du repertoire non vide")
printf("Le repertoire ’%s’ n’est pas vide et ne peut pas etre supprime
} else {
printf("Erreur inattendue: Repertoire ’%s’ supprime alors qu’il ne devr
}
// Nettoyage manuel (pour que le test puisse etre execute a nouveau)
printf("\nNettoyage des repertoires crees pour l’exemple...\n");
if (unlink(file_in_dir) == -1) { // Supprimer le fichier
perror("unlink fichier");
}
if (rmdir(nested_dir_name) == -1) { // Supprimer le sous-repertoire vide
perror("rmdir sous-repertoire");
}
41
Programmation Système avec Linux et C Polytechnique Douala - Génie Logiciel N4
if (rmdir(dir_name) == -1) { // Supprimer le repertoire parent vide
perror("rmdir repertoire parent");
}
printf("Nettoyage termine.\n");
return EXIT_SUCCESS;
}
\end{verbatim>
\end{itemize>
\begin{center}
\rule{0.8\textwidth}{0.5pt}
\textbf{Exercice 2.3 : Opérations sur les Répertoires}
\begin{enumerate}[label=(\alph*)]
\item Écrivez un programme C qui prend un chemin de répertoire en argument.
\item Créez un programme qui crée une structure de répertoires imbriqués co
\end{enumerate}
\rule{0.8\textwidth}{0.5pt}
42
Chapitre 3
Gestion des Processus
Ce chapitre est dédié à la gestion des processus, l’une des pierres angulaires de la program-
mation système. Nous commencerons par définir ce qu’est un processus et son cycle de vie
sous Linux. Ensuite, nous aborderons en détail les appels système essentiels pour créer de
nouveaux processus (fork(), exec()), attendre leur terminaison (wait(), waitpid()), et
les terminer proprement. Nous explorerons également les concepts de processus zombies et
orphelins, ainsi que les notions avancées comme les groupes de processus et les daemons. Com-
prendre la gestion des processus est fondamental pour concevoir des applications concurrentes
et des services système sous Linux.
3.1. Concepts de Processus
3.1.1. Processus vs. Programme
Il est crucial de bien distinguer les termes "programme" et "processus" en programmation
système :
— Programme : Un programme est une entité passive. C’est un ensemble d’instructions et
de données stockées sur un support de stockage persistant (comme un fichier exécutable
sur disque). Il s’agit du code source (ex : .c) ou du fichier binaire compilé (ex : .out,
a.out, ou un fichier sans extension).
— Processus : Un processus est une entité active. C’est une instance d’un programme
en cours d’exécution. Lorsqu’un programme est chargé en mémoire et que le système
d’exploitation commence à l’exécuter, il devient un processus. Chaque processus possède
son propre espace d’adressage mémoire, ses propres descripteurs de fichiers, son propre
état (registres du CPU, compteur ordinal, etc.). Plusieurs processus peuvent être des
instances du même programme (par exemple, plusieurs utilisateurs exécutant le même
éditeur de texte).
En résumé : un programme est la recette, un processus est la cuisson de cette recette.
43
Programmation Système avec Linux et C Polytechnique Douala - Génie Logiciel N4
3.1.2. Structure d’un Processus (PID, PPID)
Chaque processus sous Linux (et les systèmes de type UNIX) est identifié de manière
unique et possède une structure bien définie gérée par le noyau :
— Identifiant de Processus (PID) : Chaque processus se voit attribuer un numéro unique
appelé PID (Process ID) par le noyau lorsqu’il est créé. C’est un entier non négatif. Les
PIDs sont généralement des entiers consécutifs et sont réutilisés après la terminaison des
processus. L’appel système getpid() permet à un processus d’obtenir son propre PID.
— Identifiant de Processus Parent (PPID) : Chaque processus, à l’exception du pro-
cessus initial du système (souvent init ou systemd, avec PID 1), est créé par un autre
processus. Le processus qui crée un nouveau processus est appelé son parent, et le nou-
veau processus est l’enfant. Le PPID (Parent Process ID) est le PID du processus parent.
L’appel système getppid() permet à un processus d’obtenir le PID de son parent.
— Espace d’adressage mémoire : Chaque processus a son propre espace d’adressage
mémoire virtuel isolé des autres processus. Cela garantit qu’un processus ne peut pas ac-
cidentellement (ou malicieusement) accéder ou modifier la mémoire d’un autre processus.
Cet espace d’adressage est divisé en sections (texte, données, pile, tas - voir Chapitre 8).
— Descripteurs de fichiers ouverts : Un processus maintient une table de descripteurs
de fichiers (FDs) pour tous les fichiers ouverts, sockets, pipes, etc.
— État du CPU : Inclut les valeurs des registres du processeur, le compteur ordinal (ins-
truction pointer), etc.
— Informations d’authentification : UID (User ID), GID (Group ID) effectifs et réels,
qui déterminent les permissions du processus.
— Signaux en attente : Ensemble des signaux en attente d’être traités par le processus.
— Environnement : Liste de variables d’environnement (ex : PATH, HOME).
Vous pouvez utiliser la commande ps aux ou top dans le terminal Linux pour voir les PIDs
et PPIDs des processus en cours d’exécution.
3.1.3. Cycle de Vie d’un Processus
Un processus passe par plusieurs états au cours de son existence :
1. Nouveau (New) : Le processus est en cours de création. Les ressources sont allouées.
2. Prêt (Ready) : Le processus est créé, ses ressources sont allouées, et il est en attente
d’être assigné à un CPU par l’ordonnanceur du système. Il est en file d’attente.
3. Exécution (Running) : Le processus est actuellement en cours d’exécution sur le CPU.
Ses instructions sont traitées.
4. Bloqué / En attente (Waiting / Blocked) : Le processus est en attente d’un évé-
nement (par exemple, la fin d’une opération d’E/S, la disponibilité d’une ressource, la
réception d’un signal). Il ne peut pas être exécuté tant que l’événement ne s’est pas
produit.
5. Terminé (Terminated / Zombie) : Le processus a fini son exécution (volontairement
ou suite à une erreur/signal). Il libère la plupart de ses ressources, mais son entrée dans
la table des processus du noyau reste jusqu’à ce que son processus parent lise son statut
de terminaison. À ce stade, il est un processus zombie.
44
Polytechnique Douala - Génie Logiciel N4 Programmation Système avec Linux et C
6. Tué (Killed) : Le processus a été supprimé du système et ses ressources ont été complè-
tement libérées.
L’ordonnanceur du noyau est responsable de la transition entre les états "Prêt" et "Exécu-
tion".
3.2. Création de Processus
La création de nouveaux processus est une opération fondamentale en programmation sys-
tème. Linux utilise le modèle UNIX traditionnel de fork() suivi d’un exec() pour démarrer
de nouveaux programmes.
3.2.1. fork()
L’appel système fork() est utilisé pour créer un nouveau processus en dupliquant le
processus appelant. Le nouveau processus, appelé le processus enfant, est une copie presque
identique du processus parent. Il obtient son propre espace d’adressage mémoire, qui est
initialement une copie (logique, souvent par "copy-on-write") de celui du parent. Il hérite des
descripteurs de fichiers ouverts du parent.
— Syntaxe :
#include <unistd.h> // Pour fork, pid_t
pid_t fork(void);
— Valeur de retour :
— Succès :
— Dans le processus parent : fork() retourne le PID du processus enfant qui vient
d’être créé.
— Dans le processus enfant : fork() retourne 0. C’est la manière pour l’enfant de
savoir qu’il est l’enfant.
— Échec : -1, et errno est définie (ex : EAGAIN si trop de processus sont déjà créés,
ENOMEM si pas assez de mémoire).
Après un fork(), les deux processus (parent et enfant) continuent l’exécution à partir de l’ins-
truction qui suit l’appel à fork(). Il est courant d’utiliser une instruction if/else if/else
pour différencier le code exécuté par le parent et l’enfant en fonction de la valeur de retour
de fork().
— Exemple de duplication de processus avec fork() : Ce programme illustre comment un
seul appel à fork() résulte en deux processus distincts, chacun exécutant une branche
différente du code.
#include <unistd.h> // Pour fork, getpid, getppid
#include <stdio.h> // Pour printf, perror
#include <stdlib.h> // Pour EXIT_FAILURE, EXIT_SUCCESS
#include <sys/wait.h> // Pour wait (dans le parent)
int main() {
pid_t pid; // Variable pour stocker le PID de l’enfant dans le parent, ou 0 da
45
Programmation Système avec Linux et C Polytechnique Douala - Génie Logiciel N4
printf("Avant le fork : Mon PID est %d\n", getpid());
pid = fork(); // Appel systeme fork
if (pid == -1) {
// Erreur lors de la creation du processus enfant
perror("Erreur de fork");
return EXIT_FAILURE;
} else if (pid == 0) {
// Code execute UNIQUEMENT par le processus ENFANT
printf("Je suis le processus ENFANT.\n");
printf("Mon PID est %d, mon PPID est %d (PID de mon parent).\n", getpid(),
// L’enfant peut faire son travail et se terminer
return EXIT_SUCCESS; // L’enfant se termine ici
} else {
// Code execute UNIQUEMENT par le processus PARENT
printf("Je suis le processus PARENT.\n");
printf("Mon PID est %d, le PID de mon ENFANT est %d.\n", getpid(), pid);
// Le parent peut attendre la terminaison de l’enfant
int status;
pid_t terminated_child_pid = wait(&status); // wait() bloque le parent jus
if (terminated_child_pid == -1) {
perror("wait");
return EXIT_FAILURE;
}
printf("Parent: L’enfant avec le PID %d s’est termine.\n", terminated_chil
if (WIFEXITED(status)) { // Verifie si l’enfant s’est termine normalement
printf("Parent: Statut de sortie de l’enfant : %d.\n", WEXITSTATUS(sta
}
}
printf("Fin du programme. Mon PID est %d\n", getpid());
// Cette ligne sera executee par les deux processus (parent et enfant)
// si l’enfant n’est pas termine par un exit() ou un return dans son bloc.
return EXIT_SUCCESS;
}
3.2.2. exec()
Alors que fork() crée une copie d’un processus, la famille de fonctions exec() est utilisée
pour charger et exécuter un nouveau programme dans l’espace d’adressage du processus
46
Polytechnique Douala - Génie Logiciel N4 Programmation Système avec Linux et C
appelant. Lorsque exec() réussit, le code du programme appelant est entièrement remplacé
par le nouveau programme, et l’exécution commence au début du nouveau programme. Le
PID du processus ne change pas.
Il existe plusieurs variantes de exec(), chacune avec des conventions d’arguments légère-
ment différentes. Elles commencent toutes par execl (liste d’arguments) ou execv (tableau
d’arguments), et peuvent avoir des suffixes supplémentaires :
— l (list) : Les arguments sont passés en tant que liste séparée par des virgules, terminée
par un NULL.
— v (vector ) : Les arguments sont passés en tant que tableau de pointeurs de chaînes (char
*const argv[]), terminé par NULL.
— p (path) : Le système recherche l’exécutable dans le PATH de l’environnement (similaire à
la façon dont le shell trouve les commandes). Sans p, le chemin complet de l’exécutable
doit être fourni.
— e (environment) : Un tableau de chaînes pour les variables d’environnement est passé
explicitement. Sans e, le nouvel environnement hérite de l’environnement du processus
appelant.
Les plus courantes sont execlp() et execvp().
— Syntaxe générale (exemple pour execlp()) :
#include <unistd.h> // Pour execlp
int execlp(const char *file, const char *arg, ... /* (char *) NULL */);
— file : Le nom de l’exécutable à lancer. Si file ne contient pas de ’/’, execlp utilise le
PATH de l’environnement.
— arg, ... : Les arguments de ligne de commande à passer au nouveau programme, terminés
par NULL. Le premier argument (arg) doit généralement être le nom du programme lui-
même (comme argv[0]).
— Syntaxe générale (exemple pour execvp()) :
#include <unistd.h> // Pour execvp
int execvp(const char *file, char *const argv[]);
— file : Le nom de l’exécutable à lancer.
— argv : Un tableau de chaînes de caractères (char *const argv[]) pour les arguments,
terminé par NULL.
Note importante : Les fonctions exec() ne retournent jamais en cas de succès. Si elles re-
tournent, c’est qu’une erreur s’est produite (ex : fichier non trouvé, permissions insuffisantes),
et dans ce cas, elles retournent -1 et errno est définie.
execl(), execv(), execlp(), execvp()
— Exemple d’utilisation de fork() et execlp() : Ce programme crée un processus enfant,
et cet enfant exécute le programme ls -l /tmp. Le parent attend la fin de l’enfant.
#include <unistd.h> // Pour fork, execlp, wait
#include <stdio.h> // Pour printf, perror
#include <stdlib.h> // Pour EXIT_FAILURE, EXIT_SUCCESS
#include <sys/wait.h> // Pour WEXITSTATUS, WIFEXITED
47
Programmation Système avec Linux et C Polytechnique Douala - Génie Logiciel N4
int main() {
pid_t pid;
printf("Parent: Demarrage du programme.\n");
pid = fork(); // Creation du processus enfant
if (pid == -1) {
perror("Erreur de fork");
return EXIT_FAILURE;
} else if (pid == 0) {
// Code du processus ENFANT
printf("Enfant: Je vais executer ’ls -l /tmp’. Mon PID est %d.\n", getpid(
// exec_ (l)ist (p)ath: cherche ’ls’ dans le PATH de l’environnement
execlp("ls", "ls", "-l", "/tmp", NULL);
// Si execlp retourne, c’est qu’il y a une erreur
perror("Erreur execlp");
return EXIT_FAILURE; // Important de sortir en cas d’echec de exec
} else {
// Code du processus PARENT
printf("Parent: Enfant cree avec PID %d. J’attends sa terminaison.\n", pid
int status;
if (wait(&status) == -1) { // Attendre la terminaison de l’enfant
perror("wait");
return EXIT_FAILURE;
}
if (WIFEXITED(status)) {
printf("Parent: L’enfant s’est termine normalement avec le statut %d.\
} else if (WIFSIGNALED(status)) {
printf("Parent: L’enfant s’est termine par un signal non intercepte %d
}
}
printf("Parent: Fin du programme principal.\n");
return EXIT_SUCCESS;
}
3.2.3. wait() et waitpid()
Après avoir créé un processus enfant avec fork(), il est souvent nécessaire pour le pro-
cessus parent d’attendre la terminaison de cet enfant. Cela permet au parent de récupérer
le statut de terminaison de l’enfant et d’éviter la création de processus zombies.
48
Polytechnique Douala - Génie Logiciel N4 Programmation Système avec Linux et C
wait()
— Syntaxe :
#include <sys/wait.h> // Pour wait, WIFEXITED, WEXITSTATUS, etc.
pid_t wait(int *wstatus);
— wstatus : Un pointeur vers un entier où le statut de terminaison de l’enfant sera
stocké. Si NULL, le statut n’est pas récupéré.
— Comportement : wait() bloque le processus appelant (le parent) jusqu’à ce que
n’importe quel de ses processus enfants se termine.
— Valeur de retour :
— Succès : Le PID du processus enfant qui s’est terminé.
— Échec : -1, et errno est définie (ex : ECHILD si le parent n’a pas d’enfants à
attendre).
waitpid()
waitpid() offre un contrôle plus fin que wait().
— Syntaxe :
#include <sys/wait.h> // Pour waitpid
pid_t waitpid(pid_t pid, int *wstatus, int options);
— pid : Spécifie quel enfant attendre :
— > 0 : Attendre l’enfant avec le PID spécifié.
— = 0 : Attendre n’importe quel enfant du même groupe de processus que le processus
appelant.
— = -1 : Attendre n’importe quel enfant (similaire à wait()).
— < -1 : Attendre n’importe quel enfant dont le GID (Group ID) est égal à la valeur
absolue de pid.
— wstatus : Idem que pour wait().
— options : Drapeaux (OR bit-à-bit) pour modifier le comportement :
— WNOHANG : Rend l’appel non bloquant. Si aucun enfant n’est terminé, waitpid()
retourne 0 immédiatement.
— WUNTRACED : Retourne si un enfant est arrêté mais n’a pas été tracé.
— Valeur de retour :
— Succès : Le PID de l’enfant qui s’est terminé.
— 0 : Si WNOHANG est spécifié et qu’aucun enfant n’est disponible.
— Échec : -1, et errno est définie.
Macros d’analyse du statut : Après wait() ou waitpid(), le statut de terminaison
est un entier. Des macros sont fournies pour l’interpréter :
— WIFEXITED(wstatus) : Vrai si l’enfant s’est terminé normalement (par exit() ou
return de main()).
— WEXITSTATUS(wstatus) : Si WIFEXITED est vrai, retourne le code de sortie de l’enfant.
— WIFSIGNALED(wstatus) : Vrai si l’enfant s’est terminé à cause d’un signal non inter-
cepté.
— WTERMSIG(wstatus) : Si WIFSIGNALED est vrai, retourne le numéro du signal qui a
49
Programmation Système avec Linux et C Polytechnique Douala - Génie Logiciel N4
causé la terminaison.
— WIFSTOPPED(wstatus) : Vrai si l’enfant a été arrêté par un signal (ex : SIGSTOP).
— WSTOPSIG(wstatus) : Si WIFSTOPPED est vrai, retourne le numéro du signal qui l’a
arrêté.
— Exemple d’attente de terminaison de processus avec waitpid() : Ce programme
montre comment le parent peut attendre la terminaison d’un enfant spécifique et
récupérer son statut de sortie.
#include <sys/wait.h> // Pour waitpid, WIFEXITED, WEXITSTATUS
#include <unistd.h> // Pour fork, sleep
#include <stdio.h> // Pour printf, perror
#include <stdlib.h> // Pour EXIT_FAILURE, EXIT_SUCCESS
int main() {
pid_t pid;
int exit_status = 0; // Statut que l’enfant va retourner
printf("Parent (PID: %d): Demarrage du programme.\n", getpid());
pid = fork(); // Creation du processus enfant
if (pid == -1) {
perror("Erreur de fork");
return EXIT_FAILURE;
} else if (pid == 0) {
// Code du processus ENFANT
printf("Enfant (PID: %d, PPID: %d): Je vais dormir 3 secondes puis sort
getpid(), getppid(), exit_status);
sleep(3); // Simule un travail
printf("Enfant (PID: %d): Termine.\n", getpid());
exit(exit_status); // L’enfant se termine et retourne un statut
} else {
// Code du processus PARENT
int status;
pid_t terminated_pid;
printf("Parent (PID: %d): Enfant cree avec PID %d. J’attends sa termina
getpid(), pid);
// Attendre la terminaison de l’enfant specifique par son PID
// Le 0 dans les options signifie bloquant, sans WNOHANG
terminated_pid = waitpid(pid, &status, 0);
if (terminated_pid == -1) {
perror("waitpid");
return EXIT_FAILURE;
50
Polytechnique Douala - Génie Logiciel N4 Programmation Système avec Linux et C
printf("Parent (PID: %d): L’enfant avec le PID %d s’est termine.\n", ge
// Analyser le statut de terminaison
if (WIFEXITED(status)) {
printf("Parent: L’enfant s’est termine normalement. Code de sortie
} else if (WIFSIGNALED(status)) {
printf("Parent: L’enfant s’est termine a cause d’un signal non inte
} else if (WIFSTOPPED(status)) {
printf("Parent: L’enfant a ete arrete par un signal. Numero du sign
}
}
printf("Parent (PID: %d): Fin du programme.\n", getpid());
return EXIT_SUCCESS;
}
3.2.4. Processus Zombies et Orphelins
Processus Zombies
Un processus zombie (ou "defunct" en anglais) est un processus qui a terminé
son exécution, mais dont l’entrée dans la table des processus du noyau n’a pas encore
été supprimée car son processus parent n’a pas encore lu son statut de terminaison via
wait() ou waitpid().
— Pourquoi cela se produit ? : Le noyau conserve une petite quantité d’informations
sur un processus terminé (son PID, statut de sortie, temps CPU, etc.) afin que le
processus parent puisse le récupérer. Tant que le parent n’a pas "recueilli" ces infor-
mations, le processus enfant reste dans l’état zombie.
— Conséquences : Les processus zombies ne consomment pas de ressources CPU ni de
mémoire vive (car leur code et leurs données ont été libérés), mais ils occupent une
entrée dans la table des processus du noyau. Si trop de zombies s’accumulent (par
exemple, si un parent crée de nombreux enfants et ne les attend jamais), cela peut
potentiellement épuiser la table des processus du noyau et empêcher la création de
nouveaux processus.
— Comment éviter les zombies ? : Le processus parent doit toujours appeler wait()
ou waitpid() pour chacun de ses enfants après leur terminaison.
— Identification : La commande ps aux | grep Z ou top peut révéler les processus
zombies (état Z).
— Exemple de processus zombie :
#include <unistd.h> // fork, sleep
#include <stdio.h> // printf
#include <stdlib.h> // exit
51
Programmation Système avec Linux et C Polytechnique Douala - Génie Logiciel N4
int main() {
pid_t pid = fork();
if (pid == -1) {
perror("fork");
return 1;
} else if (pid == 0) {
// Processus enfant
printf("Enfant (PID %d): Je vais mourir et devenir un zombie.\n", getpi
exit(0); // L’enfant se termine
} else {
// Processus parent
printf("Parent (PID %d): Mon enfant (PID %d) est mort.\n", getpid(), pi
printf("Parent: Je ne vais PAS attendre mon enfant. Il va devenir un zo
sleep(10); // Le parent dort, sans appeler wait()
printf("Parent: Je me reveille et me termine. L’enfant devrait etre sup
}
return 0;
}
Pour observer le zombie : compilez et exécutez ce code. Ouvrez un autre terminal et
tapez ps aux | grep Z pendant que le parent dort. Vous devriez voir le processus
enfant avec l’état Z. Une fois le parent terminé, le système init (ou systemd) adoptera
le zombie et le nettoiera.
Processus Orphelins
Un processus orphelin est un processus enfant dont le processus parent s’est terminé
avant que l’enfant ne se termine.
— Pourquoi cela se produit ? : Le parent meurt avant l’enfant.
— Adoption par init/systemd : Sous Linux, les processus orphelins ne restent pas sans
parent. Ils sont immédiatement adoptés par le processus init (PID 1) ou systemd (sur
les systèmes modernes). Le nouveau parent (init/systemd) est responsable de collecter
leur statut de terminaison lorsqu’ils se terminent, évitant ainsi qu’ils ne deviennent
des zombies persistants.
— Conséquences : Les processus orphelins sont généralement inoffensifs. Ils continuent
leur exécution normale.
— Exemple de processus orphelin :
#include <unistd.h> // fork, sleep, getpid, getppid
#include <stdio.h> // printf
#include <stdlib.h> // exit
int main() {
pid_t pid = fork();
52
Polytechnique Douala - Génie Logiciel N4 Programmation Système avec Linux et C
if (pid == -1) {
perror("fork");
return 1;
} else if (pid == 0) {
// Processus enfant
printf("Enfant (PID %d): Mon PPID est %d (parent original).\n", getpid(
printf("Enfant: Je vais dormir 5 secondes pendant que mon parent meurt.
sleep(5);
printf("Enfant (PID %d): Je me reveille. Mon nouveau PPID est %d (je su
printf("Enfant: Je me termine.\n");
exit(0);
} else {
// Processus parent
printf("Parent (PID %d): J’ai cree un enfant (PID %d) et je vais me ter
// Le parent se termine sans attendre l’enfant
exit(0);
}
// Cette ligne ne sera jamais atteinte par le parent
return 0;
}
Pour observer l’orphelin : compilez et exécutez ce code. Le parent se terminera rapi-
dement. Utilisez ps -ef | grep <PID_enfant> ou top pour voir le PID de l’enfant
et observer son PPID changer pour 1 (init/systemd) après la terminaison du parent.
3.3. Terminaison de Processus
Un processus peut se terminer de plusieurs manières, soit volontairement, soit de
manière forcée.
3.3.1. exit() et _exit()
Ces deux fonctions sont utilisées pour terminer un processus de manière normale, en
lui donnant un statut de sortie.
— exit() :
#include <stdlib.h> // Pour exit
void exit(int status);
— C’est la manière la plus courante et la plus propre de terminer un processus.
— Elle exécute des routines de nettoyage enregistrées avec atexit() (si elles existent).
— Elle vide les buffers des flux d’E/S standard (par exemple, s’assure que tout ce qui
a été écrit avec printf() est effectivement affiché).
— Elle ferme tous les fichiers ouverts par le processus.
53
Programmation Système avec Linux et C Polytechnique Douala - Génie Logiciel N4
— Elle renvoie le status au processus parent.
— _exit() (underscore exit) :
#include <unistd.h> // Pour _exit
void _exit(int status);
— C’est une version plus "brute" et rapide de exit().
— Elle ne vide pas les buffers d’E/S.
— Elle n’exécute pas les routines enregistrées par atexit().
— Elle ferme tous les descripteurs de fichiers.
— Elle est généralement utilisée dans les processus enfants après un fork() avant un
exec() ou lorsqu’une terminaison immédiate et sans nettoyage supplémentaire est
requise (par exemple, pour éviter des deadlocks avec des routines de nettoyage).
— Codes de sortie : Un statut de 0 (ou EXIT_SUCCESS) indique une terminaison nor-
male et réussie. Une valeur différente de 0 (ex : EXIT_FAILURE) indique une erreur.
— Exemple exit() vs _exit() :
#include <stdio.h> // Pour printf, atexit
#include <stdlib.h> // Pour exit, EXIT_SUCCESS, EXIT_FAILURE
#include <unistd.h> // Pour _exit, fork, sleep
// Fonction de nettoyage enregistree avec atexit
void cleanup_function() {
printf("Cleanup function executed.\n");
}
int main() {
pid_t pid;
// Enregistrer la fonction de nettoyage
if (atexit(cleanup_function) != 0) {
fprintf(stderr, "Erreur atexit.\n");
return EXIT_FAILURE;
}
// Tester exit()
printf("Test de exit() :\n");
pid = fork();
if (pid == -1) { perror("fork"); return EXIT_FAILURE; }
if (pid == 0) { // Enfant 1
printf("Enfant 1 (exit): Je vais me terminer avec exit().\n");
printf("Ceci devrait apparaitre.\n"); // Ce printf sera flushé
exit(EXIT_SUCCESS); // Appelle la fonction de nettoyage et flushe les b
} else {
wait(NULL); // Attendre enfant 1
}
printf("------------------------------------------\n");
54
Polytechnique Douala - Génie Logiciel N4 Programmation Système avec Linux et C
// Tester _exit()
printf("Test de _exit() :\n");
pid = fork();
if (pid == -1) { perror("fork"); return EXIT_FAILURE; }
if (pid == 0) { // Enfant 2
printf("Enfant 2 (_exit): Je vais me terminer avec _exit().\n");
printf("Ceci POURRAIT NE PAS APPARAITRE.\n"); // Ce printf pourrait ne
_exit(EXIT_SUCCESS); // Ne pas appeler la fonction de nettoyage et ne p
} else {
wait(NULL); // Attendre enfant 2
}
printf("------------------------------------------\n");
printf("Programme principal termine.\n");
return EXIT_SUCCESS;
}
Vous remarquerez que la "Cleanup function executed." n’apparaît qu’une seule fois (pour
le processus parent et le premier enfant qui utilise ‘exit()‘), et que le deuxième ‘printf‘ de
l’enfant utilisant ‘e xit()‘peutnepastreaf f ich.
3.3.2. abort()
La fonction abort() termine anormalement un processus. Elle envoie un signal SIGABRT
au processus lui-même. Si le processus ne gère pas ce signal, l’action par défaut est de
terminer le processus et de générer un core dump (une image de la mémoire du processus
au moment de l’arrêt, utile pour le débogage).
— Syntaxe :
#include <stdlib.h> // Pour abort
void abort(void);
— Quand l’utiliser ? : Principalement pour indiquer une erreur irrécupérable dans le
programme, souvent dans des situations où l’état interne est corrompu et où une
sortie propre n’est pas possible. C’est moins courant dans la programmation système
courante, mais utile pour des bibliothèques bas niveau qui détectent des invariants
brisés.
3.4. Processus Avancés
3.4.1. Groupes de Processus et Sessions
Linux organise les processus en structures hiérarchiques :
— Processus : L’unité de base d’exécution.
55
Programmation Système avec Linux et C Polytechnique Douala - Génie Logiciel N4
— Groupes de Processus (Process Groups - PGID) : Un groupe de processus
est une collection d’un ou plusieurs processus. Chaque groupe a un ID de groupe
de processus (PGID), qui est le PID du processus "leader" du groupe. Les signaux
peuvent être envoyés à un groupe entier (ex : kill -<signal> -<PGID>). Tous les
processus dans un pipe (cmd1 | cmd2 | cmd3) appartiennent généralement au même
groupe de processus.
— Sessions (Session ID - SID) : Une session est une collection d’un ou plusieurs
groupes de processus. Chaque session a un leader de session (son SID est le PID du
leader). Les sessions sont souvent associées à des terminaux de connexion (TTY). Le
processus leader de session est celui qui a créé la session via l’appel setsid(). Chaque
session a un terminal de contrôle qui lui est associé.
Utilité : Ces structures sont fondamentales pour la gestion des travaux (job control ) dans
les shells (ex : mettre un processus en arrière-plan, le suspendre, le tuer). Elles permettent
de gérer un ensemble de processus comme une seule entité.
3.4.2. Daemons
Un daemon est un processus en arrière-plan qui s’exécute de manière non interactive,
sans être attaché à un terminal de contrôle. Les daemons fournissent des services système
(serveurs web, serveurs de base de données, serveurs d’impression, etc.).
— Caractéristiques clés d’un daemon :
1. Détachement du terminal de contrôle : Le daemon doit se détacher du ter-
minal qui l’a lancé afin qu’il ne soit pas affecté par la fermeture du terminal ou
des signaux envoyés au groupe de processus du terminal. Ceci est réalisé en créant
une nouvelle session (avec setsid()) et en s’assurant que le processus n’a pas de
terminal de contrôle.
2. Exécution en arrière-plan : Il s’exécute sans intervention de l’utilisateur.
3. Longue durée de vie : Il est conçu pour s’exécuter indéfiniment.
4. Pas d’E/S standard : Ses descripteurs de fichiers standard (stdin, stdout,
stderr) sont généralement redirigés vers /dev/null ou des fichiers de log.
5. Changent de répertoire courant : Ils changent leur répertoire de travail cou-
rant en / pour éviter de bloquer des systèmes de fichiers ou d’être affectés par le
démontage d’un répertoire.
— Étapes classiques pour créer un daemon :
1. fork() : Le processus parent se termine, laissant l’enfant en orphelin (adopté
par init/systemd). Ceci garantit que le processus appelant (shell) ne reste pas
bloqué et que le nouveau processus est exécuté en arrière-plan.
2. setsid() : L’enfant devient le leader d’une nouvelle session et se détache de tout
terminal de contrôle.
3. fork() (une deuxième fois) : La plupart des daemons effectuent un second fork().
Le parent de ce second fork() se termine, et l’enfant (le vrai daemon) n’est plus
le leader de session. Cela empêche le daemon d’acquérir un nouveau terminal de
contrôle accidentellement.
56
Polytechnique Douala - Génie Logiciel N4 Programmation Système avec Linux et C
4. chdir("/") : Changer le répertoire de travail courant à la racine pour éviter de
bloquer le démontage d’autres répertoires.
5. umask(0) : Réinitialiser le masque de création de fichier pour que le daemon ait
un contrôle total sur les permissions des fichiers qu’il crée.
6. Redirection des E/S standard : Fermer stdin, stdout, stderr et les ouvrir à
nouveau vers /dev/null ou des fichiers de log spécifiques pour éviter toute sortie
inattendue sur un terminal inexistant.
7. Boucle principale : Le daemon entre dans une boucle infinie où il effectue son travail
(par exemple, écouter les connexions réseau).
— Exemple de squelette de Daemon : Ce code fournit une base pour la création d’un
processus daemon simple.
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <syslog.h> // Pour la journalisation systeme
#include <signal.h> // Pour la gestion des signaux
void daemonize() {
pid_t pid;
// 1. Premier fork: le parent se termine, l’enfant devient un orphelin
pid = fork();
if (pid < 0) { // Erreur de fork
exit(EXIT_FAILURE);
}
if (pid > 0) { // Parent, se termine
exit(EXIT_SUCCESS);
}
// 2. L’enfant devient le leader d’une nouvelle session (se detache du term
if (setsid() < 0) {
exit(EXIT_FAILURE);
}
// 3. Ignorer les signaux SIGHUP (terminal deconnexion) et SIGTERM
// Ces signaux pourraient etre envoyes a la session nouvellement creee
signal(SIGHUP, SIG_IGN); // Ignorer SIGHUP
signal(SIGTERM, SIG_IGN); // Ignorer SIGTERM (pour le moment, pourrait etre
// 4. Deuxieme fork: empeche le daemon d’acquérir un terminal de controle
pid = fork();
if (pid < 0) {
57
Programmation Système avec Linux et C Polytechnique Douala - Génie Logiciel N4
exit(EXIT_FAILURE);
}
if (pid > 0) { // Parent du second fork, se termine
exit(EXIT_SUCCESS);
}
// 5. Changer le repertoire de travail a la racine
if (chdir("/") < 0) {
exit(EXIT_FAILURE);
}
// 6. Reinitialiser le umask pour avoir un controle complet sur les permiss
umask(0);
// 7. Rediriger les descripteurs de fichiers standard vers /dev/null
// Fermer stdin, stdout, stderr
close(STDIN_FILENO);
close(STDOUT_FILENO);
close(STDERR_FILENO);
// Ouvrir /dev/null pour stdin, stdout, stderr
// Les nouveaux fd 0, 1, 2 pointeront vers /dev/null
open("/dev/null", O_RDONLY); // stdin
open("/dev/null", O_WRONLY); // stdout
open("/dev/null", O_WRONLY); // stderr
}
int main() {
daemonize();
// Initialiser le systeme de journalisation (syslog)
openlog("mon_daemon_log", LOG_PID | LOG_CONS, LOG_DAEMON);
syslog(LOG_NOTICE, "Mon daemon demarre. PID: %d", getpid());
// Boucle principale du daemon (simule un travail)
while (1) {
sleep(5); // Le daemon dort 5 secondes
syslog(LOG_INFO, "Mon daemon est toujours en vie et travaille.");
}
syslog(LOG_NOTICE, "Mon daemon s’arrete.");
closelog();
return EXIT_SUCCESS;
}
58
Polytechnique Douala - Génie Logiciel N4 Programmation Système avec Linux et C
Pour tester ce daemon, compilez-le (ex : ‘gcc -o mon_daemon mon_daemon.c‘) et
exécutez-le en arrière-plan (‘./mon_daemon &‘). Vous devriez pouvoir le voir avec
‘ps aux | grep mon_daemon‘ et consulter ses logs avec ‘tail -f /var/log/syslog‘ (ou
‘journalctl -f‘ sur les systèmes modernes utilisant systemd).
Exercice
3.1 : Gestion de Processus Avancée
[label=()]Écrivez un programme C qui prend deux arguments de ligne de commande :
un entier ‘N‘ et un nom de programme. Votre programme doit ensuite créer ‘N‘ proces-
sus enfants. Chaque processus enfant doit exécuter le programme spécifié en argument
en lui passant son propre PID comme argument. Le processus parent doit attendre la
terminaison de tous ses enfants et afficher leur PID et leur statut de sortie. Modifiez
l’exemple de processus zombie pour qu’il soit corrigé. Le parent doit correctement
récupérer le statut de terminaison de l’enfant pour éviter que l’enfant ne devienne un
zombie persistant. Vérifiez avec ‘ps aux‘. Implémentez un programme qui se trans-
forme en daemon. Ce daemon doit écrire un message "Daemon is running" dans un
fichier de log toutes les 10 secondes. Assurez-vous que le daemon se détache correcte-
ment du terminal et gère ses descripteurs de fichiers standard. Utilisez ‘syslog‘ pour
la journalisation.
Correction Exercice 3.1, partie (b) - Correction de processus zombie
3.
1.
2. #include <unistd.h> // fork, sleep, getpid
#include <stdio.h> // printf, perror
#include <stdlib.h> // exit, EXIT_SUCCESS, EXIT_FAILURE
#include <sys/wait.h> // wait, WIFEXITED, WEXITSTATUS
int main() {
pid_t pid = fork();
if (pid == -1) {
perror("fork");
return EXIT_FAILURE;
} else if (pid == 0) {
// Processus enfant
printf("Enfant (PID %d): Je vais dormir 2 secondes, puis je me termine.\n", ge
sleep(2);
printf("Enfant (PID %d): Termine.\n", getpid());
exit(123); // L’enfant se termine avec un statut specifique
} else {
// Processus parent
int status;
pid_t terminated_pid;
printf("Parent (PID %d): Mon enfant (PID %d) est en cours.\n", getpid(), pid);
59
Programmation Système avec Linux et C Polytechnique Douala - Génie Logiciel N4
printf("Parent: J’attends la terminaison de mon enfant...\n");
// Le parent appelle wait() pour attendre l’enfant et recuperer son statut
terminated_pid = wait(&status);
if (terminated_pid == -1) {
perror("wait");
return EXIT_FAILURE;
}
printf("Parent (PID %d): L’enfant avec le PID %d s’est termine.\n", getpid(),
// Analyser le statut de terminaison de l’enfant
if (WIFEXITED(status)) {
printf("Parent: L’enfant s’est termine normalement. Code de sortie : %d.\n
} else if (WIFSIGNALED(status)) {
printf("Parent: L’enfant s’est termine a cause d’un signal non intercepte.
}
printf("Parent (PID %d): Fin du programme.\n", getpid());
}
return EXIT_SUCCESS;
}
Pour vérifier : Compilez et exécutez le programme. Le parent attendra et affichera le
statut de l’enfant. Si vous utilisez ‘ps aux | grep Z‘ pendant l’exécution (avant que le
parent ne se termine), vous ne devriez pas voir le processus enfant dans l’état zombie, car
le parent l’aura correctement "recueilli" une fois qu’il est terminé.
60
Chapitre 4
Communication Inter-Processus (IPC)
Ce chapitre est consacré à la Communication Inter-Processus (IPC), un aspect crucial de
la programmation système qui permet à des processus distincts d’échanger des informa-
tions et de se coordonner. Nous explorerons les différents mécanismes d’IPC disponibles
sous Linux, en commençant par les pipes anonymes et nommés (FIFOs). Nous aborderons
ensuite la mémoire partagée, les queues de messages et les sémaphores du System V, puis
nous ferons un bref aperçu des mécanismes d’IPC POSIX, qui offrent des alternatives plus
modernes. Comprendre l’IPC est essentiel pour construire des systèmes distribués ou des
applications complexes qui tirent parti de la concurrence des processus.
4.1. Concepts d’IPC
4.1.1. Pourquoi l’IPC ?
Dans un système d’exploitation, les processus sont conçus pour être isolés les uns des
autres. Chaque processus dispose de son propre espace d’adressage mémoire virtuel, ce
qui garantit la sécurité et la stabilité du système (un processus ne peut pas corrompre
la mémoire d’un autre). Cependant, dans de nombreux scénarios, des processus doivent
collaborer pour accomplir une tâche plus grande ou échanger des données. C’est là qu’in-
tervient la Communication Inter-Processus (IPC).
Les principales raisons de la nécessité de l’IPC sont :
— Partage d’informations : Des processus différents peuvent avoir besoin de parta-
ger des données (ex : un processus producteur génère des données qu’un processus
consommateur traite).
— Accélération modulaire : Une application complexe peut être divisée en plusieurs
processus coopératifs, chacun gérant une sous-tâche. Cela peut améliorer la perfor-
mance en exploitant le parallélisme sur des systèmes multi-cœurs.
— Modularité : Permet de développer des applications complexes en modules séparés,
améliorant la maintenabilité et la réutilisabilité.
— Persistance des informations : Certains mécanismes IPC peuvent permettre à des
informations de persister au-delà de la durée de vie d’un processus.
— Synchronisation : Les processus doivent se coordonner pour éviter des conditions de
61
Programmation Système avec Linux et C Polytechnique Douala - Génie Logiciel N4
course ou des incohérences lors de l’accès à des ressources partagées.
4.1.2. Mécanismes d’IPC
Linux (et les systèmes de type UNIX) offre une riche panoplie de mécanismes IPC. Ils
peuvent être classés de diverses manières, mais voici les plus courants :
Mécanisme IPC Type de Communica- Cas d’Utilisation Typiques
tion
Pipes (Tuyaux) Unidirectionnelle, flux d’oc-Communication entre processus
tets liés (parent-enfant) ; chaînage de
commandes shell (|)
FIFOs (Pipes Nom- Unidirectionnelle, flux d’oc- Communication entre processus
més) tets non liés, via un fichier spécial
Mémoire Partagée Bidirectionnelle, accès di- Partage rapide de grandes quan-
rect à la mémoire tités de données ; nécessite une
synchronisation externe
Queues de Messages Bidirectionnelle, messages Échange de messages structu-
structurés rés, avec priorité ; communica-
tion asynchrone
Sémaphores Synchronisation, contrôle Protection de sections critiques,
d’accès gestion des ressources concur-
rentes
Sockets Bidirectionnelle, flux ou da- Communication réseau (local ou
tagrammes distribué), client-serveur
Signaux Unidirectionnelle, événe- Notification d’événements, ges-
ments asynchrones tion des erreurs (voir Chapitre 5)
Dans ce chapitre, nous nous concentrerons sur les Pipes, FIFOs, Mémoire Partagée,
Queues de Messages et Sémaphores. Les Sockets seront abordés en détail dans le Chapitre
7.
4.2. Pipes (Tuyaux)
Les pipes sont parmi les mécanismes IPC les plus simples et les plus anciens. Ils sont
utilisés pour la communication unidirectionnelle entre processus.
4.2.1. Pipes Anonymes (pipe())
Un pipe anonyme est un canal de communication temporaire et unidirectionnel qui
ne peut être utilisé que par des processus liés (généralement un processus parent et son
enfant, créés via fork()). Il est créé en mémoire par le noyau et n’a pas de nom dans le
système de fichiers.
— Syntaxe :
#include <unistd.h> // Pour pipe
62
Polytechnique Douala - Génie Logiciel N4 Programmation Système avec Linux et C
int pipe(int pipefd[2]);
— pipefd : Un tableau de deux entiers qui contiendra les descripteurs de fichiers du pipe.
— pipefd[0] : Descripteur de fichier pour la lecture du pipe.
— pipefd[1] : Descripteur de fichier pour l’écriture dans le pipe.
— Comportement :
— Les données écrites dans pipefd[1] peuvent être lues depuis pipefd[0].
— Les pipes sont généralement utilisés après un fork() : un processus écrit dans le
pipe et l’autre lit.
— Si le côté lecture d’un pipe est fermé et qu’un processus tente d’écrire dedans, il
recevra un signal SIGPIPE (qui tue le processus par défaut).
— Si le côté écriture d’un pipe est fermé et qu’un processus tente de lire, read()
renverra 0 (fin de fichier) après que toutes les données restantes aient été lues.
— Valeur de retour :
— Succès : 0.
— Échec : -1, et errno est définie.
— Exemple de communication avec un pipe : Ce programme crée un pipe, puis un pro-
cessus enfant. Le parent écrit un message dans le pipe, et l’enfant lit ce message et
l’affiche.
#include <unistd.h> // Pour pipe, fork, read, write, close
#include <stdio.h> // Pour printf, perror
#include <stdlib.h> // Pour EXIT_FAILURE, EXIT_SUCCESS
#include <string.h> // Pour strlen, strcpy
#include <sys/wait.h> // Pour wait (dans le parent)
#define BUFFER_SIZE 256
int main() {
int pipefd[2]; // pipefd[0] pour la lecture, pipefd[1] pour l’ecriture
pid_t pid;
char buffer[BUFFER_SIZE];
const char *message_parent = "Salut de la part du parent!";
// 1. Creation du pipe
if (pipe(pipefd) == -1) {
perror("Erreur lors de la creation du pipe");
return EXIT_FAILURE;
}
printf("Pipe cree: lecture FD=%d, ecriture FD=%d\n", pipefd[0], pipefd[1]);
// 2. Creation du processus enfant
pid = fork();
if (pid == -1) {
perror("Erreur de fork");
63
Programmation Système avec Linux et C Polytechnique Douala - Génie Logiciel N4
close(pipefd[0]);
close(pipefd[1]);
return EXIT_FAILURE;
}
if (pid == 0) { // Code du processus ENFANT
// L’enfant va lire du pipe, donc il ferme le descripteur d’ecriture du
close(pipefd[1]);
printf("Enfant (PID %d): En attente de message du parent...\n", getpid(
// Lire le message du pipe
ssize_t bytes_read = read(pipefd[0], buffer, sizeof(buffer) - 1);
if (bytes_read == -1) {
perror("Enfant: Erreur lors de la lecture du pipe");
close(pipefd[0]);
return EXIT_FAILURE;
}
buffer[bytes_read] = ’\0’; // Assurer que la chaine est terminee par ’\
printf("Enfant (PID %d) a recu: ’%s’\n", getpid(), buffer);
// Fermer le descripteur de lecture du pipe
close(pipefd[0]);
return EXIT_SUCCESS;
} else { // Code du processus PARENT
// Le parent va ecrire dans le pipe, donc il ferme le descripteur de le
close(pipefd[0]);
printf("Parent (PID %d): Envoi du message a l’enfant...\n", getpid());
// Ecrire le message dans le pipe
if (write(pipefd[1], message_parent, strlen(message_parent) + 1) == -1)
perror("Parent: Erreur lors de l’ecriture dans le pipe");
close(pipefd[1]);
return EXIT_FAILURE;
}
printf("Parent (PID %d) a envoye: ’%s’\n", getpid(), message_parent);
// Fermer le descripteur d’ecriture du pipe
close(pipefd[1]);
// Attendre la terminaison de l’enfant
if (wait(NULL) == -1) {
perror("Parent: Erreur lors de l’attente de l’enfant");
return EXIT_FAILURE;
}
64
Polytechnique Douala - Génie Logiciel N4 Programmation Système avec Linux et C
printf("Parent: Enfant termine. Fin du programme.\n");
return EXIT_SUCCESS;
}
}
4.2.2. Pipes Nommés (FIFOs) (mkfifo())
Les pipes anonymes sont limités aux processus ayant une relation parent-enfant. Pour
permettre la communication entre processus non liés, on utilise des pipes nommés,
aussi appelés FIFOs (First-In, First-Out). Un FIFO est un type spécial de fichier dans
le système de fichiers, ce qui permet à des processus distincts de l’ouvrir par son nom.
— Syntaxe de création :
#include <sys/types.h> // Pour mode_t
#include <sys/stat.h> // Pour mkfifo
int mkfifo(const char *pathname, mode_t mode);
— pathname : Le chemin d’accès du fichier FIFO à créer.
— mode : Les permissions du FIFO (utilisé de la même manière que pour open(), par
exemple 0666).
— Comportement :
— Une fois créé avec mkfifo(), le FIFO peut être ouvert par plusieurs processus en
utilisant open() (comme un fichier régulier) pour la lecture ou l’écriture.
— Il suit toujours la sémantique FIFO : les données sont lues dans le même ordre
qu’elles ont été écrites.
— Comme un pipe anonyme, c’est unidirectionnel. Pour une communication bidirec-
tionnelle, deux FIFOs sont nécessaires (un pour chaque direction).
— Les opérations read() et write() sur un FIFO se bloquent par défaut jusqu’à ce
que l’autre extrémité soit ouverte.
— Valeur de retour :
— Succès : 0.
— Échec : -1, et errno est définie (ex : EEXIST si le FIFO existe déjà, EACCES pour
problème de permissions).
— Suppression : Un FIFO est un fichier du système de fichiers et doit être supprimé
avec unlink() (ou rm depuis la ligne de commande) une fois qu’il n’est plus nécessaire.
— Exemple de communication avec un FIFO (deux programmes séparés) : Ce scéna-
rio est typiquement implémenté avec deux programmes distincts, un émetteur et un
récepteur.
// Programme 1: fifo_writer.c
#include <fcntl.h> // Pour open, O_WRONLY
#include <sys/stat.h> // Pour mkfifo
#include <sys/types.h> // Pour mode_t
#include <unistd.h> // Pour write, close
#include <stdio.h> // Pour printf, perror
#include <stdlib.h> // Pour EXIT_FAILURE, EXIT_SUCCESS
65
Programmation Système avec Linux et C Polytechnique Douala - Génie Logiciel N4
#include <string.h> // Pour strlen
#define FIFO_NAME "/tmp/my_fifo"
int main() {
int fd;
const char *message = "Bonjour via le FIFO !";
// Creer le FIFO si il n’existe pas
if (mkfifo(FIFO_NAME, 0666) == -1) {
perror("Erreur lors de la creation du FIFO (peut-etre qu’il existe deja
// Si le FIFO existe deja, ce n’est pas une erreur critique pour l’ecri
}
printf("Ecrivain: Attente d’un lecteur pour ouvrir le FIFO ’%s’...\n", FIFO
// Ouvrir le FIFO en mode ecriture. Bloquant jusqu’a ce qu’un lecteur l’ouv
fd = open(FIFO_NAME, O_WRONLY);
if (fd == -1) {
perror("Erreur lors de l’ouverture du FIFO en ecriture");
return EXIT_FAILURE;
}
printf("Ecrivain: FIFO ouvert. Envoi du message...\n");
// Ecrire le message dans le FIFO
if (write(fd, message, strlen(message) + 1) == -1) {
perror("Erreur lors de l’ecriture dans le FIFO");
close(fd);
return EXIT_FAILURE;
}
printf("Ecrivain: Message envoye: ’%s’\n", message);
close(fd);
printf("Ecrivain: FIFO ferme. Programme termine.\n");
return EXIT_SUCCESS;
}
// Programme 2: fifo_reader.c
#include <fcntl.h> // Pour open, O_RDONLY
#include <sys/stat.h> // Pour mkfifo
#include <sys/types.h> // Pour mode_t
#include <unistd.h> // Pour read, close, unlink
#include <stdio.h> // Pour printf, perror
#include <stdlib.h> // Pour EXIT_FAILURE, EXIT_SUCCESS
#include <string.h> // Pour memset
66
Polytechnique Douala - Génie Logiciel N4 Programmation Système avec Linux et C
#define FIFO_NAME "/tmp/my_fifo"
#define BUFFER_SIZE 256
int main() {
int fd;
char buffer[BUFFER_SIZE];
// Creer le FIFO si il n’existe pas (le lecteur doit aussi s’assurer de sa
if (mkfifo(FIFO_NAME, 0666) == -1) {
perror("Erreur lors de la creation du FIFO (peut-etre qu’il existe deja
}
printf("Lecteur: Attente d’un ecrivain pour ouvrir le FIFO ’%s’...\n", FIFO
// Ouvrir le FIFO en mode lecture. Bloquant jusqu’a ce qu’un ecrivain l’ouv
fd = open(FIFO_NAME, O_RDONLY);
if (fd == -1) {
perror("Erreur lors de l’ouverture du FIFO en lecture");
// Nettoyage si erreur et pas de FIFO deja cree
// unlink(FIFO_NAME); // Si on veut nettoyer le FIFO apres une erreur
return EXIT_FAILURE;
}
printf("Lecteur: FIFO ouvert. Lecture du message...\n");
// Lire le message du FIFO
ssize_t bytes_read = read(fd, buffer, sizeof(buffer) - 1);
if (bytes_read == -1) {
perror("Erreur lors de la lecture du FIFO");
close(fd);
// unlink(FIFO_NAME); // Nettoyage
return EXIT_FAILURE;
}
buffer[bytes_read] = ’\0’; // Assurer terminaison de chaine
printf("Lecteur: Message recu: ’%s’\n", buffer);
close(fd);
printf("Lecteur: FIFO ferme. Suppression du FIFO...\n");
// Supprimer le FIFO du systeme de fichiers
if (unlink(FIFO_NAME) == -1) {
perror("Erreur lors de la suppression du FIFO");
return EXIT_FAILURE;
}
printf("Lecteur: FIFO supprime. Programme termine.\n");
return EXIT_SUCCESS;
}
67
Programmation Système avec Linux et C Polytechnique Douala - Génie Logiciel N4
Pour tester : 1. Compilez les deux fichiers : ‘gcc fifo_writer.c -o writer‘ et ‘gcc fifo_reader.c
-o reader‘. 2. Ouvrez deux terminaux. Dans le premier, exécutez ‘./reader‘. Il se blo-
quera en attendant un écrivain. 3. Dans le second terminal, exécutez ‘./writer‘. Le
message sera envoyé, et les deux programmes se termineront.
Exercice
4.1 : Communication avec Pipes
[label=()]Écrivez un programme C qui implémente un client et un serveur utilisant
des pipes nommés (FIFOs).
1. — Le serveur crée un FIFO (‘/tmp/req_fifo‘) pour recevoir les requêtes et un autre
FIFO (‘/tmp/res_fifo‘) pour envoyer les réponses.
— Le client envoie une chaîne de caractères au serveur via ‘req_fifo‘ et attend une
réponse du serveur via ‘res_fifo‘.
— Le serveur lit la chaîne, la convertit en majuscules, puis la renvoie au client.
— Assurez-vous de gérer la création et la suppression propre des FIFOs.
2. Modifiez l’exemple du pipe anonyme pour permettre une communication bidirec-
tionnelle entre le parent et l’enfant. Le parent envoie un message à l’enfant, l’enfant
le reçoit et renvoie une confirmation au parent, que le parent attendra.
4.3. Mémoire Partagée (System V)
La mémoire partagée est un mécanisme IPC qui permet à plusieurs processus de
mapper une même région de mémoire physique dans leurs propres espaces d’adressage
virtuels. Une fois la mémoire mappée, les processus peuvent y lire et y écrire directement,
comme s’il s’agissait de leur propre mémoire. C’est le mécanisme IPC le plus rapide car
il n’implique aucune copie de données entre les espaces noyau et utilisateur (zéro-copie).
— Clés (Keys) : Pour identifier un segment de mémoire partagée de manière unique à
l’échelle du système, on utilise des clés IPC (key_t). Ces clés peuvent être générées
à partir d’un chemin d’accès et d’un ID de projet avec ftok().
— Identifiants (IDs) : Une fois qu’un segment est créé avec une clé, le noyau lui attribue
un identifiant unique (shmid_t).
— Nécessité de synchronisation : L’accès concurrent à la mémoire partagée peut
entraîner des conditions de course. Par conséquent, la mémoire partagée doit presque
toujours être utilisée en combinaison avec des mécanismes de synchronisation (comme
les sémaphores ou les mutex) pour garantir l’intégrité des données.
4.3.1. shmget(), shmat(), shmdt(), shmctl()
Ces quatre appels système sont les fonctions principales pour gérer la mémoire partagée
System V.
68
Polytechnique Douala - Génie Logiciel N4 Programmation Système avec Linux et C
shmget()
Crée un nouveau segment de mémoire partagée ou obtient l’ID d’un segment existant.
— Syntaxe :
#include <sys/ipc.h> // Pour key_t
#include <sys/shm.h> // Pour shmget
int shmget(key_t key, size_t size, int shmflg);
— key : Une clé IPC (key_t). Peut être IPC_PRIVATE pour créer un segment unique
non accessible par d’autres processus sauf via fork() (hérité). Ou une clé générée par
ftok().
— size : La taille du segment de mémoire partagée en octets.
— shmflg : Drapeaux de création et de permission (OR bit-à-bit) :
— IPC_CREAT : Crée le segment s’il n’existe pas.
— IPC_EXCL : Utilisé avec IPC_CREAT. Si le segment existe déjà, shmget() échoue
(EEXIST).
— Permissions : Un masque octal (ex : 0666) spécifiant les permissions du segment
(lecture/écriture pour propriétaire/groupe/autres).
— Valeur de retour :
— Succès : L’identifiant du segment de mémoire partagée (shmid_t).
— Échec : -1, et errno est définie.
shmat()
Attache un segment de mémoire partagée (identifié par shmid) à l’espace d’adressage
virtuel du processus appelant.
— Syntaxe :
#include <sys/shm.h> // Pour shmat
void *shmat(int shmid, const void *shmaddr, int shmflg);
— shmid : L’identifiant du segment de mémoire partagée obtenu avec shmget().
— shmaddr : L’adresse désirée où le segment devrait être attaché dans l’espace d’adressage
du processus. Si NULL, le système choisit une adresse appropriée.
— shmflg : Drapeaux (ex : SHM_RDONLY pour attacher en lecture seule).
— Valeur de retour :
— Succès : Un pointeur vers l’adresse virtuelle où le segment est attaché. Ce pointeur
peut être transtypé vers le type de données désiré (ex : char *, int *).
— Échec : (void *)-1, et errno est définie.
shmdt()
Détache le segment de mémoire partagée de l’espace d’adressage du processus appe-
lant. Cela ne supprime pas le segment du système.
— Syntaxe :
#include <sys/shm.h> // Pour shmdt
int shmdt(const void *shmaddr);
69
Programmation Système avec Linux et C Polytechnique Douala - Génie Logiciel N4
— shmaddr : L’adresse à laquelle le segment a été attaché (retournée par shmat()).
— Valeur de retour :
— Succès : 0.
— Échec : -1, et errno est définie.
shmctl()
Effectue diverses opérations de contrôle sur un segment de mémoire partagée (obten-
tion d’informations, modification de permissions, suppression).
— Syntaxe :
#include <sys/shm.h> // Pour shmctl
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
— shmid : L’identifiant du segment de mémoire partagée.
— cmd : La commande à exécuter :
— IPC_RMID : Marque le segment pour suppression. Il sera réellement supprimé une
fois que tous les processus l’auront détaché.
— IPC_STAT : Obtient des informations sur le segment et les stocke dans buf.
— IPC_SET : Définit les permissions et d’autres attributs du segment en utilisant les
informations de buf.
— buf : Un pointeur vers une structure struct shmid_ds utilisée pour IPC_STAT ou
IPC_SET.
— Valeur de retour : 0 en cas de succès, -1 en cas d’échec.
— Exemple d’utilisation de la mémoire partagée (producteur-consommateur simplifié) :
Ce programme montre comment deux processus non liés peuvent communiquer via la
mémoire partagée. Le producteur écrit un message, et le consommateur le lit. Notez
que la synchronisation n’est pas gérée dans cet exemple, ce qui est crucial en pratique.
// Programme 1: shm_producer.c
#include <sys/ipc.h> // Pour ftok, IPC_CREAT
#include <sys/shm.h> // Pour shmget, shmat, shmdt, shmctl
#include <stdio.h> // Pour printf, perror
#include <stdlib.h> // Pour EXIT_FAILURE, EXIT_SUCCESS
#include <string.h> // Pour strcpy, strlen
#include <unistd.h> // Pour sleep
#define SHM_SIZE 1024 // Taille du segment de memoire partagee
#define KEY_FILE "/tmp/shm_key_file" // Fichier pour generer la cle IPC
int main() {
key_t key;
int shmid;
char *shm_ptr; // Pointeur vers la memoire partagee
// 1. Generer une cle IPC unique
70
Polytechnique Douala - Génie Logiciel N4 Programmation Système avec Linux et C
// ftok genere une cle a partir d’un chemin d’acces et d’un ID de projet
key = ftok(KEY_FILE, ’R’);
if (key == -1) {
perror("ftok");
return EXIT_FAILURE;
}
printf("Producteur: Cle IPC genere: %d\n", key);
// 2. Creer le segment de memoire partagee
// IPC_CREAT | 0666: cree le segment s’il n’existe pas, avec permissions rw
shmid = shmget(key, SHM_SIZE, IPC_CREAT | 0666);
if (shmid == -1) {
perror("shmget");
return EXIT_FAILURE;
}
printf("Producteur: Segment de memoire partagee cree avec ID: %d\n", shmid)
// 3. Attacher le segment a l’espace d’adressage du processus
shm_ptr = shmat(shmid, NULL, 0); // NULL: systeme choisit l’adresse, 0: per
if (shm_ptr == (char *)-1) {
perror("shmat");
shmctl(shmid, IPC_RMID, NULL); // Nettoyer le segment si attachement ec
return EXIT_FAILURE;
}
printf("Producteur: Segment de memoire partagee attache a l’adresse: %p\n",
// 4. Ecrire des donnees dans la memoire partagee
strcpy(shm_ptr, "Message du Producteur via Memoire Partagee!");
printf("Producteur: Message ecrit: ’%s’\n", shm_ptr);
printf("Producteur: Attente de 5 secondes pour laisser le consommateur lire
sleep(5); // Laisser le temps au consommateur de lire
// 5. Detacher le segment de la memoire du processus
if (shmdt(shm_ptr) == -1) {
perror("shmdt");
return EXIT_FAILURE;
}
printf("Producteur: Segment detache.\n");
// 6. Supprimer le segment de memoire partagee du systeme
// Ceci ne peut etre fait qu’apres que tous les processus l’aient detache
// Ou il sera supprime automatiquement au redemarrage du systeme
if (shmctl(shmid, IPC_RMID, NULL) == -1) {
perror("shmctl (IPC_RMID)");
71
Programmation Système avec Linux et C Polytechnique Douala - Génie Logiciel N4
return EXIT_FAILURE;
}
printf("Producteur: Segment de memoire partagee supprime du systeme.\n");
return EXIT_SUCCESS;
}
// Programme 2: shm_consumer.c
#include <sys/ipc.h> // Pour ftok
#include <sys/shm.h> // Pour shmget, shmat, shmdt
#include <stdio.h> // Pour printf, perror
#include <stdlib.h> // Pour EXIT_FAILURE, EXIT_SUCCESS
#include <string.h> // Pour strlen
#include <unistd.h> // Pour sleep
#define SHM_SIZE 1024
#define KEY_FILE "/tmp/shm_key_file"
int main() {
key_t key;
int shmid;
char *shm_ptr;
// 1. Generer la meme cle IPC que le producteur
key = ftok(KEY_FILE, ’R’);
if (key == -1) {
perror("ftok");
return EXIT_FAILURE;
}
printf("Consommateur: Cle IPC genere: %d\n", key);
// 2. Obtenir l’ID du segment de memoire partagee (ne pas le creer)
// 0666: Permet l’acces si le segment existe deja
shmid = shmget(key, SHM_SIZE, 0666);
if (shmid == -1) {
perror("shmget (obtenir)");
fprintf(stderr, "Assurez-vous que le producteur est execute en premier
return EXIT_FAILURE;
}
printf("Consommateur: Segment de memoire partagee obtenu avec ID: %d\n", sh
// 3. Attacher le segment a l’espace d’adressage du processus
shm_ptr = shmat(shmid, NULL, 0);
if (shm_ptr == (char *)-1) {
perror("shmat");
72
Polytechnique Douala - Génie Logiciel N4 Programmation Système avec Linux et C
return EXIT_FAILURE;
}
printf("Consommateur: Segment de memoire partagee attache a l’adresse: %p\n
// 4. Lire les donnees de la memoire partagee
printf("Consommateur: Message lu: ’%s’\n", shm_ptr);
// 5. Modifier la memoire partagee pour indiquer qu’on a lu
strcpy(shm_ptr, "ACK du Consommateur!");
printf("Consommateur: Message de confirmation ecrit.\n");
// 6. Detacher le segment de la memoire du processus
if (shmdt(shm_ptr) == -1) {
perror("shmdt");
return EXIT_FAILURE;
}
printf("Consommateur: Segment detache.\n");
return EXIT_SUCCESS;
}
Pour tester : 1. Créez un fichier vide pour la clé IPC : ‘touch /tmp/shm_key_file‘. 2.
Compilez les deux programmes : ‘gcc shm_producer.c -o producer‘ et ‘gcc shm_consumer.c
-o consumer‘. 3. Ouvrez deux terminaux. Dans le premier, exécutez ‘./producer‘. 4.
Pendant que le producteur dort (après avoir écrit le message), exécutez ‘./consumer‘
dans le second terminal. 5. Le consommateur lira et modifiera la mémoire partagée.
Le producteur se réveillera et affichera le message modifié par le consommateur, puis
supprimera le segment. Remarque importante : Dans un scénario réel, il faut uti-
liser des sémaphores ou d’autres mécanismes de synchronisation pour s’assurer que
le consommateur ne lit pas avant que le producteur n’écrive, et que le producteur ne
supprime pas le segment avant que le consommateur ne l’ait lu.
4.3.2. Synchronisation de la Mémoire Partagée (avec Sémaphores
ou Mutex)
Comme mentionné, l’accès direct à la mémoire partagée par plusieurs processus peut
entraîner des conditions de course (race conditions) et des incohérences de données.
Par exemple, si un processus écrit une partie des données et qu’un autre tente de les lire
avant que l’écriture ne soit terminée. Pour résoudre ce problème, la mémoire partagée
est presque toujours combinée avec des mécanismes de synchronisation. Les plus courants
sont :
— Sémaphores (System V ou POSIX) : Utilisés pour contrôler l’accès à une ressource
partagée. Un processus "acquiert" (décrémente) le sémaphore avant d’accéder à la
mémoire partagée, et le "libère" (incrémente) après.
— Mutex (avec pthreads ou POSIX mutexes mappés en mémoire partagée) :
73
Programmation Système avec Linux et C Polytechnique Douala - Génie Logiciel N4
Fournissent une exclusion mutuelle, garantissant qu’un seul thread/processus peut
accéder à une section critique de code à la fois.
Nous verrons les sémaphores plus en détail dans la Section 5.
Exercice
4.2 : Mémoire Partagée
[label=()]Écrivez un programme (unique, avec un fork()) qui utilise la mémoire
partagée pour permettre au processus parent et au processus enfant de partager un
entier. Le parent initialise l’entier à 0, l’enfant l’incrémente 10 fois, puis le parent
lit la valeur finale et l’affiche. (Ignorez temporairement la synchronisation ; nous la
gérerons plus tard). Écrivez un programme qui simule un tableau noir partagé :
1.
2. — Créez un segment de mémoire partagée pour stocker une chaîne de caractères (par
exemple, 256 octets).
— Un processus "rédacteur" (writer) peut ouvrir le segment et écrire un message.
— Un processus "lecteur" (reader) peut ouvrir le segment et lire le message.
— Pensez à comment vous indiqueriez qu’un nouveau message est disponible sans
utiliser de mécanisme de synchronisation (par exemple, en écrivant un caractère
spécial à la fin). Notez les limites de cette approche sans synchronisation.
4.4. Queues de Messages (System V)
Les queues de messages (ou "files de messages") sont un mécanisme IPC qui permet
aux processus d’échanger des blocs de données de taille variable, appelés "messages". Les
messages sont stockés temporairement par le noyau dans une file d’attente jusqu’à ce
qu’un processus les lise. C’est un mécanisme de communication asynchrone et orienté
message.
— Caractéristiques :
— Chaque message a un type (un entier long), ce qui permet à un processus de lire
sélectivement les messages d’un certain type.
— Les messages ont une taille maximale définie par le système.
— Les messages sont copiés du processus émetteur vers la file d’attente du noyau,
puis de la file d’attente vers le processus récepteur.
4.4.1. msgget(), msgsnd(), msgrcv(), msgctl()
Ces fonctions gèrent les files de messages System V.
msgget()
Crée une nouvelle file de messages ou obtient l’identifiant d’une file existante.
— Syntaxe :
#include <sys/ipc.h> // Pour key_t, IPC_CREAT
#include <sys/msg.h> // Pour msgget
int msgget(key_t key, int msgflg);
74
Polytechnique Douala - Génie Logiciel N4 Programmation Système avec Linux et C
— key : Clé IPC (comme pour la mémoire partagée, peut être IPC_PRIVATE ou générée
par ftok()).
— msgflg : Drapeaux de création et de permission (ex : IPC_CREAT | 0666).
— Valeur de retour : L’identifiant de la file de messages (msqid_t) en cas de succès,
-1 en cas d’échec.
msgsnd()
Envoie un message à la file de messages spécifiée.
— Syntaxe :
#include <sys/msg.h> // Pour msgsnd
int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
— msqid : L’identifiant de la file de messages.
— msgp : Un pointeur vers la structure du message à envoyer. Cette structure doit com-
mencer par un membre de type long pour le type du message.
struct my_msgbuf {
long mtype; // Type du message
char mtext[256]; // Corps du message
};
— msgsz : La taille du corps du message en octets (excluant le membre mtype).
— msgflg : Drapeaux (ex : IPC_NOWAIT pour rendre l’opération non bloquante).
— Valeur de retour : 0 en cas de succès, -1 en cas d’échec.
msgrcv()
Reçoit un message depuis la file de messages spécifiée.
— Syntaxe :
#include <sys/msg.h> // Pour msgrcv
ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);
— msqid : L’identifiant de la file de messages.
— msgp : Un pointeur vers le buffer où le message reçu sera stocké (doit correspondre à
la structure utilisée par msgsnd()).
— msgsz : La taille maximale du corps du message à recevoir.
— msgtyp : Spécifie quel type de message recevoir :
— 0 : Recevoir le premier message dans la file.
— > 0 : Recevoir le premier message de ce type.
— < 0 : Recevoir le premier message dont le type est inférieur ou égal à la valeur
absolue de msgtyp.
— msgflg : Drapeaux (ex : IPC_NOWAIT, MSG_NOERROR pour tronquer les messages trop
longs).
— Valeur de retour : Le nombre d’octets lus (taille du corps du message) en cas de
75
Programmation Système avec Linux et C Polytechnique Douala - Génie Logiciel N4
succès, -1 en cas d’échec.
msgctl()
Effectue diverses opérations de contrôle sur une file de messages.
— Syntaxe :
#include <sys/msg.h> // Pour msgctl
int msgctl(int msqid, int cmd, struct msqid_ds *buf);
— msqid : L’identifiant de la file de messages.
— cmd : La commande à exécuter :
— IPC_RMID : Supprime la file de messages du système.
— IPC_STAT : Obtient des informations sur la file.
— IPC_SET : Définit des attributs pour la file.
— buf : Pointeur vers une structure struct msqid_ds pour les commandes IPC_STAT
ou IPC_SET.
— Valeur de retour : 0 en cas de succès, -1 en cas d’échec.
— Exemple de communication avec files de messages (émetteur-récepteur) : Ce scénario
est typiquement implémenté avec deux programmes séparés.
// Programme 1: msg_sender.c
#include <sys/types.h> // Pour key_t
#include <sys/ipc.h> // Pour ftok, IPC_CREAT
#include <sys/msg.h> // Pour msgget, msgsnd
#include <stdio.h> // Pour printf, perror
#include <stdlib.h> // Pour EXIT_FAILURE, EXIT_SUCCESS
#include <string.h> // Pour strcpy, strlen
#define MSG_KEY_FILE "/tmp/msg_queue_key"
#define MSG_TYPE 1
#define MSG_MAX_SIZE 256
// Structure pour les messages
struct my_msgbuf {
long mtype;
char mtext[MSG_MAX_SIZE];
};
int main() {
key_t key;
int msqid;
struct my_msgbuf sbuf;
// 1. Generer une cle IPC unique
key = ftok(MSG_KEY_FILE, ’Q’);
if (key == -1) {
76
Polytechnique Douala - Génie Logiciel N4 Programmation Système avec Linux et C
perror("ftok");
return EXIT_FAILURE;
}
printf("Emetteur: Cle IPC genere: %d\n", key);
// 2. Obtenir l’ID de la file de messages (la cree si elle n’existe pas)
msqid = msgget(key, IPC_CREAT | 0666);
if (msqid == -1) {
perror("msgget");
return EXIT_FAILURE;
}
printf("Emetteur: File de messages obtenue/cree avec ID: %d\n", msqid);
// 3. Preparer le message
sbuf.mtype = MSG_TYPE;
strcpy(sbuf.mtext, "Bonjour via la file de messages!");
// 4. Envoyer le message
// sizeof(sbuf.mtext) pour la taille du corps du message
if (msgsnd(msqid, &sbuf, strlen(sbuf.mtext) + 1, 0) == -1) { // +1 pour le
perror("msgsnd");
// Ne pas supprimer la file ici, le recepteur s’en chargera
return EXIT_FAILURE;
}
printf("Emetteur: Message de type %ld envoye: ’%s’\n", sbuf.mtype, sbuf.mte
printf("Emetteur: Programme termine.\n");
return EXIT_SUCCESS;
}
// Programme 2: msg_receiver.c
#include <sys/types.h> // Pour key_t
#include <sys/ipc.h> // Pour ftok, IPC_RMID
#include <sys/msg.h> // Pour msgget, msgrcv, msgctl
#include <stdio.h> // Pour printf, perror
#include <stdlib.h> // Pour EXIT_FAILURE, EXIT_SUCCESS
#include <string.h> // Pour memset
#define MSG_KEY_FILE "/tmp/msg_queue_key"
#define MSG_TYPE 1
#define MSG_MAX_SIZE 256
// Structure pour les messages
struct my_msgbuf {
long mtype;
77
Programmation Système avec Linux et C Polytechnique Douala - Génie Logiciel N4
char mtext[MSG_MAX_SIZE];
};
int main() {
key_t key;
int msqid;
struct my_msgbuf rbuf;
ssize_t bytes_received;
// 1. Generer la meme cle IPC que l’emetteur
key = ftok(MSG_KEY_FILE, ’Q’);
if (key == -1) {
perror("ftok");
return EXIT_FAILURE;
}
printf("Recepteur: Cle IPC genere: %d\n", key);
// 2. Obtenir l’ID de la file de messages
msqid = msgget(key, 0666); // Pas de IPC_CREAT ici, on suppose qu’elle exis
if (msqid == -1) {
perror("msgget");
fprintf(stderr, "Assurez-vous que l’emetteur est execute en premier et
return EXIT_FAILURE;
}
printf("Recepteur: File de messages obtenue avec ID: %d\n", msqid);
printf("Recepteur: En attente de messages de type %ld...\n", (long)MSG_TYPE
// 3. Recevoir le message
// MSG_TYPE: ne recevoir que les messages de ce type
// 0: bloquant si aucun message disponible
bytes_received = msgrcv(msqid, &rbuf, sizeof(rbuf.mtext), MSG_TYPE, 0);
if (bytes_received == -1) {
perror("msgrcv");
// Suppression de la file si erreur apres avoir tente de recevoir
msgctl(msqid, IPC_RMID, NULL);
return EXIT_FAILURE;
}
rbuf.mtext[bytes_received] = ’\0’; // Assurer terminaison de chaine
printf("Recepteur: Message recu de type %ld: ’%s’\n", rbuf.mtype, rbuf.mtex
// 4. Supprimer la file de messages du systeme
if (msgctl(msqid, IPC_RMID, NULL) == -1) {
perror("msgctl (IPC_RMID)");
78
Polytechnique Douala - Génie Logiciel N4 Programmation Système avec Linux et C
return EXIT_FAILURE;
}
printf("Recepteur: File de messages supprimee du systeme.\n");
return EXIT_SUCCESS;
}
Pour tester : 1. Créez un fichier vide pour la clé IPC : ‘touch /tmp/msg_queue_key‘. 2.
Compilez les deux programmes : ‘gcc msg_sender.c -o sender‘ et ‘gcc msg_receiver.c
-o receiver‘. 3. Ouvrez deux terminaux. Dans le premier, exécutez ‘./receiver‘. Il se
bloquera en attendant un message. 4. Dans le second terminal, exécutez ‘./sender‘. Le
message sera envoyé, et le récepteur le recevra et supprimera la file.
Exercice
4.3 : Queues de Messages
[label=()]Implémentez un système de "chat" simple entre deux processus utilisant
des files de messages.
1. — Processus A (client) envoie des messages de type 1 au processus B (serveur).
— Processus B (serveur) lit les messages de type 1, les met en majuscules, et les
renvoie au processus A comme messages de type 2.
— Le processus A lit les messages de type 2 et les affiche.
— Utilisez fork() pour créer les deux processus à partir d’un seul exécutable. Le
parent sera le client, l’enfant sera le serveur.
— Gérez la terminaison propre et la suppression de la file de messages.
4.5. Sémaphores (System V)
Un sémaphore est un mécanisme IPC utilisé principalement pour la synchronisa-
tion des processus (et des threads). Il s’agit d’une variable entière non négative qui est
manipulée par deux opérations atomiques :
— P (Proberen - Tester) ou wait() / decrement() : Décrémente la valeur du séma-
phore. Si la valeur résultante est négative, le processus est bloqué jusqu’à ce que le
sémaphore devienne positif.
— V (Verhogen - Augmenter) ou post() / increment() : Incrémente la valeur du
sémaphore. Si des processus sont bloqués en attente sur ce sémaphore, l’un d’eux est
débloqué.
Les sémaphores sont souvent utilisés pour contrôler l’accès à une ressource partagée (par
exemple, une section critique de code ou un segment de mémoire partagée), garantissant
qu’un seul processus (ou un nombre limité de processus) y accède à la fois.
— Sémaphores binaires : Un sémaphore dont la valeur ne peut être que 0 ou 1. Simi-
laire à un mutex (verrou d’exclusion mutuelle).
— Sémaphores de comptage : Un sémaphore dont la valeur peut être n’importe quel
entier non négatif, utilisé pour gérer un pool de ressources.
79
Programmation Système avec Linux et C Polytechnique Douala - Génie Logiciel N4
— Ensemble de sémaphores : Contrairement aux sémaphores POSIX, les sémaphores
System V sont toujours créés en ensembles (arrays) de sémaphores.
4.5.1. semget(), semop(), semctl()
Ces fonctions gèrent les sémaphores System V.
semget()
Crée un nouvel ensemble de sémaphores ou obtient l’identifiant d’un ensemble existant.
— Syntaxe :
#include <sys/ipc.h> // Pour key_t
#include <sys/sem.h> // Pour semget
int semget(key_t key, int nsems, int semflg);
— key : Clé IPC (IPC_PRIVATE ou ftok()).
— nsems : Le nombre de sémaphores dans l’ensemble (ex : 1 pour un sémaphore unique,
ou plus pour un ensemble de ressources).
— semflg : Drapeaux de création et de permission (ex : IPC_CREAT | 0666).
— Valeur de retour : L’identifiant de l’ensemble de sémaphores (semid_t) en cas de
succès, -1 en cas d’échec.
semop()
Exécute une ou plusieurs opérations sur les sémaphores d’un ensemble. C’est l’appel
système pour les opérations P (attendre/décrémenter) et V (libérer/incrémenter).
— Syntaxe :
#include <sys/sem.h> // Pour semop
int semop(int semid, struct sembuf *sops, size_t nsops);
— semid : L’identifiant de l’ensemble de sémaphores.
— sops : Un pointeur vers un tableau de structures struct sembuf, où chaque structure
décrit une opération à effectuer sur un sémaphore spécifique dans l’ensemble.
struct sembuf {
unsigned short sem_num; // Numero du semaphora dans l’ensemble (0 a nsems-1
short sem_op; // Operation: -1 pour P, +1 pour V, 0 pour attendre
short sem_flg; // Drapeaux (IPC_NOWAIT, SEM_UNDO)
};
— nsops : Le nombre d’opérations dans le tableau sops.
— Valeur de retour : 0 en cas de succès, -1 en cas d’échec.
semctl()
Effectue diverses opérations de contrôle sur un ensemble de sémaphores (obtention
d’informations, initialisation de valeurs, suppression).
80
Polytechnique Douala - Génie Logiciel N4 Programmation Système avec Linux et C
— Syntaxe :
#include <sys/sem.h> // Pour semctl
// Union semun necessaire pour semctl, mais n’est pas standardise.
// Elle doit etre definie manuellement ou incluse via _GNU_SOURCE si disponible
int semctl(int semid, int semnum, int cmd, ...);
— Union semun : Historiquement, un quatrième argument était passé à semctl(),
qui était une union semun. Cependant, semun n’est pas définie dans le standard
POSIX et peut varier. Pour la portabilité ou pour compiler avec des options strictes,
il est souvent nécessaire de la définir soi-même ou d’utiliser #define _GNU_SOURCE
avant les includes pour que glibc la fournisse.
union semun {
int val; // Pour SETVAL
struct semid_ds *buf; // Pour IPC_STAT, IPC_SET
unsigned short *array; // Pour GETALL, SETALL
struct seminfo *__buf; // Pour IPC_INFO (Linux specific)
};
— semid : L’identifiant de l’ensemble de sémaphores.
— semnum : Le numéro du sémaphore dans l’ensemble (0-indexé).
— cmd : La commande à exécuter :
— IPC_RMID : Supprime l’ensemble de sémaphores du système.
— SETVAL : Initialise la valeur d’un sémaphore donné à val.
— GETVAL : Obtient la valeur courante d’un sémaphore.
— IPC_STAT, IPC_SET, GETALL, SETALL, etc.
— ... : Argument optionnel (l’union semun) dont le type dépend de la commande.
— Valeur de retour : 0 en cas de succès (ou la valeur du sémaphore pour GETVAL), -1
en cas d’échec.
— Exemple d’utilisation de sémaphores (protection d’une section critique) : Ce pro-
gramme montre comment deux processus peuvent utiliser un sémaphore pour protéger
l’accès à une variable partagée (ici, un simple fichier) afin d’éviter les conditions de
course.
// Programme: sem_producer_consumer.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <sys/wait.h> // Pour wait
// Definir l’union semun pour semctl, car elle n’est pas standardise
union semun {
int val; // Pour SETVAL
struct semid_ds *buf; // Pour IPC_STAT, IPC_SET
81
Programmation Système avec Linux et C Polytechnique Douala - Génie Logiciel N4
unsigned short *array; // Pour GETALL, SETALL
struct seminfo *__buf; // Pour IPC_INFO (Linux specific)
};
#define SEM_KEY_FILE "/tmp/sem_key_file"
#define NUM_SEMS 1 // Nous avons besoin d’un seul semaphora
#define SEM_ID 0 // Le premier (et unique) semaphora dans l’ensemble
// Operation P (Proberen): decremente le semaphora, bloque si negatif
void P(int sem_id) {
struct sembuf sb = {SEM_ID, -1, 0}; // sem_num, sem_op, sem_flg
if (semop(sem_id, &sb, 1) == -1) {
perror("semop P");
exit(EXIT_FAILURE);
}
}
// Operation V (Verhogen): incremente le semaphora
void V(int sem_id) {
struct sembuf sb = {SEM_ID, 1, 0}; // sem_num, sem_op, sem_flg
if (semop(sem_id, &sb, 1) == -1) {
perror("semop V");
exit(EXIT_FAILURE);
}
}
int main() {
key_t key;
int sem_id;
union semun sem_arg;
pid_t pid;
// 1. Generer une cle IPC
key = ftok(SEM_KEY_FILE, ’S’);
if (key == -1) {
perror("ftok");
return EXIT_FAILURE;
}
printf("Parent: Cle IPC genere: %d\n", key);
// 2. Creer un ensemble de semaphores (un seul semaphore ici)
// IPC_CREAT | 0666: cree le semaphora si n’existe pas, avec permissions rw
sem_id = semget(key, NUM_SEMS, IPC_CREAT | 0666);
if (sem_id == -1) {
perror("semget");
82
Polytechnique Douala - Génie Logiciel N4 Programmation Système avec Linux et C
return EXIT_FAILURE;
}
printf("Parent: Ensemble de semaphores cree avec ID: %d\n", sem_id);
// 3. Initialiser le semaphore a 1 (pour l’exclusion mutuelle)
sem_arg.val = 1;
if (semctl(sem_id, SEM_ID, SETVAL, sem_arg) == -1) {
perror("semctl SETVAL");
semctl(sem_id, IPC_RMID, NULL); // Nettoyer
return EXIT_FAILURE;
}
printf("Parent: Semaphore %d initialise a 1.\n", SEM_ID);
// Creer un fichier partagé pour simuler une ressource
FILE *fp = fopen("shared_resource.txt", "w");
if (fp == NULL) {
perror("fopen");
semctl(sem_id, IPC_RMID, NULL);
return EXIT_FAILURE;
}
fprintf(fp, "0\n"); // Valeur initiale
fclose(fp);
printf("Fichier shared_resource.txt cree avec valeur initiale 0.\n");
// 4. Fork pour creer le processus enfant
pid = fork();
if (pid == -1) {
perror("fork");
semctl(sem_id, IPC_RMID, NULL);
return EXIT_FAILURE;
}
if (pid == 0) { // Processus Enfant (Consommateur/Incrementer)
// L’enfant accedera au meme semaphore via la cle
printf("Enfant (PID %d): En attente d’acces a la ressource...\n", getpi
P(sem_id); // Attendre (P) que le semaphore soit disponible
printf("Enfant (PID %d): Acces a la section critique.\n", getpid());
// --- SECTION CRITIQUE ---
// Lire la valeur, l’incrementer et la re-ecrire
fp = fopen("shared_resource.txt", "r+");
if (fp == NULL) { perror("fopen enfant"); V(sem_id); exit(EXIT_FAILURE)
int value;
fscanf(fp, "%d", &value);
83
Programmation Système avec Linux et C Polytechnique Douala - Génie Logiciel N4
value++;
fseek(fp, 0, SEEK_SET); // Revenir au debut du fichier
fprintf(fp, "%d\n", value);
fclose(fp);
// --- FIN SECTION CRITIQUE ---
printf("Enfant (PID %d): Valeur incremente a %d. Liberation du semaphor
V(sem_id); // Liberer (V) le semaphore
return EXIT_SUCCESS;
} else { // Processus Parent (Producteur/Incrementer)
printf("Parent (PID %d): En attente d’acces a la ressource...\n", getpi
P(sem_id); // Attendre (P) que le semaphore soit disponible
printf("Parent (PID %d): Acces a la section critique.\n", getpid());
// --- SECTION CRITIQUE ---
fp = fopen("shared_resource.txt", "r+");
if (fp == NULL) { perror("fopen parent"); V(sem_id); exit(EXIT_FAILURE)
int value;
fscanf(fp, "%d", &value);
value++;
fseek(fp, 0, SEEK_SET);
fprintf(fp, "%d\n", value);
fclose(fp);
// --- FIN SECTION CRITIQUE ---
printf("Parent (PID %d): Valeur incremente a %d. Liberation du semaphor
V(sem_id); // Liberer (V) le semaphore
// Attendre que l’enfant se termine
if (wait(NULL) == -1) {
perror("wait parent");
semctl(sem_id, IPC_RMID, NULL);
return EXIT_FAILURE;
}
// Lire la valeur finale (apres que les deux processus aient incremente
fp = fopen("shared_resource.txt", "r");
if (fp == NULL) { perror("fopen final"); return EXIT_FAILURE; }
int final_value;
fscanf(fp, "%d", &final_value);
fclose(fp);
printf("Parent: Valeur finale dans le fichier: %d\n", final_value);
// 5. Supprimer l’ensemble de semaphores
84
Polytechnique Douala - Génie Logiciel N4 Programmation Système avec Linux et C
if (semctl(sem_id, 0, IPC_RMID, sem_arg) == -1) { // 0 pour sem_num est
perror("semctl IPC_RMID");
return EXIT_FAILURE;
}
printf("Parent: Ensemble de semaphores supprime.\n");
// Nettoyer le fichier
unlink("shared_resource.txt");
unlink(SEM_KEY_FILE); // Supprimer le fichier cle
return EXIT_SUCCESS;
}
}
Pour tester : 1. Créez un fichier vide pour la clé IPC : ‘touch /tmp/sem_key_file‘. 2.
Compilez le programme : ‘gcc sem_producer_consumer.c -o sem_prog‘. 3. Exécutez
‘./sem_prog‘. La valeur finale dans le fichier devrait être 2 (si les deux processus ont
réussi à incrémenter sans condition de course).
Exercice
4.4 : Synchronisation avec Sémaphores System V
[label=()]Modifiez l’Exercice 4.2 (a) où le parent et l’enfant partagent un entier
via la mémoire partagée. Intégrez un sémaphore System V pour garantir que seul
un processus à la fois incrémente l’entier. Le parent incrémente 5 fois, l’enfant
incrémente 5 fois. La valeur finale de l’entier partagé devrait être 10. Implémentez
un problème producteur-consommateur avec un buffer de taille finie en utilisant :
1.
2. — La mémoire partagée pour le buffer et un entier pour le compteur de messages.
— Deux sémaphores System V : un pour contrôler l’accès exclusif au buffer (mutex),
un pour compter les places vides (empty), et un pour compter les places pleines
(full).
Le producteur ajoute des éléments au buffer, le consommateur les retire.
4.6. IPC POSIX (Vue d’Ensemble)
Les mécanismes IPC System V (mémoire partagée, files de messages, sémaphores) sont
puissants mais peuvent être un peu complexes à utiliser et ont des limites historiques. Les
normes POSIX (Portable Operating System Interface) ont défini des alternatives plus
modernes et souvent plus simples, disponibles sur la plupart des systèmes UNIX-like, y
compris Linux.
85
Programmation Système avec Linux et C Polytechnique Douala - Génie Logiciel N4
4.6.1. Files de messages POSIX, Sémaphores POSIX, Mémoire
partagée POSIX
Les IPC POSIX sont généralement plus "orientés fichier" ou "orientés nom", ce qui
les rend parfois plus faciles à manipuler et à gérer leur durée de vie.
— Mémoire Partagée POSIX :
— Fonctions : shm_open(), ftruncate(), mmap(), munmap(), shm_unlink().
— Avantages : Intégration plus fluide avec mmap(), noms basés sur des chemins (ex :
/my_shm_region), persistance contrôlée par shm_unlink().
— Files de Messages POSIX :
— Fonctions : mq_open(), mq_send(), mq_receive(), mq_close(), mq_unlink().
— Avantages : API plus simple, messages peuvent être prioritaires, noms basés sur
des chemins (ex : /my_queue).
— Sémaphores POSIX :
— Fonctions : sem_open(), sem_wait(), sem_post(), sem_close(), sem_unlink(),
sem_init(), sem_destroy().
— Avantages : Peuvent être nommés (pour processus non liés) ou sans nom (pour
threads ou processus liés via mémoire partagée), API plus intuitive que System V.
Bien que les IPC System V soient toujours utilisés dans de nombreux systèmes existants,
les IPC POSIX sont souvent préférés pour les nouvelles implémentations en raison de leur
conception plus propre et de leur meilleure intégration dans les pratiques de programma-
tion modernes. Il est important de connaître les deux ensembles d’API car vous pourriez
rencontrer des applications utilisant l’un ou l’autre.
86
Chapitre 5
Gestion des Signaux
Ce chapitre explore la gestion des signaux, un mécanisme essentiel pour la communi-
cation asynchrone entre le noyau et les processus, ou entre processus. Nous définirons
ce qu’est un signal, identifierons les types de signaux courants et leurs actions par dé-
faut. Une grande partie du chapitre sera consacrée aux techniques d’envoi et, surtout, de
capture et de gestion des signaux par un programme C, en utilisant les appels système
signal() et sigaction(). Enfin, nous aborderons brièvement les temporisateurs basés
sur les signaux. La maîtrise des signaux est cruciale pour le développement d’applications
robustes, capables de réagir aux événements système et de gérer leur propre terminaison
ou des erreurs de manière contrôlée.
5.1. Concepts de Signaux
5.1.1. Qu’est-ce qu’un Signal ?
Un signal est une forme d’interruption logicielle asynchrone envoyée à un processus
pour lui notifier un événement. C’est un mécanisme de communication de bas niveau et
unidirectionnel (d’un expéditeur à un récepteur).
Les signaux peuvent être générés par diverses sources :
— Le noyau (Kernel) : En réponse à des événements matériels ou logiciels. Par exemple,
une division par zéro génère SIGFPE, un accès mémoire invalide génère SIGSEGV, ou
un processus enfant qui se termine génère SIGCHLD pour son parent.
— D’autres processus : Un processus peut envoyer un signal à un autre processus (ou
à lui-même) en utilisant la fonction kill().
— Un terminal : Lorsqu’un utilisateur tape certaines combinaisons de touches (par
exemple, Ctrl+C pour SIGINT, Ctrl+Z pour SIGTSTP).
La nature asynchrone des signaux signifie qu’ils peuvent arriver à tout moment pendant
l’exécution d’un processus, interrompant son flux normal. Le processus peut alors choisir
de réagir au signal en exécutant une fonction spécifique (un gestionnaire de signal ou
"handler"), ou laisser le système d’exploitation gérer le signal avec son action par défaut.
87
Programmation Système avec Linux et C Polytechnique Douala - Génie Logiciel N4
5.1.2. Types de Signaux Courants (SIGINT, SIGTERM, SIG-
KILL, SIGCHLD)
Linux définit un grand nombre de signaux, chacun ayant un but spécifique. Ils sont
généralement identifiés par des noms commençant par "SIG" (par exemple, SIGINT pour
Signal Interrupt). Voici quelques-uns des signaux les plus importants et couramment
rencontrés :
88
Polytechnique Douala - Génie Logiciel N4 Programmation Système avec Linux et C
Nom du Si- Numéro Description Action
gnal (ex.) par Dé-
faut
SIGHUP 1 Hangup (déconnexion du terminal). Souvent utilisé Terminer
pour demander à un daemon de recharger sa confi-
guration.
SIGINT 2 Interrupt (interruption du terminal, généré par Terminer
Ctrl+C).
SIGQUIT 3 Quit (interruption du terminal, généré par Ctrl+. Terminer
et Core
Dump
SIGILL 4 Illegal Instruction (instruction illégale). Terminer
et Core
Dump
SIGABRT 6 Abort (interruption anormale, généré par Terminer
abort()). et Core
Dump
SIGFPE 8 Floating-Point Exception (erreur arithmétique, ex : Terminer
division par zéro). et Core
Dump
SIGKILL 9 Kill (terminaison immédiate). Ne peut pas être Terminer
intercepté ni ignoré.
SIGSEGV 11 Segmentation Violation (violation de segment, ac- Terminer
cès mémoire invalide). et Core
Dump
SIGPIPE 13 Broken Pipe (écriture sur un pipe ou socket sans Terminer
lecteur).
SIGALRM 14 Alarm Clock (déclenchement d’un timer, généré par Terminer
alarm()).
SIGTERM 15 Terminate (demande de terminaison normale). Terminer
Peut être intercepté.
SIGCHLD 17 Child Stopped or Terminated (enfant arrêté ou ter- Ignorer
miné). Envoyer au processus parent.
SIGCONT 18 Continue (reprendre un processus arrêté). Continuer
SIGSTOP 19 Stop (arrêt inconditionnel, ne peut pas être inter- Arrêter
cepté ni ignoré).
SIGTSTP 20 Stop from TTY (arrêt du terminal, généré par Arrêter
Ctrl+Z).
La liste complète des signaux et leurs numéros (qui peuvent légèrement varier selon les
architectures) se trouve dans la page de manuel ‘man 7 signal‘.
89
Programmation Système avec Linux et C Polytechnique Douala - Génie Logiciel N4
5.1.3. Actions par Défaut des Signaux
Pour chaque signal, le système d’exploitation définit une action par défaut. Un pro-
cessus peut choisir de laisser le système d’exploitation effectuer cette action, ou il peut
modifier le comportement par défaut pour la plupart des signaux (à l’exception de SIGKILL
et SIGSTOP qui ne peuvent jamais être interceptés, ignorés ou modifiés).
Les actions par défaut possibles sont :
1. Terminer le processus (Term) : Le processus est arrêté.
2. Ignorer le signal (Ign) : Le signal est simplement ignoré et n’a aucun effet sur le
processus.
3. Terminer le processus et générer un Core Dump (Core) : Le processus est
arrêté, et une image de son espace d’adressage mémoire est écrite sur le disque (fichier
‘core‘), ce qui est utile pour le débogage post-mortem.
4. Arrêter le processus (Stop) : Le processus est suspendu. Il peut être repris par un
signal SIGCONT.
5. Reprendre l’exécution du processus (Cont) : Le processus reprend son exécution
s’il était arrêté.
Exercice
5.1 : Comprendre les Signaux
[label=()]Donnez un exemple de situation où un processus recevrait un SIGFPE et un
SIGSEGV. Expliquez pourquoi il est essentiel que SIGKILL et SIGSTOP ne puissent pas
être interceptés ni ignorés. Quel est le signal le plus approprié pour demander à un
serveur de s’arrêter proprement, et pourquoi ?
5.2. Envoi de Signaux
Un processus peut envoyer un signal à un autre processus (ou à lui-même) en utilisant
des appels système spécifiques.
5.2.1. kill()
L’appel système kill() est utilisé pour envoyer un signal à un processus ou à un
groupe de processus. Malgré son nom, il n’est pas uniquement utilisé pour "tuer" des
processus, mais pour envoyer n’importe quel signal.
—
3.
1.
2. Syntaxe :
#include <sys/types.h> // Pour pid_t
#include <signal.h> // Pour les definitions des signaux
int kill(pid_t pid, int sig);
— pid : L’identifiant du processus ou du groupe de processus cible :
— pid > 0 : Le signal sig est envoyé au processus avec l’ID pid.
90
Polytechnique Douala - Génie Logiciel N4 Programmation Système avec Linux et C
— pid = 0 : Le signal sig est envoyé à tous les processus du groupe de processus
du processus appelant (à l’exception des processus ‘init‘ et du processus appelant
lui-même).
— pid = -1 : Le signal sig est envoyé à tous les processus du système pour lesquels
le processus appelant a la permission d’envoyer un signal (sauf ‘init‘).
— pid < -1 : Le signal sig est envoyé à tous les processus dont le GID (Group ID)
est égal à la valeur absolue de pid.
— sig : Le numéro du signal à envoyer (par exemple, SIGTERM, SIGINT, SIGUSR1). Si
0, aucun signal n’est envoyé, mais les vérifications d’erreur sont effectuées (peut être
utilisé pour vérifier l’existence d’un PID).
— Permissions : Un processus peut envoyer un signal à un autre processus si :
— Il a les mêmes UID (User ID) réels ou effectifs que le processus cible.
— Il a les mêmes UID réels ou effectifs que le PID du processus parent du processus
cible (pour les zombies).
— Il a les privilèges de super-utilisateur (root).
— Valeur de retour :
— Succès : 0.
— Échec : -1, et errno est définie (ex : ESRCH si le PID n’existe pas, EPERM si per-
missions insuffisantes).
— Exemple d’utilisation de kill() : Ce programme crée un processus enfant. Le parent
envoie un signal SIGUSR1 à l’enfant après un court délai. L’enfant affichera qu’il a reçu
le signal (si un handler est configuré, que nous verrons plus tard).
#include <stdio.h> // For printf, perror
#include <stdlib.h> // For EXIT_FAILURE, EXIT_SUCCESS
#include <unistd.h> // For fork, sleep, getpid
#include <signal.h> // For kill, SIGUSR1
#include <sys/wait.h> // For wait
// Un simple handler de signal pour l’enfant
void child_signal_handler(int sig) {
if (sig == SIGUSR1) {
printf("Enfant (PID %d): J’ai recu SIGUSR1 de mon parent!\n", getpid())
}
}
int main() {
pid_t pid = fork();
if (pid == -1) {
perror("fork");
return EXIT_FAILURE;
} else if (pid == 0) { // Code du processus enfant
// Enfant: Configurer un handler pour SIGUSR1
if (signal(SIGUSR1, child_signal_handler) == SIG_ERR) {
perror("signal");
91
Programmation Système avec Linux et C Polytechnique Douala - Génie Logiciel N4
return EXIT_FAILURE;
}
printf("Enfant (PID %d): Pret a recevoir SIGUSR1. Je dors...\n", getpid
sleep(5); // Dort pour attendre le signal du parent
printf("Enfant (PID %d): Je me reveille et me termine.\n", getpid());
return EXIT_SUCCESS;
} else { // Code du processus parent
printf("Parent (PID %d): Enfant cree avec PID %d.\n", getpid(), pid);
printf("Parent: Je dors 2 secondes avant d’envoyer SIGUSR1 a l’enfant..
sleep(2); // Laisser le temps a l’enfant de se preparer
// Envoyer SIGUSR1 a l’enfant
if (kill(pid, SIGUSR1) == -1) {
perror("kill");
// Dans un cas reel, on peut vouloir kill -9 l’enfant ou gerer l’er
} else {
printf("Parent: SIGUSR1 envoye a l’enfant (PID %d).\n", pid);
}
// Attendre la terminaison de l’enfant
if (wait(NULL) == -1) {
perror("wait");
return EXIT_FAILURE;
}
printf("Parent: Programme termine.\n");
return EXIT_SUCCESS;
}
}
5.2.2. raise()
La fonction raise() est une fonction de la librairie standard C (qui utilise kill() en
interne) pour envoyer un signal au processus appelant lui-même.
— Syntaxe :
#include <signal.h> // Pour raise
int raise(int sig);
— sig : Le numéro du signal à envoyer au processus courant.
— Valeur de retour :
— Succès : 0.
— Échec : Non-zéro.
— Exemple d’utilisation de raise() :
#include <stdio.h> // For printf
#include <stdlib.h> // For exit
92
Polytechnique Douala - Génie Logiciel N4 Programmation Système avec Linux et C
#include <signal.h> // For raise, SIGTERM
// Simple handler pour SIGTERM
void handle_term(int sig) {
printf("J’ai recu SIGTERM et je me termine proprement.\n");
exit(0);
}
int main() {
printf("Je vais configurer un handler pour SIGTERM.\n");
if (signal(SIGTERM, handle_term) == SIG_ERR) {
perror("signal");
return 1;
}
printf("Je vais maintenant envoyer SIGTERM a moi-meme avec raise().\n");
// Le programme va s’arreter ici et executer handle_term
raise(SIGTERM);
// Cette ligne ne devrait pas etre atteinte si le handler est execute et ap
printf("Cette ligne ne devrait jamais s’afficher.\n");
return 0;
}
Exercice
5.2 : Envoi de Signaux
[label=()]Écrivez un programme C qui génère deux processus enfants. Le premier
enfant doit attendre 5 secondes puis envoyer un SIGTERM au deuxième enfant. Le
deuxième enfant doit avoir un gestionnaire de signal pour SIGTERM qui affiche un
message et se termine proprement. Le parent doit attendre la terminaison des deux
enfants. Créez un programme qui entre dans une boucle infinie. Au bout de 3 secondes,
il utilise raise(SIGKILL) pour se terminer. Observez le comportement et expliquez
pourquoi le programme ne peut pas "capturer" ce signal.
5.3. Capture et Gestion des Signaux
Le cœur de la programmation des signaux réside dans la capacité d’un processus à
intercepter un signal et à exécuter une fonction spécifique (un gestionnaire de signal ou
signal handler ) en réponse, plutôt que de laisser le système d’exploitation effectuer l’action
par défaut.
93
Programmation Système avec Linux et C Polytechnique Douala - Génie Logiciel N4
5.3.1. signal() (API obsolète mais simple)
L’appel système signal() est la fonction la plus simple pour établir un gestionnaire
de signal. Cependant, elle est considérée comme obsolète et non fiable par les standards
POSIX pour plusieurs raisons, notamment son comportement non spécifié ou hérité sur
certaines plateformes. Il est préférable d’utiliser sigaction() pour un code robuste.
—1.
2. Syntaxe :
#include <signal.h> // Pour signal, SIG_DFL, SIG_IGN, SIG_ERR
typedef void (*sighandler_t)(int); // Definition du type pour le handler
sighandler_t signal(int signum, sighandler_t handler);
— signum : Le numéro du signal à gérer.
— handler : Peut être :
— Un pointeur vers une fonction de gestion de signal personnalisée. Cette fonction
prend un seul argument de type int (le numéro du signal reçu) et ne retourne rien
(void).
— SIG_DFL : Restaurer l’action par défaut du signal.
— SIG_IGN : Ignorer le signal.
— Valeur de retour :
— Succès : L’adresse du précédent gestionnaire de signal pour signum.
— Échec : SIG_ERR, et errno est définie.
Limitations de signal() :
— Non fiabilité : Sur de nombreux systèmes anciens, après l’exécution d’un gestionnaire
de signal, le comportement par défaut de signal() était de réinitialiser le gestionnaire
à SIG_DFL avant d’appeler le handler. Cela nécessitait de réenregistrer le handler à
l’intérieur du handler lui-même, ce qui pouvait créer des conditions de course.
— Interruption des appels système : Un appel système bloquant (comme read() sur
un pipe) peut être interrompu par un signal. signal() ne fournit pas de mécanisme
pour redémarrer automatiquement ces appels système.
— Blocage de signaux : Ne permet pas de spécifier quels autres signaux doivent être
bloqués pendant l’exécution du gestionnaire.
Bien que signal() soit plus simple, il est fortement recommandé d’utiliser sigaction()
pour les applications robustes.
5.3.2. sigaction() (API moderne et fiable)
L’appel système sigaction() est la méthode standard et fiable pour la gestion des
signaux sous POSIX. Il offre un contrôle beaucoup plus fin sur le comportement des
signaux, notamment la gestion des signaux qui peuvent être bloqués pendant l’exécution
d’un gestionnaire et le redémarrage des appels système interrompus.
— Syntaxe :
#include <signal.h> // Pour sigaction, struct sigaction, sigemptyset, sigaddset
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact
94
Polytechnique Douala - Génie Logiciel N4 Programmation Système avec Linux et C
— signum : Le numéro du signal à gérer.
— act : Un pointeur vers une structure struct sigaction qui spécifie la nouvelle action
pour signum.
— oldact : Un pointeur vers une structure struct sigaction où l’ancienne action du
signal sera stockée (peut être NULL si non désiré).
— La structure struct sigaction :
struct sigaction {
union {
void (*sa_handler)(int); // Fonction de gestion du signal (pour
void (*sa_sigaction)(int, siginfo_t *, void *); // Fonction de gestion
} __sigaction_handler; // Nom specifique au GNU C Library
sigset_t sa_mask; // Masque des signaux a bloquer pendan
int sa_flags; // Drapeaux pour modifier le comporteme
void (*sa_restorer)(void); // Non utilise directement par l’utilis
};
#define sa_handler __sigaction_handler.sa_handler
#define sa_sigaction __sigaction_handler.sa_sigaction
Les champs importants sont :
— sa_handler : Pointeur vers la fonction de gestion du signal. C’est la fonction qui
sera appelée lorsque le signal signum est reçu.
— sa_mask : Un masque de signaux (type sigset_t). Les signaux inclus dans ce
masque seront bloqués pendant l’exécution du gestionnaire. Cela empêche l’inter-
ruption du gestionnaire par d’autres signaux. Le signal lui-même est automatique-
ment bloqué.
— sigemptyset(&set) : Initialise set à un ensemble vide de signaux.
— sigfillset(&set) : Initialise set à inclure tous les signaux.
— sigaddset(&set, signum) : Ajoute signum à l’ensemble set.
— sigdelset(&set, signum) : Supprime signum de l’ensemble set.
— sigismember(&set, signum) : Vérifie si signum est dans l’ensemble set.
— sa_flags : Drapeaux qui modifient le comportement de sigaction() et du ges-
tionnaire :
— SA_RESTART : Si un appel système est interrompu par le signal, il est automa-
tiquement redémarré. Très utile pour éviter des erreurs EINTR.
— SA_SIGINFO : Si ce drapeau est défini, sa_sigaction est utilisé à la place de
sa_handler. sa_sigaction reçoit trois arguments, fournissant des informa-
tions détaillées sur le signal (ex : PID de l’envoyeur, cause de l’erreur).
— SA_NOCLDWAIT : Pour SIGCHLD, indique de ne pas créer de zombies et de ne pas
attendre les enfants.
— sa_restorer : Champ obsolète, ne pas utiliser directement.
— Valeur de retour :
— Succès : 0.
— Échec : -1, et errno est définie.
Bonnes pratiques pour les gestionnaires de signaux :
— Les gestionnaires de signaux doivent être courts et simples.
95
Programmation Système avec Linux et C Polytechnique Douala - Génie Logiciel N4
— N’appelez que des fonctions asynchronously-safe (réentrantes) à l’intérieur d’un
handler. La plupart des fonctions de la librairie standard (comme printf(), malloc())
ne le sont pas et peuvent entraîner des deadlocks ou des corruptions de données si ap-
pelées dans un handler. Les fonctions sûres sont listées dans ‘man 7 signal‘.
— Il est souvent préférable de définir un drapeau global (volatile sig_atomic_t) dans
le handler, puis de vérifier ce drapeau dans la boucle principale du programme pour
effectuer les actions complexes.
— Exemple de gestion de signal avec sigaction() : Ce programme capture SIGINT
(Ctrl+C) et SIGUSR1. Il montre comment définir des actions différentes et gérer les
informations de signal.
#include <signal.h> // Pour sigaction, sigemptyset, SIGINT, SIGUSR1, SIG_ERR
#include <stdio.h> // Pour printf, perror
#include <stdlib.h> // For EXIT_SUCCESS, EXIT_FAILURE
#include <unistd.h> // For sleep, getpid, getppid
#include <errno.h> // For errno
// Drapeau global pour indiquer la terminaison propre
volatile sig_atomic_t terminate_program = 0;
// Handler pour SIGINT (Ctrl+C)
void handle_sigint(int sig) {
printf("\nSignal SIGINT (Ctrl+C) recu! Demande de terminaison propre...\n")
terminate_program = 1; // Demande la terminaison du programme
}
// Handler pour SIGUSR1 (avec informations detailees)
void handle_sigusr1_info(int sig, siginfo_t *info, void *ucontext) {
printf("\nSignal SIGUSR1 recu (depuis PID %d)!\n", info->si_pid);
printf(" Code du signal (si_code): %d\n", info->si_code);
// On peut ajouter d’autres informations de ’info’ si SA_SIGINFO est utilis
}
int main() {
struct sigaction sa_int, sa_usr1;
printf("Mon PID est %d.\n", getpid());
// --- Configuration du handler pour SIGINT ---
sa_int.sa_handler = handle_sigint; // Associer la fonction de gestion
sigemptyset(&sa_int.sa_mask); // Aucun signal supplementaire ne sera b
sa_int.sa_flags = 0; // Pas de drapeaux specifiques, handler
if (sigaction(SIGINT, &sa_int, NULL) == -1) {
perror("sigaction pour SIGINT");
return EXIT_FAILURE;
96
Polytechnique Douala - Génie Logiciel N4 Programmation Système avec Linux et C
}
printf("Handler pour SIGINT configure.\n");
// --- Configuration du handler pour SIGUSR1 avec SA_SIGINFO ---
sa_usr1.sa_sigaction = handle_sigusr1_info; // Utiliser sa_sigaction pour l
sigemptyset(&sa_usr1.sa_mask); // Aucun signal supplementaire
sa_usr1.sa_flags = SA_SIGINFO; // ACTIVER SA_SIGINFO pour uti
if (sigaction(SIGUSR1, &sa_usr1, NULL) == -1) {
perror("sigaction pour SIGUSR1");
return EXIT_FAILURE;
}
printf("Handler pour SIGUSR1 configure (avec SA_SIGINFO).\n");
printf("Appuyez sur Ctrl+C pour arreter proprement, ou envoyez ’kill -SIGUS
// Boucle principale du programme
while (!terminate_program) {
printf("Programme en cours d’execution...\n");
sleep(2); // Simule un travail
}
printf("Programme termine proprement.\n");
return EXIT_SUCCESS;
}
Pour tester : 1. Compilez : ‘gcc -o signalt estsignalt est.c‘.2.Excutez : ‘./signalt est‘.N otezleP IDaf f ic
C‘.Leprogrammedevraitaf f icherlemessageduhandler‘SIGIN T ‘etseterminerproprement.4.Excut
lenouveau.Dansunautre terminal, utilisez‘kill−SIGU SR1 < P IDd up rogramme >
‘.Leprogrammedevraitaf f icherlemessageduhandler‘SIGU SR1‘avecleP IDdel′ envoyeur.
5.3.3. Bloquer et Débloquer des Signaux (sigprocmask())
L’appel système sigprocmask() permet à un processus de manipuler son masque de
signaux bloqués. Un signal bloqué est un signal qui ne sera pas délivré au processus immé-
diatement. Il restera en attente ("pending") jusqu’à ce qu’il soit débloqué, moment auquel il
sera délivré (une seule fois s’il est un signal non fiable).
C’est très utile pour protéger des sections critiques de code qui ne doivent pas être
interrompues par des signaux.
— Syntaxe :
#include <signal.h> // Pour sigprocmask, sigset_t, SIG_BLOCK, SIG_UNBLOCK, S
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
— how : Spécifie comment le masque de signaux doit être modifié :
— SIG_BLOCK : Les signaux dans set sont ajoutés au masque actuel.
97
Programmation Système avec Linux et C Polytechnique Douala - Génie Logiciel N4
— SIG_UNBLOCK : Les signaux dans set sont supprimés du masque actuel.
— SIG_SETMASK : Le masque actuel est remplacé par set.
— set : Un pointeur vers un ensemble de signaux (sigset_t) qui sera utilisé pour
modifier le masque.
— oldset : Un pointeur vers un ensemble de signaux où le masque précédent sera
stocké (peut être NULL si non désiré).
— Valeur de retour : 0 en cas de succès, -1 en cas d’échec.
— Exemple d’utilisation de sigprocmask() : Ce programme bloque temporairement
le signal SIGINT pendant l’exécution d’une section critique, puis le débloque.
#include <stdio.h> // For printf, perror
#include <stdlib.h> // For EXIT_FAILURE, EXIT_SUCCESS
#include <unistd.h> // For sleep
#include <signal.h> // For sigprocmask, sigemptyset, sigaddset, SIGINT
void sigint_handler(int sig) {
printf("\nATTENTION: SIGINT recu alors qu’il etait bloque ou juste apres
}
int main() {
sigset_t block_mask, old_mask;
struct sigaction sa;
// Configurer le handler pour SIGINT (juste pour demonstration)
sa.sa_handler = sigint_handler;
sigemptyset(&sa.sa_mask);
sa.sa_flags = 0;
if (sigaction(SIGINT, &sa, NULL) == -1) {
perror("sigaction");
return EXIT_FAILURE;
}
// Preparer un ensemble de signaux a bloquer (juste SIGINT ici)
sigemptyset(&block_mask);
sigaddset(&block_mask, SIGINT);
printf("Appuyez sur Ctrl+C a tout moment pour voir le comportement.\n");
printf("Programme demarre. Je dors 2s (Ctrl+C aura un effet normal).\n")
sleep(2);
printf("\n--- Entree dans la section critique ---\n");
// Bloquer SIGINT
// Les signaux SIGINT envoyes pendant cette phase seront mis en attente
if (sigprocmask(SIG_BLOCK, &block_mask, &old_mask) == -1) {
perror("sigprocmask SIG_BLOCK");
return EXIT_FAILURE;
98
Polytechnique Douala - Génie Logiciel N4 Programmation Système avec Linux et C
}
printf("SIGINT est maintenant bloque. Appuyez sur Ctrl+C maintenant...\n
for (int i = 0; i < 5; i++) {
printf("Section critique: Travail en cours (%d/5)...\n", i + 1);
sleep(1);
}
printf("--- Fin de la section critique ---\n");
// Debloquer SIGINT
// Si un SIGINT a ete recu pendant la section critique, il sera delivre
if (sigprocmask(SIG_SETMASK, &old_mask, NULL) == -1) {
perror("sigprocmask SIG_SETMASK");
return EXIT_FAILURE;
}
printf("SIGINT est maintenant debloque. S’il a ete mis en attente, il es
printf("Programme continue apres section critique. Je dors 2s (Ctrl+C no
sleep(2);
printf("Fin du programme.\n");
return EXIT_SUCCESS;
}
Pour tester : 1. Compilez et exécutez le programme. 2. Pendant que le programme
affiche "SIGINT est maintenant bloque...", appuyez plusieurs fois sur ‘Ctrl+C‘.
Vous ne devriez pas voir le message du handler immédiatement. 3. Une fois que
le programme sort de la section critique et affiche "SIGINT est maintenant de-
bloque...", si vous avez appuyé sur ‘Ctrl+C‘ pendant la section critique, le message
du handler devrait apparaître immédiatement.
5.3.4. Signaux Fiables et non Fiables
Cette distinction est une nuance historique importante dans la gestion des signaux UNIX,
bien qu’aujourd’hui, la plupart des systèmes modernes utilisent un comportement fiable par
défaut.
— Signaux non fiables (Unreliable Signals) :
— C’est le comportement des premiers systèmes UNIX et de l’API signal().
— Problème principal : Après la délivrance d’un signal et l’exécution de son
gestionnaire, le système réinitialisait l’action de ce signal à son comportement
par défaut (SIG_DFL). Cela signifiait que si un programme voulait continuer
à intercepter le signal, il devait réenregistrer son gestionnaire à l’intérieur du
gestionnaire de signal lui-même.
— Conséquence : Cela introduisait une fenêtre de course (race window) où
un autre signal du même type pouvait être reçu avant que le gestionnaire ne
99
Programmation Système avec Linux et C Polytechnique Douala - Génie Logiciel N4
soit réinstallé, et dans ce cas, l’action par défaut aurait été appliquée (souvent,
la terminaison du processus). Il pouvait aussi y avoir une perte de signaux si
plusieurs arrivaient rapidement.
— Signaux fiables (Reliable Signals) :
— Introduits par POSIX.1 et implémentés par des appels système comme sigaction().
— Comportement : Lorsqu’un signal est délivré, son gestionnaire est exécuté.
Pendant l’exécution du gestionnaire, le signal lui-même est automatiquement
bloqué (masqué), empêchant sa ré-entrée. Après l’exécution du gestionnaire,
le masque est restauré et le signal est de nouveau débloqué. Le gestionnaire
reste installé (n’est pas réinitialisé à SIG_DFL).
— Conséquence : Cela garantit que les signaux ne sont pas perdus et que le
gestionnaire ne s’interrompt pas lui-même. Si un signal du même type est envoyé
pendant l’exécution du handler, il reste en attente et sera délivré une fois le
handler terminé et le signal débloqué.
En résumé, utilisez toujours sigaction() pour la gestion des signaux, car elle fournit un
comportement fiable et plus de contrôle.
Exercice 5.3 :
Gestion Avancée des Signaux
[label=()]Écrivez un programme qui simule une section critique. Votre pro-
gramme doit :
1. — Configurer un handler pour SIGINT qui affiche un message et met à jour un
drapeau global.
— Entrer dans une boucle infinie où il effectue un "travail" (par exemple, sleep(1)).
— Toutes les 5 secondes, entrer dans une "section critique" où le signal SIGINT
est bloqué à l’aide de sigprocmask().
— Dans la section critique, afficher un message indiquant qu’elle est active et
qu’elle ne peut pas être interrompue.
— Après la section critique, débloquer SIGINT. Si le drapeau global a été mis à
jour par le handler (indiquant un SIGINT en attente), le programme doit se
terminer proprement. Sinon, il continue sa boucle.
2. (Défi) Modifiez le programme précédent pour qu’il utilise sa_sigaction et SA_SIGINFO
pour le handler SIGINT, et affiche le PID de l’envoyeur (qui sera lui-même si
Ctrl+C).
5.4. Temporisateurs (Timers)
Les temporisateurs sont des mécanismes qui permettent à un processus de recevoir un
signal à des intervalles de temps spécifiques ou après un certain délai.
5.4.1. alarm()
L’appel système alarm() est une fonction simple pour programmer un seul signal SIGALRM
après un nombre spécifié de secondes réelles (temps réel du mur d’horloge).
100
Polytechnique Douala - Génie Logiciel N4 Programmation Système avec Linux et C
— Syntaxe :
#include <unistd.h> // Pour alarm
unsigned int alarm(unsigned int seconds);
— seconds : Le nombre de secondes après lequel le signal SIGALRM sera envoyé. Si 0,
tout timer alarm précédent est annulé.
— Comportement :
— Seul un timer alarm() peut être actif à la fois par processus. Un nouvel appel
annule le précédent.
— Le signal SIGALRM est envoyé par le noyau. L’action par défaut est de termi-
ner le processus. Pour faire quelque chose d’utile, vous devez configurer un
gestionnaire pour SIGALRM.
— Valeur de retour :
— Le nombre de secondes restantes avant le déclenchement de l’alarme précédente
(si elle existait).
— 0 si aucune alarme n’était en attente.
— Exemple d’utilisation de alarm() pour un timeout : Ce programme utilise alarm()
pour limiter le temps d’attente d’une entrée utilisateur.
#include <stdio.h> // For printf, fgets
#include <stdlib.h> // For EXIT_SUCCESS, EXIT_FAILURE
#include <unistd.h> // For alarm, sleep
#include <signal.h> // For signal, SIGALRM
// Handler pour SIGALRM
void alarm_handler(int sig) {
printf("\nTIMEOUT! Le temps est ecoule.\n");
exit(EXIT_FAILURE); // Termine le programme en cas de timeout
}
int main() {
char input_buffer[100];
// Configurer le handler pour SIGALRM
if (signal(SIGALRM, alarm_handler) == SIG_ERR) {
perror("signal for SIGALRM");
return EXIT_FAILURE;
}
printf("Vous avez 5 secondes pour entrer quelque chose: ");
alarm(5); // Demarrer l’alarme de 5 secondes
// Lire l’entree de l’utilisateur
if (fgets(input_buffer, sizeof(input_buffer), stdin) != NULL) {
// Si l’utilisateur entre quelque chose avant le timeout, annuler l’
alarm(0); // Annuler l’alarme en cours
101
Programmation Système avec Linux et C Polytechnique Douala - Génie Logiciel N4
printf("Merci pour votre entree: %s", input_buffer);
} else {
// Si fgets retourne NULL et que l’alarme n’a pas ete annulee,
// c’est probablement parce que l’alarme s’est declenchee et a appel
// Ou une erreur s’est produite.
perror("fgets");
return EXIT_FAILURE;
}
printf("Programme termine normalement.\n");
return EXIT_SUCCESS;
}
5.4.2. setitimer()
L’appel système setitimer() fournit des capacités de minuterie plus avancées et plus
flexibles que alarm(). Il permet de définir des timers qui se répètent à des intervalles réguliers.
— Syntaxe :
#include <sys/time.h> // Pour setitimer, struct itimerval, ITIMER_REAL
int setitimer(int which, const struct itimerval *new_value, struct itimerval
— which : Spécifie le type de timer :
— ITIMER_REAL : Minuteur réel (temps réel). Déclenche SIGALRM. Décrémente en
temps réel.
— ITIMER_VIRTUAL : Minuteur virtuel. Déclenche SIGVTALRM. Décrémente uni-
quement lorsque le processus exécute du code.
— ITIMER_PROF : Minuteur de profilage. Déclenche SIGPROF. Décrémente lorsque
le processus exécute du code ou lorsque le système exécute du code pour le
compte du processus.
— new_value : Pointeur vers une structure struct itimerval qui définit le délai
initial et l’intervalle de répétition.
— old_value : Pointeur vers une structure struct itimerval où l’état précédent
du timer sera stocké (peut être NULL).
— La structure struct itimerval :
struct itimerval {
struct timeval it_interval; // Prochaine valeur pour it_value (pour repe
struct timeval it_value; // Valeur initiale du timer (delai avant le
};
struct timeval {
time_t tv_sec; // Secondes
suseconds_t tv_usec; // Microsecondes
};
102
Polytechnique Douala - Génie Logiciel N4 Programmation Système avec Linux et C
— Comportement :
— Si it_value est non nul, le timer se déclenche après ce délai initial.
— Si it_interval est non nul, le timer se recharge avec cette valeur après chaque
déclenchement, créant une séquence de signaux.
— Si it_value est nul, le timer est désarmé. Si it_interval est nul, le timer ne
se répète pas.
— Valeur de retour : 0 en cas de succès, -1 en cas d’échec.
— Exemple d’utilisation de setitimer() pour un timer répété : Ce programme utilise
setitimer() pour afficher un message toutes les 2 secondes.
#include <stdio.h> // For printf, perror
#include <stdlib.h> // For EXIT_FAILURE, EXIT_SUCCESS
#include <unistd.h> // For sleep
#include <signal.h> // For sigaction, sigemptyset, SIGALRM
#include <sys/time.h> // For setitimer, struct itimerval, ITIMER_REAL
// Compteur pour les ticks du timer
volatile int timer_ticks = 0;
// Handler pour SIGALRM (declenche par ITIMER_REAL)
void timer_handler(int sig) {
timer_ticks++;
printf("Tick du timer %d!\n", timer_ticks);
if (timer_ticks >= 5) {
printf("Fin des ticks du timer.\n");
exit(EXIT_SUCCESS); // Termine le programme apres 5 ticks
}
}
int main() {
struct sigaction sa;
struct itimerval timer_spec;
// 1. Configurer le handler pour SIGALRM
sa.sa_handler = timer_handler;
sigemptyset(&sa.sa_mask);
sa.sa_flags = 0;
if (sigaction(SIGALRM, &sa, NULL) == -1) {
perror("sigaction for SIGALRM");
return EXIT_FAILURE;
}
// 2. Configurer le timer
// Delai initial de 2 secondes
timer_spec.it_value.tv_sec = 2;
timer_spec.it_value.tv_usec = 0;
103
Programmation Système avec Linux et C Polytechnique Douala - Génie Logiciel N4
// Intervalle de repetition de 2 secondes
timer_spec.it_interval.tv_sec = 2;
timer_spec.it_interval.tv_usec = 0;
// 3. Demarrer le timer ITIMER_REAL
if (setitimer(ITIMER_REAL, &timer_spec, NULL) == -1) {
perror("setitimer");
return EXIT_FAILURE;
}
printf("Timer demarre. Attente de 5 ticks...\n");
// Boucle infinie pendant que le timer s’active
while (1) {
pause(); // Met le processus en pause jusqu’a ce qu’un signal soit r
}
return EXIT_SUCCESS; // Inaccessible dans cet exemple, l’exit est dans l
}
Exercice 5.4 :
Temporisateurs
[label=()]Écrivez un programme C qui affiche un compte à rebours de 10 à 0
secondes. Chaque seconde, le programme doit afficher le nombre de secondes
restantes. Utilisez alarm() ou setitimer() pour déclencher l’affichage chaque
seconde. Le programme doit se terminer lorsque le compte à rebours atteint 0.
(Défi) Modifiez l’exercice précédent pour qu’il utilise deux timers ITIMER_REAL :
1.
2. — Le premier timer déclenche un signal toutes les 1 seconde et affiche le compte
à rebours.
— Le deuxième timer déclenche un signal toutes les 3 secondes et affiche un mes-
sage "Checkpoint !".
Le programme doit se terminer après 10 secondes (déclenchement du premier ti-
mer).
104
Chapitre 6
Threads et Synchronisation
Ce chapitre introduit les threads, une alternative aux processus pour la concurrence, per-
mettant à un programme d’exécuter plusieurs parties de son code simultanément au sein du
même espace d’adressage. Nous détaillerons les différences entre threads et processus, leurs
avantages et inconvénients. Le cœur du chapitre sera la programmation multi-threadée avec
les pthreads POSIX, en explorant la création, la gestion et la terminaison des threads. Nous
mettrons un accent particulier sur les défis de la concurrence (conditions de course, interblo-
cages) et les mécanismes de synchronisation essentiels tels que les mutex et les variables de
condition. Maîtriser les threads est fondamental pour écrire des applications performantes
qui exploitent les architectures multi-cœurs.
6.1. Concepts de Threads
6.1.1. Threads vs. Processus
En programmation système, la **concurrence** peut être atteinte soit par des processus,
soit par des threads. Bien qu’ils permettent tous deux l’exécution parallèle de tâches, ils
diffèrent fondamentalement dans leur isolation et leur partage de ressources.
105
Programmation Système avec Linux et C Polytechnique Douala - Génie Logiciel N4
Processus (Process) Thread (Fil d’Exécution)
Isolement Mémoire : Chaque processus Isolement Mémoire : Les threads au
a son propre espace d’adressage mémoire sein d’un même processus partagent le
virtuel. Ils sont isolés les uns des autres MÊME espace d’adressage mémoire.
par le système d’exploitation.
Coût de Création/Commutation : Coût de Création/Commutation :
Lourd. La création d’un processus et la Léger. La création d’un thread et la com-
commutation de contexte entre processus mutation de contexte entre threads sont
sont coûteuses en temps et en ressources beaucoup moins coûteuses.
(CPU, mémoire).
Communication : Nécessite des méca- Communication : Partagent directe-
nismes IPC explicites (pipes, mémoire ment la mémoire, ce qui rend la communi-
partagée, files de messages, sockets) pour cation plus rapide mais nécessite une syn-
échanger des données. chronisation explicite.
Résilience : Plus robuste. Une erreur Résilience : Moins robuste. Une erreur
(crash) dans un processus n’affecte géné- (crash) dans un thread affecte l’ensemble
ralement pas les autres processus. du processus (tous les autres threads).
Ressources Partagées : Fichiers ou- Ressources Partagées : Partagent les
verts, descripteurs de fichiers, informa- descripteurs de fichiers, les variables glo-
tions de processus sont copiés ou doivent bales, les fonctions, l’espace d’adressage.
être explicitement passés. Chaque thread a sa propre pile d’exécu-
tion et ses propres registres CPU.
Exemple : Ouvrir plusieurs navigateurs Exemple : Un navigateur web unique
web distincts, chaque navigateur est un avec plusieurs onglets, chaque onglet peut
processus. être géré par un thread distinct dans le
même processus.
6.1.2. Avantages et Inconvénients des Threads
Avantages :
— Performance :
— Utilisation des cœurs CPU : Permettent d’exploiter pleinement les proces-
seurs multi-cœurs en exécutant des tâches en parallèle.
— Réactivité : Une application peut rester réactive (par exemple, son interface
utilisateur) pendant qu’un thread distinct effectue une tâche longue en arrière-
plan.
— Coût de commutation réduit : Le passage d’un thread à l’autre est beaucoup
plus rapide que le passage d’un processus à l’autre car il n’implique pas de
changement d’espace d’adressage mémoire.
106
Polytechnique Douala - Génie Logiciel N4 Programmation Système avec Linux et C
— Partage de données simplifié : Les threads partagent directement la mémoire,
ce qui rend l’échange de données beaucoup plus facile et plus rapide que les méca-
nismes IPC entre processus.
— Économie de ressources : Les threads consomment moins de ressources système
(mémoire, descripteurs de fichiers) que des processus complets.
— Modélisation de problèmes : Simplifie la conception de certaines applications
où des tâches différentes doivent s’exécuter concurremment et partager beaucoup
d’informations.
Inconvénients :
— Complexité de la synchronisation : Le partage de mémoire impose la nécessité
d’une synchronisation rigoureuse. C’est la principale difficulté : sans synchronisa-
tion adéquate, les conditions de course et les interblocages sont fréquents.
— Débogage difficile : Les conditions de course et les deadlocks sont souvent non
déterministes, ce qui rend leur reproduction et leur débogage très difficiles.
— Manque d’isolation : Si un thread plante, c’est l’ensemble du processus (et tous
ses threads) qui plante. Moins robuste que les processus.
— Problèmes de portabilité : Bien que les pthreads (POSIX threads) soient un
standard, des subtilités peuvent exister entre les implémentations.
6.1.3. Modèle de Mémoire des Threads
Lorsque plusieurs threads s’exécutent au sein d’un même processus, ils partagent la plu-
part des ressources du processus, mais chacun possède ses propres éléments privés.
— Ressources partagées entre les threads :
— Espace d’adressage virtuel : C’est le plus important. Tous les threads ac-
cèdent à la même mémoire (variables globales, données du tas allouées par
malloc(), segments de code).
— Descripteurs de fichiers : Tous les descripteurs de fichiers ouverts par le
processus sont accessibles à tous les threads.
— Signaux : Le traitement des signaux est souvent une responsabilité de l’en-
semble du processus, bien que des threads individuels puissent bloquer certains
signaux.
— Ressources privées à chaque thread :
— Pile d’exécution (Stack) : Chaque thread a sa propre pile d’exécution. C’est
là que sont stockées les variables locales des fonctions appelées par ce thread, les
paramètres de fonction, et les adresses de retour. Cela garantit que les appels
de fonctions dans un thread ne perturbent pas les appels de fonctions dans un
autre.
— Registres CPU : Chaque thread a son propre ensemble de registres du pro-
cesseur (compteur ordinal, pointeur de pile, etc.). C’est ce qui est sauvegardé
et restauré lors d’une commutation de contexte entre threads.
— Données locales au thread (Thread-Local Storage - TLS) : Certaines
variables peuvent être déclarées comme locales à un thread (par exemple, avec le
mot-clé _Thread_local en C11, ou des fonctions pthread_getspecific()/pthread_setspec
Elles ont une instance distincte pour chaque thread.
107
Programmation Système avec Linux et C Polytechnique Douala - Génie Logiciel N4
— Priorité et politique d’ordonnancement : Chaque thread peut avoir sa
propre priorité d’ordonnancement et sa propre politique.
La figure ci-dessous illustre le modèle de mémoire partagée entre les threads :
[Image of Thread Memory Model showing shared code, data, heap and separate stacks for
each thread]
6.2. Création et Gestion de Threads (POSIX Threads -
pthreads)
La norme POSIX définit une API standard pour la gestion des threads, connue sous le
nom de **pthreads**. Cette API est largement utilisée sur les systèmes UNIX-like, y compris
Linux. Pour compiler un programme utilisant pthreads, vous devez généralement lier avec
la bibliothèque pthreads en ajoutant l’option ‘-pthread‘ (ou ‘-lpthread‘) à votre commande
‘gcc‘.
6.2.1. pthread_create()
La fonction pthread_create() est l’appel fondamental pour créer un nouveau thread.
— Syntaxe :
#include <pthread.h> // Pour pthread_create
int pthread_create(pthread_t *restrict thread,
const pthread_attr_t *restrict attr,
void *(*start_routine)(void *),
void *restrict arg);
— thread : Un pointeur vers une variable de type pthread_t qui sera utilisée pour
stocker l’ID du nouveau thread. Cet ID est opaque (vous ne pouvez pas directement
l’utiliser comme un entier simple).
— attr : Un pointeur vers une structure d’attributs de thread (pthread_attr_t).
Permet de spécifier des propriétés du thread comme la taille de la pile, la politique
d’ordonnancement, la priorité, etc. Si NULL, les attributs par défaut sont utilisés.
— start_routine : Un pointeur vers la fonction que le nouveau thread va exécuter.
Cette fonction doit prendre un argument de type void * et retourner un void *.
C’est le point d’entrée du nouveau thread.
— arg : Un pointeur générique (void *) vers un argument qui sera passé à la start_routine.
Si vous avez plusieurs arguments, vous pouvez les regrouper dans une structure et
passer un pointeur vers cette structure.
— Valeur de retour :
— Succès : 0.
— Échec : Un numéro d’erreur (ex : EAGAIN si trop de threads, ENOMEM si pas
assez de mémoire). Attention : Les fonctions pthreads retournent des numéros
d’erreur directement, elles ne définissent pas errno.
— Exemple de création de thread : Ce programme crée un thread qui affiche un
message, puis le thread principal attend la terminaison de ce nouveau thread.
108
Polytechnique Douala - Génie Logiciel N4 Programmation Système avec Linux et C
#include <pthread.h> // Pour les fonctions pthreads (pthread_create, pthread
#include <stdio.h> // Pour printf, fprintf, perror
#include <stdlib.h> // Pour EXIT_SUCCESS, EXIT_FAILURE
#include <unistd.h> // Pour sleep
// Structure pour passer plusieurs arguments au thread (si necessaire)
typedef struct {
int id;
char *message;
} ThreadArgs;
// Fonction qui sera executee par le nouveau thread
// Prend un argument de type void* et retourne un void*
void *ma_fonction_thread(void *arguments) {
ThreadArgs *args = (ThreadArgs *)arguments; // Cast de l’argument generi
printf("Thread %d: Debut d’execution. Message: ’%s’\n", args->id, args->
// Simuler un travail
sleep(2);
printf("Thread %d: Fin d’execution.\n", args->id);
// Le thread peut retourner une valeur (ici NULL)
return NULL;
}
int main() {
pthread_t mon_thread_id; // Variable pour stocker l’ID du nouveau thread
ThreadArgs args_pour_thread; // Arguments a passer au thread
args_pour_thread.id = 1;
args_pour_thread.message = "Bonjour depuis le thread!";
printf("Thread principal: Creation d’un nouveau thread...\n");
// Creation du thread
// Arguments:
// 1. &mon_thread_id: Pointeur vers l’ID du thread a creer
// 2. NULL: Attributs par defaut du thread
// 3. ma_fonction_thread: Fonction que le thread va executer
// 4. (void *)&args_pour_thread: Argument a passer a ma_fonction_thread
if (pthread_create(&mon_thread_id, NULL, ma_fonction_thread, (void *)&ar
fprintf(stderr, "Erreur de creation de thread\n");
return EXIT_FAILURE;
}
109
Programmation Système avec Linux et C Polytechnique Douala - Génie Logiciel N4
printf("Thread principal: Thread cree avec l’ID: %lu\n", (unsigned long)
printf("Thread principal: Je continue a faire mon travail en parallele..
sleep(1); // Simule un travail du thread principal
// Attendre la terminaison du thread nouvellement cree
printf("Thread principal: Attente de la terminaison du thread enfant...\
if (pthread_join(mon_thread_id, NULL) != 0) { // NULL signifie que nous
fprintf(stderr, "Erreur lors de l’attente du thread\n");
return EXIT_FAILURE;
}
printf("Thread principal: Thread enfant termine. Fin du programme.\n");
return EXIT_SUCCESS;
}
Pour compiler : ‘gcc -o thread_create_example thread_create_example.c -pthread‘
(L’option ‘-pthread‘ est essentielle pour lier la bibliothèque pthreads).
6.2.2. pthread_join()
La fonction pthread_join() est l’équivalent thread de waitpid() pour les processus.
Elle permet au thread appelant (généralement le thread principal) d’attendre la terminaison
d’un thread spécifique.
— Syntaxe :
#include <pthread.h> // Pour pthread_join
int pthread_join(pthread_t thread, void **retval);
— thread : L’ID du thread que le thread appelant doit attendre.
— retval : Un pointeur vers un pointeur void *. Si non NULL, la valeur de retour
du thread terminé sera stockée à l’adresse pointée par retval.
— Comportement :
— Le thread appelant est bloqué jusqu’à ce que le thread spécifié par thread se
termine.
— Lorsque pthread_join() retourne, toutes les ressources allouées au thread ter-
miné sont libérées.
— Un thread qui n’a pas été "joint" (c’est-à-dire que pthread_join() n’a pas été
appelé pour lui) reste dans un état "zombie" de thread jusqu’à ce que le pro-
cessus se termine. Bien que moins problématique que les zombies de processus
(car ils consomment moins de ressources), c’est une bonne pratique de toujours
joindre les threads, sauf si le thread est détaché.
— Valeur de retour :
— Succès : 0.
— Échec : Un numéro d’erreur (ex : EDEADLK si deadlock, ESRCH si thread non
trouvé).
— Exemple de retour de valeur avec pthread_join() :
110
Polytechnique Douala - Génie Logiciel N4 Programmation Système avec Linux et C
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
// Fonction de thread qui retourne une valeur
void *calculate_sum(void *arg) {
int *limit = (int *)arg;
int sum = 0;
for (int i = 1; i <= *limit; i++) {
sum += i;
}
printf("Thread de calcul: Somme de 1 a %d est %d\n", *limit, sum);
// Retourner la somme (doit etre cast en void*)
return (void *)(long)sum; // Cast en long pour eviter les avertissements
}
int main() {
pthread_t thread_id;
int upper_limit = 100;
void *thread_return_value; // Pointeur pour recuperer la valeur de retou
if (pthread_create(&thread_id, NULL, calculate_sum, (void *)&upper_limit
fprintf(stderr, "Erreur de creation de thread\n");
return EXIT_FAILURE;
}
printf("Thread principal: Attente de la fin du thread de calcul...\n");
// Attendre la terminaison du thread et recuperer sa valeur de retour
if (pthread_join(thread_id, &thread_return_value) != 0) {
fprintf(stderr, "Erreur lors de l’attente du thread\n");
return EXIT_FAILURE;
}
// Cast de la valeur de retour (doit etre cast en long puis en int)
int sum_from_thread = (int)(long)thread_return_value;
printf("Thread principal: Le thread de calcul a retourne la somme: %d\n"
return EXIT_SUCCESS;
}
111
Programmation Système avec Linux et C Polytechnique Douala - Génie Logiciel N4
6.2.3. pthread_exit()
La fonction pthread_exit() est utilisée pour terminer un thread en spécifiant une valeur
de retour. Lorsqu’un thread appelle pthread_exit(), il libère ses ressources spécifiques au
thread.
— Syntaxe :
#include <pthread.h> // Pour pthread_exit
void pthread_exit(void *retval);
— retval : La valeur de retour du thread. Cette valeur est disponible pour un autre
thread qui appelle pthread_join() sur ce thread.
— Comportement :
— Un thread se termine lorsque sa fonction start_routine retourne, ou lorsqu’il
appelle pthread_exit().
— Si le thread principal appelle exit() ou return de main(), cela terminera l’en-
semble du processus et tous ses threads. Pour que le thread principal se termine
sans affecter les autres threads (s’il y en a), il doit appeler pthread_exit().
6.2.4. pthread_self(), pthread_equal()
pthread_self()
Permet à un thread d’obtenir son propre identifiant.
— Syntaxe :
#include <pthread.h> // Pour pthread_self
pthread_t pthread_self(void);
— Valeur de retour : L’ID du thread appelant.
pthread_equal()
Compare deux identifiants de thread. Les identifiants de thread sont des types opaques
et ne doivent pas être comparés directement avec ==.
— Syntaxe :
#include <pthread.h> // Pour pthread_equal
int pthread_equal(pthread_t t1, pthread_t t2);
— t1, t2 : Les deux identifiants de thread à comparer.
— Valeur de retour : Non-zéro si les IDs sont égaux, 0 sinon.
— Exemple de pthread_self() et pthread_equal() :
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
void *my_thread_func(void *arg) {
112
Polytechnique Douala - Génie Logiciel N4 Programmation Système avec Linux et C
pthread_t self_id = pthread_self();
printf("Thread enfant: Mon ID est %lu\n", (unsigned long)self_id);
sleep(1);
return NULL;
}
int main() {
pthread_t main_thread_id = pthread_self(); // ID du thread principal
pthread_t child_thread_id;
printf("Thread principal: Mon ID est %lu\n", (unsigned long)main_thread_
if (pthread_create(&child_thread_id, NULL, my_thread_func, NULL) != 0) {
fprintf(stderr, "Erreur de creation de thread\n");
return EXIT_FAILURE;
}
// Comparer le thread principal avec le thread enfant
if (pthread_equal(main_thread_id, child_thread_id)) {
printf("Les IDs du thread principal et de l’enfant sont egaux (ce qu
} else {
printf("Les IDs du thread principal et de l’enfant sont differents.\
}
// Attendre l’enfant
pthread_join(child_thread_id, NULL);
return EXIT_SUCCESS;
}
Exercice 6.1 :
Création et Gestion de Threads
[label=()]Écrivez un programme qui crée 5 threads. Chaque thread doit afficher
son propre ID (obtenu via pthread_self()) et un message comme "Salut du
thread X !". Le thread principal doit attendre la terminaison de tous les 5 threads
avant de se terminer lui-même. Modifiez l’exercice précédent. Chaque thread créé
doit recevoir un entier comme argument (son "numéro d’ordre" de 1 à 5). Le
thread doit calculer la somme des entiers de 1 à son numéro d’ordre et retour-
ner cette somme. Le thread principal doit récupérer et afficher la somme re-
tournée par chaque thread. Implémentez un programme où le thread principal
crée un thread "travailleur" qui entre dans une boucle infinie. Le thread prin-
cipal doit dormir 3 secondes, puis "annuler" (cancel) le thread travailleur (uti-
lisez pthread_cancel()). Le travailleur doit pouvoir faire un peu de nettoyage
avant de se terminer (voir pthread_cleanup_push()/pthread_cleanup_pop()).
Attention : L’annulation de threads est complexe et doit être utilisée
113
Programmation Système avec Linux et C Polytechnique Douala - Génie Logiciel N4
avec prudence. Pour cet exercice, une gestion basique suffira.
Correction Exercice 6.1, partie (b) - Retour de valeur de threads
3.
1.
2. #include <pthread.h> // Pour pthreads
#include <stdio.h> // Pour printf, fprintf
#include <stdlib.h> // Pour malloc, free, EXIT_SUCCESS, EXIT_FAILURE
// Structure pour passer le numero d’ordre au thread
typedef struct {
int order_num;
} ThreadData;
// Fonction de thread qui calcule la somme et la retourne
void *calculate-sum-thread(void *arg) {
ThreadData *data = (ThreadData *)arg;
int sum = 0;
for (int i = 1; i <= data->order-num; i++) {
sum += i;
}
printf("Thread %d: Somme calculee (1 a %d) = %d\n", data->order-num, data->orde
// Retourne la somme.
// Cast en (void*)(long) est necessaire pour eviter les avertissements
// sur la conversion d’un int en pointeur, et assure la portabilite sur les sys
return (void *)(long)sum;
}
int main() {
pthread_t threads[5]; // Tableau pour stocker les IDs des threads
ThreadData *thread-args[5]; // Tableau pour stocker les pointeurs vers les argu
int i;
int return_sum; // Pour recuperer la somme de chaque thread
printf("Thread principal: Creation de 5 threads...\n");
for (i = 0; i < 5; i++) {
// Allouer dynamiquement la structure ThreadData pour chaque thread
// afin que chaque thread ait ses propres donnees
thread-args[i] = (ThreadData *)malloc(sizeof(ThreadData));
if (thread-args[i] == NULL) {
perror("malloc");
// Liberer la memoire deja allouee si erreur
for (int j = 0; j < i; j++) free(thread-args[j]);
return EXIT_FAILURE;
}
114
Polytechnique Douala - Génie Logiciel N4 Programmation Système avec Linux et C
thread-args[i]->order-num = i + 1; // Numero d’ordre de 1 a 5
// Creer le thread
if (pthread_create(&threads[i], NULL, calculate-sum-thread, (void *)thread-
fprintf(stderr, "Erreur de creation de thread %d\n", i + 1);
free(thread-args[i]); // Liberer cet argument en cas d’echec
// Liberer la memoire deja allouee si erreur
for (int j = 0; j < i; j++) free(thread-args[j]);
return EXIT_FAILURE;
}
}
printf("Thread principal: Attente de la terminaison des threads...\n");
for (i = 0; i < 5; i++) {
void *thread_return_value; // Pointeur pour recuperer la valeur de retour
// Attendre la terminaison de chaque thread et recuperer sa valeur
if (pthread_join(threads[i], &thread_return_value) != 0) {
fprintf(stderr, "Erreur lors de l’attente du thread %d\n", i + 1);
free(thread-args[i]); // Liberer l’argument du thread
return EXIT_FAILURE;
}
// Recuperer la somme retournee par le thread
return_sum = (int)(long)thread_return_value; // Cast inverse
printf("Thread principal: Le thread %d a retourne la somme: %d\n", i + 1, r
free(thread-args[i]); // Liberer la memoire allouee pour les arguments du t
}
printf("Thread principal: Tous les threads sont termines. Fin du programme.\n")
return EXIT_SUCCESS;
}
6.3. Problèmes de Concurrence
La programmation multi-threadée, bien que puissante, introduit des défis importants liés
à l’accès concurrent aux ressources partagées. Les deux problèmes les plus courants sont les
conditions de course et les interblocages.
115
Programmation Système avec Linux et C Polytechnique Douala - Génie Logiciel N4
6.3.1. Conditions de Course (Race Conditions)
Une **condition de course** se produit lorsque deux ou plusieurs threads tentent d’ac-
céder et de modifier une même ressource partagée (par exemple, une variable globale, un
fichier, un segment de mémoire partagée) de manière concurrente, et que le résultat final
de l’opération dépend de l’ordre non déterministe dans lequel les threads accèdent à cette
ressource.
— Scénario typique : Une opération qui devrait être atomique (indivisible) est
divisée en plusieurs sous-opérations (lecture, modification, écriture) qui peuvent
être interrompues par l’ordonnanceur du CPU.
— Exemple : Deux threads incrémentent une variable globale ‘compteur‘ qui est
initialisée à 0. Chaque thread exécute ‘compteur++‘ 100 000 fois. On s’attend à
ce que la valeur finale soit 200 000. Cependant, ‘compteur++‘ est généralement
implémenté en trois étapes :
1. Lire la valeur actuelle de ‘compteur‘ en mémoire.
2. Incrémenter cette valeur.
3. Écrire la nouvelle valeur dans ‘compteur‘ en mémoire.
Si le thread A lit ‘compteur‘ (valeur 100), puis est interrompu avant d’écrire, et
que le thread B lit ‘compteur‘ (toujours 100), l’incrémente à 101 et écrit 101, puis
le thread A reprend son exécution (il a toujours 100 en mémoire), l’incrémente à
101 et écrit 101, alors une incrémentation a été perdue. Le résultat est incorrect.
— Exemple de condition de course : Ce programme démontre une condition de course
sur une variable globale sans protection. Le résultat final de ‘compteur‘ sera presque
certainement inférieur à 200 000.
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
// Variable globale partagee par les threads
int compteur = 0;
// Fonction de thread qui incremente le compteur
void *increment_counter(void *arg) {
for (int i = 0; i < 100000; i++) {
compteur++; // Operation non atomique, sujet a condition de course
}
return NULL;
}
int main() {
pthread_t t1, t2;
printf("Valeur initiale du compteur: %d\n", compteur);
116
Polytechnique Douala - Génie Logiciel N4 Programmation Système avec Linux et C
// Creation de deux threads pour incrementer le compteur
if (pthread_create(&t1, NULL, increment_counter, NULL) != 0) {
fprintf(stderr, "Erreur de creation thread 1\n");
return EXIT_FAILURE;
}
if (pthread_create(&t2, NULL, increment_counter, NULL) != 0) {
fprintf(stderr, "Erreur de creation thread 2\n");
pthread_join(t1, NULL); // S’assurer que t1 ne reste pas en l’air
return EXIT_FAILURE;
}
// Attente des deux threads
pthread_join(t1, NULL);
pthread_join(t2, NULL);
printf("Valeur finale du compteur (attendu 200000): %d\n", compteur);
// Le resultat sera probablement different de 200000
return EXIT_SUCCESS;
}
Pour résoudre les conditions de course, des mécanismes de **synchronisation** sont néces-
saires pour garantir l’accès exclusif aux ressources partagées (sections critiques).
6.3.2. Interblocages (Deadlocks)
Un **interblocage** (ou *deadlock*) est une situation dans laquelle deux ou plusieurs
threads (ou processus) sont bloqués indéfiniment, chacun attendant une ressource qui est
détenue par un autre thread du même ensemble bloqué.
Les quatre conditions de Coffman pour qu’un interblocage se produise sont :
1. Exclusion Mutuelle : Au moins une ressource doit être non partageable (c’est-
à-dire qu’un seul thread peut l’utiliser à la fois).
2. Maintien et Attente (Hold and Wait) : Un thread qui détient déjà au moins
une ressource demande une nouvelle ressource qui est actuellement détenue par un
autre thread.
3. Non-Préemption : Une ressource ne peut être libérée par son détenteur qu’après
avoir accompli sa tâche ; elle ne peut pas être préemptée de force par un autre
thread.
4. Attente Circulaire (Circular Wait) : Il existe une chaîne de threads où chaque
thread attend une ressource détenue par le thread suivant dans la chaîne, et le
dernier thread de la chaîne attend une ressource détenue par le premier thread.
Pour éviter un interblocage, il faut empêcher qu’au moins une de ces quatre conditions ne
soit satisfaite.
— Exemple d’interblocage simple : Ce programme démontre un interblocage classique
où deux threads tentent d’acquérir deux mutex dans des ordres différents, menant
117
Programmation Système avec Linux et C Polytechnique Douala - Génie Logiciel N4
à un blocage mutuel.
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h> // Pour sleep
pthread_mutex_t mutex1 = PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_t mutex2 = PTHREAD_MUTEX_INITIALIZER;
void *thread_function1(void *arg) {
printf("Thread 1: Tente d’acquerir mutex1...\n");
pthread_mutex_lock(&mutex1);
printf("Thread 1: Mutex1 acquis. Tente d’acquerir mutex2...\n");
sleep(1); // Simule un travail ou delai
pthread_mutex_lock(&mutex2); // Se bloque ici si Thread 2 detient mutex2
printf("Thread 1: Mutex2 acquis. Section critique executee.\n");
pthread_mutex_unlock(&mutex2);
pthread_mutex_unlock(&mutex1);
printf("Thread 1: Mutexes liberes. Termine.\n");
return NULL;
}
void *thread_function2(void *arg) {
printf("Thread 2: Tente d’acquerir mutex2...\n");
pthread_mutex_lock(&mutex2);
printf("Thread 2: Mutex2 acquis. Tente d’acquerir mutex1...\n");
sleep(1); // Simule un travail ou delai
pthread_mutex_lock(&mutex1); // Se bloque ici si Thread 1 detient mutex1
printf("Thread 2: Mutex1 acquis. Section critique executee.\n");
pthread_mutex_unlock(&mutex1);
pthread_mutex_unlock(&mutex2);
printf("Thread 2: Mutexes liberes. Termine.\n");
return NULL;
}
int main() {
pthread_t t1, t2;
printf("Main: Creation des threads...\n");
pthread_create(&t1, NULL, thread_function1, NULL);
pthread_create(&t2, NULL, thread_function2, NULL);
118
Polytechnique Douala - Génie Logiciel N4 Programmation Système avec Linux et C
pthread_join(t1, NULL);
pthread_join(t2, NULL);
printf("Main: Les threads ont termine (ou se sont interbloques).\n");
pthread_mutex_destroy(&mutex1);
pthread_mutex_destroy(&mutex2);
return EXIT_SUCCESS;
}
Lors de l’exécution, vous verrez probablement les messages des deux threads indi-
quant qu’ils ont acquis un mutex, puis se bloquent en essayant d’acquérir l’autre,
sans jamais atteindre la fin. La solution la plus simple ici est de toujours acquérir
les mutex dans le même ordre.
Exercice 6.2 :
Problèmes de Concurrence
[label=()]Analysez l’exemple de condition de course donné. Expliquez en détail
pourquoi la valeur finale du compteur n’est pas toujours correcte en décrivant le
déroulement possible des instructions de bas niveau des deux threads. Modifiez
l’exemple d’interblocage pour résoudre le problème. Expliquez quelle condition de
Coffman vous avez empêchée et comment votre modification garantit l’absence
d’interblocage dans ce cas.
6.4. Synchronisation des Threads
Pour prévenir les conditions de course et gérer les interactions entre threads, pthreads
fournit divers mécanismes de synchronisation.
6.4.1. Mutex (Exclusion Mutuelle)
Un **mutex** (pour MUTual EXclusion) est le mécanisme de synchronisation le plus
fondamental. C’est un verrou binaire qui protège une **section critique** de code, garan-
tissant que seule une seule thread peut exécuter cette section à la fois. Si une thread tente
d’acquérir un mutex déjà verrouillé, elle est bloquée jusqu’à ce que le mutex soit libéré.
—2. Cycle de vie d’un mutex :
1.
1. Initialisation : Le mutex doit être initialisé avant utilisation.
2. Verrouillage (lock) : Une thread acquiert le verrou sur le mutex.
3. Déverrouillage (unlock) : La thread libère le verrou sur le mutex.
4. Destruction : Le mutex doit être détruit après utilisation pour libérer ses
ressources.
— Règles d’or :
119
Programmation Système avec Linux et C Polytechnique Douala - Génie Logiciel N4
— Chaque ressource partagée doit être protégée par un mutex.
— Une thread doit verrouiller le mutex avant d’accéder à la ressource partagée et
le déverrouiller après.
— Ne jamais quitter une section critique sans libérer le mutex.
pthread_mutex_init(), pthread_mutex_lock(), pthread_mutex_unlock(), pthread_mutex_destroy()
— pthread_mutex_init() : Initialise un mutex.
#include <pthread.h>
int pthread_mutex_init(pthread_mutex_t *restrict mutex,
const pthread_mutexattr_t *restrict attr);
— mutex : Pointeur vers le mutex à initialiser.
— attr : Pointeur vers les attributs du mutex (souvent NULL pour les attributs
par défaut).
Initialisation statique : Vous pouvez aussi initialiser un mutex statiquement :
pthread_mutex_t my_mutex = PTHREAD_MUTEX_INITIALIZER;
— pthread_mutex_lock() : Verrouille le mutex. Si le mutex est déjà verrouillé par
une autre thread, la thread appelante est bloquée jusqu’à ce que le mutex soit
libéré.
#include <pthread.h>
int pthread_mutex_lock(pthread_mutex_t *mutex);
— pthread_mutex_unlock() : Déverrouille le mutex, le rendant disponible pour
d’autres threads. Si des threads étaient bloquées en attente sur ce mutex, l’une
d’elles est débloquée.
#include <pthread.h>
int pthread_mutex_unlock(pthread_mutex_t *mutex);
— pthread_mutex_destroy() : Détruit un mutex, libérant les ressources qu’il utilise.
Il ne doit être appelé que sur un mutex non verrouillé.
#include <pthread.h>
int pthread_mutex_destroy(pthread_mutex_t *mutex);
Toutes ces fonctions retournent 0 en cas de succès, un numéro d’erreur en cas
d’échec.
— Exemple de mutex (résolution de la condition de course) : Ce programme reprend
l’exemple de l’incrémentation du compteur, mais utilise un mutex pour protéger
l’accès à la variable partagée, garantissant ainsi un résultat correct.
#include <pthread.h> // Pour pthreads, mutex
#include <stdio.h> // Pour printf, fprintf
#include <stdlib.h> // Pour EXIT_FAILURE, EXIT_SUCCESS
// Variable globale partagee
int compteur = 0;
120
Polytechnique Douala - Génie Logiciel N4 Programmation Système avec Linux et C
// Declaration et initialisation statique du mutex
pthread_mutex_t mon_mutex = PTHREAD_MUTEX_INITIALIZER;
// Fonction de thread qui incremente le compteur de maniere securisee
void *increment_safe(void *arg) {
for (int i = 0; i < 100000; i++) {
// Verrouiller le mutex avant d’entrer dans la section critique
pthread_mutex_lock(&mon_mutex);
// --- SECTION CRITIQUE ---
compteur++; // Cette operation est maintenant protegee
// --- FIN SECTION CRITIQUE ---
// Deverrouiller le mutex apres la section critique
pthread_mutex_unlock(&mon_mutex);
}
return NULL;
}
int main() {
pthread_t t1, t2;
printf("Valeur initiale du compteur: %d\n", compteur);
// Creation des threads
if (pthread_create(&t1, NULL, increment_safe, NULL) != 0) {
fprintf(stderr, "Erreur de creation thread 1\n");
return EXIT_FAILURE;
}
if (pthread_create(&t2, NULL, increment_safe, NULL) != 0) {
fprintf(stderr, "Erreur de creation thread 2\n");
pthread_join(t1, NULL); // S’assurer que t1 ne reste pas en l’air
return EXIT_FAILURE;
}
// Attente des threads
pthread_join(t1, NULL);
pthread_join(t2, NULL);
printf("Valeur finale du compteur (attendu 200000): %d\n", compteur);
// Le resultat sera maintenant 200000
// Detruire le mutex apres utilisation
pthread_mutex_destroy(&mon_mutex);
return EXIT_SUCCESS;
}
121
Programmation Système avec Linux et C Polytechnique Douala - Génie Logiciel N4
6.4.2. Variables de Condition
Les **variables de condition** sont un mécanisme de synchronisation plus avancé, utilisé
pour permettre aux threads d’attendre la satisfaction d’une condition spécifique. Elles sont
toujours utilisées en conjonction avec un mutex.
— Scénario d’utilisation : Une thread a besoin d’accéder à une ressource, mais
seulement si une certaine condition est vraie (par exemple, un buffer n’est pas vide
pour un consommateur, ou un buffer n’est pas plein pour un producteur). Au lieu
de "boucler" (busy-waiting) en vérifiant constamment la condition (ce qui gaspille
du CPU), la thread peut se mettre en attente sur une variable de condition jusqu’à
ce qu’une autre thread signale que la condition est potentiellement vraie.
— Relation avec les mutex : Une thread doit d’abord verrouiller un mutex avant
de vérifier la condition ou de se mettre en attente sur la variable de condition. Le
mutex est libéré atomiquement pendant l’attente et ré-acquis avant de reprendre
l’exécution.
pthread_cond_init(), pthread_cond_wait(), pthread_cond_signal(), pthread_cond_broadcast(),
pthread_cond_destroy()
— pthread_cond_init() : Initialise une variable de condition.
#include <pthread.h>
int pthread_cond_init(pthread_cond_t *restrict cond,
const pthread_condattr_t *restrict attr);
Initialisation statique : pthread_cond_t my_cond = PTHREAD_COND_INITIALIZER;
— pthread_cond_wait() : Libère atomiquement le mutex et bloque la thread appe-
lante jusqu’à ce que la variable de condition soit signalée. Lorsque la thread est
réveillée, elle ré-acquiert le mutex avant de retourner.
#include <pthread.h>
int pthread_cond_wait(pthread_cond_t *restrict cond,
pthread_mutex_t *restrict mutex);
— pthread_cond_signal() : Réveille au moins une thread qui attend sur la variable
de condition. Si aucune thread n’attend, le signal est perdu.
#include <pthread.h>
int pthread_cond_signal(pthread_cond_t *cond);
— pthread_cond_broadcast() : Réveille toutes les threads qui attendent sur la va-
riable de condition.
#include <pthread.h>
int pthread_cond_broadcast(pthread_cond_t *cond);
— pthread_cond_destroy() : Détruit une variable de condition.
#include <pthread.h>
int pthread_cond_destroy(pthread_cond_t *cond);
122
Polytechnique Douala - Génie Logiciel N4 Programmation Système avec Linux et C
Toutes ces fonctions retournent 0 en cas de succès, un numéro d’erreur en cas
d’échec.
— Exemple de producteur-consommateur avec variables de condition : Ce programme
simple illustre l’utilisation des variables de condition pour synchroniser un produc-
teur et un consommateur qui partagent un buffer limité.
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h> // Pour sleep
#define BUFFER_SIZE 5 // Taille du buffer partage
int buffer[BUFFER_SIZE];
int count = 0; // Nombre d’elements dans le buffer
int in = 0; // Index d’insertion
int out = 0; // Index de lecture
// Mutex pour proteger l’acces au buffer et a la variable ’count’
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
// Variables de condition pour signaler si le buffer est plein ou vide
pthread_cond_t not_full = PTHREAD_COND_INITIALIZER; // Signale quand le buf
pthread_cond_t not_empty = PTHREAD_COND_INITIALIZER; // Signale quand le buf
// Fonction du producteur
void *producer(void *arg) {
int item;
for (int i = 0; i < 10; i++) {
item = i + 1; // Produire un element
pthread_mutex_lock(&mutex); // Verrouiller l’acces au buffer
// Tant que le buffer est plein, attendre
while (count == BUFFER_SIZE) {
printf("Producteur: Buffer plein, attente...\n");
pthread_cond_wait(¬_full, &mutex); // Relache mutex et attend
}
// Ajouter l’element au buffer
buffer[in] = item;
in = (in + 1) % BUFFER_SIZE;
count++;
printf("Producteur: Produit %d. Elements dans buffer: %d\n", item, c
pthread_cond_signal(¬_empty); // Signaler au consommateur que le
pthread_mutex_unlock(&mutex); // Deverrouiller le mutex
123
Programmation Système avec Linux et C Polytechnique Douala - Génie Logiciel N4
sleep(1); // Simuler un delai de production
}
return NULL;
}
// Fonction du consommateur
void *consumer(void *arg) {
int item;
for (int i = 0; i < 10; i++) {
pthread_mutex_lock(&mutex); // Verrouiller l’acces au buffer
// Tant que le buffer est vide, attendre
while (count == 0) {
printf("Consommateur: Buffer vide, attente...\n");
pthread_cond_wait(¬_empty, &mutex); // Relache mutex et atten
}
// Consommer l’element du buffer
item = buffer[out];
out = (out + 1) % BUFFER_SIZE;
count--;
printf("Consommateur: Consomme %d. Elements dans buffer: %d\n", item
pthread_cond_signal(¬_full); // Signaler au producteur que le buf
pthread_mutex_unlock(&mutex); // Deverrouiller le mutex
sleep(2); // Simuler un delai de consommation
}
return NULL;
}
int main() {
pthread_t prod_thread, cons_thread;
printf("Main: Demarrage du systeme producteur-consommateur.\n");
// Creation des threads producteur et consommateur
pthread_create(&prod_thread, NULL, producer, NULL);
pthread_create(&cons_thread, NULL, consumer, NULL);
// Attendre la terminaison des threads
pthread_join(prod_thread, NULL);
pthread_join(cons_thread, NULL);
124
Polytechnique Douala - Génie Logiciel N4 Programmation Système avec Linux et C
printf("Main: Tous les threads sont termines.\n");
// Nettoyage des mutex et variables de condition
pthread_mutex_destroy(&mutex);
pthread_cond_destroy(¬_full);
pthread_cond_destroy(¬_empty);
return EXIT_SUCCESS;
}
6.4.3. Sémaphores POSIX
Les sémaphores POSIX (par opposition aux sémaphores System V vus au Chapitre 4)
sont une autre forme de sémaphores qui peuvent être utilisés pour la synchronisation entre
threads ou entre processus. Ils sont plus flexibles et souvent plus simples à utiliser que leurs
homologues System V pour de nombreux cas.
— Types de sémaphores POSIX :
— Sémaphores nommés : Identifiés par un nom (chemin). Ils peuvent être uti-
lisés pour la synchronisation entre processus non liés.
— Sémaphores non nommés : Résident en mémoire partagée. Ils peuvent être
utilisés pour la synchronisation entre threads au sein du même processus
(quand ils sont alloués sur le tas) ou entre processus liés via mémoire partagée
(quand ils sont alloués dans un segment de mémoire partagée).
sem_open(), sem_close(), sem_unlink(), sem_wait(), sem_post()
Ces fonctions sont pour les **sémaphores nommés POSIX**.
— sem_open() : Ouvre (ou crée) un sémaphore nommé.
#include <semaphore.h> // Pour sem_open, sem_t
sem_t *sem_open(const char *name, int oflag, ...);
— name : Le nom du sémaphore (doit commencer par ’/’).
— oflag : Drapeaux (ex : O_CREAT, O_EXCL).
— ... : Arguments optionnels (mode_t mode, unsigned int value) pour O_CREAT.
Valeur de retour : Pointeur sem_t * en cas de succès, SEM_FAILED en cas d’échec.
— sem_wait() : Décrémente (opérateur P) la valeur du sémaphore. Bloque si la valeur
est 0.
#include <semaphore.h>
int sem_wait(sem_t *sem);
— sem_post() : Incrémente (opérateur V) la valeur du sémaphore. Réveille une
thread bloquée si la valeur était 0.
#include <semaphore.h>
int sem_post(sem_t *sem);
125
Programmation Système avec Linux et C Polytechnique Douala - Génie Logiciel N4
— sem_close() : Ferme un sémaphore ouvert avec sem_open().
#include <semaphore.h>
int sem_close(sem_t *sem);
— sem_unlink() : Supprime un sémaphore nommé du système. Il reste actif tant
qu’il y a des sem_open() actifs, puis est supprimé.
#include <semaphore.h>
int sem_unlink(const char *name);
Toutes ces fonctions retournent 0 en cas de succès, -1 en cas d’échec (et définissent
errno).
— Exemple de sémaphore POSIX nommé (pour la synchronisation entre processus) :
Ce programme (à compiler en deux exécutables séparés) utilise un sémaphore
nommé pour synchroniser l’accès à une ressource critique.
// Programme 1: posix_sem_writer.c (Producteur)
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <semaphore.h> // Pour les semaphores POSIX
#include <fcntl.h> // Pour les drapeaux O_CREAT
#include <errno.h> // Pour errno
#define SEM_NAME "/my-posix-semaphore" // Nom du semaphore POSIX
int main() {
sem_t *sem;
// Ouvrir (ou creer) le semaphore
// 0666: permissions rw-rw-rw-
// 1: valeur initiale du semaphore (mutex binaire)
sem = sem_open(SEM_NAME, O_CREAT, 0666, 1);
if (sem == SEM_FAILED) {
perror("sem_open");
return EXIT_FAILURE;
}
printf("Ecrivain: Semaphore ’%s’ ouvert/cree.\n", SEM_NAME);
for (int i = 0; i < 3; i++) {
printf("Ecrivain: Tente d’acquerir le semaphore...\n");
if (sem_wait(sem) == -1) { // Operation P
perror("sem_wait");
sem_close(sem);
sem_unlink(SEM_NAME); // Nettoyage en cas d’erreur
return EXIT_FAILURE;
126
Polytechnique Douala - Génie Logiciel N4 Programmation Système avec Linux et C
}
printf("Ecrivain: Semaphore acquis. Ecriture dans la ressource criti
// Simuler un travail dans la section critique
sleep(2);
printf("Ecrivain: Liberation du semaphore.\n");
if (sem_post(sem) == -1) { // Operation V
perror("sem_post");
sem_close(sem);
sem_unlink(SEM_NAME);
return EXIT_FAILURE;
}
sleep(1); // Delai entre les tentatives d’acquisition
}
printf("Ecrivain: Fermeture du semaphore.\n");
if (sem_close(sem) == -1) {
perror("sem_close");
return EXIT_FAILURE;
}
printf("Ecrivain: Programme termine. Supprime le semaphore du systeme.\n
if (sem_unlink(SEM_NAME) == -1) { // Supprime le semaphore du systeme
perror("sem_unlink");
return EXIT_FAILURE;
}
return EXIT_SUCCESS;
}
// Programme 2: posix_sem_reader.c (Consommateur)
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <semaphore.h> // Pour les semaphores POSIX
#include <fcntl.h> // Pour les drapeaux O_CREAT
#include <errno.h> // Pour errno
#define SEM_NAME "/my-posix-semaphore" // Nom du semaphore POSIX
int main() {
sem_t *sem;
// Ouvrir le semaphore (ne pas le creer, on s’attend a ce qu’il existe)
sem = sem_open(SEM_NAME, 0); // Le 0 comme deuxieme argument signifie qu
if (sem == SEM_FAILED) {
perror("sem_open");
127
Programmation Système avec Linux et C Polytechnique Douala - Génie Logiciel N4
fprintf(stderr, "Assurez-vous que l’ecrivain est execute en premier
return EXIT_FAILURE;
}
printf("Lecteur: Semaphore ’%s’ ouvert.\n", SEM_NAME);
for (int i = 0; i < 3; i++) {
printf("Lecteur: Tente d’acquerir le semaphore...\n");
if (sem_wait(sem) == -1) { // Operation P
perror("sem_wait");
sem_close(sem);
return EXIT_FAILURE;
}
printf("Lecteur: Semaphore acquis. Lecture de la ressource critique
// Simuler un travail dans la section critique
sleep(1);
printf("Lecteur: Liberation du semaphore.\n");
if (sem_post(sem) == -1) { // Operation V
perror("sem_post");
sem_close(sem);
return EXIT_FAILURE;
}
sleep(2); // Delai entre les tentatives d’acquisition
}
printf("Lecteur: Fermeture du semaphore.\n");
if (sem_close(sem) == -1) {
perror("sem_close");
return EXIT_FAILURE;
}
printf("Lecteur: Programme termine.\n");
return EXIT_SUCCESS;
}
Pour compiler : ‘gcc posix-sem-writer.c -o writer -pthread‘ et ‘gcc posix-sem-
reader.c -o reader -pthread‘ Pour exécuter : Lancez ‘./writer‘ et ‘./reader‘ dans
des terminaux séparés. Observez comment ils se synchronisent pour accéder à la
"ressource critique".
Exercice 6.3 :
Synchronisation Avancée
[label=()]Reprenez l’Exercice 4.2 (b) (tableau noir partagé entre processus) et
implémentez une synchronisation robuste en utilisant des **sémaphores POSIX
nommés** pour garantir que :
1. — Seul un processus à la fois peut écrire sur le tableau noir.
— Les lecteurs attendent qu’un nouveau message soit disponible avant de lire.
128
Polytechnique Douala - Génie Logiciel N4 Programmation Système avec Linux et C
— Les écrivains attendent que le tableau noir soit "vide" (lu) avant d’écrire un
nouveau message.
Vous aurez probablement besoin de plus d’un sémaphore.
2. Implémentez un problème des "lecteurs-rédacteurs" simple en utilisant des mutex
et des variables de condition. Les rédacteurs peuvent écrire un à la fois, tandis que
plusieurs lecteurs peuvent lire simultanément.
129
Programmation Système avec Linux et C Polytechnique Douala - Génie Logiciel N4
130
Chapitre 7
Programmation Réseau avec Sockets
Ce chapitre vous initie à la programmation réseau en C sous Linux, en se concentrant sur
l’API des sockets Berkeley, la méthode la plus courante pour la communication réseau de bas
niveau. Nous commencerons par les concepts fondamentaux du réseau (modèle client-serveur,
IP, ports, TCP/UDP). Ensuite, nous explorerons en détail la création et la gestion des sockets
TCP pour les applications client et serveur, y compris les appels système pour l’établisse-
ment de connexions, l’envoi et la réception de données. Enfin, nous aborderons brièvement
les sockets UDP et les techniques pour gérer plusieurs clients de manière concurrente. Com-
prendre la programmation réseau est essentiel pour développer des applications distribuées,
des services web et des outils de communication.
7.1. Concepts Fondamentaux du Réseau
Pour programmer des applications réseau, il est essentiel de comprendre les principes
sous-jacents de la communication sur un réseau IP (Internet Protocol).
7.1.1. Modèle Client-Serveur
Le **modèle client-serveur** est l’architecture la plus répandue pour les applications
réseau. Il repose sur deux types d’entités :
— Serveur : Un processus qui attend les requêtes de clients. Il offre un service et
écoute sur un port réseau spécifique. Une fois une requête reçue, il la traite et envoie
une réponse au client. Un serveur est généralement toujours en cours d’exécution
et gère plusieurs clients simultanément.
— Client : Un processus qui initie une connexion à un serveur pour demander un
service. Il envoie une requête au serveur, attend la réponse, puis ferme générale-
ment la connexion ou continue d’interagir. Les clients sont actifs et initiateurs de
communication.
Exemples : navigateur web (client) et serveur web (serveur), client de messagerie instantanée
et serveur de messagerie.
131
Programmation Système avec Linux et C Polytechnique Douala - Génie Logiciel N4
7.1.2. Adresses IP et Noms de Domaine
— Adresse IP (Internet Protocol) : Une adresse IP est un identifiant numérique
unique attribué à chaque appareil connecté à un réseau utilisant le protocole In-
ternet. Elle permet d’identifier l’appareil sur le réseau et de diriger les paquets de
données vers la bonne destination.
— IPv4 : Adresses de 32 bits, généralement représentées par quatre nombres
décimaux séparés par des points (ex : 192.168.1.1).
— IPv6 : Adresses de 128 bits, représentées par des groupes de chiffres hexadéci-
maux (ex : 2001:0db8:85a3:0000:0000:8a2e:0370:7334).
— Nom de Domaine (Domain Name) : Un nom de domaine est un identifiant
alphabétique (ex : google.com, polytechnique-douala.cm) qui est plus facile à
retenir pour les humains qu’une adresse IP numérique. Le **DNS (Domain Name
System)** est un service qui traduit les noms de domaine en adresses IP corres-
pondantes.
7.1.3. Ports
Un **port** est un numéro qui identifie une application ou un service spécifique s’exécu-
tant sur un appareil avec une adresse IP donnée. Alors que l’adresse IP identifie la machine,
le port identifie le processus sur cette machine qui écoute ou envoie des données.
— Les ports sont des nombres entiers sur 16 bits, de 0 à 65535.
— Ports bien connus (0-1023) : Réservés pour des services standard (ex : HTTP
80, HTTPS 443, FTP 21, SSH 22). Nécessitent des privilèges root pour l’écoute.
— Ports enregistrés (1024-49151) : Peuvent être utilisés par des applications
utilisateur.
— Ports dynamiques/privés (49152-65535) : Utilisés pour les connexions sor-
tantes des clients, attribués dynamiquement.
Une combinaison Adresse IP:Port forme une **socket** (au sens abstrait, pas l’API).
7.1.4. Protocoles TCP et UDP
Les deux protocoles de transport les plus courants sur Internet sont TCP et UDP.
— TCP (Transmission Control Protocol) :
— Orienté connexion : Établit une connexion bidirectionnelle et fiable entre
deux points avant d’envoyer des données.
— Fiable : Garantit la livraison des données, l’ordre des paquets, et gère la re-
transmission des paquets perdus. Gère aussi le contrôle de flux et la congestion.
— Basé sur un flux d’octets : Les données sont transmises comme un flux
continu d’octets sans limites de message.
— Utilisation : Applications nécessitant une fiabilité élevée (Web, email, transfert
de fichiers, SSH).
— UDP (User Datagram Protocol) :
— Sans connexion : N’établit pas de connexion préalable. Les données sont
envoyées sous forme de datagrammes indépendants.
132
Polytechnique Douala - Génie Logiciel N4 Programmation Système avec Linux et C
— Non fiable : Ne garantit ni la livraison, ni l’ordre, ni la détection de duplica-
tion. Pas de contrôle de flux ou de congestion.
— Basé sur des datagrammes : Chaque message est une unité autonome, avec
sa propre adresse de destination.
— Utilisation : Applications où la vitesse est plus critique que la fiabilité, ou
où l’application gère sa propre fiabilité (streaming vidéo/audio, jeux en ligne,
DNS).
Exercice 7.1 :
Concepts Réseau
[label=()]Décrivez les étapes qu’un client DNS réalise pour traduire un nom de
domaine en adresse IP. Pourquoi un serveur web utilise-t-il TCP plutôt qu’UDP ?
Donnez au moins deux raisons. Sur votre machine Linux, utilisez la commande
netstat -tuln et expliquez ce que vous observez en termes de protocoles (TCP/UDP)
et de ports en écoute.
7.2. API Sockets (Berkeley Sockets)
L’API Sockets de Berkeley est la bibliothèque standard pour la programmation réseau
sous UNIX et Linux. Elle fournit un ensemble de fonctions pour créer et manipuler des points
de terminaison de communication appelés sockets.
7.2.1. Descripteurs de Sockets
Tout comme les fichiers, les sockets sont gérés par le noyau via des **descripteurs de
fichiers** (File Descriptors - FD). Lorsqu’un socket est créé, l’appel système socket() renvoie
un entier, qui est un descripteur de fichier. Toutes les opérations ultérieures sur ce socket
(lecture, écriture, acceptation de connexions, etc.) utilisent ce descripteur. Les descripteurs de
sockets peuvent être manipulés avec des fonctions d’E/S de fichier standard comme close().
7.2.2. Types de Sockets (SOCK_STREAM, SOCK_DGRAM)
Lorsque vous créez un socket, vous spécifiez son type, qui détermine le protocole de
transport sous-jacent et son comportement :
—3.
1.
2. SOCK_STREAM : Fournit un service de flux de données bidirectionnel, fiable, orienté
connexion. Il utilise généralement TCP. C’est le type de socket le plus courant pour
la plupart des applications réseau.
— SOCK_DGRAM : Fournit un service de datagrammes sans connexion et non fiable. Il
utilise généralement UDP. Les messages sont envoyés sous forme d’unités indépen-
dantes (datagrammes).
— SOCK_RAW : Permet un accès direct aux protocoles IP (sans en-têtes de transport).
Utilisé pour des applications spécialisées (ex : ping, scanners de ports).
133
Programmation Système avec Linux et C Polytechnique Douala - Génie Logiciel N4
7.3. Socket TCP (Mode Connecté)
La création d’une application TCP client-serveur implique une séquence d’appels système
spécifiques pour l’établissement de la connexion et l’échange de données.
7.3.1. Création d’un Socket (socket())
C’est le premier appel système pour créer un point de terminaison de communication.
— Syntaxe :
#include <sys/socket.h> // Pour socket, AF_INET, SOCK_STREAM
int socket(int domain, int type, int protocol);
— domain : Famille d’adresses ou domaine de communication.
— AF_INET : IPv4.
— AF_INET6 : IPv6.
— AF_UNIX ou AF_LOCAL : Sockets de domaine UNIX (pour communication locale
entre processus sur la même machine).
— type : Type de socket (ex : SOCK_STREAM pour TCP, SOCK_DGRAM pour UDP).
— protocol : Protocole spécifique à utiliser. Pour SOCK_STREAM et SOCK_DGRAM, 0 est
généralement suffisant pour laisser le système choisir le protocole par défaut (TCP
pour SOCK_STREAM, UDP pour SOCK_DGRAM).
— Valeur de retour :
— Succès : Un nouveau descripteur de socket (un entier non négatif).
— Échec : -1, et errno est définie.
7.3.2. Liaison à une Adresse (bind())
Cet appel système attribue une adresse IP et un numéro de port au socket. C’est géné-
ralement fait par les serveurs pour écouter sur un port connu. Les clients laissent souvent le
système attribuer un port éphémère.
— Syntaxe :
#include <sys/socket.h> // Pour bind, sockaddr
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
— sockfd : Le descripteur de socket retourné par socket().
— addr : Un pointeur vers une structure d’adresse qui spécifie l’adresse IP et le
numéro de port. La structure générique est sockaddr, mais en pratique, on utilise
des structures spécifiques à la famille d’adresses, comme sockaddr_in pour IPv4.
#include <netinet/in.h> // Pour sockaddr_in, in_addr, htons
struct sockaddr_in {
sa_family_t sin_family; // AF_INET (famille d’adresse)
in_port_t sin_port; // Numero de port (en ordre d’octets reseau)
struct in_addr sin_addr; // Adresse IP (en ordre d’octets reseau)
// char sin_zero[8]; // Remplissage pour la compatibilite (obsolete)
};
134
Polytechnique Douala - Génie Logiciel N4 Programmation Système avec Linux et C
struct in_addr {
in_addr_t s_addr; // Adresse IP (generalement INADDR_ANY)
};
htons() (host to network short) et htonl() (host to network long) sont utilisées
pour convertir les nombres de l’ordre d’octets de l’hôte vers l’ordre d’octets réseau
(big-endian). ntohs() et ntohl() font l’inverse.
— Pour l’adresse IP, INADDR_ANY (ou 0.0.0.0) signifie que le serveur écoutera sur
toutes les interfaces réseau disponibles de la machine.
— addrlen : La taille de la structure d’adresse.
— Valeur de retour : 0 en cas de succès, -1 en cas d’échec.
7.3.3. Mise en Écoute (listen())
Cette fonction est utilisée par les serveurs pour indiquer qu’ils sont prêts à accepter des
connexions entrantes sur un socket lié.
— Syntaxe :
#include <sys/socket.h> // Pour listen
int listen(int sockfd, int backlog);
— sockfd : Le descripteur de socket lié (bind()).
— backlog : Le nombre maximal de connexions en attente (en file d’attente) que le
système peut gérer avant de les refuser.
— Valeur de retour : 0 en cas de succès, -1 en cas d’échec.
7.3.4. Acceptation des Connexions (accept())
Après listen(), un serveur utilise accept() pour accepter une nouvelle connexion en-
trante d’un client.
— Syntaxe :
#include <sys/socket.h> // Pour accept
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
— sockfd : Le descripteur de socket d’écoute retourné par listen().
— addr : Un pointeur vers une structure sockaddr (ou sockaddr_in) qui sera remplie
avec l’adresse du client qui s’est connecté.
— addrlen : Un pointeur vers une socklen_t qui contient la taille de la structure
addr avant l’appel et qui sera mise à jour avec la taille réelle de l’adresse client
après l’appel.
— Comportement : accept() est une fonction bloquante. Elle attendra qu’un client
se connecte.
— Valeur de retour :
— Succès : Un nouveau descripteur de socket. Ce nouveau descripteur est uti-
lisé pour la communication avec ce client spécifique. Le descripteur original
(sockfd) continue d’écouter pour de nouvelles connexions.
135
Programmation Système avec Linux et C Polytechnique Douala - Génie Logiciel N4
— Échec : -1, et errno est définie.
7.3.5. Connexion à un Serveur (connect())
Cette fonction est utilisée par un client pour établir une connexion à un serveur distant.
— Syntaxe :
#include <sys/socket.h> // Pour connect
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
— sockfd : Le descripteur de socket client.
— addr : Pointeur vers une structure d’adresse (sockaddr_in pour IPv4) du serveur
auquel se connecter.
— addrlen : La taille de la structure d’adresse.
— Comportement : connect() est une fonction bloquante. Elle tente d’établir une
connexion TCP avec le serveur spécifié.
— Valeur de retour : 0 en cas de succès, -1 en cas d’échec.
7.3.6. Envoi et Réception de Données (send(), recv())
Une fois la connexion TCP établie, send() et recv() sont utilisées pour échanger des
données.
send()
Envoie des données sur un socket connecté.
— Syntaxe :
#include <sys/socket.h> // Pour send
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
— sockfd : Le descripteur de socket connecté (le nouveau FD retourné par accept()
pour le serveur, ou le FD client pour le client).
— buf : Pointeur vers le buffer contenant les données à envoyer.
— len : La longueur des données à envoyer en octets.
— flags : Drapeaux (ex : MSG_OOB pour données hors-bande, MSG_DONTWAIT pour
non-bloquant). Généralement 0.
— Valeur de retour :
— Succès : Le nombre d’octets réellement envoyés.
— Échec : -1, et errno est définie.
recv()
Reçoit des données sur un socket connecté.
— Syntaxe :
#include <sys/socket.h> // Pour recv
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
136
Polytechnique Douala - Génie Logiciel N4 Programmation Système avec Linux et C
— sockfd : Le descripteur de socket connecté.
— buf : Pointeur vers le buffer où les données reçues seront stockées.
— len : La taille maximale du buffer.
— flags : Drapeaux (généralement 0).
— Valeur de retour :
— Succès : Le nombre d’octets lus. 0 si le pair a fermé la connexion.
— Échec : -1, et errno est définie.
— Exemple Serveur TCP simple : Ce programme implémente un serveur TCP basique
qui écoute sur un port, accepte une seule connexion client, reçoit un message, et
lui envoie une réponse.
#include <stdio.h> // Pour printf, perror
#include <stdlib.h> // Pour exit, EXIT_FAILURE, EXIT_SUCCESS
#include <string.h> // Pour strlen, memset
#include <unistd.h> // Pour close, read, write
#include <sys/socket.h> // Pour socket, bind, listen, accept, send, recv,
#include <netinet/in.h> // Pour sockaddr_in, INADDR_ANY, htons
#include <arpa/inet.h> // Pour inet_ntoa (conversion IP a string)
#define PORT 8080 // Port sur lequel le serveur ecoutera
#define BUFFER_SIZE 1024 // Taille du buffer pour les donnees
#define BACKLOG 3 // Nombre maximum de connexions en attente dans la
int main() {
int server_fd, new_socket; // descripteurs de sockets
struct sockaddr_in address; // Structure pour stocker l’adresse du serve
int opt = 1; // Option pour setsockopt (reutiliser l’adre
socklen_t addrlen = sizeof(address); // Longueur de la structure d’adres
char buffer[BUFFER_SIZE] = {0}; // Buffer pour les donnees recues
const char *hello_message = "Hello from server!"; // Message a envoyer a
printf("Serveur TCP demarre.\n");
// 1. Creation du socket du serveur
// AF_INET: Famille d’adresses IPv4
// SOCK_STREAM: Type de socket pour un service oriente connexion (TCP)
// 0: Protocole par defaut (TCP)
if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
perror("Erreur lors de la creation du socket serveur");
return EXIT_FAILURE;
}
printf("Socket serveur cree (FD: %d).\n", server_fd);
// 2. Configuration des options du socket
// SO_REUSEADDR | SO_REUSEPORT: Permet de reutiliser rapidement une adre
// meme si une connexion precedente est en
137
Programmation Système avec Linux et C Polytechnique Douala - Génie Logiciel N4
if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt,
perror("Erreur lors de la configuration des options du socket");
close(server_fd); // Fermer le socket en cas d’erreur
return EXIT_FAILURE;
}
printf("Options du socket configurees.\n");
// 3. Preparation de la structure d’adresse du serveur
address.sin_family = AF_INET; // Famille d’adresses IPv4
address.sin_addr.s_addr = INADDR_ANY; // Ecoute sur toutes les interfa
address.sin_port = htons(PORT); // Port d’ecoute (conversion en
// 4. Liaison du socket a l’adresse et au port
if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) == -1)
perror("Erreur lors de la liaison du socket (bind)");
close(server_fd);
return EXIT_FAILURE;
}
printf("Socket lie a l’adresse %s sur le port %d.\n", inet_ntoa(address.
// 5. Mise en ecoute des connexions entrantes
// BACKLOG: Nombre maximum de connexions en attente dans la file
if (listen(server_fd, BACKLOG) == -1) {
perror("Erreur lors de la mise en ecoute (listen)");
close(server_fd);
return EXIT_FAILURE;
}
printf("Serveur en ecoute des connexions entrantes...\n");
// 6. Acceptation d’une connexion cliente entrante
// Cette fonction bloque jusqu’a ce qu’un client se connecte
new_socket = accept(server_fd, (struct sockaddr *)&address, &addrlen);
if (new_socket == -1) {
perror("Erreur lors de l’acceptation de la connexion (accept)");
close(server_fd);
return EXIT_FAILURE;
}
printf("Client connecte depuis %s:%d (FD: %d).\n",
inet_ntoa(address.sin_addr), ntohs(address.sin_port), new_socket)
// 7. Lecture du message envoye par le client
ssize_t bytes_received = recv(new_socket, buffer, BUFFER_SIZE - 1, 0); /
if (bytes_received == -1) {
perror("Erreur lors de la reception du message (recv)");
close(new_socket);
138
Polytechnique Douala - Génie Logiciel N4 Programmation Système avec Linux et C
close(server_fd);
return EXIT_FAILURE;
} else if (bytes_received == 0) {
printf("Client a ferme la connexion avant d’envoyer un message.\n");
} else {
buffer[bytes_received] = ’\0’; // Assurer que la chaine est terminee
printf("Message du client: ’%s’\n", buffer);
}
// 8. Envoi d’une reponse au client
if (send(new_socket, hello_message, strlen(hello_message), 0) == -1) {
perror("Erreur lors de l’envoi de la reponse (send)");
close(new_socket);
close(server_fd);
return EXIT_FAILURE;
}
printf("Message de reponse envoye au client: ’%s’\n", hello_message);
// 9. Fermeture des sockets
printf("Fermeture du socket de communication client (FD: %d).\n", new_so
close(new_socket); // Ferme le socket de communication avec ce client
printf("Fermeture du socket d’ecoute du serveur (FD: %d).\n", server_fd)
close(server_fd); // Ferme le socket d’ecoute du serveur
printf("Serveur termine.\n");
return EXIT_SUCCESS;
}
Pour compiler : ‘gcc server-tcp-simple.c -o server-tcp‘ Pour exécuter : ‘./server-tcp‘
— Exemple Client TCP simple : Ce programme implémente un client TCP basique
qui se connecte à un serveur (local), lui envoie un message, reçoit une réponse et
affiche le message reçu.
#include <stdio.h> // Pour printf, perror
#include <stdlib.h> // Pour exit, EXIT_FAILURE, EXIT_SUCCESS
#include <string.h> // Pour strlen, memset
#include <unistd.h> // Pour close, read, write
#include <sys/socket.h> // Pour socket, connect, send, recv
#include <arpa/inet.h> // Pour htons, inet_pton (conversion string a IP b
#include <netinet/in.h> // Pour sockaddr_in
#define PORT 8080 // Port du serveur
#define SERVER_IP "127.0.0.1" // Adresse IP du serveur local (loopback)
#define BUFFER_SIZE 1024 // Taille du buffer pour les donnees
139
Programmation Système avec Linux et C Polytechnique Douala - Génie Logiciel N4
int main() {
int client_socket_fd = 0; // Descripteur de socket pour le client
struct sockaddr_in serv_addr; // Structure d’adresse du serveur
char buffer[BUFFER_SIZE] = {0}; // Buffer pour les donnees recues
const char *client_message = "Hello from client!"; // Message a envoyer
printf("Client TCP demarre.\n");
// 1. Creation du socket client
// AF_INET: Famille d’adresses IPv4
// SOCK_STREAM: Type de socket pour un service oriente connexion (TCP)
// 0: Protocole par defaut (TCP)
if ((client_socket_fd = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
perror("Erreur lors de la creation du socket client");
return EXIT_FAILURE;
}
printf("Socket client cree (FD: %d).\n", client_socket_fd);
// 2. Preparation de la structure d’adresse du serveur
serv_addr.sin_family = AF_INET; // Famille d’adresses IPv4
serv_addr.sin_port = htons(PORT); // Port du serveur (conversion
// Convertir l’adresse IP du serveur de format texte (ex: "127.0.0.1") e
// inet_pton retourne 1 en cas de succes, 0 si l’adresse n’est pas valid
if (inet_pton(AF_INET, SERVER_IP, &serv_addr.sin_addr) <= 0) {
perror("Adresse IP invalide / Adresse non supportee");
close(client_socket_fd);
return EXIT_FAILURE;
}
printf("Adresse du serveur pretee: %s:%d.\n", SERVER_IP, PORT);
// 3. Connexion au serveur
printf("Tentative de connexion au serveur...\n");
if (connect(client_socket_fd, (struct sockaddr *)&serv_addr, sizeof(serv
perror("Erreur lors de la connexion au serveur");
close(client_socket_fd);
return EXIT_FAILURE;
}
printf("Connecte au serveur %s:%d.\n", SERVER_IP, PORT);
// 4. Envoi du message au serveur
if (send(client_socket_fd, client_message, strlen(client_message), 0) ==
perror("Erreur lors de l’envoi du message (send)");
close(client_socket_fd);
140
Polytechnique Douala - Génie Logiciel N4 Programmation Système avec Linux et C
return EXIT_FAILURE;
}
printf("Message envoye au serveur: ’%s’\n", client_message);
// 5. Lecture de la reponse du serveur
ssize_t bytes_received = recv(client_socket_fd, buffer, BUFFER_SIZE - 1,
if (bytes_received == -1) {
perror("Erreur lors de la reception de la reponse (recv)");
close(client_socket_fd);
return EXIT_FAILURE;
} else if (bytes_received == 0) {
printf("Serveur a ferme la connexion.\n");
} else {
buffer[bytes_received] = ’\0’; // Assurer que la chaine est terminee
printf("Message du serveur: ’%s’\n", buffer);
}
// 6. Fermeture du socket client
printf("Fermeture du socket client (FD: %d).\n", client_socket_fd);
close(client_socket_fd);
printf("Client termine.\n");
return EXIT_SUCCESS;
}
Pour compiler : ‘gcc client-tcp-simple.c -o client-tcp‘ Pour exécuter : ‘.‘/client-tcp‘
(après avoir lancé le serveur)
7.3.7. Fermeture de Connexions (shutdown(), close())
Pour fermer une connexion socket, deux fonctions principales sont disponibles :
close()
L’appel système close() est utilisé pour fermer un descripteur de fichier, y compris les
descripteurs de sockets.
— Comportement : Lorsque close() est appelé sur un socket, le système tente
de vider les données restantes dans les buffers d’envoi et ferme la connexion. Si
d’autres processus ou threads partagent le même descripteur de fichier via fork()
ou dup(), close() ne ferme que la référence de ce processus/thread au socket.
La connexion ne sera complètement coupée que lorsque le dernier descripteur de
fichier pointant vers ce socket sera fermé.
141
Programmation Système avec Linux et C Polytechnique Douala - Génie Logiciel N4
shutdown()
La fonction shutdown() offre un contrôle plus granulaire sur la fermeture d’un socket,
permettant de fermer indépendamment les flux d’envoi et de réception.
— Syntaxe :
#include <sys/socket.h> // Pour shutdown
int shutdown(int sockfd, int how);
— sockfd : Le descripteur de socket.
— how : Spécifie la partie de la connexion à fermer :
— SHUT_RD : Arrête les opérations de lecture. Les données entrantes sont ignorées.
— SHUT_WR : Arrête les opérations d’écriture. Le pair distant recevra un EOF
(read() retournera 0). Les données en buffer sont envoyées avant la fermeture.
— SHUTR DW R : Arrtelaf oislesoprationsdelectureetd′ criture.
— Utilité : shutdown() est utile dans les scénarios où un processus veut signaler à
l’autre extrémité qu’il n’enverra plus de données (par SHUT_WR) mais qu’il souhaite
continuer à recevoir des données. Cela est important pour les protocoles où un
côté envoie une requête et s’attend à une réponse, puis souhaite fermer sa partie
écriture tout en recevant la réponse complète.
Exercice 7.2 :
Client-Serveur TCP
[label=()]Modifiez le serveur TCP simple pour qu’il puisse traiter des requêtes en
boucle. Après avoir répondu à un client, il doit revenir à l’état d’écoute pour de
nouvelles connexions. Le client doit envoyer un message, recevoir une réponse, puis
se déconnecter. Implémentez un client TCP qui envoie une chaîne de caractères
au serveur, puis utilise shutdown(SHUT_WR) pour signaler la fin de son envoi, tout
en attendant et en lisant une réponse du serveur. Le serveur doit lire jusqu’à la
fin de la connexion du client, puis renvoyer une chaîne modifiée (par exemple,
en majuscules). Créez un programme client-serveur TCP simple qui échange des
fichiers. Le client envoie un nom de fichier au serveur. Si le serveur a le fichier, il
le lit et l’envoie au client. Sinon, il envoie un message d’erreur.
7.4. Socket UDP (Mode Non Connecté)
Les sockets UDP fonctionnent différemment des sockets TCP. Comme UDP est un pro-
tocole sans connexion, il n’y a pas d’étapes de listen(), accept(), ou connect(). Les
datagrammes sont envoyés et reçus directement.
7.4.1. Envoi et Réception de Datagrammes (sendto(), recvfrom())
Pour les sockets UDP, les fonctions sendto() et recvfrom() sont utilisées, car elles
incluent l’adresse de destination/source dans leurs arguments.
142
Polytechnique Douala - Génie Logiciel N4 Programmation Système avec Linux et C
sendto()
Envoie un datagramme à une adresse spécifiée.
—
3.
1.
2. Syntaxe :
#include <sys/socket.h> // Pour sendto
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
const struct sockaddr *dest_addr, socklen_t addrlen);
— sockfd : Le descripteur de socket UDP.
— buf : Pointeur vers le buffer contenant les données à envoyer.
— len : La longueur des données en octets.
— flags : Drapeaux (généralement 0).
— dest_addr : Pointeur vers la structure d’adresse (sockaddr_in pour IPv4) du
destinataire.
— addrlen : La taille de la structure dest_addr.
— Valeur de retour : Le nombre d’octets envoyés en cas de succès, -1 en cas d’échec.
recvfrom()
Reçoit un datagramme et obtient l’adresse de l’expéditeur.
— Syntaxe :
#include <sys/socket.h> // Pour recvfrom
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
struct sockaddr *src_addr, socklen_t *addrlen);
— sockfd : Le descripteur de socket UDP.
— buf : Pointeur vers le buffer où les données reçues seront stockées.
— len : La taille maximale du buffer.
— flags : Drapeaux (généralement 0).
— src_addr : Pointeur vers une structure sockaddr qui sera remplie avec l’adresse
de l’expéditeur du datagramme.
— addrlen : Pointeur vers une socklen_t qui contient la taille de src_addr avant
l’appel et sera mise à jour avec la taille réelle de l’adresse de l’expéditeur après
l’appel.
— Valeur de retour : Le nombre d’octets lus en cas de succès, -1 en cas d’échec.
— Exemple Serveur UDP simple : Ce programme implémente un serveur UDP qui
écoute sur un port, reçoit des datagrammes et renvoie une réponse à l’expéditeur.
#include <stdio.h> // Pour printf, perror
#include <stdlib.h> // Pour exit, EXIT_FAILURE, EXIT_SUCCESS
#include <string.h> // Pour strlen, memset
#include <unistd.h> // Pour close
#include <sys/socket.h> // Pour socket, bind, sendto, recvfrom
#include <netinet/in.h> // Pour sockaddr_in, INADDR_ANY, htons
#include <arpa/inet.h> // Pour inet_ntoa
#define PORT 8081 // Port UDP
143
Programmation Système avec Linux et C Polytechnique Douala - Génie Logiciel N4
#define BUFFER_SIZE 1024
int main() {
int sockfd;
char buffer[BUFFER_SIZE];
struct sockaddr_in serv_addr, cli_addr; // Adresses serveur et client
printf("Serveur UDP demarre.\n");
// 1. Creation du socket UDP
sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd == -1) {
perror("Erreur lors de la creation du socket UDP");
return EXIT_FAILURE;
}
printf("Socket UDP cree (FD: %d).\n", sockfd);
// 2. Preparation de l’adresse du serveur
memset(&serv_addr, 0, sizeof(serv_addr)); // Initialiser a zero
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = INADDR_ANY;
serv_addr.sin_port = htons(PORT);
// 3. Liaison du socket a l’adresse et au port
if (bind(sockfd, (const struct sockaddr *)&serv_addr, sizeof(serv_addr))
perror("Erreur lors de la liaison du socket UDP (bind)");
close(sockfd);
return EXIT_FAILURE;
}
printf("Socket UDP lie a l’adresse %s sur le port %d.\n", inet_ntoa(serv
printf("Serveur UDP en attente de messages...\n");
socklen_t len = sizeof(cli_addr); // Longueur de l’adresse client
int n;
// Boucle de reception de messages UDP
while (1) {
// 4. Reception d’un datagramme
n = recvfrom(sockfd, (char *)buffer, BUFFER_SIZE - 1,
MSG_WAITALL, (struct sockaddr *)&cli_addr, &len);
if (n == -1) {
perror("Erreur lors de la reception du datagramme (recvfrom)");
close(sockfd);
return EXIT_FAILURE;
144
Polytechnique Douala - Génie Logiciel N4 Programmation Système avec Linux et C
}
buffer[n] = ’\0’; // Assurer terminaison de chaine
printf("Message recu de %s:%d : ’%s’\n",
inet_ntoa(cli_addr.sin_addr), ntohs(cli_addr.sin_port), buffe
// 5. Envoi d’une reponse a l’expediteur
const char *response = "Message recu par le serveur UDP!";
sendto(sockfd, (const char *)response, strlen(response),
MSG_CONFIRM, (const struct sockaddr *)&cli_addr, len);
printf("Reponse envoyee a %s:%d\n", inet_ntoa(cli_addr.sin_addr), nt
}
close(sockfd); // Cette ligne n’est jamais atteinte dans une boucle infi
return EXIT_SUCCESS;
}
Pour compiler : ‘gcc server-udp-simple.c -o server-udp‘ Pour exécuter : ‘./server-
udp‘
— Exemple Client UDP simple : Ce programme implémente un client UDP qui envoie
un datagramme à un serveur et attend une réponse.
#include <stdio.h> // Pour printf, perror
#include <stdlib.h> // Pour exit, EXIT_FAILURE, EXIT_SUCCESS
#include <string.h> // Pour strlen, memset
#include <unistd.h> // Pour close
#include <sys/socket.h> // Pour socket, sendto, recvfrom
#include <arpa/inet.h> // Pour htons, inet_pton
#include <netinet/in.h> // Pour sockaddr_in
#define PORT 8081 // Port du serveur UDP
#define SERVER_IP "127.0.0.1" // Adresse IP du serveur
#define BUFFER_SIZE 1024
int main() {
int sockfd;
char buffer[BUFFER_SIZE];
struct sockaddr_in serv_addr; // Adresse du serveur
printf("Client UDP demarre.\n");
// 1. Creation du socket UDP
sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd == -1) {
perror("Erreur lors de la creation du socket UDP");
return EXIT_FAILURE;
145
Programmation Système avec Linux et C Polytechnique Douala - Génie Logiciel N4
}
printf("Socket UDP cree (FD: %d).\n", sockfd);
// 2. Preparation de l’adresse du serveur
memset(&serv_addr, 0, sizeof(serv_addr)); // Initialiser a zero
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(PORT);
// Convertir l’adresse IP de texte en binaire
if (inet_pton(AF_INET, SERVER_IP, &serv_addr.sin_addr) <= 0) {
perror("Adresse IP invalide / Adresse non supportee");
close(sockfd);
return EXIT_FAILURE;
}
printf("Adresse du serveur pretee: %s:%d.\n", SERVER_IP, PORT);
const char *message = "Bonjour serveur UDP!";
// 3. Envoi du datagramme au serveur
sendto(sockfd, (const char *)message, strlen(message),
MSG_CONFIRM, (const struct sockaddr *)&serv_addr, sizeof(serv_add
printf("Message envoye au serveur: ’%s’\n", message);
socklen_t len = sizeof(serv_addr);
int n;
// 4. Reception de la reponse du serveur
n = recvfrom(sockfd, (char *)buffer, BUFFER_SIZE - 1,
MSG_WAITALL, (struct sockaddr *)&serv_addr, &len);
if (n == -1) {
perror("Erreur lors de la reception de la reponse (recvfrom)");
close(sockfd);
return EXIT_FAILURE;
}
buffer[n] = ’\0’; // Assurer terminaison de chaine
printf("Reponse du serveur: ’%s’\n", buffer);
// 5. Fermeture du socket client
close(sockfd);
printf("Client termine.\n");
return EXIT_SUCCESS;
}
Pour compiler : ‘gcc client-udp-simple.c -o client-udp‘ Pour exécuter : ‘./client-udp‘
146
Polytechnique Douala - Génie Logiciel N4 Programmation Système avec Linux et C
(après avoir lancé le serveur UDP)
Exercice 7.3 :
Client-Serveur UDP
[label=()]Écrivez un programme client-serveur UDP qui implémente un service de
"ping". Le client envoie un message "PING" au serveur. Le serveur reçoit le mes-
sage et renvoie "PONG" à l’expéditeur. Le client affiche la réponse. Modifiez le
client UDP pour qu’il puisse envoyer des datagrammes en boucle (par exemple,
toutes les 1 seconde). Le serveur doit simplement recevoir et afficher tous les mes-
sages. Implémentez un client-serveur UDP simple pour un service de diffusion
(broadcast). Le client envoie un message en broadcast (à l’adresse ‘255.255.255.255‘
pour IPv4 sur le réseau local ou une adresse de broadcast spécifique au sous-réseau).
Le serveur doit recevoir le message et afficher l’adresse IP et le port de l’expédi-
teur. (Attention : la diffusion peut nécessiter des options de socket spéciales comme
‘SO-BROADCAST‘).
7.5. Gestion de Plusieurs Clients Concurremment
Un serveur qui ne peut gérer qu’un seul client à la fois est rarement suffisant dans un
environnement réel. Les serveurs doivent souvent gérer plusieurs clients simultanément. Il
existe plusieurs approches pour la concurrence :
7.5.1. Serveurs Forking (multiprocessus)
Dans cette approche, pour chaque nouvelle connexion cliente acceptée, le serveur parent
appelle fork() pour créer un nouveau processus enfant. Ce processus enfant est alors respon-
sable de la communication avec ce client spécifique, tandis que le parent continue d’écouter
les nouvelles connexions.
—2. Avantages :
3.
1.
— Isolation : Chaque client est géré par un processus séparé, ce qui offre une
forte isolation. Un crash dans un processus enfant n’affecte généralement pas
les autres clients ni le processus serveur principal.
— Simplicité de programmation : Pas de problèmes de variables globales par-
tagées ou de mutex entre les processus.
— Inconvénients :
— Coût élevé : La création d’un nouveau processus est relativement coûteuse en
termes de mémoire et de temps CPU (copie de l’espace d’adressage du parent).
— Communication IPC : Si les processus enfants doivent communiquer entre
eux ou avec le parent, des mécanismes IPC (pipes, mémoire partagée, etc.) sont
nécessaires.
— Gestion des zombies : Le parent doit appeler wait() ou waitpid() pour
éviter les processus zombies.
— Exemple de Serveur Forking (multiprocessus) :
#include <stdio.h>
147
Programmation Système avec Linux et C Polytechnique Douala - Génie Logiciel N4
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/wait.h> // Pour waitpid, WNOHANG
#define PORT 8080
#define BUFFER_SIZE 1024
#define BACKLOG 10
// Fonction pour gerer la connexion avec un client
void handle_client(int client_socket_fd) {
char buffer[BUFFER_SIZE] = {0};
ssize_t bytes_received;
printf("Enfant (PID %d): Debut de gestion du client (FD: %d).\n", getpid
// Lire le message du client
bytes_received = recv(client_socket_fd, buffer, BUFFER_SIZE - 1, 0);
if (bytes_received == -1) {
perror("Enfant: Erreur de reception (recv)");
} else if (bytes_received == 0) {
printf("Enfant: Client a ferme la connexion.\n");
} else {
buffer[bytes_received] = ’\0’;
printf("Enfant (PID %d): Message du client: ’%s’\n", getpid(), buffe
// Envoyer une reponse
const char *response = "Message recu par le serveur multiprocessus!"
if (send(client_socket_fd, response, strlen(response), 0) == -1) {
perror("Enfant: Erreur d’envoi (send)");
}
printf("Enfant (PID %d): Reponse envoyee.\n", getpid());
}
close(client_socket_fd); // Fermer le socket de communication du client
printf("Enfant (PID %d): Fin de gestion du client.\n", getpid());
exit(EXIT_SUCCESS); // L’enfant se termine apres avoir gere le client
}
// Gestionnaire de signal pour SIGCHLD pour nettoyer les zombies
void sigchld_handler(int sig) {
// WNOHANG: ne pas bloquer si aucun enfant n’est termine
148
Polytechnique Douala - Génie Logiciel N4 Programmation Système avec Linux et C
// C’est important dans un handler pour ne pas bloquer le processus prin
while (waitpid(-1, NULL, WNOHANG) > 0) {
// Un enfant zombie a ete nettoye
printf("Parent: Un processus enfant zombie a ete nettoye.\n");
}
}
int main() {
int server_fd, new_socket;
struct sockaddr_in address;
socklen_t addrlen = sizeof(address);
pid_t child_pid;
// Configurer le handler pour SIGCHLD
struct sigaction sa;
sa.sa_handler = sigchld_handler;
sigemptyset(&sa.sa_mask);
sa.sa_flags = SA_RESTART | SA_NOCLDSTOP; // SA_NOCLDSTOP: ne pas recevoi
if (sigaction(SIGCHLD, &sa, NULL) == -1) {
perror("sigaction");
return EXIT_FAILURE;
}
server_fd = socket(AF_INET, SOCK_STREAM, 0);
if (server_fd == -1) { perror("socket failed"); return EXIT_FAILURE; }
int opt = 1;
if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt,
perror("setsockopt"); close(server_fd); return EXIT_FAILURE; }
address.sin_family = AF_INET;
address.sin_addr.s_addr = INADDR_ANY;
address.sin_port = htons(PORT);
if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) < 0) {
perror("bind failed"); close(server_fd); return EXIT_FAILURE; }
if (listen(server_fd, BACKLOG) < 0) {
perror("listen"); close(server_fd); return EXIT_FAILURE; }
printf("Serveur Forking en ecoute sur le port %d. PID parent: %d\n", POR
while (1) {
new_socket = accept(server_fd, (struct sockaddr *)&address, &addrlen
if (new_socket == -1) {
149
Programmation Système avec Linux et C Polytechnique Douala - Génie Logiciel N4
if (errno == EINTR) { // Interrompu par un signal (ex: SIGCHLD),
continue;
}
perror("accept");
close(server_fd);
return EXIT_FAILURE;
}
printf("Parent: Connexion acceptee depuis %s:%d (nouveau FD: %d).\n"
inet_ntoa(address.sin_addr), ntohs(address.sin_port), new_soc
child_pid = fork();
if (child_pid == -1) {
perror("fork failed");
close(new_socket); // Fermer le socket client non gere
continue; // Essayer d’accepter une autre connexion
} else if (child_pid == 0) { // Processus enfant
close(server_fd); // L’enfant n’a pas besoin du socket d’ecoute
handle_client(new_socket); // Gerer le client
// handle_client contient exit(EXIT_SUCCESS)
} else { // Processus parent
close(new_socket); // Le parent n’a pas besoin du socket de comm
}
}
close(server_fd);
return EXIT_SUCCESS;
}
Pour compiler : ‘gcc server-forking.c -o server-forking‘ Pour exécuter : ‘./server-
forking‘ Testez avec plusieurs instances du client TCP simple (‘./client-tcp‘) lancées
en parallèle.
7.5.2. Serveurs Threading (multithreaded)
Au lieu de créer un nouveau processus, le serveur parent crée un nouveau thread pour
chaque connexion cliente acceptée. Tous les threads partagent le même espace d’adressage
mémoire.
— Avantages :
— Faible coût : La création et la commutation de threads sont beaucoup plus
rapides et moins coûteuses que celles des processus.
— Partage de données facile : Les threads partagent la mémoire, ce qui sim-
plifie le partage de données entre les handlers de clients (nécessite une synchro-
nisation !).
— Inconvénients :
— Complexité de la synchronisation : La gestion des conditions de course et
150
Polytechnique Douala - Génie Logiciel N4 Programmation Système avec Linux et C
des interblocages devient cruciale en raison du partage de mémoire.
— Moins d’isolation : Une erreur dans un thread peut potentiellement faire
crasher tout le serveur.
— Débogage difficile : Les problèmes de concurrence sont notoirement difficiles
à déboguer.
— Exemple de Serveur Threading (multithreaded) :
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <pthread.h> // Pour pthreads
#define PORT 8080
#define BUFFER_SIZE 1024
#define BACKLOG 10
// Structure pour passer les arguments au thread client
typedef struct {
int client_socket_fd;
struct sockaddr_in client_address;
} ThreadClientArgs;
// Fonction qui sera executee par chaque thread pour gerer un client
void *handle_client-thread(void *arg) {
ThreadClientArgs *args = (ThreadClientArgs *)arg;
int client_socket_fd = args->client_socket_fd;
struct sockaddr_in client_address = args->client_address; // Copie de l’
char buffer[BUFFER_SIZE] = {0};
ssize_t bytes_received;
printf("Thread client (ID: %lu) pour %s:%d: Debut de gestion du client (
(unsigned long)pthread_self(), inet_ntoa(client_address.sin_addr)
// Lire le message du client
bytes_received = recv(client_socket_fd, buffer, BUFFER_SIZE - 1, 0);
if (bytes_received == -1) {
perror("Thread client: Erreur de reception (recv)");
} else if (bytes_received == 0) {
printf("Thread client: Client a ferme la connexion.\n");
} else {
buffer[bytes_received] = ’\0’;
printf("Thread client (ID: %lu): Message du client: ’%s’\n", (unsign
151
Programmation Système avec Linux et C Polytechnique Douala - Génie Logiciel N4
// Envoyer une reponse
const char *response = "Message recu par le serveur multithreaded!";
if (send(client_socket_fd, response, strlen(response), 0) == -1) {
perror("Thread client: Erreur d’envoi (send)");
}
printf("Thread client (ID: %lu): Reponse envoyee.\n", (unsigned long
}
close(client_socket_fd); // Fermer le socket de communication du client
printf("Thread client (ID: %lu): Fin de gestion du client.\n", (unsigned
free(arg); // Liberer la memoire allouee pour les arguments du thread
pthread_exit(NULL); // Le thread se termine
}
int main() {
int server_fd, new_socket;
struct sockaddr_in address;
socklen_t addrlen = sizeof(address);
pthread_t client_thread_id;
server_fd = socket(AF_INET, SOCK_STREAM, 0);
if (server_fd == -1) { perror("socket failed"); return EXIT_FAILURE; }
int opt = 1;
if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt,
perror("setsockopt"); close(server_fd); return EXIT_FAILURE; }
address.sin_family = AF_INET;
address.sin_addr.s_addr = INADDR_ANY;
address.sin_port = htons(PORT);
if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) < 0) {
perror("bind failed"); close(server_fd); return EXIT_FAILURE; }
if (listen(server_fd, BACKLOG) < 0) {
perror("listen"); close(server_fd); return EXIT_FAILURE; }
printf("Serveur Threading en ecoute sur le port %d. PID parent: %d\n", P
while (1) {
new_socket = accept(server_fd, (struct sockaddr *)&address, &addrlen
if (new_socket == -1) {
perror("accept");
close(server_fd);
152
Polytechnique Douala - Génie Logiciel N4 Programmation Système avec Linux et C
return EXIT_FAILURE;
}
// Allouer de la memoire pour passer les arguments au thread
ThreadClientArgs *args = (ThreadClientArgs *)malloc(sizeof(ThreadCli
if (args == NULL) {
perror("malloc for thread args");
close(new_socket);
continue;
}
args->client_socket_fd = new_socket;
args->client_address = address; // Copie la structure
// Creation d’un nouveau thread pour gerer le client
if (pthread_create(&client_thread_id, NULL, handle_client-thread, (v
fprintf(stderr, "Erreur de creation de thread pour le client.\n"
close(new_socket); // Fermer le socket si le thread ne peut pas
free(args);
continue;
}
// Detacher le thread pour qu’il libere ses ressources automatiqueme
// et eviter d’avoir a appeler pthread_join()
pthread_detach(client_thread_id);
printf("Parent: Connexion acceptee pour %s:%d. Thread cree (ID: %lu)
inet_ntoa(address.sin_addr), ntohs(address.sin_port), (unsign
}
close(server_fd);
return EXIT_SUCCESS;
}
Pour compiler : ‘gcc server-threading.c -o server-threading -pthread‘ Pour exécuter :
‘./server-threading‘ Testez avec plusieurs instances du client TCP simple (‘./client-
tcp‘) lancées en parallèle.
7.5.3. Multiplexage des E/S (select(), poll(), epoll())
Pour les serveurs qui doivent gérer un grand nombre de connexions simultanément sans les
coûts des processus ou des threads pour chaque connexion, le multiplexage des E/S est une
technique avancée. Elle permet à un seul processus/thread de surveiller plusieurs descripteurs
de fichiers (sockets) pour l’activité (prêts à lire, prêts à écrire, erreur).
— Scénario : Un serveur a un seul thread principal qui gère la logique de plusieurs
clients en surveillant leurs sockets. Quand un socket est prêt (ex : des données sont
arrivées), le serveur lit/écrit ces données et continue à surveiller les autres sockets.
153
Programmation Système avec Linux et C Polytechnique Douala - Génie Logiciel N4
— Avantages :
— Très efficace pour gérer un grand nombre de connexions peu actives (faible
surcharge CPU/mémoire par connexion).
— Évite les problèmes de concurrence liés au partage de ressources si la logique
de traitement est entièrement séquentielle.
— Inconvénients :
— Plus complexe à programmer que les modèles forking/threading simples.
— Moins efficace pour les clients qui effectuent des calculs longs (car cela bloque-
rait le seul thread du serveur).
— Fonctions courantes :
— select() : Le plus ancien et le plus portable. Limité par le nombre de des-
cripteurs de fichiers (souvent 1024). Parcourt tous les descripteurs pour trouver
l’activité.
— poll() : Plus récent que select(), pas de limite sur le nombre de descripteurs.
Fonctionne sur un tableau de structures ‘pollfd‘. Parcourt toujours tous les
descripteurs.
— epoll() : Linux-spécifique (le plus performant et le plus récent). Très efficace
pour un grand nombre de connexions (milliers ou millions). Il ne parcourt
pas tous les descripteurs ; le noyau notifie directement des événements sur les
descripteurs actifs. Utilisé dans les serveurs web haute performance comme
Nginx.
— Exemple de Serveur avec select() : Ce programme montre comment un serveur
peut gérer plusieurs clients sans créer de processus ou de threads séparés pour
chaque client.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/select.h> // Pour select, FD_SET, FD_CLR, FD_ISSET, FD_ZERO
#define PORT 8080
#define BUFFER_SIZE 1024
#define MAX_CLIENTS 30 // Nombre maximal de clients que le serveur peut gere
int main() {
int master_socket, addrlen, new_socket, client_socket[MAX_CLIENTS],
max_sd, activity, i, sd;
struct sockaddr_in address;
char buffer[BUFFER_SIZE];
// Ensemble de descripteurs de sockets a surveiller
fd_set readfds;
154
Polytechnique Douala - Génie Logiciel N4 Programmation Système avec Linux et C
// Initialiser les sockets clients a 0 (indique qu’ils ne sont pas utili
for (i = 0; i < MAX_CLIENTS; i++) {
client_socket[i] = 0;
}
// 1. Creation du socket principal (master socket)
master_socket = socket(AF_INET, SOCK_STREAM, 0);
if (master_socket == 0) {
perror("socket failed");
return EXIT_FAILURE;
}
// 2. Configuration pour reutiliser l’adresse et le port
int opt = 1;
if (setsockopt(master_socket, SOL_SOCKET, SO_REUSEADDR, (char *)&opt, si
perror("setsockopt failed");
close(master_socket);
return EXIT_FAILURE;
}
// 3. Preparation de la structure d’adresse
address.sin_family = AF_INET;
address.sin_addr.s_addr = INADDR_ANY;
address.sin_port = htons(PORT);
// 4. Liaison du socket
if (bind(master_socket, (struct sockaddr *)&address, sizeof(address)) <
perror("bind failed");
close(master_socket);
return EXIT_FAILURE;
}
printf("Serveur Select en ecoute sur le port %d\n", PORT);
// 5. Mise en ecoute des connexions entrantes
if (listen(master_socket, 3) < 0) {
perror("listen");
close(master_socket);
return EXIT_FAILURE;
}
addrlen = sizeof(address);
puts("Attente de connexions...");
while (1) {
155
Programmation Système avec Linux et C Polytechnique Douala - Génie Logiciel N4
// Reinitialiser l’ensemble de descripteurs de lecture
FD_ZERO(&readfds);
// Ajouter le master socket a l’ensemble
FD_SET(master_socket, &readfds);
max_sd = master_socket;
// Ajouter les sockets clients valides a l’ensemble
for (i = 0; i < MAX_CLIENTS; i++) {
sd = client_socket[i];
if (sd > 0) { // Si le socket est valide
FD_SET(sd, &readfds);
}
if (sd > max_sd) { // Mettre a jour le plus grand FD
max_sd = sd;
}
}
// 6. Attendre l’activite sur l’un des sockets
// La fonction select() bloque jusqu’a ce qu’il y ait une activite s
activity = select(max_sd + 1, &readfds, NULL, NULL, NULL);
if ((activity < 0) && (errno != EINTR)) {
perror("select error");
break;
}
// 7. Si activite sur le master socket, c’est une nouvelle connexion
if (FD_ISSET(master_socket, &readfds)) {
new_socket = accept(master_socket, (struct sockaddr *)&address,
if (new_socket < 0) {
perror("accept");
continue;
}
printf("Nouvelle connexion, socket fd est %d, ip est %s, port es
new_socket, inet_ntoa(address.sin_addr), ntohs(address.si
// Ajouter le nouveau socket client au tableau
for (i = 0; i < MAX_CLIENTS; i++) {
if (client_socket[i] == 0) {
client_socket[i] = new_socket;
printf("Ajout du socket a la liste de sockets au descrip
break;
}
}
156
Polytechnique Douala - Génie Logiciel N4 Programmation Système avec Linux et C
// 8. Sinon, c’est de l’activite sur un socket client existant
for (i = 0; i < MAX_CLIENTS; i++) {
sd = client_socket[i];
if (FD_ISSET(sd, &readfds)) {
// Verifier si c’est pour la fermeture de connexion
ssize_t valread = recv(sd, buffer, BUFFER_SIZE, 0);
if (valread == 0) { // Client deconnecte
getpeername(sd, (struct sockaddr*)&address, (socklen_t*)
printf("Client deconnecte, ip %s, port %d \n",
inet_ntoa(address.sin_addr), ntohs(address.sin_po
close(sd);
client_socket[i] = 0; // Marquer comme libre
} else { // Lecture de donnees
buffer[valread] = ’\0’;
printf("Message du client %d: %s\n", sd, buffer);
// Envoyer une reponse (ici, renvoyer le meme message)
send(sd, buffer, strlen(buffer), 0);
}
}
}
}
// Nettoyage final
close(master_socket);
for (i = 0; i < MAX_CLIENTS; i++) {
if (client_socket[i] != 0) {
close(client_socket[i]);
}
}
return EXIT_SUCCESS;
}
Pour compiler : ‘gcc server-select.c -o server-select‘ Pour exécuter : ‘./server-select‘
Testez avec plusieurs instances du client TCP simple (‘./client-tcp‘). Vous verrez
un seul processus serveur gérer toutes les communications.
Exercice 7.4 :
Gestion de Clients Concurremment
[label=()]Modifiez le serveur forking pour qu’il ne se termine pas après une seule
connexion. Il doit continuer à accepter de nouvelles connexions et à créer de nou-
veaux processus enfants pour chaque client. Modifiez le serveur threading pour
qu’il soit plus robuste. Utilisez pthread_detach() pour chaque thread client afin
157
Programmation Système avec Linux et C Polytechnique Douala - Génie Logiciel N4
d’éviter d’avoir à joindre les threads, et assurez-vous que toutes les ressources
sont correctement libérées. (Défi) En utilisant l’API poll() (qui est similaire à
select() mais utilise des structures pollfd), implémentez un serveur de chat
simple où les messages envoyés par un client sont relayés à tous les autres clients
connectés.
158