.. _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; }