Processus et Threads
Processus§
Un processus est une instance d’un programme en cours d’exécution. C’est l’unité d’allocation de ressources de l’OS.
Composition d’un processus§
Un processus comprend :
- Le code exécutable (segment texte) — les instructions du programme
- Les données (segment data) — variables globales et statiques initialisées
- Le tas (heap) — mémoire allouée dynamiquement (
malloc,new) - La pile (stack) — variables locales, paramètres, adresses de retour
- Les descripteurs de fichiers — fichiers, sockets, pipes ouverts
- Le contexte matériel — valeur des registres, compteur programme
PCB — Process Control Block§
Structure de données maintenue par le noyau pour chaque processus. Contient :
| Champ | Contenu |
|---|---|
| PID | Identifiant unique du processus |
| PPID | PID du processus parent |
| État | Nouveau / Prêt / En cours / Bloqué / Terminé |
| Compteur programme | Adresse de la prochaine instruction |
| Registres CPU | Sauvegardés lors d’un changement de contexte |
| Informations mémoire | Tables de pages, limites de segments |
| Liste des fichiers ouverts | Descripteurs de fichiers |
| Informations de scheduling | Priorité, temps CPU consommé |
| Informations de sécurité | UID, GID, capacités |
États d’un processus§
fork()
│
▼
NOUVEAU
│ admitted
▼
PRÊT ◄──────────── interrupt / I/O complète
│
scheduler dispatch
│
▼
EN COURS ───── I/O ou événement ──► BLOQUÉ
│
│ exit()
▼
TERMINÉ
Création de processus (Unix)§
#include <unistd.h>
pid_t pid = fork(); // Crée une copie exacte du processus
if (pid < 0) {
// Erreur
} else if (pid == 0) {
// Code exécuté par le processus fils
execl("/bin/ls", "ls", "-la", NULL); // Remplace l'image du processus
} else {
// Code exécuté par le processus parent
// pid contient l'ID du fils
wait(NULL); // Attendre la fin du fils
}
fork() crée un processus fils qui est une copie du parent (Copy-On-Write). exec() remplace l’image du processus par un nouveau programme. La séquence fork() + exec() est le mécanisme standard de création de processus sous Unix.
Communication inter-processus (IPC)§
| Mécanisme | Description | Usage |
|---|---|---|
| Pipes | Canal unidirectionnel, lié au cycle de vie | ls | grep .py |
| Named Pipes (FIFO) | Pipe persistant dans le système de fichiers | Processus sans lien de parenté |
| Signaux | Notifications asynchrones | SIGTERM, SIGKILL, SIGHUP |
| Shared Memory | Zone mémoire partagée | Échanges rapides de données |
| Semaphores | Synchronisation entre processus | Accès exclusif à une ressource |
| Sockets | Communication réseau ou locale (Unix sockets) | Client-serveur |
| Message Queues | Files de messages structurés | Échanges asynchrones |
Threads§
Un thread (fil d’exécution) est la plus petite unité d’exécution schedulable. Plusieurs threads s’exécutent dans le même processus et partagent ses ressources.
Processus vs Thread§
| Dimension | Processus | Thread |
|---|---|---|
| Espace mémoire | Séparé (isolation totale) | Partagé au sein du processus |
| Création | Lente (copie des ressources) | Rapide (quelques registres) |
| Communication | Via IPC (coûteux) | Via mémoire partagée (direct) |
| Isolation | Forte | Faible (un thread peut corrompre les autres) |
| Coût | Élevé | Faible |
| Parallélisme | Multi-CPU naturel | Multi-CPU selon le modèle |
Ce que partage un thread§
Un thread partage avec les autres threads du même processus :
- Le segment de code, le tas, les variables globales
- Les descripteurs de fichiers
- L’espace d’adressage virtuel
Un thread possède en propre :
- Son identifiant (TID)
- Son compteur programme
- Ses registres CPU
- Sa pile d’exécution
- Son état (prêt, en cours, bloqué)
Modèles de threading§
1:1 (Kernel-level threads) : chaque thread utilisateur correspond à un thread noyau. Parallélisme réel sur multi-cœurs. Contexte switch coûteux (implique le noyau). Modèle de Linux/Windows.
N:1 (User-level threads) : plusieurs threads utilisateur mappés sur un seul thread noyau (bibliothèque en espace utilisateur). Pas de parallélisme réel. Context switch très rapide. Problème : un appel système bloquant bloque tous les threads.
M:N (Hybrid) : M threads utilisateur mappés sur N threads noyau (N < M). Compromis. Utilisé par Go (goroutines), Haskell (green threads). Complexe à implémenter.
Threads en Python§
import threading
import time
def tache(nom, duree):
print(f"Thread {nom} démarre")
time.sleep(duree)
print(f"Thread {nom} termine")
# Créer des threads
t1 = threading.Thread(target=tache, args=("A", 2))
t2 = threading.Thread(target=tache, args=("B", 1))
t1.start()
t2.start()
t1.join() # Attendre la fin de t1
t2.join() # Attendre la fin de t2
print("Tous les threads ont terminé")
# Thread avec héritage
class MonThread(threading.Thread):
def __init__(self, nom):
super().__init__()
self.nom = nom
def run(self):
print(f"Thread {self.nom} en cours")
# GIL (Global Interpreter Lock) en Python
# Le GIL empêche deux threads Python d'exécuter du bytecode simultanément.
# Conséquence : les threads Python n'apportent pas de parallélisme pour le CPU-bound.
# Pour le parallélisme CPU : utiliser multiprocessing.
# Les threads sont utiles pour l'I/O-bound (réseau, disque).
Threads avec partage de ressources§
import threading
compteur = 0
verrou = threading.Lock()
def incrementer(n):
global compteur
for _ in range(n):
with verrou: # Acquisition et libération automatique
compteur += 1
threads = [threading.Thread(target=incrementer, args=(10000,)) for _ in range(10)]
for t in threads:
t.start()
for t in threads:
t.join()
print(f"Compteur final : {compteur}") # 100000 (garanti avec le verrou)
Ordonnancement (Scheduling)§
Le scheduler décide quel processus/thread s’exécute sur quel cœur à quel moment.
Critères d’évaluation§
- Utilisation CPU : maximiser le pourcentage d’utilisation
- Débit (Throughput) : nombre de processus terminés par unité de temps
- Temps de rotation (Turnaround) : temps total de soumission à la fin
- Temps d’attente : temps passé dans la file Prêt
- Temps de réponse : délai entre la soumission et la première réponse
Algorithmes§
FCFS — First Come First Served : les processus sont exécutés dans l’ordre d’arrivée. Simple mais sujet à l’effet convoi (un long processus bloque tous les suivants).
SJF — Shortest Job First : exécuter d’abord le processus avec le temps CPU le plus court estimé. Optimal pour minimiser le temps d’attente moyen. Problème : nécessite de connaître le temps d’exécution à l’avance.
SRTF — Shortest Remaining Time First : version préemptive de SJF. Un nouveau processus plus court préempte le processus en cours.
Round Robin : chaque processus reçoit un quantum de temps fixe (10-100 ms). Si non terminé, il retourne à la fin de la file. Adapté aux systèmes interactifs.
Processus : P1(24), P2(3), P3(3) — quantum = 4
Gantt chart :
P1(0-4) | P2(4-7) | P3(7-10) | P1(10-14) | P1(14-18) | P1(18-22) | P1(22-24)
Schedulers à priorités : chaque processus a une priorité. Le processus de plus haute priorité s’exécute en premier. Risque de famine (starvation) pour les processus de basse priorité.
Files multiniveaux avec rétroaction (MLFQ) : plusieurs files de priorités. Les processus commencent en haute priorité et descendent s’ils consomment tout leur quantum. Les I/O-bound remontent. Simule le comportement de SJF sans connaître les temps d’exécution à l’avance. Utilisé par Linux (CFS — Completely Fair Scheduler).
CFS — Completely Fair Scheduler (Linux)§
Le scheduler par défaut de Linux depuis 2.6.23. Chaque processus reçoit une fraction équitable du CPU proportionnelle à sa priorité (nice value). Utilise un arbre rouge-noir ordonné par “virtual runtime” pour sélectionner le prochain processus en O(log n).
# Modifier la priorité d'un processus (nice : -20 à +19)
nice -n 10 commande # lancer avec nice 10
renice -n 5 -p 1234 # changer le nice d'un processus existant
ps -o pid,ni,comm -p 1234 # voir le nice value
Changement de contexte§
Lors d’un changement de contexte, le noyau :
- Sauvegarde l’état du processus sortant dans son PCB (registres, compteur programme)
- Met à jour les structures de données de scheduling
- Charge l’état du processus entrant depuis son PCB
- Passe au nouveau processus
Coût : 1-10 µs. À éviter pour les workloads très latence-sensitifs (c’est pourquoi les event loops (Node.js, asyncio) peuvent être plus efficaces que les threads pour l’I/O).
Signaux Unix§
Un signal est une forme d’interruption logicielle envoyée à un processus. Le noyau utilise les signaux pour notifier des événements ou des conditions exceptionnelles.
Signaux courants§
| Signal | Numéro | Description | Action par défaut |
|---|---|---|---|
| SIGINT | 2 | Interruption clavier (Ctrl+C) | Terminer |
| SIGTERM | 15 | Demande de terminaison (gracieuse) | Terminer |
| SIGKILL | 9 | Terminaison forcée (non interceptable) | Terminer |
| SIGSEGV | 11 | Violation de segmentation mémoire | Core dump + terminer |
| SIGHUP | 1 | Déconnexion du terminal | Terminer |
| SIGALRM | 14 | Alarme timer (alarm()) | Terminer |
| SIGCHLD | 17 | Changement d’état d’un fils | Ignorer |
| SIGPIPE | 13 | Écriture dans un pipe sans lecteur | Terminer |
| SIGUSR1/2 | 10/12 | Signaux utilisateur définis | Terminer |
Signaux standard vs temps réel§
| Critère | Standard | Temps réel (SIGRTMIN-SIGRTMAX) |
|---|---|---|
| File d’attente | Non (un seul signal en attente) | Oui (plusieurs instances) |
| Données additionnelles | Non | Oui (via sigqueue()) |
| Ordre garanti | Non spécifié | Garanti par numéro |
| Priorité | Plus haute que temps réel (Linux) | Par numéro de signal |
Gestion des signaux en C§
#include <signal.h>
#include <stdio.h>
// Gestionnaire de signal
void gestionnaire(int sig) {
printf("Signal %d reçu\n", sig);
}
int main() {
// Enregistrer un gestionnaire pour SIGINT
signal(SIGINT, gestionnaire);
// Méthode recommandée : sigaction (plus précis)
struct sigaction sa;
sa.sa_handler = gestionnaire;
sigemptyset(&sa.sa_mask);
sa.sa_flags = SA_RESTART; // Redémarrer les appels système interrompus
sigaction(SIGTERM, &sa, NULL);
// Envoyer un signal à un processus
kill(pid, SIGTERM);
// Attendre un signal
pause(); // Suspendre jusqu'au prochain signal
return 0;
}
Pipes — Communication inter-processus§
Un pipe est un mécanisme IPC permettant de connecter la sortie d’un processus à l’entrée d’un autre, sans fichier intermédiaire.
Types de pipes :
| Type | Nom | Communication | Usage |
|---|---|---|---|
| Unnamed pipe | pipe() | Unidirectionnel | Parent-enfant uniquement |
| Named pipe | mkfifo() (FIFO) | Uni ou bidirectionnel | Processus sans lien de parenté |
#include <unistd.h>
int fd[2];
pipe(fd); // fd[0] = lecture, fd[1] = écriture
pid_t pid = fork();
if (pid == 0) {
// Processus fils : lit depuis le pipe
close(fd[1]); // Fermer l'extrémité écriture
read(fd[0], buffer, sizeof(buffer));
} else {
// Processus parent : écrit dans le pipe
close(fd[0]); // Fermer l'extrémité lecture
write(fd[1], message, strlen(message) + 1);
}
# Pipe shell : stdout de ls devient stdin de grep
ls -la | grep ".md"
# Named pipe
mkfifo /tmp/mon_pipe
cat /tmp/mon_pipe & # lecteur en arrière-plan
echo "données" > /tmp/mon_pipe # écrivain
Appels système§
Les appels système sont l’interface entre les processus utilisateur et le noyau.
| Catégorie | Exemples |
|---|---|
| Contrôle de processus | fork(), exec(), wait(), exit(), kill() |
| Gestion de fichiers | open(), read(), write(), close(), lseek() |
| Gestion des signaux | signal(), sigaction(), kill(), pause() |
| IPC | pipe(), mkfifo(), shmget(), msgget() |
| Information | getpid(), getppid(), time() |