Introduction
Sous Linux et plus généralement sous les systèmes UNIX, les processus ne sont pas seulement organisés individuellement.
Ils peuvent être regroupés dans des groupes de processus, eux-mêmes rassemblés dans des sessions.
Cette organisation permet notamment au shell de gérer :
- les commandes exécutées au premier plan ;
- les commandes exécutées en arrière-plan ;
- les pipelines ;
- les tâches suspendues ;
- les tâches reprises avec fg ou bg ;
- les signaux générés par le terminal ;
- la fermeture d’un terminal.
Code: Select all
Session
└── Groupes de processus
└── Processus
Parmi les groupes de processus de cette session, un seul peut être le groupe de processus au premier plan du terminal.
Les autres groupes sont considérés comme étant en arrière-plan.
1. Les groupes de processus
1.1 Définition
Un groupe de processus est un ensemble d’un ou plusieurs processus qui partagent le même identifiant de groupe de processus.
Cet identifiant est appelé :
Code: Select all
PGID
- un PID ;
- un PPID ;
- un PGID ;
- un SID.
Pour ce processus :
Code: Select all
PID == PGID
Cela permet au shell de placer plusieurs processus dans un même groupe, notamment lorsqu’ils appartiennent au même pipeline.
Exemple :
Code: Select all
cat fichier.txt | grep erreur | less
Les trois processus peuvent donc partager le même PGID.
1.2 Obtenir le groupe du processus appelant
Code: Select all
#include <unistd.h>
pid_t getpgrp(void);
Exemple :
Code: Select all
#include <stdio.h>
#include <unistd.h>
int main(void)
{
printf("PID : %ld\n", (long)getpid());
printf("PGID : %ld\n", (long)getpgrp());
return 0;
}
1.3 Obtenir le groupe d’un processus précis
Code: Select all
#include <unistd.h>
pid_t getpgid(pid_t pid);
Si pid vaut 0, la fonction retourne le PGID du processus appelant.
Exemple :
Code: Select all
pid_t pgid = getpgid(0);
if (pgid == -1) {
perror("getpgid");
}
1.4 Créer ou modifier un groupe de processus
Code: Select all
#include <unistd.h>
int setpgid(pid_t pid, pid_t pgid);
Signification des paramètres :
- pid == 0 : le processus appelant est ciblé ;
- pgid == 0 : le PID du processus ciblé est utilisé comme PGID.
Code: Select all
setpgid(0, 0);
Code: Select all
PID == PGID
Code: Select all
#include <stdio.h>
#include <unistd.h>
int main(void)
{
if (setpgid(0, 0) == -1) {
perror("setpgid");
return 1;
}
printf("PID : %ld\n", (long)getpid());
printf("PGID : %ld\n", (long)getpgrp());
return 0;
}
1.5 Restrictions de setpgid()
Un processus ne peut pas déplacer librement n’importe quel autre processus.
Les principales restrictions sont les suivantes :
- le processus ciblé doit appartenir à la même session ;
- un processus peut généralement modifier son propre groupe ;
- un parent peut modifier le groupe de l’un de ses enfants avant que celui-ci n’appelle exec() ;
- un chef de session ne peut pas changer de groupe de processus ;
- un processus ne peut pas rejoindre un groupe appartenant à une autre session.
Cette technique évite une condition de compétition.
Peu importe lequel s’exécute en premier : les processus du pipeline finissent dans le bon groupe.
2. Les sessions
2.1 Définition
Une session est un ensemble de groupes de processus.
L’identifiant d’une session est appelé :
Code: Select all
SID
Pour le chef de session :
Code: Select all
PID == PGID == SID
Elle peut contenir :
- le shell ;
- le groupe de processus du shell ;
- plusieurs groupes de processus correspondant à des tâches ;
- un terminal de contrôle.
2.2 Obtenir l’identifiant de session
Code: Select all
#include <unistd.h>
pid_t getsid(pid_t pid);
Si pid vaut 0, elle retourne le SID du processus appelant.
Exemple :
Code: Select all
#include <stdio.h>
#include <unistd.h>
int main(void)
{
printf("PID : %ld\n", (long)getpid());
printf("PGID : %ld\n", (long)getpgrp());
printf("SID : %ld\n", (long)getsid(0));
return 0;
}
2.3 Créer une nouvelle session
Code: Select all
#include <unistd.h>
pid_t setsid(void);
- elle crée une nouvelle session ;
- elle crée un nouveau groupe de processus ;
- elle fait du processus appelant le chef de session ;
- elle fait du processus appelant le chef du nouveau groupe ;
- elle détache initialement le processus de tout terminal de contrôle.
Code: Select all
PID == PGID == SID
2.4 Pourquoi setsid() peut échouer
setsid() échoue si le processus appelant est déjà chef d’un groupe de processus.
La technique classique consiste donc à créer un enfant avec fork(), puis à terminer le parent.
L’enfant hérite de l’ancien PGID, mais possède un nouveau PID.
Il n’est donc normalement pas chef de groupe et peut appeler setsid().
Exemple :
Code: Select all
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(void)
{
pid_t pid = fork();
if (pid == -1) {
perror("fork");
return 1;
}
if (pid > 0) {
_exit(EXIT_SUCCESS);
}
if (setsid() == -1) {
perror("setsid");
return 1;
}
printf("PID : %ld\n", (long)getpid());
printf("PGID : %ld\n", (long)getpgrp());
printf("SID : %ld\n", (long)getsid(0));
return 0;
}
3. Le terminal de contrôle
3.1 Définition
Une session peut posséder un unique terminal de contrôle.
Le terminal de contrôle permet :
- de fournir les entrées et sorties interactives ;
- de produire des signaux à partir du clavier ;
- de distinguer les processus au premier plan et en arrière-plan ;
- de détecter la fermeture ou la déconnexion du terminal.
3.2 Acquisition d’un terminal de contrôle
Un chef de session n’ayant pas encore de terminal de contrôle peut, selon le système, en acquérir un en ouvrant un terminal.
L’opération TIOCSCTTY permet de demander explicitement qu’un terminal devienne le terminal de contrôle de la session.
Exemple conceptuel :
Code: Select all
#include <sys/ioctl.h>
ioctl(fd, TIOCSCTTY, 0);
3.3 Obtenir le nom du terminal de contrôle
Code: Select all
#include <stdio.h>
char *ctermid(char *s);
Exemple :
Code: Select all
#include <stdio.h>
int main(void)
{
char terminal[L_ctermid];
if (ctermid(terminal) != NULL) {
printf("Terminal de contrôle : %s\n", terminal);
}
return 0;
}
3.4 Obtenir le terminal associé à un descripteur
Code: Select all
#include <unistd.h>
char *ttyname(int fd);
Exemple :
Code: Select all
#include <stdio.h>
#include <unistd.h>
int main(void)
{
char *name = ttyname(STDIN_FILENO);
if (name != NULL) {
printf("Terminal de stdin : %s\n", name);
} else {
perror("ttyname");
}
return 0;
}
3.5 Vérifier qu’un descripteur représente un terminal
Code: Select all
#include <unistd.h>
int isatty(int fd);
Exemple :
Code: Select all
if (isatty(STDIN_FILENO)) {
printf("stdin est un terminal\n");
}
4. Premier plan et arrière-plan
4.1 Groupe de processus au premier plan
Dans une session associée à un terminal, un seul groupe de processus peut être au premier plan.
Les processus de ce groupe peuvent normalement :
- lire depuis le terminal ;
- recevoir les signaux générés par le clavier ;
- interagir directement avec l’utilisateur.
4.2 Groupe de processus en arrière-plan
Les processus en arrière-plan peuvent continuer à s’exécuter.
Cependant, ils ne peuvent pas toujours utiliser librement le terminal.
Une tentative de lecture depuis le terminal peut provoquer :
Code: Select all
SIGTTIN
Code: Select all
SIGTTOU
4.3 Obtenir le groupe au premier plan
Code: Select all
#include <unistd.h>
pid_t tcgetpgrp(int fd);
Exemple :
Code: Select all
#include <stdio.h>
#include <unistd.h>
int main(void)
{
pid_t pgid = tcgetpgrp(STDIN_FILENO);
if (pgid == -1) {
perror("tcgetpgrp");
return 1;
}
printf("PGID au premier plan : %ld\n", (long)pgid);
return 0;
}
4.4 Modifier le groupe au premier plan
Code: Select all
#include <unistd.h>
int tcsetpgrp(int fd, pid_t pgid);
Le groupe ciblé doit appartenir à la même session.
Le shell utilise cette fonction lorsqu’il :
- lance une commande au premier plan ;
- place une tâche au premier plan avec fg ;
- récupère le contrôle du terminal après la fin d’une tâche ;
- reprend le contrôle après la suspension d’une tâche.
5. Les signaux générés par le terminal
5.1 Principe
Les combinaisons de touches du terminal ne ciblent généralement pas un processus isolé.
Elles provoquent l’envoi d’un signal à tout le groupe de processus au premier plan.
Cela est indispensable pour les pipelines.
Exemple :
Code: Select all
cat fichier | grep erreur | less
5.2 Signaux principaux
- Ctrl+C produit généralement SIGINT ;
- Ctrl+\ produit généralement SIGQUIT ;
- Ctrl+Z produit généralement SIGTSTP.
- SIGSTOP arrête un processus et ne peut être ni intercepté ni ignoré ;
- SIGCONT reprend un processus arrêté ;
- SIGTTIN peut arrêter un groupe d’arrière-plan qui tente de lire le terminal ;
- SIGTTOU peut arrêter un groupe d’arrière-plan effectuant certaines opérations sur le terminal ;
- SIGHUP signale généralement une déconnexion ou une fermeture du terminal.
6. Le signal SIGHUP
6.1 Signification
SIGHUP signifie historiquement hangup.
Il correspond à la perte de la connexion avec le terminal.
Il peut être envoyé lorsque :
- une connexion terminal est interrompue ;
- une fenêtre de terminal est fermée ;
- le processus contrôlant une session se termine ;
- un groupe de processus arrêté devient orphelin.
6.2 Utilisation moderne
Certains services utilisent SIGHUP comme convention pour recharger leur configuration.
Exemple conceptuel :
Code: Select all
kill -SIGHUP PID
6.3 Fermeture du terminal
Lorsqu’un terminal est fermé, le shell peut envoyer SIGHUP aux tâches qu’il contrôle.
Une commande lancée simplement en arrière-plan peut donc être terminée :
Code: Select all
programme &
Code: Select all
nohup programme &
7. Le contrôle des tâches par le shell
7.1 Définition d’une tâche
Une tâche, appelée job, correspond généralement à un groupe de processus contrôlé par le shell.
Un pipeline complet constitue donc souvent une seule tâche.
Exemple :
Code: Select all
grep erreur fichier.txt | less &
7.2 Lancer une tâche en arrière-plan
Le caractère & demande au shell de lancer la commande en arrière-plan.
Code: Select all
commande &
Code: Select all
[1] 18392
Il ne faut pas le confondre avec un PID.
7.3 Afficher les tâches
La commande :
Code: Select all
jobs
États courants :
- Running ;
- Stopped ;
- Done.
7.4 Suspendre une tâche
Lorsque l’utilisateur appuie sur :
Code: Select all
Ctrl+Z
La tâche passe alors à l’état arrêté.
Le shell récupère ensuite le contrôle du terminal.
7.5 Reprendre une tâche en arrière-plan
La commande :
Code: Select all
bg
Le shell envoie généralement :
Code: Select all
SIGCONT
7.6 Reprendre une tâche au premier plan
La commande :
Code: Select all
fg
- le shell donne le terminal au groupe de la tâche avec tcsetpgrp() ;
- il envoie SIGCONT si la tâche était arrêtée ;
- il attend un changement d’état ;
- il récupère le terminal après la fin ou la suspension de la tâche.
8. États d’une tâche
Une tâche peut se trouver dans plusieurs états :
Code: Select all
Exécution au premier plan
Exécution en arrière-plan
Arrêtée
Terminée
Code: Select all
Premier plan -- Ctrl+Z / SIGTSTP --> Arrêtée
Arrêtée -- bg / SIGCONT --> Arrière-plan
Arrêtée -- fg / SIGCONT --> Premier plan
Arrière-plan -- fg --> Premier plan
Tout état -- terminaison --> Terminée
9. Mise en œuvre du job control
9.1 Création d’un pipeline
Pour lancer un pipeline, un shell doit approximativement :
- créer les processus avec fork() ;
- créer les tubes avec pipe() ;
- placer tous les processus du pipeline dans le même groupe avec setpgid() ;
- conserver une structure représentant la tâche ;
- donner le terminal au groupe si la tâche est au premier plan ;
- lancer les programmes avec une fonction de la famille exec() ;
- surveiller les changements d’état avec waitpid() ;
- récupérer le terminal après la fin ou la suspension de la tâche.
9.2 Exemple d’organisation
Code: Select all
Shell :
PID = 1204
PGID = 1204
SID = 1204
Pipeline :
Processus 1 : PID = 1228
Processus 2 : PID = 1229
Processus 3 : PID = 1230
PGID commun = 1228
SID commun = 1204
Ainsi :
Code: Select all
PID du premier processus == PGID du pipeline
9.3 Signaux envoyés à tout le pipeline
Si le pipeline est au premier plan, les signaux du terminal sont envoyés à tous ses processus.
Code: Select all
Ctrl+C
Code: Select all
Ctrl+Z
Le contrôle des tâches fonctionne donc au niveau du groupe de processus.
10. Attente et surveillance des tâches
10.1 waitpid()
Code: Select all
#include <sys/wait.h>
pid_t waitpid(pid_t pid, int *status, int options);
- la terminaison d’un processus ;
- l’arrêt d’un processus ;
- la reprise d’un processus.
Code: Select all
waitpid(-pgid, &status, options);
10.2 Options utiles
Code: Select all
WUNTRACED
Code: Select all
WCONTINUED
Code: Select all
WNOHANG
10.3 Macros d’analyse du statut
Code: Select all
WIFEXITED(status)
Code: Select all
WEXITSTATUS(status)
Code: Select all
WIFSIGNALED(status)
Code: Select all
WTERMSIG(status)
Code: Select all
WIFSTOPPED(status)
Code: Select all
WSTOPSIG(status)
Code: Select all
WIFCONTINUED(status)
11. Envoi de signaux à un groupe
11.1 kill()
Code: Select all
#include <signal.h>
int kill(pid_t pid, int sig);
Exemple :
Code: Select all
kill(-pgid, SIGTERM);
11.2 killpg()
Code: Select all
#include <signal.h>
int killpg(pid_t pgrp, int sig);
Exemple :
Code: Select all
if (killpg(pgid, SIGCONT) == -1) {
perror("killpg");
}
12. Gestion de SIGTSTP et SIGCONT
12.1 Programmes ordinaires
La majorité des programmes n’ont pas besoin de gérer spécialement SIGTSTP et SIGCONT.
L’action par défaut de SIGTSTP arrête le processus.
SIGCONT reprend ensuite son exécution.
12.2 Programmes interactifs
Certains programmes interactifs doivent nettoyer le terminal avant leur suspension.
C’est notamment le cas :
- des éditeurs de texte ;
- des interfaces plein écran ;
- des programmes utilisant un mode terminal brut ;
- des programmes modifiant l’affichage ou les attributs du terminal.
- restaurer les réglages normaux du terminal ;
- restaurer l’affichage ;
- réinstaller l’action par défaut de SIGTSTP ;
- se renvoyer le signal ;
- réinstaller son gestionnaire après SIGCONT ;
- restaurer son propre mode terminal.
12.3 Séquence correcte
Code: Select all
1. Restaurer SIGTSTP à SIG_DFL
2. Débloquer SIGTSTP
3. Se renvoyer SIGTSTP
4. Le processus s’arrête
5. SIGCONT reprend le processus
6. Réinstaller le gestionnaire SIGTSTP
7. Restaurer l’état de l’application
12.4 Exemple simplifié
Code: Select all
#include <signal.h>
#include <unistd.h>
static void handle_sigtstp(int sig)
{
struct sigaction sa;
(void)sig;
sigemptyset(&sa.sa_mask);
sa.sa_flags = 0;
sa.sa_handler = SIG_DFL;
sigaction(SIGTSTP, &sa, NULL);
raise(SIGTSTP);
sa.sa_handler = handle_sigtstp;
sigaction(SIGTSTP, &sa, NULL);
}
13. Sécurité dans les gestionnaires de signaux
13.1 Fonctions async-signal-safe
Un gestionnaire de signal peut interrompre le programme à presque n’importe quel moment.
Il ne doit donc pas appeler n’importe quelle fonction.
Des fonctions comme :
Code: Select all
printf()
malloc()
free()
strsignal()
Une fonction fréquemment utilisable est :
Code: Select all
write()
Code: Select all
volatile sig_atomic_t
13.2 Exemple avec sig_atomic_t
Code: Select all
#include <signal.h>
#include <unistd.h>
static volatile sig_atomic_t stopped = 0;
static void handler(int sig)
{
if (sig == SIGTSTP) {
stopped = 1;
}
}
14. Signaux ignorés hérités du shell
14.1 Héritage après exec()
Lorsqu’un processus appelle une fonction de la famille exec() :
- les gestionnaires personnalisés sont généralement réinitialisés à l’action par défaut ;
- les signaux configurés avec SIG_IGN restent ignorés.
Le nouveau programme hérite alors de cette disposition.
14.2 Ne pas réactiver un signal ignoré arbitrairement
Une application doit vérifier l’ancienne disposition avant d’installer son propre gestionnaire.
Exemple :
Code: Select all
#include <signal.h>
struct sigaction old_action;
struct sigaction new_action;
if (sigaction(SIGTSTP, NULL, &old_action) == -1) {
perror("sigaction");
}
if (old_action.sa_handler != SIG_IGN) {
sigemptyset(&new_action.sa_mask);
new_action.sa_flags = 0;
new_action.sa_handler = handler;
sigaction(SIGTSTP, &new_action, NULL);
}
15. Groupes de processus orphelins
15.1 Définition
Un groupe de processus orphelin n’est pas simplement un groupe dont le parent principal est mort.
La définition dépend des relations entre :
- les processus parents ;
- les groupes de processus ;
- les sessions.
- dans la même session ;
- dans un autre groupe de processus.
15.2 Différence avec un processus orphelin
Un processus orphelin est simplement un processus dont le parent s’est terminé.
Un groupe de processus orphelin est une notion liée à toute la structure :
Code: Select all
Session
Groupes de processus
Relations parent-enfant
15.3 Pourquoi c’est important
Un processus arrêté dans un groupe orphelin pourrait rester suspendu indéfiniment.
Il n’existe potentiellement plus de shell ou de parent de la même session pour lui envoyer SIGCONT.
15.4 Réaction du noyau
Lorsqu’un groupe devient orphelin et contient au moins un processus arrêté, le noyau envoie à chaque membre :
Code: Select all
SIGHUP
puis
SIGCONT
SIGCONT garantit que les processus arrêtés reprennent leur exécution afin de pouvoir traiter le signal ou se terminer.
15.5 Cas de SIGTSTP, SIGTTIN et SIGTTOU
Les signaux suivants sont liés à l’arrêt interactif ou au terminal :
Code: Select all
SIGTSTP
SIGTTIN
SIGTTOU
Code: Select all
errno == EIO
SIGSTOP reste particulier : il ne peut être ni intercepté ni ignoré.
16. Exemple conceptuel de groupe orphelin
Avant la mort du parent :
Code: Select all
Session
├── Groupe du shell
│ └── shell
│
└── Groupe de la tâche
├── parent
└── enfant arrêté
Code: Select all
Session
├── Groupe du shell
│ └── shell
│
└── Groupe orphelin
└── enfant arrêté
Code: Select all
SIGHUP
SIGCONT
17. Exemple minimal : créer un nouveau groupe
Code: Select all
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(void)
{
printf("Avant setpgid()\n");
printf("PID : %ld\n", (long)getpid());
printf("PGID : %ld\n", (long)getpgrp());
printf("SID : %ld\n", (long)getsid(0));
if (setpgid(0, 0) == -1) {
perror("setpgid");
return EXIT_FAILURE;
}
printf("\nAprès setpgid()\n");
printf("PID : %ld\n", (long)getpid());
printf("PGID : %ld\n", (long)getpgrp());
printf("SID : %ld\n", (long)getsid(0));
return EXIT_SUCCESS;
}
18. Exemple minimal : créer une session
Code: Select all
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(void)
{
pid_t pid = fork();
if (pid == -1) {
perror("fork");
return EXIT_FAILURE;
}
if (pid > 0) {
_exit(EXIT_SUCCESS);
}
if (setsid() == -1) {
perror("setsid");
return EXIT_FAILURE;
}
printf("Nouvelle session créée\n");
printf("PID : %ld\n", (long)getpid());
printf("PGID : %ld\n", (long)getpgrp());
printf("SID : %ld\n", (long)getsid(0));
return EXIT_SUCCESS;
}
19. Exemple minimal : observer le premier plan
Code: Select all
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(void)
{
if (!isatty(STDIN_FILENO)) {
fprintf(stderr, "stdin n'est pas un terminal\n");
return EXIT_FAILURE;
}
pid_t foreground = tcgetpgrp(STDIN_FILENO);
if (foreground == -1) {
perror("tcgetpgrp");
return EXIT_FAILURE;
}
printf("PID du processus : %ld\n", (long)getpid());
printf("PGID du processus : %ld\n", (long)getpgrp());
printf("PGID au premier plan : %ld\n", (long)foreground);
return EXIT_SUCCESS;
}
20. API essentielles du chapitre
Groupes de processus
Code: Select all
getpgrp()
getpgid()
setpgid()
Code: Select all
getsid()
setsid()
Code: Select all
ctermid()
ttyname()
isatty()
tcgetpgrp()
tcsetpgrp()
ioctl()
Code: Select all
TIOCSCTTY
Code: Select all
fork()
execve()
_exit()
pipe()
read()
write()
close()
Code: Select all
wait()
waitpid()
Code: Select all
WUNTRACED
WCONTINUED
WNOHANG
Code: Select all
WIFEXITED()
WEXITSTATUS()
WIFSIGNALED()
WTERMSIG()
WIFSTOPPED()
WSTOPSIG()
WIFCONTINUED()
Code: Select all
sigaction()
sigemptyset()
sigaddset()
sigprocmask()
kill()
killpg()
raise()
pause()
Code: Select all
SIG_DFL
SIG_IGN
Code: Select all
SIGHUP
SIGINT
SIGQUIT
SIGTSTP
SIGSTOP
SIGCONT
SIGTTIN
SIGTTOU
SIGCHLD
21. Résumé final
Sous Linux, les processus sont organisés selon une hiérarchie à deux niveaux :
Code: Select all
Session
└── Groupes de processus
└── Processus
Une session est identifiée par un SID.
Le chef d’un groupe possède généralement :
Code: Select all
PID == PGID
Code: Select all
PID == PGID == SID
Un seul groupe de processus peut être au premier plan de ce terminal.
Les signaux produits par le clavier sont envoyés à tout le groupe au premier plan.
Le shell utilise les groupes de processus pour contrôler les pipelines et les tâches.
Les fonctions principales utilisées sont :
Code: Select all
setpgid()
setsid()
tcgetpgrp()
tcsetpgrp()
waitpid()
killpg()
sigaction()
Lorsqu’un groupe arrêté devient orphelin, le noyau lui envoie généralement :
Code: Select all
SIGHUP
SIGCONT
Ce mécanisme constitue la base du contrôle des tâches interactives sous Linux et UNIX.
