.. _processus:
Partie 4 - Processus
====================
.. raw:: html
Support présentation
Qu'est-ce qu'un processus ?
---------------------------
Un **processus** est une instance d'un programme en cours d'exécution.
Un même programme peut donc avoir plusieurs exécutions simultanées.
Nous pouvons faire le parallèle avec la programmation objet où une classe (``Voiture``) est un programme et chaque instanciation de celle-ci (``ma_voiture_rouge``, ``ma_voiture_bleue``, ...) est un processus.
Un processus est une unité gérée par le système d'exploitation, qui dispose de ses propres ressources :
- une **zone mémoire privée** (code, données, pile, tas, ...)
- un **PID** (*Process ID*) : identifiant unique du processus
- un **PPID** (*Parent PID*) : identifiant du processus parent
- un **UID** (*User ID*) : utilisateur propriétaire
- des **descripteurs de fichiers** (liaisons avec fichiers, entrées/sorties, sockets, ...)
- un **contexte d'exécution** (compteur ordinal, registres CPU, état)
Toutes ces informations sont regroupées dans une structure noyau appelée **PCB** (*Process Control Block*).
Un processus peut engendrer d'autres processus (*processus enfants*), formant une arborescence.
On peut l'afficher avec ``pstree`` (``-p`` pour afficher les PID) :
.. code-block:: bash
pstree -p
Le processus initial lancé au démarrage (PID 1) est souvent nommé ``init`` ou ``systemd``.
Il adopte les processus orphelins et supervise de nombreux services (fichiers, périphériques, réseau, etc.).
La commande ``ps`` permet d'afficher les processus, par défaut uniquement ceux rattachés à la session courante :
.. code-block:: console
$ # & lance la commande en arrière plan, fg permet de la récupérer ensuite
$ sleep 10 &
[1] 3816906
$ ps
PID TTY TIME CMD
3812988 pts/2 00:00:00 bash
3816906 pts/2 00:00:00 sleep
3817006 pts/2 00:00:00 ps
Différentes options permettent d'afficher plus d'informations comme ``a`` pour aussi voir les processus lancés par les autres utilisateurs, ``u`` pour donner plus d'informations ou `x` pour montrer les processus qui n'ont pas de terminal de contrôle :
.. code-block:: console
$ ps aux
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
root 1 0.0 0.0 168072 11744 ? Ss 13:17 0:02 /sbin/init splash
root 2 0.0 0.0 0 0 ? S 13:17 0:00 [kthreadd]
root 3 0.0 0.0 0 0 ? S 13:17 0:00 [pool_workqueue_release]
root 4 0.0 0.0 0 0 ? I< 13:17 0:00 [kworker/R-rcu_g]
root 5 0.0 0.0 0 0 ? I< 13:17 0:00 [kworker/R-rcu_p]
root 6 0.0 0.0 0 0 ? I< 13:17 0:00 [kworker/R-slub_]
root 7 0.0 0.0 0 0 ? I< 13:17 0:00 [kworker/R-netns]
root 9 0.0 0.0 0 0 ? I< 13:17 0:00 [kworker/0:0H-events_highpri]
root 12 0.0 0.0 0 0 ? I< 13:17 0:00 [kworker/R-mm_pe]
root 13 0.0 0.0 0 0 ? I 13:17 0:00 [rcu_tasks_kthread]
root 14 0.0 0.0 0 0 ? I 13:17 0:00 [rcu_tasks_rude_kthread]
root 15 0.0 0.0 0 0 ? I 13:17 0:00 [rcu_tasks_trace_kthread]
Dans un même terminal, un seul processus peut tourner à la fois en avant-plan (*foreground*), pendant que d'autres peuvent tourner en arrière-plan (*background*).
Pour lancer un programme en arrière plan, comme vu ci-dessus, ajoutez un ``&`` après la commande :
.. code-block:: shell
$ # gedit ouvre l'éditeur dans la fenêtre, vous pouvez remplacer par nano sinon
$ gedit fichier.txt
$ # en fermant la fenêtre on reprend la main sur le terminal
$ # en ajoutant & :
$ gedit fichier.txt &
[1] 200404
$ # gedit s'ouvre en arrière plan et le terminal est toujours accessible
$ # le 200404 est le pid du processus lancé avec gedit
Cas d'utilisation de fork (processus)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
- Exécution d'un autre programme : un shell utilise fork + exec pour lancer une nouvelle commande (ls, gcc, …).
- Isolation et robustesse : un crash dans un processus n'affecte pas les autres (contrairement aux threads).
- Services système : démons/serveurs Unix (ex. sshd, cron) créent un processus enfant par connexion ou tâche.
- Sécurité : sandboxing ou chroot → isoler un processus potentiellement dangereux.
- Traitement concurrent simple : lancer plusieurs programmes indépendants (ex. simulation parallèle où chaque processus travaille sur une portion différente de données, puis communication via pipes/sockets).
Cycle de vie d'un processus
---------------------------
Au cours de son exécution, un processus passe par plusieurs **états** :
- **Nouveau** (*new*) : créé par ``fork`` et initialisé en mémoire. Il est chargé en mémoire et l'ordonnanceur (*scheduler*) le passe en état prêt (*ready*)
- **Prêt** (*ready*) : en attente d'être ordonnancé
- **Exécution** (*running*) : le processus utilise le CPU
- **Bloqué** (*blocked*) : en attente d'un événement (I/O, signal). Il repasse en état prêt quand l'événement survient
- **Stoppé** (*Stopped*) : suspension volontaire/forcée par un ``SIGSTOP`` ou un debugger
- **Zombie** : terminé mais non récupéré par son parent (via ``wait`` ou ``waitpid``)
- **Terminé** (*terminated*) : effacé de la table des processus
.. .. image:: _images/process_states.png
.. :alt: Schéma du cycle de vie d'un processus
.. :align: center
.. :width: 500px
Un processus peut aussi être interrompu par le noyau :
- **OOM Killer** (*Out-Of-Memory Killer*) : si le système manque de mémoire (RAM + swap), le noyau Linux peut tuer avec un signal ``SIGKILL`` un ou plusieurs processus pour libérer de la mémoire (en priorité les plus gourmands ou moins prioritaires)
- **Limites d'exécution** : temps CPU, fichiers ouverts, etc.
- **Erreurs critiques** :
- ``SIGSEGV`` : accès mémoire invalide
- ``SIGFPE`` : division par zéro
- ``SIGILL`` : instruction invalide
- ``SIGBUS`` : accès mémoire mal aligné
Lorsqu'un utilisateur ferme sa session ou que la machine s'éteint, les processus sont tués, sauf s'ils ont été détachés (``nohup``, ``screen``, ``tmux``).
Création de processus : ``fork``
--------------------------------
Un processus est créé par ``fork()``, qui duplique le processus courant.
L'enfant hérite de la mémoire, des descripteurs de fichiers et du contexte.
La fonction ``getpid()`` retourne le PID du processus et ``getppid()`` celui du processus parent.
La fonction ``wait()`` permet au parent d'attendre la fin d'exécution de l'enfant avant de terminer le programme.
.. literalinclude:: ../code/fork.c
:language: c
:linenos:
.. literalinclude:: ../code/fork_output.txt
:language: console
Après un ``fork`` :
- Dans le **parent**, la valeur de retour est le **PID de l'enfant**.
- Dans l\'**enfant**, la valeur de retour est **0**.
- Si erreur, retourne -1 dans le parent pour signaler que l'enfant n'a pas pu être créé.
La mémoire est copiée de façon logique grâce au mécanisme *Copy-On-Write* (voir :ref:`mémoire `), les pages mémoire sont réellement dupliquées qu'en cas de modification.
.. figure:: _images/processus_child_parent.excalidraw.png
:alt: Schéma présentant la création d'une variable ``int i = 4;`` suivi de la création d'un fork avec ``pid_t pid = fork();``, dans le processus enfant, ``i = 2`` ce qui ne modifie pas la valeur de ``i`` dans le parent car la mémoire est dupliquée entre le parent et l'enfant.
:width: 400px
:align: center
Synchronisation parent / enfant : ``wait``
------------------------------------------
En général, il faut mieux qu'un parent attende la terminaison de ses enfants avant qu'il se termine lui même.
L'appel à ``wait`` ou ``waitpid`` (````, ``man 2 wait``) permet au parent d'attendre l'enfant :
.. code-block:: c
int status;
if (wait(&status) == -1) {
perror("wait");
exit(1);
}
if (WIFEXITED(status)) {
printf("Code retour : %d\n", WEXITSTATUS(status));
} else if (WIFSIGNALED(status)) {
printf("Tué par signal %d\n", WTERMSIG(status));
}
``waitpid`` permet de cibler un enfant en particulier.
.. code-block:: c
waitpid(pid, &status, 0);
Dans les deux cas, ``status`` est l'adresse d'un entier où sera enregistrée le compte rendu de terminaison.
Remplacement d'un processus : ``exec``
--------------------------------------
La famille ``exec*`` remplace l'image mémoire du processus courant par un nouveau programme.
Le PID reste identique, mais tout le code et les données sont remplacés.
Exemple :
.. code-block:: c
#include
int main() {
// lance la commande ls située dans /bin/ls avec l'argument -l
execl("/bin/ls", "ls", "-l", NULL);
// si exec fonctionne sans problème, cette ligne n'est jamais atteinte
// si exec échoue (execl retourne -1), affiche l'erreur correspondante
perror("execl");
return 1;
}
Variantes de ``exec``
~~~~~~~~~~~~~~~~~~~~~
Toutes ces fonctions remplacent l'image mémoire du processus courant par un nouveau programme.
La différence porte sur comment on passe les arguments et l'environnement.
- ``execl(path, arg0, arg1, ..., NULL)``
Arguments passés séparément (liste).
.. code-block:: c
execl("/bin/ls", "ls", "-l", NULL);
- ``execv(path, argv)``
Arguments passés dans un tableau de chaînes.
.. code-block:: c
char *args[] = {"ls", "-l", NULL};
execv("/bin/ls", args);
- ``execle(path, arg0, ..., NULL, envp)``
Comme ``execl`` mais permet de fournir un environnement spécifique.
.. code-block:: c
char *env[] = {"MYVAR=123", NULL};
execle("/usr/bin/env", "env", NULL, env);
- ``execve(path, argv, envp)``
Version système de base (toutes les autres s'appuient dessus).
Demande explicitement arguments + environnement.
.. code-block:: c
char *args[] = {"env", NULL};
char *env[] = {"MYVAR=hello", NULL};
execve("/usr/bin/env", args, env);
- ``execlp(file, arg0, ..., NULL)``
Comme ``execl`` mais recherche le fichier dans le ``PATH``.
.. code-block:: c
execlp("ls", "ls", "-l", NULL); // inutile de préciser /bin/ls
- ``execvp(file, argv)``
Comme ``execv`` mais recherche dans le ``PATH``.
.. code-block:: c
char *args[] = {"ls", "-l", NULL};
execvp("ls", args);
- ``execvpe(file, argv, envp)`` *(GNU/Linux uniquement)*
Comme ``execvp`` mais permet aussi de passer un environnement spécifique.
.. code-block:: c
char *args[] = {"env", NULL};
char *env[] = {"MYVAR=from_execvpe", NULL};
execvpe("env", args, env);
- ``fexecve(fd, argv, envp)``
Lance un programme à partir d'un **descripteur de fichier ouvert** (``fd``).
Pratique pour exécuter un binaire déjà ouvert, sans dépendre de son chemin.
.. code-block:: c
int fd = open("/bin/ls", O_RDONLY);
char *args[] = {"ls", "-l", NULL};
char *env[] = {NULL};
fexecve(fd, args, env);
Résumé mnémotechnique des variantes ``exec*`` :
Passage des arguments :
- Si la fonction contient un ``l`` → liste d'arguments.
- Si la fonction contient un ``v`` → vecteur (tableau argv[])
``PATH`` :
- Si la fonction contient un ``p`` → recherche dans le PATH, sinon pas de recherche et il faut donner le chemin complet
Environnement :
- Si la fonction contient un ``e`` → il faut passer les variables d'environnement en argument sinon le même environnement est conservé
Cas particulier : fexecve → utilise un **descripteur de fichier ouvert** (fd) au lieu d'un chemin
Usage classique : **fork + exec** pour lancer un autre programme dans un enfant :
.. code-block:: c
pid_t pid = fork();
if (pid == 0) {
execl("/bin/date", "date", NULL);
perror("execl");
_exit(1);
}
.. note::
**Pourquoi utiliser ``_exit()`` et non ``exit()`` après un ``fork`` raté ?**
- ``exit()`` est une fonction de la **libc** : elle exécute les fonctions
enregistrées avec ``atexit()``, vide les tampons stdio (``printf``, ``fprintf``),
ferme les fichiers, etc.
- Après un ``fork()``, le processus enfant hérite **des tampons et descripteurs du parent**.
Appeler ``exit()`` dans l'enfant peut donc provoquer des effets indésirables :
par exemple, réimprimer du texte déjà présent dans un tampon ou fermer
deux fois un fichier hérité.
- ``_exit()`` est l'**appel système direct** qui termine immédiatement le processus,
sans passer par ces étapes de nettoyage. C'est le choix sûr
lorsqu'un ``exec*`` échoue dans l'enfant.
**Règle pratique :**
- Dans l\'**enfant**, après un ``exec*`` raté → utiliser ``_exit()``
- Dans le **parent**, ou en fin de programme → utiliser ``exit()``
Zombies et orphelins
--------------------
- **Zombie** : processus terminé dont le parent n'a pas encore lu le code retour.
- visible dans ``ps`` avec l'état ``Z``
- disparaît dès que le parent fait ``wait``
- **Orphelin** : processus dont le parent est mort → adopté par ``init`` (PID 1).
Priorités et ordonnanceur
-------------------------
Le noyau Linux planifie les processus grâce à un **ordonnanceur** (*scheduler*).
Chaque processus se voit attribuer du temps CPU selon une **politique d'ordonnancement** et une **priorité**.
Politiques d'ordonnancement
~~~~~~~~~~~~~~~~~~~~~~~~~~~
Les politiques définissent comment le CPU est partagé :
- ``SCHED_OTHER`` (*CFS – Completely Fair Scheduler*) : politique par défaut, temps partagé équitable entre tous les processus.
- ``SCHED_FIFO`` (*First In First Out*) : temps réel, priorité stricte, les processus tournent tant qu'ils ne bloquent pas volontairement (pas de préemption par un autre FIFO de même priorité).
- ``SCHED_RR`` (*Round Robin*) : temps réel, similaire à FIFO mais avec un quantum de temps fixe par processus.
On peut visualiser/choisir la politique d'un processus avec la commande ``chrt`` :
.. code-block:: console
# Afficher la politique et la priorite du processus
chrt -p
# Lancer un processus en temps réel round-robin avec priorité 20
sudo chrt -r 20 ./mon_prog
Nice et priorité utilisateur
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Chaque processus possède une valeur de **niceness** (gentillesse) qui influence sa priorité CPU (avec ``SCHED_OTHER``).
- Plage de ``-20`` (très prioritaire) à ``+19`` (très gentil, peu prioritaire).
- Plus la valeur est **basse**, plus le processus reçoit de CPU.
Exemples :
.. code-block:: bash
# Lancer un programme avec une priorité plus faible (nice=10)
nice -n 10 ./long_calcul
# Modifier la priorité d'un processus existant
renice -5 -p PID
Afficher la priorité et la valeur nice avec ``ps`` :
.. code-block:: bash
ps -o pid,comm,pri,ni -p
# Exemples de colonnes
PID COMMAND PRI NI
4242 mon_prog 25 0
- **PRI** : priorité interne du noyau (calculée à partir de NI)
- **NI** : valeur nice visible/utilisateur
Limites de ressources
~~~~~~~~~~~~~~~~~~~~~
En plus de la priorité, Linux permet de fixer des **limites** par processus, via ``ulimit`` (bash) ou ``setrlimit`` (C en POSIX).
Exemples :
.. code-block:: bash
ulimit -a # afficher toutes les limites
ulimit -t 5 # limiter le temps CPU à 5 secondes
ulimit -n 64 # limiter le nombre de fichiers ouverts à 64
Ces limites évitent qu'un processus consomme toutes les ressources du système (ex. boucle infinie, fuite mémoire).
Résumé
~~~~~~
- **Politique** = règle de planification (CFS, FIFO, RR)
- **Nice** = priorité relative utilisateur (-20 à +19)
- **Limites** = garde-fous pour éviter les abus
Un processus très "méchant" (``nice = -20``) en temps réel (``SCHED_FIFO``) peut monopoliser le CPU et bloquer tout le reste → à utiliser avec prudence !
Outils d'observation
--------------------
- ``ps -o pid,ppid,stat,cmd -p `` : état du processus
- ``top`` / ``htop`` : charge CPU/mémoire en temps réel
- ``pstree -p`` : hiérarchie parent/enfant
- ``strace -f ./prog`` : trace des appels système
- ``/proc//`` : informations détaillées sur le processus
Exemple :
.. code-block:: console
$ echo $$
12345
$ ls /proc/$$
attr cmdline cwd environ fd/ maps status task
Après le fork, le parent et l'enfant sont des copies indépendantes.
L'enfant reçoit une copie virtuelle exacte de l'espace mémoire du parent (heap, stack, variables globales, ...).
Les pages mémoire sont donc copiés pour chaque enfant car il y a eu une modification.
Voir l'exemple suivant où une variable est modifiée dans le parent :
.. literalinclude:: ../code/fork_variable.c
:language: c
:linenos:
.. code-block:: console
parent : ma_variable = 4 (adresse = 0x7ffd055fdc60)
child : ma_variable = 4 (adresse = 0x7ffd055fdc60)
parent : ma_variable = 5 (adresse = 0x7ffd055fdc60)
child : ma_variable = 4 (adresse = 0x7ffd055fdc60)
On peut voir que l'adresse de la variable est la même, pourtant, la valeur n'est plus la même.
C'est le cas parce que chaque processus possède sa propre mémoire virtuelle avec sa propre copie des pages mémoires (voir :ref:`mémoire `) copiée uniquement en cas de modification grâce au *Copy-on-Write*.
Les pages mémoire sont marquées en lecture seule et partagées temporairement entre les processus, si l'un des deux modifie une variable, le noyau copie alors la page à ce moment là pour garantir l'isolation.
.. note::
**Exploration pratique : le répertoire ``/proc``**
Sous Linux, chaque processus dispose d'un dossier dans le pseudo-système de fichiers ``/proc`` :
- ``/proc//`` contient des fichiers représentant l'état et les ressources du processus.
- ``/proc/self/`` est un raccourci vers le dossier du processus courant.
Exemples :
.. code-block:: console
$ echo $$ # Affiche le PID du shell courant
12345
$ ls /proc/$$ # Liste les fichiers associés à ce processus
attr cwd fd maps status
cmdline environ mem mounts task
$ cat /proc/$$/cmdline # Commande ayant lancé ce processus
/bin/bash
$ head -n 5 /proc/$$/status
Name: bash
State: S (sleeping)
Tgid: 12345
Pid: 12345
PPid: 678
Quelques fichiers utiles :
- ``cmdline`` : commande ayant lancé le processus
- ``status`` : informations détaillées (UID, état, mémoire, etc.)
- ``fd/`` : descripteurs de fichiers ouverts
- ``maps`` : mapping mémoire du processus
Ce mécanisme montre que le noyau expose les processus comme des fichiers, ce qui permet de les interroger facilement sans appel système compliqué.
Résumé pratique : modèle ``fork`` → ``exec`` → ``wait``
-------------------------------------------------------
Schéma classique pour lancer un nouveau programme :
1. le parent fait ``fork``
2. l'enfant fait ``exec``
3. le parent fait ``wait``
.. code-block:: c
:linenos:
#include
#include
#include
#include
int main(void) {
pid_t pid = fork();
if (pid < 0) { perror("fork"); exit(1); }
if (pid == 0) {
execl("/bin/ls", "ls", "-l", NULL);
perror("execl");
_exit(1);
} else {
int status = 0;
waitpid(pid, &status, 0);
if (WIFEXITED(status))
printf("Code retour = %d\n", WEXITSTATUS(status));
}
}
Vérifiez toujours la valeur de retour des appels système (``fork``, ``exec``, ``open``, ...). En cas d'échec, utilisez ``perror()`` pour afficher un message clair sur la cause de l'erreur (voir :ref:`gestion des erreurs `).
.. _ipc:
Communication inter-processus
-----------------------------
Introduction
~~~~~~~~~~~~
Un **processus** est isolé : il possède sa propre mémoire virtuelle et ne peut pas accéder directement à la mémoire des autres processus.
Pour collaborer, les processus ont besoin de mécanismes de **communication inter-processus (IPC – Inter Process Communication)**.
Linux (et POSIX en général) propose plusieurs mécanismes IPC :
- **Signaux** : messages simples envoyés par le noyau ou un autre processus (ex. SIGINT, SIGKILL).
- **Pipes** (tubes) : communication en flux entre processus apparentés.
- **FIFOs** (ou *named pipes*) : tubes nommés accessibles entre processus non apparentés.
- **Files de messages** : envoi/reception de messages structurés. (non abordés dans ce cours)
- **Mémoire partagée** : plusieurs processus accèdent à une même zone mémoire. (non abordés dans ce cours)
- **Sémaphores** : synchronisation d'accès à des ressources partagées. (non abordés dans ce cours)
- **Sockets** : communication locale ou réseau (voir chapitre :ref:`Sockets `).
Signaux
~~~~~~~
Un **signal** est un mécanisme asynchrone de communication inter-processus.
Il permet d'envoyer une interruption logicielle à un processus.
Il peut être généré par :
- le clavier (Ctrl+C → ``SIGINT``),
- un autre processus (``kill``),
- le noyau (division par zéro → ``SIGFPE``).
La commande ``kill -L`` permet de lister les différents signaux (ou voir section 7 du man pour ``signal``) :
.. code-block:: console
> kill -L
1) SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL 5) SIGTRAP
6) SIGABRT 7) SIGBUS 8) SIGFPE 9) SIGKILL 10) SIGUSR1
11) SIGSEGV 12) SIGUSR2 13) SIGPIPE 14) SIGALRM 15) SIGTERM
16) SIGSTKFLT 17) SIGCHLD 18) SIGCONT 19) SIGSTOP 20) SIGTSTP
21) SIGTTIN 22) SIGTTOU 23) SIGURG 24) SIGXCPU 25) SIGXFSZ
26) SIGVTALRM 27) SIGPROF 28) SIGWINCH 29) SIGIO 30) SIGPWR
31) SIGSYS 34) SIGRTMIN 35) SIGRTMIN+1 36) SIGRTMIN+2 37) SIGRTMIN+3
38) SIGRTMIN+4 39) SIGRTMIN+5 40) SIGRTMIN+6 41) SIGRTMIN+7 42) SIGRTMIN+8
43) SIGRTMIN+9 44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13
48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12
53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9 56) SIGRTMAX-8 57) SIGRTMAX-7
58) SIGRTMAX-6 59) SIGRTMAX-5 60) SIGRTMAX-4 61) SIGRTMAX-3 62) SIGRTMAX-2
63) SIGRTMAX-1 64) SIGRTMAX
Depuis le terminal, ils peuvent être utilisés de 3 manières :
.. code-block:: bash
# avec leur numéro :
kill -9
# le nom du signal complet :
kill -SIGKILL
# le nom du signal partiel :
kill -KILL
Parmi les signaux les plus courants, nous retrouvons :
- ``SIGINT`` : interruption (Ctrl+C)
- ``SIGTERM`` : demande d'arrêt
- ``SIGKILL`` : arrêt forcé (non interceptable)
- ``SIGCHLD`` : notification de fin d'un enfant
- ``SIGHUP`` : fermeture de terminal / déconnexion
- ``SIGSTOP`` / ``SIGTSTP`` : arrêt / suspension (Ctrl+Z)
- ``SIGCONT`` : reprise d'un processus stoppé
Exemple d'utilisation :
.. code-block:: console
$ # ouverture de mon_fichier.txt avec gedit en arrière-plan
$ gedit mon_fichier.txt &
[1] 202266
$ # envoi d'un signal SIGINT au processus
$ kill -SIGINT 202266
[1]+ Interrompre gedit mon_fichier.txt
Un processus peut intercepter un signal avec une fonction ayant comme signature ``void on_signal(int sig)`` que l'on appelle *handler* (gestionnaire).
On met en suite en place le gestionnaire avec la fonction (voir ``man 2 sigaction``) :
.. code-block:: c
#include
int sigaction(
int signum,
const struct sigaction *act,
struct sigaction *oldact
);
On déclare une ``struct sigaction`` où on initialise le champ ``sigset_t sa_mask`` à l'ensemble **vide** (``sigemptyset(&sa.sa_mask);``) pour ne bloquer aucun signal supplémentaire pendant l'exécution du gestionnaire (le signal courant est déjà bloqué par défaut, sauf si l'on met ``SA_NODEFER``), on positionne en général ``sa_flags`` à ``SA_RESTART`` pour que certains appels bloquants (``read``, ``nanosleep``, etc.) soient relancés automatiquement s'ils sont interrompus par un signal (mettre ``0`` si, au contraire, vous souhaitez gérer un échec avec ``EINTR``), puis on initialise ``sa_handler`` vers la fonction gestionnaire.
Pour finir, on donne à sigaction le numéro de signal à bloquer (défini avec des macro comme ``SIGINT``), la ``struct sigaction`` et ``NULL`` et le signal ``SIGINT`` pourra être intercepté par le programme.
.. Plusieurs fonctions permettent la gestion d'un ensemble de signaux :
.. .. code-block:: c
.. // initialiser (vide)
.. int sigemptyset(sigset_t *ens);
.. // remplir
.. int sigfillset(sigset_t *ens);
.. // ajouter
.. int sigaddset(sigset_t *ens, int sig);
.. // supprimer
.. int sigdelset(sigset_t *ens, int sig);
.. // tester
.. int sigismember(sigset_t *ens, int sig);
Exemple, intercepter SIGINT (Ctrl+C) et afficher le signal :
.. code-block:: c
:linenos:
#include // sigaction, sig_atomic_t, SIGINT
#include // printf, perror
#include // EXIT_SUCCESS/EXIT_FAILURE
#include // strsignal
#include // pause
// Variables modifiées par le handler : type sûr et signal-safe
// indicateur qu'un signal est reçu
static volatile sig_atomic_t stop = 0;
// numéro du dernier signal reçu
static volatile sig_atomic_t last_signal = 0;
// Handler qui récupère le signal utilisé et l'enregistre dans last_signal
void on_signal(int sig) {
// sig est le numéro du signal envoyé
last_signal = sig;
// demande l'arrêt de la boucle d'attente
stop = 1;
}
int main(void) {
struct sigaction sa = {0};
// fonction à appeler à la réception du signal
sa.sa_handler = on_signal;
// aucun signal supplémentaire masqué pendant le handler
sigemptyset(&sa.sa_mask);
// tenter de relancer certains appels bloquants
sa.sa_flags = SA_RESTART;
if (sigaction(SIGINT, &sa, NULL) == -1) {
perror("sigaction");
return EXIT_FAILURE;
}
printf("PID = %d. Appuyez sur Ctrl+C pour envoyer SIGINT.\n", getpid());
// Attente passive d'un signal
while (!stop) {
// endort le processus jusqu'à réception d'un signal
pause();
}
printf("\nSignal reçu : %d (%s)\n",
(int)last_signal,
strsignal((int)last_signal));
return EXIT_SUCCESS;
}
Output :
.. code-block:: console
PID = 12345. Appuyez sur Ctrl+C pour envoyer SIGINT.
^C
Signal reçu : 2 (Interrupt)
``struct sigaction`` permet de définir **comment** un processus réagit à un signal :
- ``sa_handler`` : Pointeur de fonction appelé à la réception du signal (prototype ``void (*)(int)``).
- ``sa_sigaction`` : Handler "étendu" (prototype ``void (*)(int, siginfo_t *, void *)``) utilisé si le flag ``SA_SIGINFO`` est activé dans ``sa_flags``. Permet d'accéder à des infos supplémentaires dans ``siginfo_t`` (PID émetteur, code d'erreur, adresse fautive pour ``SIGSEGV``, etc.).
- ``sa_mask`` : Ensemble de signaux à masquer automatiquement pendant l'exécution du handler. On le prépare avec les fonctions sur *signal set* :
- ``sigemptyset(&sa.sa_mask)`` : vide l'ensemble
- ``sigaddset(&sa.sa_mask, SIGTERM)`` : ajoute un signal à masquer
- ``sigdelset`` / ``sigismember`` : retirer / tester
- ``sa_flags`` : Options de comportement, les plus utiles :
- ``SA_RESTART`` : relance certains appels bloquants interrompus par le signal (ex. ``read``, ``nanosleep``)
- ``SA_SIGINFO`` : active ``sa_sigaction`` au lieu de ``sa_handler`` (handler 3 arguments)
- ``SA_NOCLDSTOP`` : ne pas recevoir ``SIGCHLD`` quand un enfant est stoppé (seulement quand il meurt)
- ``SA_NOCLDWAIT`` : ne crée pas de zombies pour les enfants (le noyau les "récolte" automatiquement)
- ``SA_NODEFER`` : le signal courant n'est pas bloqué pendant l'exécution du handler (attention aux réentrances)
- ``SA_RESETHAND`` : après la première exécution, rétablit ``SIG_DFL`` pour ce signal
Explications (libc et POSIX utilisées) :
- ``volatile sig_atomic_t`` :
- ``sig_atomic_t`` est un type entier que la norme garantit atomique vis-à-vis des signaux (un accès atomique signifie que la lecture ou l'écriture se fait en une seule opération indivisible, même si un signal interrompt le programme).
- ``volatile`` informe le compilateur que la valeur peut changer de façon asynchrone (par le handler).
- On l'utilise pour communiquer proprement entre le handler et le code principal (ici ``stop`` et ``last_signal``).
- ``pause()`` : met le processus en sommeil jusqu'à la réception d'un signal. Quand le signal arrive, le handler s'exécute, puis ``pause()`` se réveille (avec une erreur ``EINTR`` interne).
- ``strsignal(int)`` : retourne une chaîne lisible correspondant au numéro de signal (``Interrupt`` pour ``SIGINT``)
- ``printf`` / ``perror`` :
- Faciles pour afficher des messages hors handler
- À éviter dans le handler (non *async-signal-safe*, si deux signaux arrivent même temps par exemple)
Pipes anonymes
~~~~~~~~~~~~~~
Un **pipe** (tube en français) est un canal de communication unidirectionnel entre deux processus apparentés (souvent un parent et son enfant).
.. code-block:: c
#include
int pipe(int fd[2]);
Ou ``fd`` donne des descripteurs de fichier :
- ``fd[0]`` : extrémité **lecture** (sortie du tube).
- ``fd[1]`` : extrémité **écriture** (entrée du tube).
La lecture et l'écriture se fait avec ``read`` (``man 2 read``) et ``write`` (``man 2 write``).
Si on veut échanger des messages dans les deux sens, alors il faut créer deux tubes.
Il faut bien penser à fermer l'extrémité inutile dans chaque processus.
.. code-block:: c
:linenos:
#include
#include
#include
#include
#include
int main() {
int fd[2]; // fd[0] = lecture, fd[1] = écriture
// Création du pipe
if (pipe(fd) == -1) {
perror("pipe");
exit(EXIT_FAILURE);
}
pid_t pid = fork();
if (pid < 0) {
perror("fork");
exit(EXIT_FAILURE);
}
if (pid > 0) {
// Parent
close(fd[0]); // ferme le côté lecture
char message[] = "Bonjour du parent !";
write(fd[1], message, strlen(message) + 1);
close(fd[1]); // ferme le côté écriture
} else {
// Enfant
close(fd[1]); // ferme le côté écriture
char buffer[100];
read(fd[0], buffer, sizeof(buffer));
printf("Enfant a reçu : %s\n", buffer);
close(fd[0]); // ferme le côté lecture
}
return 0;
}
Il est aussi possible de simplifier les I/O avec les fonctions ``fprintf``/``fgets`` en utilisant un ``FILE *`` avec la fonction ``fdopen`` :
.. code-block:: c
int p[2];
pipe(p);
pid_t pid = fork();
if (pid > 0) { // parent écrit
close(p[0]);
FILE *out = fdopen(p[1], "w");
setvbuf(out, NULL, _IOLBF, 0); // buffering ligne
fprintf(out, "ping#1\n");
fflush(out);
fclose(out);
} else { // enfant lit
close(p[1]);
FILE *in = fdopen(p[0], "r");
char line[256];
if (fgets(line, sizeof line, in)) {
line[strcspn(line, "\r\n")] = 0;
printf("Enfant a reçu: %s\n", line);
}
fclose(in);
}
Pipes nommés (FIFO)
~~~~~~~~~~~~~~~~~~~
Un **pipe nommé** (*FIFO*, *First In First Out*) est similaire mais existe dans le système de fichiers : il peut être ouvert par des processus non apparentés.
Création :
.. code-block:: c
#include
#include
int mkfifo(const char *pathname, mode_t mode);
- ``pathname`` : chemin de la FIFO.
- ``mode`` : permissions initiales (affectées par ``umask``).
.. code-block:: c
#include
int fd = open("canal", O_RDONLY); /* ou O_WRONLY, O_RDWR, + O_NONBLOCK */
- **Blocage** par défaut :
- ``open(..., O_RDONLY)`` bloque tant qu'aucun écrivain n'a ouvert la FIFO.
- ``open(..., O_WRONLY)`` bloque tant qu'aucun lecteur n'a ouvert la FIFO.
- **Non-bloquant** (``O_NONBLOCK``) :
- ``open(..., O_RDONLY|O_NONBLOCK)`` réussit immédiatement.
- ``open(..., O_WRONLY|O_NONBLOCK)`` échoue avec **``-1``/``errno=ENXIO``** s'il n'y a pas de lecteur.
- **Fin de flux** :
- Si tous les **écrivains** ferment, les **lecteurs** voient ``read`` → **0** (EOF) lorsque la FIFO est vidée.
- Si tous les **lecteurs** ferment, un ``write`` émet **SIGPIPE** / ``EPIPE``.
- Supprimer la FIFO avec ``unlink("canal")`` (comme un fichier classique).
Dans le terminal :
.. code-block:: bash
mkfifo canal
# terminal 1
cat < canal
# terminal 2
echo "coucou" > canal
# terminal 1 affiche: coucou
En C :
Programme éméteur :
.. code-block:: c
#include
#include
#include
#include
#include
#include
#include
#include
int main(void) {
const char *path = "canal";
if (mkfifo(path, 0666) == -1 && errno != EEXIST) {
// peut déjà exister
perror("mkfifo");
}
// Ouvre la FIFO en écriture et bloquera jusqu'à un lecteur
int fd = open(path, O_WRONLY);
if (fd == -1) {
perror("open write");
exit(EXIT_FAILURE);
}
const char *message = "Hello FIFO\n";
write(fd, message, strlen(message) + 1);
close(fd);
// si besoin pour supprimer la FIFO
unlink(path);
return 0;
}
Programme récepteur :
.. code-block:: c
#include
#include
#include
#include
#include
#include
#include
#include
int main(void) {
const char *path = "canal";
// Crée la FIFO si besoin (ok si elle existe déjà)
if (mkfifo(path, 0666) == -1 && errno != EEXIST) {
perror("mkfifo");
exit(EXIT_FAILURE);
}
// Ouvre la FIFO en lecture et attend une écriture sur la FIFO
int fd = open(path, O_RDONLY);
if (fd == -1) {
perror("open read");
exit(EXIT_FAILURE);
}
// Lit le message
char buffer[100];
read(fd, buffer, sizeof(buffer));
printf("Message reçu : %s\n", buffer);
close(fd);
// unlink(path); // au besoin, supprimer la FIFO
return 0;
}