Signaux & sémaphores
Synchroniser et faire dialoguer des processus Unix. Des interruptions logicielles asynchrones aux primitives POSIX.4 d'exclusion mutuelle.
📄 Ressources du cours
🎥 Vidéos recommandées
-
🇫🇷
Part 1 - Programmation Réseaux - Concepts de base
Professeur Mohamed YOUSSFI · 38:34 · 33k vues — AboutPressCopyrightContact usCreatorsAdvertiseDevelopersTermsPrivacyPolicy & SafetyHow YouTube worksTest new features · © 2024 Google LLC
-
🇫🇷
Cours programmation socket (réseau) n°6 p.1 - UDP en langage C
Jean-Baptiste Yunès · 29:58 · 8k vues — L'API en langage C de communication par paquet.L'envoi simple. Envoi-réponse, l'exemple d'un service ECHO.Plus d'informations : http://yunes.informatique.uni
-
🇬🇧
Socket Programming Tutorial In C For Beginners | Part 1 | Eduonix
Eduonix Learning Solutions · 40:07 · 243k vues — Learn Programming, anywhere anytime - http://bit.ly/Programming19 Sockets are the low-level endpoints used for processing information across a network. Some ...
1. Besoins de communication entre processus
On peut néanmoins créer une application multitâche et avoir besoin de synchroniser ces tâches ou d'échanger des informations entre elles. Les systèmes d'exploitation sont donc munis de mécanismes appropriés.
par signaux ou sémaphores.
par fichiers, tubes (pipes), files de messages (queues, boîtes à lettres), variables implantées dans un segment de mémoire partagée.
2. Qu'est-ce qu'un signal ?
- Les signaux sont identifiés par un numéro entier et un nom symbolique décrit dans
signal.h. - Comme pour une interruption matérielle, la réception d'un signal interrompt le traitement en cours et exécute automatiquement la fonction associée au signal (programmation événementielle).
- En langage C, l'association entre le numéro du signal et la fonction est réalisée par un appel à la fonction système
signal(). - La fonction
sigaction()peut être utilisée à la place designal().
Envoyé par un processus.
Reçu par un autre processus (éventuellement le même).
Acheminé par le noyau.
Comment réagit un processus qui reçoit un signal ?
- Il interrompt le traitement en cours.
- Il exécute la fonction de traitement du signal.
- Il reprend l'exécution du traitement interrompu.
- s'il est réveillé avant la fin d'une temporisation, il ne se rendort pas ;
- par contre s'il attendait la fin d'une entrée-sortie, il continue à attendre.
Comportement associé à la réception
3. Origine des signaux
CTRL-C, CTRL-\, CTRL-Z…
Violation mémoire :
SIGSEGVDivision par zéro :
SIGFPE…
kill(), alarm().
$ kill -KILL 2400
$ kill -9 2400
La commande ci-dessus envoie le signal SIGKILL au processus d'identité 2400.
4. Liste des signaux les plus fréquents
| N° | Nom | Description |
|---|---|---|
| 1 | SIGHUP | Terminaison du processus leader |
| 2 | SIGINT | Frappe d'interruption (CTRL-C) |
| 3 | SIGQUIT | Frappe de quit (CTRL-\) |
| 4 | SIGILL | Instruction illégale |
| 6 | SIGABRT | Problème matériel ou appel de la fonction abort() |
| 7 | SIGBUS | Une erreur liée à l'accès à la mémoire |
| 8 | SIGFPE | Erreur arithmétique |
| 9 | SIGKILL | Signal de terminaison — la réaction à ce signal ne peut être redéfinie |
| 10 | SIGUSR1 | Signal utilisateur |
| 11 | SIGSEGV | Violation écriture mémoire |
| 12 | SIGUSR2 | Signal utilisateur |
| 13 | SIGPIPE | Écriture sur un tube non ouvert en lecture |
| 14 | SIGALRM | Fin de temporisateur |
| 15 | SIGTERM | Terminaison normale d'un processus |
| 17 | SIGCHLD | Terminaison (arrêt) d'un fils |
| 18 | SIGCONT | Continuation d'un processus arrêté par SIGSTOP |
| 19 | SIGSTOP | Suspension d'exécution — réaction non redéfinissable |
| 20 | SIGTSTP | Frappe du caractère de suspension (CTRL-Z) |
SIGKILL (9) et SIGSTOP (19) ne peuvent ni être ignorés, ni être redéfinis.
5. Envoi de signaux en C — kill()
#include <sys/types.h>
#include <signal.h>
int kill(pid_t pid, int sig);
La primitive kill permet d'émettre le signal sig (désigné par son nom symbolique ou son numéro) à :
- le processus d'identité
pid, sipid > 0; - tous les processus du même groupe que le processus appelant, si
pid = 0; - tous les processus du groupe dont le
gidest la valeur absolue depid, sipid < -1.
0 en cas de succès et -1 sinon.
6. Les trois manières de répondre à un signal
1. Exécution de l'action par défaut
Cinq traitements par défaut sont possibles :
| Action | Effet | Exemples |
|---|---|---|
exit | Provoque la terminaison du processus | SIGINT, SIGKILL, SIGALRM |
core | Sauvegarde l'état de la mémoire et termine le processus | SIGFPE, SIGBUS, SIGSEGV |
stop | Suspend l'exécution du processus | SIGSTOP |
ignore | Le signal est ignoré | SIGCHLD |
continue | Le processus suspendu reprend son exécution ou le signal est ignoré | SIGCONT |
2. Ignorance du signal
SIGKILL et SIGSTOP.
3. Interception puis invocation d'une fonction (handler)
Le signal peut être rattrapé par le processus destinataire, ce qui provoque un déroutement et le lancement d'une routine spécifique de traitement (handler). Après le traitement, le processus reprend où il a été interrompu.
signal() et sigaction() à partir d'un programme C/C++ ; ou en utilisant la commande trap à partir du shell.Exemple :
trap 'commande' signal1 signal2 …
7. Pipeline d'un signal — vue d'ensemble
Le noyau filtre via le masque, met le signal en attente si bloqué, sinon déroute vers le handler ; puis le contexte interrompu est restauré.
8. Préparer la réception — signal()
#include <signal.h>
void (*signal(int signum, void (*handler)(int)))(int);
L'appel système signal() permet d'installer handler comme fonction de traitement lors de la réception du signal signum. Cette fonction ne pourra prendre qu'un argument de type entier : le numéro du signal qui l'aura appelée. La fonction renvoie SIG_ERR en cas d'échec.
signumLe signal à intercepter.
handlerPointeur sur une fonction qui gère le signal — c'est simplement en C l'identificateur de cette fonction — ou une constante :
•
SIG_DFL : comportement par défaut (terminaison)•
SIG_IGN : ignorer le signal
Récupérer le nom symbolique
#include <string.h>
char *strsignal(int sig);
La fonction strsignal permet de renvoyer le nom symbolique du signal correspondant au numéro sig.
Exemple complet
#include <stdio.h>
#include <signal.h>
#include <string.h>
#include <stdlib.h>
/* fonction de déroutement */
void handler(int sig) {
printf("Bien reçu %d %s\n", sig, strsignal(sig));
if (sig == SIGINT) {
printf("Fin volontaire\n");
exit(1);
}
}
void main(void) {
signal(SIGINT, handler); /* Ctrl-c : signal 2 */
signal(SIGQUIT, handler); /* Ctrl-\ : signal 3 */
signal(SIGTSTP, handler); /* Ctrl-z : signal 20 */
/* SIGKILL est non déroutable */
if (signal(SIGKILL, handler) == SIG_ERR) perror("SIGKILL");
for (;;);
}
Constantes SIG_IGN et SIG_DFL
La fonction de traitement peut être remplacée par une des constantes suivantes :
SIG_IGN— indique que le signal doit être ignoré.SIG_DFL— indique de rétablir l'action par défaut.
/* Rendre Ctrl-C (SIGINT) inopérant
l'arrêt de ce programme doit se faire avec kill -9 pid */
#include <signal.h>
void main(void) {
signal(SIGINT, SIG_IGN);
for (;;);
}
9. L'appel système sigaction()
La primitive sigaction() associe un signal à un contexte de déroutement, représenté par une structure sigaction.
struct sigaction {
void (*sa_handler)(); /* SIG_DFL ou SIG_IGN ou ptr sur handler */
sigset_t sa_mask; /* signaux supplémentaires à bloquer
pendant l'exécution du gestionnaire */
int sa_flags; /* indicateurs optionnels pour modifier
le comportement du signal */
};
sa_handler est le seul obligatoire ; c'est un pointeur sur la fonction qui servira de handler.
Exemple
#include <stdio.h>
#include <string.h>
#include <signal.h>
#include <stdlib.h>
void handler(int sig) {
printf("Bien reçu %d %s\n", sig, strsignal(sig));
if (sig == SIGINT) {
printf("Fin volontaire\n");
exit(1);
}
}
void main(void) {
struct sigaction act;
act.sa_handler = handler;
sigaction(SIGINT, &act, NULL); /* Ctrl-c : signal 2 */
sigaction(SIGQUIT, &act, NULL); /* Ctrl-\ : signal 3 */
sigaction(SIGTSTP, &act, NULL); /* Ctrl-z : signal 20 */
/* SIGKILL est non déroutable */
if (sigaction(SIGKILL, &act, NULL) == -1) perror("SIGKILL");
for (;;);
}
sa_mask
Permet de définir des signaux supplémentaires à bloquer pendant que le handler s'exécute — utile pour éviter qu'un signal en interrompe le traitement d'un autre.
10. Sémaphores — outils de coordination
Exemple : gestion d'un stock
- Un processus
p1lit dans un registre la valeur courante V du stock et veut la décrémenter. - Un autre processus
p2préempte le premier avant ; il lit la valeur courante (toujours V), la décrémente et met à jour le registre qui contient alors V-1. p1reprend son exécution et met à jour le registre en écrivant V-1, au lieu de V-2.
11. Pourquoi utiliser un sémaphore ?
Un sémaphore sert à la synchronisation entre les processus. Un sémaphore utilisé pour l'exclusion mutuelle (garantir qu'un seul processus accède à un moment donné à une variable partagée) utilisera 2 primitives :
Si la ressource est déjà utilisée, le processus est endormi et mis dans une file d'attente.
Si la file d'attente est non vide, le processus qui a dormi le plus longtemps est réveillé.
Remarque : aux sémaphores sont associés des droits d'accès de type Unix.
12. Le mécanisme des sémaphores
Un sémaphore est un objet composé :
- une variable (la valeur du sémaphore) ;
- une file d'attente (les processus bloqués attendant la ressource).
Primitives associées
Avec une valeur positive ou nulle.
Demande d'autorisation.
Si valeur > 0 → accord, sinon blocage.
Fin d'utilisation, restitution.
Éventuel déblocage d'un processus en file.
Algorithmes des primitives P et V
/* P(sémaphore) */
si (valeur[sémaphore] > 0) alors
valeur[sémaphore] = valeur[sémaphore] - 1
sinon
si (valeur[sémaphore] == 0)
étatProcessus = Bloqué
mettre processus en file d'attente
fsi
finSi
invoquer l'ordonnanceur
/* V(sémaphore) */
valeur[sémaphore] = valeur[sémaphore] + 1
si (valeur[sémaphore] > 0) alors
extraire processus de file d'attente
étatProcessus = Prêt
finSi
invoquer l'ordonnanceur
Initialisation(sémaphore, n) : valeur[sémaphore] = n.
13. Les différents types de sémaphores
- Valeur initiale positive ou nulle.
- Décrémentée à chaque fois que l'acquisition est accordée.
- Incrémentée quand le sémaphore est relâché.
- Quand la valeur devient nulle, la demande d'acquisition est bloquante.
- Ressource globale.
14. Sémaphores POSIX.4
Les sémaphores POSIX.4 existent sous deux formes :
Mécanisme de gestion analogue à un système de fichiers — accessible par tous les processus (selon les droits).
Basés sur la mémoire.
Sémaphores nommés — création
sem_t *sem_open(const char *sem_name,
int oflags,
mode_t creation_mode,
unsigned int initial_val);
sem_name: nom du sémaphore. Il est recommandé de fournir un nom « compatible » avec un nom de fichier, commençant par un/(barre oblique) et pouvant contenir jusqu'à 255 caractères (ex."/semaphore1").oflags:O_CREATpour créer un sémaphore ;O_CREAT | O_EXCLerreur si existe déjà.
initial_val: valeur initiale.creation_mode: permissions (mode de création) du sémaphore.
SEM_FAILED + errno.
Ouverture d'un sémaphore existant
sem_t *sem_open(const char *sem_name, int oflags);
- Accède à un sémaphore déjà créé.
oflags:O_CREAT: crée un sémaphore nommé s'il n'existe pas déjà ;O_EXCL: siO_CREATest spécifié et que le sémaphore existe déjà, l'appel échoue ;O_RDWR: ouvre le sémaphore en mode lecture/écriture (habituellement non utilisé pour les sémaphores).
Fermeture et suppression
int sem_close(sem_t *sem_id);
Ferme le sémaphore. Retour : renvoie 0, en cas d'erreur -1 + errno.
int sem_unlink(const char *sem_name);
Supprime le sémaphore. Permet de supprimer un sémaphore nommé du système, précédemment créé avec sem_open.
sem_unlink ne supprime pas immédiatement le sémaphore si des processus l'utilisent encore. Le sémaphore sera effectivement supprimé lorsque toutes les références à celui-ci seront fermées.
Retour : renvoie 0, en cas d'erreur -1 + errno.
Sémaphores anonymes
Sont des sémaphores qui n'ont pas de nom associé dans le système de fichiers. Contrairement aux sémaphores nommés, ils sont basés sur la mémoire.
int sem_init(sem_t *sem_location,
int pshared,
unsigned int initial_value);
sem_location: zone mémoire, éventuellement en zone partagée.pshared: visibilité du sémaphore :pshared == 0→ local au processus courant ;pshared != 0→ partagé entre plusieurs processus.
Retour : renvoie 0, en cas d'erreur -1 + errno.
int sem_destroy(sem_t *sem_location);
Détruit le sémaphore. Retour : renvoie 0, en cas d'erreur -1 + errno.
Opérations P() et V()
/* P() — verrouille (décrémente) */
int sem_wait(sem_t *sem_id);
/* V() — déverrouille (incrémente) */
int sem_post(sem_t *sem_id);
sem_wait
Décrémente (verrouille) le sémaphore pointé par sem_id.• Si
sem_id > 0 : décrémentation et retour immédiat.• Si
sem_id = 0 : le processus appelant est mis en attente jusqu'à ce qu'une autre entité incrémente la valeur (généralement avec sem_post), ce qui libère la ressource et réveille le processus.Retour :
0, ou -1 + errno.
sem_post
Incrémente le compteur et, s'il devient positif, réveille une tâche en attente.Retour :
0, ou -1 + errno.
Tableau récapitulatif POSIX.4
| Primitive | Forme | Rôle | Retour |
|---|---|---|---|
sem_open | nommé | Crée / ouvre un sémaphore nommé | adresse · SEM_FAILED |
sem_close | nommé | Ferme le sémaphore | 0 · -1 |
sem_unlink | nommé | Supprime (différé tant que des références existent) | 0 · -1 |
sem_init | anonyme | Initialise un sémaphore en mémoire | 0 · -1 |
sem_destroy | anonyme | Détruit le sémaphore | 0 · -1 |
sem_wait | P | Verrouille (bloque si valeur = 0) | 0 · -1 |
sem_post | V | Déverrouille / réveille un attendant | 0 · -1 |
15. Application — producteur / consommateur
Un classique : un buffer circulaire de taille N partagé entre un producteur (qui dépose) et un consommateur (qui retire). Trois sémaphores coordonnent l'accès :
empty = Ncompte les cases vides.
full = 0compte les cases pleines.
mutex = 1garantit l'exclusion mutuelle sur le buffer.
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <semaphore.h>
#define N 8
int buffer[N];
int in = 0, out = 0;
sem_t empty, full, mutex;
void *producteur(void *arg) {
int item = 0;
for (;;) {
item++; /* produire un item */
sem_wait(&empty); /* P(empty) — attend une case vide */
sem_wait(&mutex); /* P(mutex) — entrée section critique */
buffer[in] = item;
in = (in + 1) % N;
sem_post(&mutex); /* V(mutex) — sortie SC */
sem_post(&full); /* V(full) — une case pleine de plus */
}
return NULL;
}
void *consommateur(void *arg) {
int item;
for (;;) {
sem_wait(&full); /* P(full) — attend une case pleine */
sem_wait(&mutex); /* P(mutex) */
item = buffer[out];
out = (out + 1) % N;
sem_post(&mutex); /* V(mutex) */
sem_post(&empty); /* V(empty) — libère une case vide */
printf("consommé %d\n", item);
}
return NULL;
}
int main(void) {
pthread_t tp, tc;
sem_init(&empty, 0, N); /* N cases vides au départ */
sem_init(&full, 0, 0); /* 0 case pleine */
sem_init(&mutex, 0, 1); /* mutex binaire */
pthread_create(&tp, NULL, producteur, NULL);
pthread_create(&tc, NULL, consommateur, NULL);
pthread_join(tp, NULL);
pthread_join(tc, NULL);
sem_destroy(&empty);
sem_destroy(&full);
sem_destroy(&mutex);
return 0;
}
P(empty) ou P(full) avant P(mutex).Inverser l'ordre conduit à un interblocage : un producteur tient le mutex et attend une case vide qu'un consommateur ne peut libérer (le consommateur attend lui-même le mutex).
16. Réviser
❓ Q/R
Qu'est-ce qu'un signal ?
Une interruption logicielle asynchrone d'un processus, envoyée par un autre processus ou par le noyau, identifiée par un numéro entier et un nom symbolique dans signal.h.
Quels signaux ne peuvent pas être déroutés ni ignorés ?
SIGKILL (9) et SIGSTOP (19). Toute tentative de redéfinir leur comportement échoue (signal() renvoie SIG_ERR, sigaction() renvoie -1).
Quelle est la différence entre SIG_IGN et SIG_DFL ?
SIG_IGN = ignorer le signal. SIG_DFL = rétablir l'action par défaut du signal (souvent terminaison).
Quelles sont les 5 actions par défaut possibles ?
exit (terminer), core (dump mémoire + terminer), stop (suspendre), ignore (ne rien faire), continue (reprendre).
Que renvoie kill() et quelles sont les conditions pour qu'il réussisse ?
Renvoie 0 en cas de succès, -1 sinon. L'émetteur et le destinataire doivent être du même propriétaire, ou bien l'émetteur doit être root.
Que se passe-t-il si pid < -1 dans kill(pid, sig) ?
Le signal est envoyé à tous les processus du groupe dont le gid est égal à la valeur absolue de pid.
À quoi sert le champ sa_mask de struct sigaction ?
À définir des signaux supplémentaires qui doivent être bloqués pendant l'exécution du gestionnaire, pour éviter qu'ils interrompent le handler.
Quelle différence entre sémaphore nommé et anonyme ?
Un sémaphore nommé a un nom dans le système de fichiers ("/sem1") et est accessible par n'importe quel processus selon les droits Unix (sem_open, sem_close, sem_unlink). Un sémaphore anonyme est en mémoire (sem_init, sem_destroy) et la portée dépend du paramètre pshared.
Qu'est-ce qu'une section critique ?
La portion de programme située entre sem_wait et sem_post — c'est-à-dire entre les primitives « attendre » et « libérer » la ressource. Un seul processus à la fois peut y être présent quand on l'utilise pour l'exclusion mutuelle.
Que fait sem_wait quand la valeur du sémaphore vaut 0 ?
Le processus appelant est mis en attente jusqu'à ce qu'une autre entité fasse sem_post, ce qui libère la ressource et réveille le processus en attente.
🃏 Flashcards
Signal · définition
En une phrase ?
tourne →signal.h.SIGINT
N° + origine ?
tourne →SIGKILL / SIGSTOP
Particularité ?
tourne →SIGSEGV / SIGFPE
N° + action par défaut ?
tourne →core (dump + termine).signal()
Prototype ?
tourne →void (*handler)(int)))(int);
SIG_IGN vs SIG_DFL
Différence ?
tourne →struct sigaction
3 champs ?
tourne →sa_handler (obligatoire), sa_mask (signaux bloqués), sa_flags (indicateurs).kill(pid, sig)
Cas pid = 0 ?
sig à tous les processus du même groupe que l'appelant.Sémaphore — composition
De quoi est-il composé ?
tourne →P() et V()
Autre nom + rôle ?
tourne →sem_open · 4 args
Lesquels ?
tourne →mode, initial_val)
→ sem_t* ou SEM_FAILED
sem_init · pshared
0 vs ≠0 ?
tourne →sem_unlink
Effet si encore utilisé ?
tourne →Producteur / consommateur
Combien de sémaphores et lesquels ?
tourne →empty=N, full=0, mutex=1.✎ Quiz éclair
signal() retourne en cas d'échec :SIG_ERR est la valeur conventionnelle d'échec pour signal() (sigaction renvoie -1).kill(0, SIGUSR1) ?pid = 0, le signal est diffusé à tout le groupe de l'appelant.struct sigaction est :sem_wait sur un sémaphore à 0 :P() doivent-elles être faites côté producteur ?📌 À retenir
- Signal = interruption logicielle asynchrone · identifié par n° + nom dans
signal.h - Cycle : interrompt → exécute handler → reprend où il en était
- 3 réponses : action par défaut · ignorer · intercepter (handler)
- 5 actions par défaut :
exit,core,stop,ignore,continue - Non déroutables :
SIGKILL(9),SIGSTOP(19) kill(pid, sig)→0/-1·pid>0proc ·=0groupe ·<-1|pid|signal(sig, handler)renvoieSIG_ERRen cas d'échec ;SIG_IGN/SIG_DFLsigaction:sa_handler(obligatoire),sa_mask,sa_flags- Sémaphore = variable + file d'attente · primitives P (Wait) et V (Signal)
- Section critique = code entre P() et V()
- POSIX.4 : nommés (
sem_open,sem_close,sem_unlink) · anonymes (sem_init,sem_destroy) sem_waitbloque si valeur = 0 ·sem_postréveille un attendant- Nom de sémaphore : commence par
/, jusqu'à 255 car. (ex."/sem1") sem_init(sem, pshared, init)·pshared=0local ·≠0partagé- Producteur/consommateur :
empty=N,full=0,mutex=1· P(empty/full) avant P(mutex)