Gestion des processus et des ressources par un système d'exploitation ===================================================================== <br><br> *Ce document est un diaporama qui résume le cours correspondant (accessible [ici](gestion-processus-ressources)).* --- # Introduction <img class="r-stretch centre image-responsive" src="data/role_os.png" alt="illustration du rôle d'un OS entre les applications et le matériel"> <small class="legende"> <strong>Fig. 1 - Schéma de l'OS à l'interface entre les applications et le matériel</strong> <br>Crédits : Mickaël Barraud, Synthèse de cours de Première NSI </small> ---- # Introduction - Les systèmes d'exploitation modernes permettent d'exécuter plusieurs tâches "simultanément" : - Par exemple : un navigateur, un IDE, un terminal, etc. - Pourtant (cf. cours de Première) ... - un programme est une suite d'instructions machines - et un processeur ne peut exécuter qu'une seule instruction machine à la fois - Comment le système d'exploitation parvient à faire en sorte que tous les programmes puissent s'exécuter de façon équitable et sans se gêner les uns les autres ? --- # Processus - Ne pas confondre : - un **programme** = fichier binaire (on dit aussi un exécutable) contenant des instructions machines - un **processus** = **programme en cours d'exécution** : le *phénomène dynamique* lié à l'exécution d'un programme par l'ordinateur. - Un processus est donc une *instance d'un programme* auquel est associé : - du code - des données/variables manipulées - des ressources : processeur, mémoire, périphériques d'entrée/sortie ---- ## Observer les processus - Sous Linux, la commande `ps` permet d'afficher des infos sur les processus - Exemple : commande `ps -aef` <img class="r-stretch centre image-responsive" src="data/ps-aef.png"> ---- ## Création d'un processus - Un processus peut être créé : - au démarrage du système - par un autre processus - par une action d'un utilisateur (lancement d'un programme) - Sous GNU/Linux : - un premier processus est créé au démarrage (c'est le processus 0, souvent appelé *Swapper*) - ce processus crée un processus appelé *init* - à partir de *init*, les autres processus nécessaires au fonctionnement de l'OS sont créés - ces processus créent eux-mêmes d'autre processus, etc. ---- ## PID et PPID - **PID** (*Process Identifier*) : numéro qui identifie chaque processus - **PPID** (*Parent Process Identifier*) : le PID du processus parent <img class="r-stretch centre image-responsive" src="data/ps-aef.png"> ---- ### ✍️ À faire Lancez un terminal et exécutez la commande `ps -aef`. 1. À quoi correspond le dernier processus lancé ? Quel est son processus parent et pourquoi ? 2. Ouvrez Writer de LibreOffice puis exécutez à nouveau la commande. - Cherchez dans la liste des processus celui ou ceux correspondant à l'exécution du programme Writer. - Cherchez le processus parent du processus correspondant à l'exécution du programme Writer. 3. Fermez LibreOffice puis exécutez à nouveau la commande et vérifiez qu'il n'y a plus de processus correspondant à l'exécution de Writer. ---- ### ✍️ À faire (suite) 4. Ouvrez le navigateur Firefox puis exécutez à nouveau la commande `ps -aef`. - Cherchez dans la liste des processus le premier correspondant à l'exécution du navigateur - Cherchez ensuite les processus fils de ce processus. 5. Ouvrez un nouvel onglet dans le navigateur et rendez-vous sur une page Web de votre choix. Exécutez à nouveau la commande, vous devriez constater qu'au moins un nouveau processus lié à l'exécution du navigateur a été créé. ---- ### Arborescence - Sous GNU/Linux, on peut voir l'arborescence des processus avec `pstree` <img class="r-stretch centre image-responsive" src="data/pstree.png"> ---- ### ✍️ À faire Testez la commande `pstree` et cherchez dans l'arborescence les processus correspondant à l'exécution du terminal, de firefox, de writer... Vous pouvez aussi ajouter l'option `-p` pour afficher l'arbre en entier et le PID de chaque processus : `pstree -p` --- # Gestion des processus et des ressources ---- ## Exécution concurrente - Les systèmes d'exploitation modernes sont capables d'exécuter plusieurs processus ~~en même temps~~ à tour de rôle. - On parle d'**exécution concurrente** car les processus sont en concurrence pour obtenir l'accès au processeur chargé de les exécuter. Note: Sur un système multiprocesseur, il est possible d'exécuter de manière parallèle plusieurs processus, autant qu'il y a de processeurs. Mais sur un même processeur, un seul processus ne peut être exécuté à la fois. ---- ## ✍️ Illustration avec Python - Créez deux fichiers vides `progA.py` et `progB.py` dans un répertoire `Documents/Theme3`. - Ouvrez les deux fichiers avec Thonny et complétez-les : **`progA.py`** ```python import time for i in range(100): print("programme A en cours, itération", i) time.sleep(0.01) # pour simuler un traitement avec des calculs ``` **`progB.py`** ```python import time for i in range(100): print("programme B en cours, itération", i) time.sleep(0.01) # pour simuler un traitement avec des calculs ``` ---- ## ✍️ Illustration avec Python (suite) - Dans le Terminal, lancez simultanément ces deux programmes avec la commande : ```shell python3 progA.py & python3 progB.py & ``` > Le caractère `&` permet de lancer l'exécution en arrière plan et de rendre la main au terminal. Note: Il faut être placé dans le bon répertoire pour lancer la commande ! ---- ## ✍️ Illustration avec Python (suite) - On constate que l'OS alloue le processeur aux deux programmes *à tour de rôle* : <img class="r-stretch centre image-responsive" src="data/concurrence.png"> Note: - On voit les PID des deux processus correspondant au départ. - Faire Ctl+C pour quitter si nécessaire. ---- ## Accès concurrents aux ressources - Une **ressource** est une entité dont a besoin un processus pour s'exécuter. - Les ressources peuvent être *matérielles* (processeur, mémoire, périphériques d'entrée/sortie, ...) ou *logicielles* (variables). - Les différents processus se partagent les ressources, on parle alors d'**accès concurrents aux ressources**. - C'est l'OS qui est chargé de gérer les processus et les ressources qui leur sont nécessaires Note: Par exemple, - les processus se partagent tous l'accès à la ressource "processeur" - un traitement de texte et un IDE Python se partagent la ressource "clavier" ou encore la ressource "disque dur" (si on enregistre les fichiers), ... - un navigateur et un logiciel de musique se partagent la ressource "carte son", ... ---- ## États d'un processus - Au cours de son existence, un processus peut se retrouver dans trois états : - état **élu** : lorsqu'il est en cours d'exécution, c'est-à-dire qu'il obtient l'accès au processeur - état **prêt** : lorsqu'il attend de pouvoir accéder au processeur - état **bloqué** : lorsque le processus est interrompu car il a besoin d'attendre une ressource quelconque Note: - état bloqué : par exemple lorsqu'une action de l'utilisateur est nécessaire (choisir un chemin pour sauvegarder un fichier, saisie au clavier dans un programme Python, etc.) ---- ## États d'un processus <img class="r-stretch centre image-responsive" src="data/etats_processus.svg" width="500"> <p class="legende"><strong>Fig. 1 - Cycle de vie d'un processus.</strong></p> ---- ### ✍️ À faire Exercices 1 et 2 --- # Ordonnancement - L'OS attribue aux processus leurs états *élu*, *prêt* et *bloqué*. - C'est en réalité l'**ordonnanceur** (un des composants du système d'exploitation) qui réalise cette tâche appelée **ordonnancement des processus**. - Objectif de l'ordonnanceur : - choisir le processus à exécuter à l'instant $t$ (le processus *élu*) - déterminer le temps durant lequel le processeur lui sera alloué. ---- ## Algorithmes d'ordonnancement <img class="r-stretch centre image-responsive" src="data/ordonnancement.svg" width="700"> <p class="legende"><strong>Fig. 2 - Ordonnancement des processus.</strong></p> ---- ### Ordonnancement *First Come First Served* (FCFS) **Principe** : Les processus sont ordonnancés selon leur ordre d'arrivée ("premier arrivé, premier servi" en français) **Exemple** : Les processus $P_1(53)$, $P_2(17)$, $P_3(68)$ et $P_4(24)$ arrivent dans cet ordre à $t=0$ : <img class="r-stretch centre image-responsive" src="data/ordo_fcfs.png" width="650"> ---- ### Ordonnancement *Shortest Job First* (SJF) **Principe** : Le processus dont le temps d'exécution est le plus court est ordonnancé en premier. **Exemple** : $P_1$, $P_2$, $P_3$ et $P_4$ arrivent à $t=0$ <img class="r-stretch centre image-responsive" src="data/ordo_sjf.png" width="650"> ---- ### Ordonnancement *Shortest Remaining Time* (SRT) **Principe** : Le processus dont le temps d'exécution restant est le plus court parmi ceux qui restent à exécuter est ordonnancé en premier. **Exemple** : $P_3$ et $P_4$ arrivent à $t=0$ ; $P_2$ à $t=20$ ; $P_1$ à $t=50$ <img class="r-stretch centre image-responsive" src="data/ordo_srt.png" width="650"> ---- ### Ordonnancement temps-partagé (*Round-Robin* ou RR) **Principe** : C'est la politique du tourniquet : allocation du processeur par tranche (= quantum $q$) de temps. **Exemple** : quantum $q = 20$ et $n = 4$ processus <img class="r-stretch centre image-responsive" src="data/ordo_rr.png" width="650"> Note: Dans ce cas, s'il y a $n$ processus, chacun d'eux obtient le processeur au bout de $(n-1)\times q$ unités de temps au plus ---- ### Ordonnancement à priorités statiques **Principe** : Allocation du processeur selon des priorités *statiques* (= numéros affectés aux processus pour toute la vie de l'application) **Exemple** : priorités $(P_1, P_2, P_3, P_4) = (3, 2, 0, 1)$ où la priorité la plus forte est 0 (attention, dans certains systèmes c'est l'inverse : 0 est alors la priorité la plus faible) <img class="centre image-responsive" src="data/ordo_prio.png" width="650"> ---- ### ✍️ À faire Exercice 3 --- # Problèmes liés à l'accès concurrent aux ressources - Les processus se partagent souvent une ou plusieurs ressources, et cela peut poser des problèmes. ---- ## Problèmes de synchronisation - On va prendre l'exemple d'une **variable partagée** (= ressource logiciel) entre plusieurs processus ---- ### Illustration avec Python - Situation proposée : - Jeu multijoueur - Variable `nb_pions` : nombre de pions dispos dans le tas commun - Fonction `prendre_un_pion()` : permet à chaque joueur de prendre un pion, s'il reste au moins un pion ! - Problématique : il ne reste qu'un seul pion et deux joueurs utilisent la fonction `prendre_un_pion()` --> cela conduit à la création de deux processus `p1` et `p2` - Avec Python : le module `multiprocessing` permet de créer des processus ---- ### Illustration avec Python <pre style="height:560px" class="language-python"><code># pions.py from multiprocessing import Process, Value import time def prendre_un_pion(nombre): if nombre.value >= 1: time.sleep(0.0005) # pour simuler un traitement avec des calculs temp = nombre.value nombre.value = temp - 1 # on décrémente le nombre de pions if __name__ == '__main__': # création de la variable partagée initialisée à 1 nb_pions = Value('i', 1) # on crée deux processus p1 = Process(target=prendre_un_pion, args=[nb_pions]) p2 = Process(target=prendre_un_pion, args=[nb_pions]) # on démarre les deux processus p1.start() p2.start() # on attend la fin des deux processus p1.join() p2.join() print("nombre final de pions :", nb_pions.value)</code></pre> ---- ### Illustration avec Python - Comportement attendu : - l'un des deux processus, par exemple `p1`, est élu : la fonction `prendre_un_pion()` décrémente le dernier pion, puis `p1` est terminé - l'autre processus `p2` est ensuite élu : il ne reste plus de pions donc il ne se passe rien, `p2` termine - Mais ... <img class="r-stretch centre image-responsive" src="data/pions.png"> ---- ### Illustration avec Python Que s'est-il passé ? <pre class="stretch language-python"><code># pions_v2.py from multiprocessing import Process, Value import time def prendre_un_pion(nombre, num_processus): print(f"début du processus {num_processus}") if nombre.value >= 1: print(f"processus {num_processus} : étape A") time.sleep(0.0005) # pour simuler un traitement avec des calculs print(f"processus {num_processus} : étape B") temp = nombre.value nombre.value = temp - 1 # on décrémente le nombre de pions print(f"nb de pions restants à la fin du processus {num_processus} : {nombre.value}") if __name__ == '__main__': nb_pions = Value('i', 1) p1 = Process(target=prendre_un_pion, args=[nb_pions, 1]) p2 = Process(target=prendre_un_pion, args=[nb_pions, 2]) p1.start() p2.start() p1.join() p2.join() print("nombre final de pions :", nb_pions.value)</code></pre> ---- ### Illustration avec Python En exécutant `pions_v2.py` dans un terminal, on obtient ce genre de choses : <img class="stretch" src="data/pions_v2.png"> ---- ```python def prendre_un_pion(nombre, num_processus): print(f"début du processus {num_processus}") if nombre.value >= 1: print(f"processus {num_processus} : étape A") time.sleep(0.0005) # pour simuler un traitement avec des calculs print(f"processus {num_processus} : étape B") temp = nombre.value nombre.value = temp - 1 # on décrémente le nombre de pions print(f"nb de pions restants à la fin du processus {num_processus} : {nombre.value}") ``` <img class="stretch" src="data/pions_v2.png"> ---- ### Analyse - Il y a un problème si les deux processus ont pu entrer dans le `if` car ils vont tous les deux décrémenter le nombre de pions. - Cela peut se produire : - si le premier à y être entré a été interrompu avant d'avoir pu décrémenter le nombre de pions - puis si le second a le temps d'entrer dans le `if` lorsqu'il récupère la main **Comment éviter les problèmes de synchronisation ?** ---- ## Les verrous - Un **verrou** : objet partagé entre plusieurs processus mais qui garantit qu'un seul processus accède à une ressource à un instant donné. - Un verrou peut être acquis par les différents processus - Le premier à en faire la demande acquiert le verrou - Si le verrou est détenu par un processus, tout autre processus souhaitant l'obtenir est bloqué jusqu'à ce qu'il soit libéré ---- ### Illustration avec Python - Le module `multiprocessing` propose un verrou à travers un objet `Lock` - Deux méthodes peuvent s'appliquer à ces objets : - `.acquire()` : pour demander le verrou (un processus faisant la demande est bloqué tant qu'il ne l'a pas obtenu) - `.release()` : pour libérer le verrou (il pourra alors être obtenu par un autre processus en faisant la demande) ---- ### Illustration avec Python <pre class="stretch language-python"><code data-line-numbers="|21,23,24|5,7,14"># pions_v3.py from multiprocessing import Process, Value, Lock import time def prendre_un_pion(v, nombre, num_processus): print(f"début du processus {num_processus}") v.acquire() # acquisition du verrou if nombre.value >= 1: print(f"processus {num_processus} : étape A") time.sleep(0.0005) print(f"processus {num_processus} : étape B") temp = nombre.value nombre.value = temp - 1 v.release() # verrou libéré print(f"nb de pions restants à la fin du processus {numero_processus} : {nombre.value}") if __name__ == '__main__': # création de la variable partagée initialisée à 1 nb_pions = Value('i', 1) # verrou partagé par les deux processus verrou = Lock() # on crée deux processus p1 = Process(target=prendre_un_pion, args=[verrou, nb_pions, 1]) p2 = Process(target=prendre_un_pion, args=[verrou, nb_pions, 2]) # on démarre les deux processus p1.start() p2.start() # on attend la fin des deux processus p1.join() p2.join() print("nombre final de pions :", nb_pions.value) </code></pre> ---- ### Illustration avec Python - En exécutant (plusieurs fois) ce script dans un terminal on constate que le nombre final de pions est toujours égal à 0. <img class="stretch centre image-responsive" src="data/pions_v3.png"> ---- ### Illustration avec Python - Le verrou est acquis **avant** le bloc `if` grâce à `v.acquire()` - Dès qu'il est acquis par un processus, ce dernier est le seul à pouvoir exécuter le code jusqu'à qu'il soit libéré avec `v.release()` **après** avoir décrémenté le nombre pions - Cette portion s'appelle une **section critique** ```python def prendre_un_pion(v, nombre, numero_processus): v.acquire() # début section critique if nombre.value >= 1: time.sleep(0.0005) temp = nombre.value nombre.value = temp - 1 # fin de la section critique v.release() ``` Note: - Cela ne veut pas dire que le processus détenant le verrou ne peut pas être interrompu (même en section critique), mais les autres processus seront bloqués lorsqu'ils essaieront d'acquérir le même verrou. - Si on analyse la dernière capture d'écran, on voit d'ailleurs que `p1` est interrompu lorsqu'il est entré en section critique, `p2` est élu mais se retrouve nécessairement bloqué car il ne peut pas acquérir le verrou tant que `p1` ne l'aura pas libéré. ---- ### ✍️ À faire Exercice 4 ---- ## Risque d'interblocage - L'utilisation de verrous peut engendrer des problèmes d'interblocage - **Interblocage** (ou *deadlock*) : situation dans laquelle tout le monde se retrouve bloqué - Exemple : <img class="stretch" src="data/carrefour.png"> ---- ## Risque d'interblocage (suite) - Autres exemples : <div style="display:flex;justify-content:center;align-items:center"> <img src="data/ciseaux.png" height="500"> <img src="data/job.png"> </div> ---- ## Risque d'interblocage (suite) - En informatique : **interblocage = situation dans laquelle plusieurs processus s'attendent mutuellement** - Cas classique : - plusieurs ressources sont partagées par plusieurs processus et l'un deux possède indéfiniment une ressource nécessaire à un autre, et réciproquement. - on parle d'*attente circulaire* - Peut être provoquée par une (mauvaise) utilisation de **plusieurs** verrous. ---- ### Illustration avec Python <pre class="stretch language-python"><code data-line-numbers="|29,30,32,33|11,12,14,15,21,22,24,25"># interblocage.py from multiprocessing import Process, Lock import time import os def f1(v1, v2): print("PID du processus 1:", os.getpid()) for i in range(100): time.sleep(0.001) v1.acquire() v2.acquire() print("processus 1 en cours, itération ", i) v2.release() v1.release() def f2(v1, v2): print("PID du processus 2:", os.getpid()) for i in range(100): time.sleep(0.001) v2.acquire() v1.acquire() print("processus 2 en cours, itération ", i) v1.release() v2.release() if __name__ == '__main__': # création de deux verrous v1 = Lock() v2 = Lock() # création de deux processus p1 = Process(target=f1, args=[v1, v2]) p2 = Process(target=f2, args=[v1, v2]) # on démarre les deux processus p1.start() p2.start() # on attend la fin des deux processus p1.join() p2.join() </code></pre> ---- ### Illustration avec Python On peut lancer (plusieurs fois si nécessaire) le script à partir du terminal et constater que l'interblocage a lieu très souvent. <img class="stretch" src="data/interblocage.png"> ---- ### Illustration avec Python **Comment l'expliquer ?** <pre style="height:450px"><code>def f1(v1, v2): print("PID du processus 1:", os.getpid()) for i in range(100): time.sleep(0.001) v1.acquire() v2.acquire() print("processus 1 en cours, itération ", i) v2.release() v1.release() def f2(v1, v2): print("PID du processus 2:", os.getpid()) for i in range(100): time.sleep(0.001) v2.acquire() v1.acquire() print("processus 2 en cours, itération ", i) v1.release() v2.release() </code></pre> Note: - `p1` acquiert v1 puis est stoppé avant d'acquérir v2, `p2` prend la main et acquiert v2 et se retrouve bloqué car v1 est détenu par `p1`. Lorsque `p1` reprend la main il est aussi bloqué car v2 est détenu par `p2`. - Chaque processus détient un verrou et attend l'autre : l'attent est infinie ---- ### Illustration avec Python - Il n'y a pas d'autre choix que d'interrompre les processus en interblocage, par exemple avec la commande `kill` - Ici, le problème est que le deux processus essaient d'acquérir les deux verrous dans l'ordre contraire. Si l'ordre d'acquisition est le même, plus aucun problème ! - Dans des problèmes complexes, les situations d'interblocages sont difficiles à détecter, d'autant plus qu'ils n'ont pas lieu tout le temps et qu'on ne peut pas prévoir l'ordonnancement des processus --- ### ✍️ À faire Exercices 5, 6 et 7 --- # Et pour les systèmes multiprocesseurs ? - Les ordinateurs actuels possèdent généralement plusieurs processeurs, ce qui permet à plusieurs processus d'être exécutés parallèlement : un par processeur. - Pour répartir les différents processus entre les différents processeurs, on distingue deux approches : - l'approche *partitionnée* : chaque processeur possède un ordonnanceur particulier et les processus sont répartis entre les différents ordonnanceurs - l'approche *globale* : un ordonnanceur global est chargé de déterminer la répartition des processus entre les différents processeurs --- # Conclusion - Un programme en cours d'exécution s'appelle un **processus**. Les systèmes d'exploitation récents permettent d'exécuter plusieurs processus "simultanément". - L'OS est chargé d'organiser l'exécution *à tour de rôle* des différents processus en veillant à ce qu'ils se partagent les différentes ressources sans se gêner les uns les autres - Les processus varient entre trois états : **élu** si le processus est exécuté par le processeur, **prêt** si le processus est prêt à être exécuté, et **bloqué** si le processus est en attente d'une ressource. - L'**ordonnanceur** définit l'ordre dans lequel les processus doivent être exécutés par le processeur, grâce à des **algorithmes d'ordonnancement**. ---- # Conclusion (suite) - Le partage des ressources par les processus peut entraîner des **problèmes de synchronisation** ou d'**interblocage**. - L'utilisation de **verrous** peut régler les problèmes de synchronisation : ils assurent qu'un processus détenant le verrou ne puisse pas être permet à un processus de ne pas être interrompu dans sa section critique par un autre processus demandant le même verrou. - Mais une mauvaise utilisation de plusieurs verrous peut entraîner des problèmes d'interblocage, situation où chaque processus attend une ressource détenu par un autre conduisant à une attent cyclique infinie. ---- <img class="stretch" src="data/meme_deadlock.jpeg"> --- **Références :** - Equipe éducative DIU EIL, cours sur le *Partage des ressources et virtualisation*, Audrey Queudet, Université de Nantes. - Cours d'Olivier Lecluse sur la [Gestion des ressources](https://www.lecluse.fr/nsi/NSI_T/archi/process/) - Documentation officielle de la bibliothèque [multiprocessing](https://docs.python.org/fr/3/library/multiprocessing.html) de Python. - Cours de David Roche sur les [processus](https://pixees.fr/informatiquelycee/term/c19c.html). - Livre *Numérique et Sciences Informatiques, 24 leçons, Terminale*, T. BALABONSKI, S. CONCHON, J.-C. FILLIATRE, K. NGUYEN, éditions ELLIPSES. --- Germain BECKER, Lycée Mounier, ANGERS Ressource éducative libre distribuée sous [Licence Creative Commons Attribution - Pas d’Utilisation Commerciale - Partage dans les Mêmes Conditions 4.0 International](http://creativecommons.org/licenses/by-nc-sa/4.0/) ![Licence Creative Commons](https://i.creativecommons.org/l/by-nc-sa/4.0/88x31.png)