6.3 Processus et Concurrence
Objectifs d'apprentissage
À la fin de cette section, vous serez capable de :
- Expliquer ce que sont les processus et comment ils interagissent avec le système d'exploitation
- Discuter de la manière dont les systèmes d'exploitation prennent en charge la concurrence
Comme nous l'avons mentionné précédemment, l'OS divise les tâches qu'il doit exécuter en processus. Il serait une perte de temps pour chaque processus (process) d'attendre que le processus actuel se termine. Au lieu de cela, l'OS exécute plus d'une tâche simultanément, ou de manière concurrente. Le modèle informatique qui améliore les performances lorsque plusieurs processeurs exécutent des instructions simultanément est le traitement concurrent (concurrent processing). Dans ce module, nous abordons les processus et la concurrence en approfondissant la gestion des processus et la communication interprocessus (inter-process communication) (IPC), les fils d'exécution, l'ordonnancement (scheduling) et la répartition, et la synchronisation (synchronization).
Processus
Pour rappel, un processus est un concept fondamental dans un OS qui représente une instance d'un programme en cours d'exécution. C'est une abstraction utilisée par l'OS pour fournir l'environnement dont un programme a besoin pour s'exécuter. Lorsque nous parlons d'un programme, nous faisons généralement référence à un ensemble d'instructions stockées sur disque ; ces instructions sont passives et ne font rien par elles-mêmes. Cependant, lorsque le programme est chargé dans la mémoire d'un ordinateur et commence son exécution, il devient une entité active connue sous le nom de processus. Cette transformation est cruciale pour toute tâche computationnelle, car elle fait passer le programme d'un état statique à un état actif où il peut effectuer des actions, manipuler des données et interagir avec d'autres processus.
Concepts en pratique
Sous le capot
La plupart des applications que nous utilisons aujourd'hui sur nos smartphones ou ordinateurs portables utilisent l'IPC et une architecture client-serveur. Il est donc important de comprendre ce qui se passe sous le capot au cas où ces applications cesseraient soudainement de fonctionner.
Dans un OS, la communication client-serveur fait référence à l'échange de données et de services entre plusieurs machines ou processus. Un processus ou une machine agit comme un client demandant un service ou des données, et une autre machine ou un autre processus agit comme un serveur fournissant ces services ou données à la machine cliente. La communication entre le serveur et le client utilise divers protocoles et mécanismes d'OS pour le passage de messages, y compris les sockets, les appels de procédure distante et la communication interprocessus.
Gestion des processus
Un processus se compose d'au moins un espace d'adressage (address space), d'un CPU state, et d'un ensemble de ressources OS. L'espace d'adressage d'un processus est illustré à la Figure 6.15. Comme vous l'avez appris plus tôt dans ce chapitre, l'espace d'adressage contient le code d'instruction du programme en cours d'exécution correspondant et les données nécessaires au programme en cours d'exécution. Les données peuvent être des données statiques (static data), qui ne changent pas au sein du programme, ou des données du tas (heap data), qui servent de collection d'éléments et utilisent une structure de données en arbre ou en pile. Un état du processeur (CPU state) se compose du compteur ordinal, du pointeur de pile (SP) et des registres à usage général. Le PC est un registre (register) CPU situé dans le processeur qui contient l'adresse de l'instruction suivante. Le pointeur de pile (SP) (stack pointer (SP)) est un registre qui indique l'emplacement du dernier élément ajouté à la pile. Un registre à usage général (GPR) (general-purpose register (GPR)) est un registre supplémentaire utilisé pour stocker des opérandes et des pointeurs ; les GPR sont l'endroit où les instructions peuvent lire et écrire la valeur de leurs paramètres, principalement lorsque le programme est interrompu. Il existe de nombreuses ressources OS telles que le CPU, les connexions réseau, le stockage de fichiers, les I/O et les canaux audio. Un espace d'adressage, un état du processeur et des ressources OS sont tout ce dont vous avez besoin pour exécuter le programme ou le reprendre s'il est interrompu à un moment donné.
Les particularités de l'espace de noms de processus de l'OS dépendent de l'OS spécifique, mais en général, le nom d'un processus est appelé un identifiant de processus (PID) (process ID (PID)), et il est de type entier. Un espace de noms PID est un ensemble de numéros uniques qui identifient les processus. L'espace de noms PID est global au système, et un seul processus à la fois a un PID particulier. Il est possible de créer un conteneur (container) pour isoler un processus, avec uniquement les fichiers et les configurations nécessaires pour l'exécuter et le faire fonctionner. Ce type d'environnement de processus isolé permet une plus grande sécurité, cohérence et portabilité entre différents systèmes. Les espaces de noms PID permettent aux conteneurs de fournir des fonctionnalités telles que la suspension de l'ensemble des processus et la reprise d'un ensemble différent de processus en mémoire. L'OS maintient une structure de données appelée bloc de contrôle de processus (PCB) (process control block (PCB)) ou descripteur de processus pour suivre l'état d'un processus et stocker toutes les informations identifiées par le PID concernant un processus. Comme illustré à la Figure 6.16, le PCB contient des informations qui servent de métadonnées pour le processus telles que le PID, l'état du processus, l'identifiant du processus parent (PPID), l'état d'exécution, le PC, le SP, les registres, les informations sur l'espace d'adressage (address space) et l'identifiant utilisateur (uid).
Supposons qu'il y ait un processus qui nécessite une entrée de la part d'un utilisateur. Comme l'OS n'utilise aucun CPU pendant qu'il attend cette entrée, il marque le processus dans le PCB comme suspendu. Lorsque l'utilisateur entre l'entrée, le statut du processus dans le PCB change. L'OS conserve les détails relatifs à l'état d'exécution de tous les processus dans le PCB lorsque le processus n'est pas en cours d'exécution. L'état du processeur (CPU state) (par exemple, PC, SP, GPR) est transféré des registres matériels vers le PCB lorsque l'exécution est suspendue. Lorsqu'un processus (process) est en cours d'exécution, son état est réparti entre le PCB et le CPU.
Voici un exemple sous Linux qui illustre comment utiliser le PID lors de la création d'un processus enfant à partir d'un processus parent principal, qui utilise la commande fork en C :
#include "unistd.h"
#include <stdio.h>
int main() {
// Forking to create a new process
pid_t pid = fork();
if (pid == 0) {
// Child process
printf("This is the child process with PID %d\n", getpid());
} else if (pid > 0) {
// Parent process
printf("This is the parent process with PID %d\n", getpid());
} else {
// Fork failed
printf("Fork failed!\n");
return 1;
}
return 0;
}
Communication interprocessus
Les processus offrent une isolation (isolation) pour garantir un haut niveau de protection (protection), mais parfois ces processus doivent communiquer et collaborer. Cela est rendu possible via la communication interprocessus (IPC) (inter-process communication (IPC)), qui est un mécanisme (mechanism) qui permet à différents processus s'exécutant sur un système d'exploitation (operating system) d'échanger des données entre eux et permet ainsi à ces processus de communiquer et de collaborer. Comme le montre la Figure 6.17, l'IPC permet à un processus, P1, de fournir une entrée à un autre processus, P2, tandis qu'un autre processus, P3, est également en cours d'exécution.
Lien vers l'apprentissage
Lisez cet article sur la communication interprocessus pour en savoir plus sur la façon dont les processus s'exécutant dans un système informatique peuvent être indépendants ou non coopératifs.
Flux, tubes et sockets
L'IPC dispose d'une gamme de mécanismes qui permettent aux processus de communiquer entre eux, tels que les tubes, la mémoire partagée et les sockets. Un tube (pipe) (parfois appelé tube nommé) est une méthode de communication de données entre deux processus qui utilise un nom spécifique et des opérations d'I/O standard, et permet ainsi le transfert de données au sein d'un système de fichiers (file system). La mémoire partagée permet aux processus de communiquer entre eux sans intermédiaire. Une socket est un point d'extrémité pour l'envoi et la réception de données entre différentes machines du même réseau utilisant les protocoles Internet.
Cela signifie que, par exemple dans la Figure 6.17, où le processus (process) P1 fournit une entrée au processus P2, il existe de nombreuses façons pour l'IPC de livrer cette entrée. Il pourrait envoyer des arguments de ligne de commande qui ne sont disponibles que pour le processus parent (c'est-à-dire que les données d'entrée sont passées à un programme lorsque le programme est invoqué à partir d'un shell) ; ou il pourrait communiquer via des fichiers (par exemple, un processus écrit, l'autre processus lit). Alternativement, l'IPC pourrait optimiser la communication de fichiers via l'utilisation de tubes avec des tampons mémoire (efficace lorsque les processus sont liés), ou il pourrait utiliser des variables d'environnement (c'est-à-dire des variables définies dans un shell qui peuvent contenir une valeur allouée dynamiquement).
Concurrence
Plusieurs activités et processus se déroulant simultanément — en d'autres termes, l'OS gérant plusieurs tâches à la fois — est appelé concurrence (concurrency). Le traitement concurrent peut être réalisé via un environnement de multiprogrammation, un environnement de multiprocesseur ou un environnement de traitement distribué (Figure 6.18). Dans un environnement de multiprogrammation, plusieurs tâches sont partagées par un seul processeur. Dans un environnement de multiprocesseur, deux processeurs ou plus qui ont une mémoire partagée sont utilisés. Dans un environnement de traitement distribué, deux ordinateurs ou plus sont connectés par un réseau de communication, et il n'y a pas de mémoire partagée entre les processeurs (c'est-à-dire que chaque ordinateur a sa propre mémoire). Le multiprocesseur est utilisé pour accélérer le traitement en exécutant des tâches en parallèle, tandis que les environnements de calcul distribué sont généralement utilisés pour implémenter des architectures client-serveur ou pair-à-pair. Comme indiqué précédemment et comme cela sera approfondi dans cette section, les fils d'exécution sont un autre moyen d'atteindre la concurrence. Cependant, un véritable parallélisme ne peut être atteint qu'en utilisant plusieurs processeurs pour exécuter plusieurs fils d'exécution simultanément.
Lien vers l'apprentissage
Lisez cet article sur la concurrence dans les systèmes d'exploitation pour en savoir plus sur les principes et les problèmes associés au concept de concurrence.
Fils d'exécution
Comme discuté précédemment dans ce chapitre, les processus représentent des programmes en cours d'exécution, et les fils d'exécution permettent aux programmes d'effectuer plusieurs choses simultanément. Un processus (process) est une instance d'un programme qui est exécuté par un OS. Chaque processus a son propre espace mémoire et ses propres ressources. L'OS crée un nouveau processus pour chaque programme exécuté et alloue les ressources nécessaires au processus. Par exemple, il alloue une taille spécifique de mémoire et de temps CPU. Le processus peut avoir un ou plusieurs fils d'exécution, chaque fil d'exécution (thread) a son propre contexte, mais tous les fils d'exécution au sein d'un processus partagent les mêmes ressources.
Les fils d'exécution sont l'unité de concurrence de l'OS et l'abstraction d'un cœur de CPU virtuel. Chaque fil d'exécution est une unité d'exécution de base qui exécute un ensemble d'instructions dans un ordre spécifique. Un fil d'exécution est un processus léger qui partage les ressources de l'OS (par exemple, la mémoire et les I/O) avec d'autres fils d'exécution au sein du même processus. Les fils d'exécution sont créés par le noyau (kernel) de l'OS, et chaque fil d'exécution a sa propre pile de registres (register). Tous les fils d'exécution d'un processus donné partagent (sharing) le même espace mémoire. Les fils d'exécution sont essentiellement des chemins d'exécution qui opèrent dans les limites d'un processus.
Par exemple, considérons les navigateurs web actuels. Chaque onglet ouvert dans un navigateur web est son propre processus avec son propre espace d'adressage (address space), mais au sein d'un onglet, plusieurs choses peuvent se passer. Un utilisateur peut faire défiler et interagir avec une page web pendant qu'une vidéo est lue en arrière-plan. Dans ce cas, un fil d'exécution pourrait être utilisé pour gérer les interactions de l'utilisateur, tandis qu'un autre fil d'exécution est utilisé pour gérer la lecture vidéo sur la page web.
Dans les premières versions du système d'exploitation utilisé sur les ordinateurs centraux IBM et DEC, la concurrence (concurrency) était réalisée via le partage de temps. En d'autres termes, une seule tâche était exécutée en utilisant un seul processus avec un seul fil d'exécution. Ce type de processus ne permettait qu'à un seul utilisateur à la fois de traiter ou d'exécuter une tâche. Cette ancienne méthode de traitement nécessitait plus de ressources telles que de la mémoire et des processeurs pour accomplir une seule tâche. À la fin des années 1970, l'approche la plus proéminente est devenue le multitâche (multitasking), qui permet à l'OS d'exécuter plusieurs processus simultanément en utilisant le découpage temporel. Une tranche de temps (time slice) est un court laps de temps qui est attribué à un processus pour l'exécution du CPU. Elle correspond au laps de temps pendant lequel un processus est alloué pour s'exécuter dans les CPU à multitâche préemptif. Dans ce cas, un ordonnanceur exécute chaque processus à chaque tranche de temps. L'approche du multitâche préemptif n'était pas suffisante pour améliorer les performances de l'OS, de sorte que vingt ans plus tard, les OS prennent toujours en charge le multitâche en utilisant plusieurs fils d'exécution. La Figure 6.19 illustre les processus à un seul fil et à plusieurs fils d'exécution.
Lorsqu'un programme est exécuté, il fonctionne comme un processus dans l'OS. Ce processus peut exécuter plusieurs fils d'exécution simultanément, permettant l'exécution parallèle de tâches au sein du même environnement d'application. Les fils d'exécution sont gérés et exécutés comme suit :
- Création de fils d'exécution : Les fils d'exécution sont créés par le processus en utilisant des appels système spécifiques au système d'exploitation, tels que pthread_create sous UNIX/Linux ou CreateThread sous Windows. Lors de leur création, chaque fil d'exécution commence son exécution à un point de départ désigné dans le code du programme. Il s'agit souvent d'une fonction passée à l'appel de création de fil d'exécution.
- Exécution et ordonnancement : Une fois créés, les fils d'exécution sont ordonnancés par l'ordonnanceur du système d'exploitation, qui leur alloue du temps CPU. L'ordonnancement peut être préemptif, où l'OS décide quand basculer entre les fils d'exécution, ou coopératif, où les fils d'exécution cèdent volontairement le contrôle pour permettre à d'autres fils d'exécution de s'exécuter.
- Partage et isolation : Les fils d'exécution partagent les mêmes ressources de processus, telles que la mémoire et les descripteurs de fichiers, rendant la communication interprocessus et le partage de données plus efficaces qu'entre des processus distincts. Cependant, ils s'exécutent dans leur propre fil d'exécution, ce qui signifie que chacun a sa propre pile, son compteur ordinal et son ensemble de registres pour suivre son état d'exécution.
- Synchronisation : Pour gérer en toute sécurité l'accès aux ressources partagées et assurer la cohérence des données, les fils d'exécution utilisent souvent des mécanismes de synchronisation tels que les mutex, les sémaphores et les variables de condition. Ces outils aident à prévenir les conditions de concurrence, où le résultat des opérations dépend de la séquence ou du timing d'autres événements incontrôlables.
- Achèvement : Un fil d'exécution termine son exécution lorsqu'il quitte sa fonction de démarrage, soit en retournant normalement, soit en étant explicitement terminé par lui-même ou par un autre fil d'exécution. Une fois l'achèvement, toutes les ressources spécifiquement allouées au fil d'exécution sont nettoyées par le système d'exploitation.
Ordonnancement/Répartition
L'ordonnancement des tâches et la détermination des ressources à utiliser sont des responsabilités centrales de l'OS — ce sont aussi les moyens par lesquels l'OS réalise le traitement concurrent (concurrent processing).
Les OS peuvent basculer le CPU d'un processus à l'autre des centaines de milliers de fois par seconde. Sur le matériel actuel, cela prend quelques microsecondes. Choisir quel processus (process) exécuter ensuite est appelé ordonnancement. L'activité de gestion du retrait du processus en cours d'exécution du CPU et de la sélection d'un autre processus basée sur une stratégie particulière est appelée ordonnancement. Dans les OS, un processus peut être dans l'un des plusieurs états. Ces états font partie du cycle de vie du processus, et les comprendre est essentiel pour saisir comment l'OS gère les processus. Les noms spécifiques et le nombre d'états peuvent varier entre les OS, mais les concepts fondamentaux restent les mêmes — ou du moins assez similaires. Chaque processus a un état d'exécution, qui indique ce qu'il fait actuellement et peut être comme suit :
- prêt : en attente d'être assigné à un cœur
- en cours d'exécution : s'exécutant sur un cœur
- bloqué (également connu sous le nom de « en attente ») : en attente d'un événement, donc non éligible pour se voir attribuer un cœur
La Figure 6.20 représente le cycle de vie d'un processus au sein d'un OS. Il commence par la création d'un processus, qui donne naissance à un nouveau processus et le place dans l'état prêt (ready state). Dans cet état, le processus est chargé en mémoire et est prêt à s'exécuter, mais il attend que le CPU devienne disponible. Lorsque l'ordonnanceur répartit le processus, celui-ci passe à l'état d'exécution (running state), où il exécute activement ses instructions. Si le processus est interrompu, il revient à l'état prêt, attendant à nouveau une chance de s'exécuter. Certains événements, tels que des requêtes d'I/O ou des défauts de page, peuvent provoquer un piège ou une exception (Exception) pour le processus en cours d'exécution. Lorsque cela se produit, le processus entre dans l'état bloqué (blocked state) car il ne peut pas continuer tant que l'événement qu'il attend, comme l'achèvement d'une opération d'I/O, ne se produit pas. Une fois l'événement attendu terminé, le processus peut quitter l'état bloqué et revenir à l'état prêt, attendant à nouveau du temps CPU.
Lorsqu'un processus termine son exécution ou est arrêté, il atteint l'état de terminaison. Cependant, la terminaison ne signifie pas nécessairement que le processus est immédiatement supprimé du système ; il peut entrer dans un état zombie. Dans cet état, le processus a terminé son travail mais occupe toujours une entrée dans la table des processus, étant effectivement dans un état d'incertitude jusqu'à ce que son processus parent reconnaisse son achèvement. Cette reconnaissance permet à l'OS de nettoyer complètement toute information restante, mettant officiellement fin au cycle de vie du processus.
Comme indiqué précédemment, chaque processus est représenté par un identifiant unique appelé identifiant de processus (PID). Un OS encapsule le processus dans un type de structure de données appelé bloc de contrôle de processus (process control block) (PCB) qui contient des informations sur le processus, telles que son statut actuel, qui pourrait être un statut d'exécution, un statut prêt ou un statut bloqué. Un PCB définit le statut du registre (register), l'identifiant de processus (process ID), le temps d'exécution, l'espace mémoire, ainsi que d'autres informations. La fonction d'ordonnancement (scheduling) du noyau (kernel) de l'OS est responsable du maintien du contenu du PCB et de l'ordonnancement des processus pour que le CPU les exécute en fonction des priorités assignées.
Une autre décision de politique (policy) prise par l'OS est de décider s'il doit allouer des ressources non-CPU telles que la mémoire et les I/O. Étant des structures de données, les PCB sont situés dans la mémoire de l'OS. Lorsqu'un processus est créé, l'OS lui alloue un PCB. Après avoir initialisé le PCB, l'OS effectue ensuite d'autres actions non liées au PCB, telles que l'allocation du PCB à la bonne file d'attente. Au fur et à mesure qu'un processus s'exécute, l'OS déplace le PCB du processus d'une file d'attente à l'autre. Lorsqu'un processus est terminé, le PCB peut être conservé pendant un certain temps. Finalement, l'OS désalloue le PCB.
L'acte de basculer le CPU d'un processus à un autre est appelé une commutation de contexte (context switch). La commutation de contexte est une procédure que le CPU d'un ordinateur suit pour passer d'une tâche à l'autre tout en s'assurant que les tâches ne sont pas en conflit. Dans la Figure 6.21, le processus P0 est dans l'état d'exécution (running state), et le processus P1 est dans l'état prêt (ready state). Lorsqu'une interruption se produit, le processus P0 doit passer de l'état d'exécution à l'état prêt, et le processus P1 doit passer de l'état prêt à l'état d'exécution. Pour ce faire, l'OS effectue ces étapes : il sauvegarde le contexte du processus P0 dans PCB0, bascule P0 de l'état prêt, sélectionne P1 à exécuter, et, enfin, met à jour PCB1 du processus P1.
Transactions et ordonnancement
Comme vous l'avez appris, l'ordonnancement est l'acte de déterminer quel processus est dans l'état prêt et doit être déplacé vers l'état d'exécution lorsque plus de ressources sont demandées qu'il ne peut en être accordé immédiatement, et dans quel ordre ces requêtes doivent être traitées. Les exemples incluent l'ordonnancement du processeur (c'est-à-dire un processeur, plusieurs fils d'exécution) et l'ordonnancement de la mémoire dans les systèmes de mémoire virtuelle (virtual memory).
Un bon algorithme d'ordonnancement minimise le temps de réponse, utilise efficacement les ressources (c'est-à-dire qu'il assure une utilisation complète en maintenant les cœurs et les disques occupés, avec un minimum de commutations de contexte), et met en œuvre l'équité en distribuant les cycles CPU de manière équitable. Idéalement, les algorithmes d'ordonnancement ne devraient pas affecter les résultats produits par le système. Les schémas d'ordonnancement (scheduling) optimaux exigeraient la capacité de prédire l'avenir, faisant des algorithmes adaptatifs le choix préféré.
Voici des exemples d'algorithmes d'ordonnancement simples :
- ordonnancement premier arrivé, premier servi (FCFS) (first come, first served (FCFS)), également appelé premier entré, premier sorti (FIFO) : Dans cet algorithme, la première tâche qui arrive au processeur est exécutée en premier. Par exemple, supposons que nous ayons deux processus — le processus P1 avec un temps d'exécution de 3 et le processus P2 avec un temps d'exécution de 2. Le temps d'arrivée pour les deux processus P1 et P2 est t0. Le système commencera par le processus P1 à t0 et terminera à t3, et le processus P2 commencera à t3 pour se terminer à t5, comme le montre la Figure 6.22. Le temps d'attente pour le processus P1 est de 0 et le temps d'attente pour le processus P2 est de 3.
Figure 6.22 Deux processus, P1 et P2, sont traités à l'aide de l'algorithme FCFS. (source : Copyright Rice University, OpenStax, sous licence CC BY 4.0)
- ordonnancement tourniquet (RR) (round-robin scheduling (RR)) : Cet algorithme, largement utilisé dans les systèmes à temps partagé, est conçu pour assurer l'équité entre les processus en donnant à chaque processus une part égale du CPU. Son fonctionnement est relativement simple mais efficace dans les environnements où un grand nombre de processus doivent être traités efficacement. L'idée principale de l'ordonnancement RR est d'attribuer une tranche de temps fixe, souvent appelée quantum, à chaque processus de la file d'attente des processus prêts. L'ordonnanceur CPU parcourt la file d'attente, allouant le CPU à chaque processus pour une durée égale à 1 quantum. Pour l'exemple précédent, fixons le quantum à 2. Alors, le processeur exécutera une partie du processus P1 et passera au processus P2, puis reviendra au processus P1 (Figure 6.23).
Figure 6.23 Deux processus, P1 et P2, sont traités à l'aide de l'algorithme RR. (source : Copyright Rice University, OpenStax, sous licence CC BY 4.0)
- temps d'achèvement le plus court en premier (STCF) (shortest time to completion first (STCF)), également appelé tâche la plus courte en premier (SJF) : Cet algorithme adopte la meilleure approche pour minimiser le temps d'attente, mais il exige que le processeur connaisse le temps de traitement à l'avance. Dans l'exemple précédent, le processeur exécutera le processus P2 avant le processus P1 car le temps d'exécution est moindre.
- temps de traitement restant le plus court (SRPT) (shortest remaining processing time (SRPT)) : En SRPT, le processeur est réaffecté à une nouvelle tâche prête avec un temps d'achèvement restant plus court chaque fois qu'une telle tâche arrive.
Synchronisation
La synchronisation en programmation concurrente est cruciale pour garantir que plusieurs fils d'exécution ou processus peuvent travailler ensemble sans interférer avec les opérations des uns et des autres sur des ressources partagées. Grâce à des mécanismes tels que les verrous, les variables de condition et les sémaphores, les développeurs peuvent concevoir des systèmes à la fois efficaces et sûrs, évitant des problèmes tels que les conditions de concurrence et les interblocages. Une façon de coordonner plusieurs activités concurrentes qui utilisent un état partagé est la synchronisation (synchronization), qui regroupe automatiquement les opérations pour assurer la coopération entre les fils d'exécution. Pour s'assurer qu'un seul fil d'exécution (thread) effectue une tâche particulière à la fois, nous pouvons utiliser un programme appelé exclusion mutuelle (mutual exclusion) qui empêche l'accès simultané à une ressource partagée. Une section critique (critical section) est une portion de code qu'un seul fil d'exécution peut exécuter à la fois. De plus, un seul fil d'exécution à la fois entrera dans cette section de code. Une section critique est le résultat de l'exclusion mutuelle. Les sections critiques et l'exclusion mutuelle sont en fait deux façons de décrire la même chose. Les sections critiques sont des séquences d'instructions qui peuvent donner des résultats incorrects si elles sont exécutées simultanément. L'exclusion mutuelle est nécessaire pour garantir qu'un processus (process) ne peut pas entrer dans sa section critique tant qu'un autre processus concurrent est actuellement présent dans sa section critique. La Figure 6.24 illustre une représentation de deux processus, le processus P1 et le processus P2. Au temps 1, le processus P1 est entré dans la section critique en imprimant sur une imprimante partagée. Le processus P1 terminera l'impression au temps 2. Pendant que le processus P1 imprime, le processus P2 tente d'imprimer, mais l'OS bloquera le processus P2 jusqu'à ce que le processus P1 atteigne le temps 2.
Un verrou (lock) est un mécanisme (mechanism) de synchronisation utilisé pour faire respecter l'exclusion mutuelle. Un fil d'exécution doit acquérir un verrou avant d'entrer dans une section critique ; si le verrou est déjà détenu par un autre fil d'exécution, le fil d'exécution qui tente d'y accéder sera bloqué jusqu'à ce que le verrou devienne disponible. Un autre mécanisme de synchronisation appelé variable de condition (condition variable) est utilisé conjointement avec les verrous pour permettre aux fils d'exécution d'attendre que certaines conditions deviennent vraies. Alors qu'un verrou facilite l'accès exclusif aux ressources, une variable de condition aide les fils d'exécution à attendre des états spécifiques du monde.
Un exemple classique de défi de synchronisation est le problème des producteurs/consommateurs, où les producteurs génèrent des données et les placent dans un tampon, et les consommateurs retirent des données du tampon pour traitement. Dans un scénario producteur/consommateur, un consommateur pourrait attendre sur une variable de condition si le tampon est vide, et un producteur pourrait signaler cette variable une fois qu'il ajoute un élément au tampon. Les principales préoccupations associées à ce scénario incluent la garantie que les producteurs n'ajoutent pas de données à un tampon plein et que les consommateurs n'essaient pas de retirer des données d'un tampon vide. Des outils de synchronisation comme les verrous et les variables de condition sont utilisés pour résoudre ces problèmes. Un interblocage (deadlock) se produit lorsqu'un ensemble de fils d'exécution sont bloqués indéfiniment, attendant que les uns et les autres libèrent des ressources. Cela peut arriver, par exemple, si le fil d'exécution (thread) A détient le verrou 1 et attend d'acquérir le verrou 2, tandis que le fil d'exécution B détient le verrou 2 et attend d'acquérir le verrou 1. Éviter les interblocages nécessite une conception minutieuse, comme l'acquisition de verrous dans un ordre cohérent ou l'utilisation de mécanismes de temporisation.
D'autres outils utilisés en synchronisation (synchronization) sont :
- Sémaphores : Similaires aux verrous, mais permettent à plusieurs fils d'exécution d'accéder à un nombre fini de ressources
- Barrières : Permettent à plusieurs fils d'exécution d'attendre que tous aient atteint un certain point de leur exécution avant que l'un d'eux ne soit autorisé à continuer
- Verrous lecture-écriture : Permettent à plusieurs lecteurs d'accéder à une ressource concurremment mais exigent un accès exclusif pour les écrivains
- Boîtes aux lettres : Canaux dédiés qui connectent directement deux processus, permettant l'échange de données entre eux ; les boîtes aux lettres se comportent comme des files d'attente mais utilisent des sémaphores pour un accès automatique contrôlé, et fonctionnent uniquement selon l'ordre premier entré, premier sorti (FIFO).
La technologie au quotidien
Multitâche
La capacité d'exécuter plusieurs programmes simultanément sur les ordinateurs d'aujourd'hui a un impact énorme sur la productivité dans toutes les industries. En programmation concurrente, la gestion des ressources partagées entre plusieurs fils d'exécution ou processus est cruciale pour maintenir l'intégrité des données et prévenir les conditions de concurrence. Un scénario courant est le problème producteur-consommateur, où un ou plusieurs fils d'exécution (producteurs) génèrent des données, et d'autres (consommateurs) les consomment. Pour éviter les conflits, des mécanismes de synchronisation comme les verrous et les variables de condition sont utilisés. Les verrous aident à garantir qu'un seul fil d'exécution accède à une section critique (critical section) de code à la fois. Dans le contexte du problème producteur-consommateur, les verrous peuvent être utilisés pour protéger les structures de données partagées, empêchant l'accès simultané par plusieurs fils d'exécution et assurant la cohérence des données.
Les variables de condition sont un autre outil de synchronisation qui permet aux fils d'exécution de coordonner leurs activités. Dans le contexte des producteurs et des consommateurs, une variable de condition pourrait signaler quand des données sont disponibles pour la consommation ou quand de l'espace est disponible pour la production. Les fils d'exécution peuvent utiliser ces signaux pour attendre ou notifier efficacement les autres de l'état des ressources partagées. La combinaison de verrous et de variables de condition offre un moyen puissant de synchroniser les interactions complexes entre producteurs et consommateurs, assurant un accès ordonné aux ressources partagées.
Malgré les avantages des mécanismes de synchronisation, l'utilisation inappropriée des verrous peut entraîner des problèmes tels que les interblocages, où deux fils d'exécution ou processus ou plus sont bloqués dans une attente circulaire, incapables de progresser car chacun attend que l'autre libère une ressource. Cette situation peut paralyser un système, et une conception et des pratiques de codage minutieuses sont nécessaires pour prévenir ou détecter et récupérer des interblocages. Pour gérer les interblocages, des techniques telles que les algorithmes de détection d'interblocages et les stratégies de prévention sont utilisées, contribuant à la robustesse des systèmes concurrents.
Allocation
La méthode qui définit la manière dont les données sont stockées en mémoire en fournissant un ensemble de requêtes de ressources et en identifiant quels processus doivent recevoir quelles ressources pour utiliser les ressources le plus efficacement possible est appelée allocation (allocation). Comme l'ordonnancement (scheduling), l'allocation est un autre type de prise de décision qu'un OS effectue sur la façon d'utiliser les ressources pour prendre en charge la concurrence (concurrency). Il existe trois formes principales d'allocation : l'allocation contiguë, l'allocation liée et l'allocation indexée.
En allocation contiguë, chaque fichier est assigné à un ensemble de blocs contigus (c'est-à-dire voisins) en mémoire. Par exemple, si un fichier nécessite trois blocs et reçoit un emplacement de départ x, le fichier sera alloué en x, x + 1, x + 2. L'entrée de répertoire (directory) avec allocation contiguë contient l'adresse du bloc de départ et la longueur du fichier. En allocation liée, chaque fichier est une liste chaînée de blocs mémoire. En utilisant le même exemple, le premier bloc sera alloué à l'emplacement x et il inclura l'adresse du deuxième bloc. L'entrée de répertoire avec allocation liée contient un pointeur vers le bloc de départ et un pointeur vers le dernier bloc. En allocation indexée, chaque fichier a un bloc d'index contenant les pointeurs vers tous les blocs de ce fichier.