Cours d'informatique pour le lycée

Processus

Prérequis

Nous allons devoir étudier les processus dans un environnement Linux. Vous allez donc devoir vous connecter sur un serveur Linux du lycée en ssh. Lancez le PowerShell Windows pour utiliser ssh et saisissez la commande suivante :

ssh votre_prenom@192.168.255.19

Remplacez votre_prenom par votre prénom. Si on vous demande de confirmer la connexion répondez « yes ». Votre mot de passe est votre date de naissance sur six chiffres.

Présentation

Un processus est un programme en cours d'exécution. Un processus est identifié par son PID : Process IDentifiant. Un processus a généralement un processus père identifié par son PPID : Parent PID. Découvrons tout ceci sur votre machine.

ps

La commande Unix permettant de lister les processus est ps.

1) Essayez en tapant ps dans la console.

Vous devez alors obtenir quelque-chose comme ce qui suit :

PID TTY          TIME CMD
 172223 pts/1    00:00:00 bash
 212663 pts/1    00:00:00 ps

Ce n'est pas très intéressant car ps n'affiche par défaut que les processus de votre terminal. Il est possible de donner des options à ps pour changer son comportement. Par exemple l'option -e lui dit d'afficher tous les processus.

2) Essayez la commande ci-dessous :

ps -e

Il doit maintenant y avoir beaucoup plus de processus affichés ! Trop d'informations sont affichées, nous allons modifier l'affichage (avec l'option -o) pour avoir seulement les informations qui nous intéressent : le PID, le PPID, l'utilisateur et la commande.

3) Essayez la commande ci-dessous :

ps -e -o pid,ppid,euser,comm

Au démarrage de l'ordinateur, le noyau Linux est chargé et il lance deux processus : systemd et kthreadd. kthreadd, avec le PID = 2 s'occupe principalement de gérer le matériel alors que systemd avec le PID = 1 (aussi appelé init) gère tout le reste (réseau, serveur graphique, tâches de fond, logiciels…). C'est pour ça que ce sont les deux seuls processus à avoir un PPID de 0 : il n'ont pas vraiment de parent. systemd sera donc l'ancètre de tous les processus que vous créerez sur l'ordinateur.

Vous avez maintenant toutes les informations nécessaires pour répondre aux questions suivantes.

4) Quel est le parent du processus mysqld ?

5) Quel est la chaïne de processus entre systemd et ps que vous avez dû lancer pour répondre à cette question ?

6) Quel est la chaîne de processus entre systemd et sshd qui gère les connexions ssh ?

Dans le but de connaître le nombre de processus nous allons simplement compter le nombre de lignes retournées par ps grâce à la commande suivante :

ps -e -o pid=,ppid=,comm= | wc -l

7) Combien de processus sont actifs sur votre machine ?

Autres commandes

pstree

Il existe la commande pstree qui permet d'afficher les processus sous forme d'arbre.

8) Testez la commande suivante :

pstree

Il est égalementy possible d'afficher le PID.

9) Testez la commande suivante :

pstree -p -T

top et htop

top et sa version plus esthétique htop permettent de voir (entre autre) les processus classés par leur utilisation du processeur.

10) Lancez htop dans un terminal et notez les trois processus avec le plus d'utilisation du processeur.

htop affiche également les threads il peut donc être difficile de trouver un processus. Il est possible de filtrer les processus affichés en appuyant sur F4 et en saisissant un critère.

11) Toujours dans htop, affichez seulement les processus sshd et donnez leur nombre.

kill et killall

Il est possible de demander à un processus de se terminer grâce à la commande kill. Pour cela, il faut connaître son PID et lui envoyer la commande :

kill PID

On dit alors qu'on a tué le processus. Cette commande demande gentillement au processus de se terminer. Il est possible de forcer l'arrêt d'un processus en lui envoyant un signal plus « radical ». Cela peut servir lorsqu'un processus est planté :

kill -9 PID

12) Ouvrez un deuxième PowerShell à coté du premier, connectez-vous au serveur et lancez top. Déterminez alors son PID.

13) Dans l'autre console, tuez alors le processus de top et remarquez la fermeture du programme.

14) Essayez maintenant de tuer le processus cron ou systemd. Que se passe-t-il ? Pourquoi ?

La commande killall a le même fonctionement que kill sauf qu'elle demande le nom du processus en entrée. Lancez encore une fois top dans une console et lancer la commande suivante dans l'autre console pour le fermer :

killall top

Cette commande permet de tuer un processus sans connaitre son PID mais surtout elle permet de tuer tous les processus ayant le même nom. Cela peut être très utile lorsqu'un logiciel a planté.

Processus et Python

PID et PPID

Voici un script simple prog1.py affichant son propre PID et celui de son parent :

import os
import time

def identifierProcessus():

    print('Voici des infos sur le processus instancié par le programme prog1.py')
    print('PID :', os.getpid())
    print('PPID :', os.getppid())

identifierProcessus()

time.sleep(20)

Pour simplifier les choses, les scripts sont placés dans mon répertoire sur le serveur. Vous n'avez donc pas à les recopier. Dans les prochaines questions, il faudra lancer les scripts à partir de la console avec une commande comme celle-ci (par exemple pour prog1.py) :

python3 /home/tbeline/prog1.py

15) En utilisant deux consoles, lancer ce script et vérifiez les informations données avec ps.

16) Qui est le père de ce script ?

Fork

Il existe une commande Linux qui permet de dupliquer un processus : fork. Pour l'utiliser dans Python, on utilisera la commande os.fork(). Cette commande est très particulière car elle crée un processus fils identique au père. Elle crée donc un script identique au premier qui va s'éxécuter en parallèle. Le seul moyen de les différencier est la valeur de retour qui vaut 0 pour le fils et le PID du fils pour le père.

17) Exécutez dans la console l'exemple prog2.py ci-dessous pour tenter de comprendre.

import os
import time

def pereFils():
    print("je suis le père")
    # Création du fils
    newpid = os.fork()
    # os.fork() renvoie 0 pour le fils et le PID du fils pour le père

	if newpid == -1:
        print("Erreur de création")

    elif newpid == 0: # dans le fils
        for loop in range(8):
            print("Dans le fils", os.getpid(), os.getppid())
            time.sleep(2)

    else: #newpid>0 -> dans le père
        for loop in range(3):
            print("Dans le père", os.getpid(), os.getppid())
            time.sleep(2)

pereFils()

18) Faite un pstree pendant l'exécution du script pour visualiser les processus.

Dans la suite, vous allez devoir modifier les scripts. Or, il n'est pas possible de modifier un fichier qui n'est pas dans votre répertoire personnel sous Linux. Vous allez donc devoir copier le script et le modifier avec un éditeur de texte en ligne de commande…

Pour copier, il faut saisir la commande suivante :

cp /home/tbeline/prog2.py ~/

Pour modifier le script, nous allons utiliser l'éditeur de texte nano qui est relativement simple. Il va falloir vous adapter mais ça devrait aller. Pour ouvrir le fichier :

nano ~/prog2.py

Une fois le fichier ouvert vous devez utiliser uniquement le clavier. Pour sauvegarder il faut faire Ctrl + O et pour quitter Ctrl + X.

Attention pour exécuter le script il faudra ensuite utiliser la commande :

python3 ~/prog1.py

19) Modifiez le script pour qu'il affiche « fin du processus père » quand le père se termine et « fin du processus fils » quand le fils se termine.

20) Quel processus devient le parent du fils lorsque son père se termine ?

Ce phénomène est normal, lorsque le père d'un processus se termine, le processus est « adopté » par systemd.

21) Faite un pstree pendant l'exécution du script pour visualiser ce phénomène. (Allongez éventuellement les boucle pour avoir plus de temps)

22) Modifiez ce script pour avoir un arbre de processus comme celui ci-dessous. Chaque processus devra afficher cinq fois « dans le … » et attendre deux secondes entre chaque affichage.

père─┬─fils 1───petit fils
     └─fils 2

Wait

Il est possible de demander à un processus d'atendre que ces fils soient terminés avec la commande wait(). Pour l'utiliser dans Python, on utilisera la commande os.wait(). On dispose du programme prog3.py suivant :

import os
import time

def pereFils():
    #Création du fils
    newpid = os.fork()

    if newpid == -1:
        print("Erreur de création")

    elif newpid == 0: # dans le fils
        for i in range(0, 5):
            time.sleep(2)

    else: # newpid > 0 -> dans le père
        childProcExitInfo = os.wait()

pereFils()

On veut qu'il affiche ceci :

Je suis le père
Le père attend que le fils ait terminé
Dans le fils
Le fils écrit 0
Le fils écrit 1
Le fils écrit 2
Le fils écrit 3
Le fils écrit 4
Le processus fils se termine
Le processus fils a terminé
Le père se termine après que le fils ait terminé

23) Compléter le programme prog3.py ci-dessus avec les instructions suivantes pour qu'il affiche ce qui est demandé.

print("Le processus fils a terminé")
print("Le père se termine après que le fils ait terminé")
print("Je suis le père")
print("Dans le fils")
print("Le processus fils se termine")
print("Le père attend que le fils ait terminé")
print("Le fils écrit",i)

Ordonnancement

Présentation

L'ordonnanceur est un composant du noyau d'un système d'exploitation qui détermine l'ordre d'exécution des processus sur les processeur d'un ordinateur. En effet, un processeur ne peut effectuer qu'une tâche à la fois. Avec plusieurs processeurs ou cœurs, il est possible d'effectuer plusieurs tâche simultanément, mais il y aura en général plus de processus que de processeurs ou cœurs. Il faut donc choisir à chaque instant quel processus va utiliser tel processeur : c'est l'ordonnancement. C'est un domaine assez complexe dont nous n'allons voir que certaines manifestations simples.

L'ordonnancement donne l'illusion que toutes les tâches sont exécutées en parrallèle alors qu'elles sont exécutées les unes à la suite des autres sur des temps très courts. L'ordonnanceur attribue du temps processeur aux processus. Ainsi si nous avons 10 processus avec la même priorité pour un seul processeur, chacun des processus va avoir droit, par exemple, à 1 ms toutes les 10 ms :

Priorité des processus

Nous venons de parler intuitivement de priorité de processus. C'est une notion centrale de l'ordonnancement. Ainsi de manière logique, ce sont les processus avec la plus grande priorité qui auront le plus de temps processeur.

Sous Linux, pour la plupart des processus, la priorité est un nombre entre 0 et 39. Attention ! plus le nombre est petit, plus le processus est prioritaire.

24) Lancez une commande htop et observez la colonne PRI qui correspond à la priorité.

Sous Linux, on peut changer soi-même la priorité d'un processus en agissant sur nice. C'est la colonne NI dans htop. Il existe une relation très simple entre la valeur de nice et la priorité.

25) Essayez de deviner la relation mathématique entre PRI et NI en observant leurs valeurs dans htop. (Ne faites pas attention aux colonnes avec une priorité négative)

Les processus ont donc une valeur de nice qui varie entre -20 et 19. Les utilisateurs normaux (comme vous) ne peuvent utiliser que des valeurs entre 0 et 19, les autres valeurs (les plus prioritaires) étant réservées à root. Nous allons voir maintenant comment donner une valeur de nice à un processus.

26) Dans une deuxième console lancez la commande suivante et notez la valeur de nice pour le processus top.

top

La plupart des processus ont une valeur de nice à 0. Cela veut dire que leur priorité est à 20. On peut donc choisir de donner une valeur plus élevée à nice pour que le processus soit moins prioritaire (par exemple pour un long calcul si on ne veut pas utiliser toutes les ressources du système). Pour cela, on utilise la syntaxe suivante :

nice -n valeur commande

27) Relancez top avec un nice à 10. Vérifiez cette valeur dans htop.

Comme votre machine n'est pas très solicitée, des valeurs de nice différentes ne changent pas grand chose car il y a plein de temps processeur disponible. Pour voir les effets de nice, nous allons utiliser un script Python qui va faire travailler votre ordinateur :

import os

newpid = os.fork()

if newpid == 0: # Dans le fils
    newpid = os.fork()
    newpid = os.fork()
    newpid = os.fork()

#else: # Pour le père
#     os.nice(5)

print(os.getpid(), ":", os.nice(0))

i = 0
while i < 100000000:
    i += 1

Ce script lance 9 processus qui comptent jusqu'à 100 millions. Seul le prof lancera ce script pour que ça ne soit pas trop le bazar sur le serveur.

Pour la suite, il faut avoir une console avec htop.

28) Demandez au prof de lancer le script et relevez la charge cpu des processus dans htop.

Demandez au prof de lancer le script en activant les deux lignes qui changent la valeur de nice pour le processus père.

29) Relevez la charge cpu moyenne des processus fils et celle du père.

30) Quel processus se termine en dernier ?

Demandez alors au prof de modifier le script pour donner une valeur de nice à 19 au père.

31) Relevez encore une fois la charge cpu moyenne des processus fils et celle du père.

32) Que constatez-vous ? Est-ce en accord avec le fonctionnement de nice ?

Interblocage

Présentation

Un interblocage se produit lorsque deux processus (ou plus) s'attendent mutuellement. Cela peut arriver lorsqu'un processus attend une ressource occupée par un autre processus, qui attend une ressource occupée par ce premier processus… Le schéma ci-dessous explique simplement cette situation :

Cas pratique

Il n'est possible d'avoir un interblocable qu'avec au moins deux processus. Nous allons utiliser ici les threads plutôt que fork. Nous utiliserons également des verrous (lock) pour permettre aux threads de s'attendre. Voici le code à utiliser :

import time
from threading import Lock, Thread


class myThread (Thread):

    def __init__(self, id, nom, verrou1, verrou2):
          Thread.__init__(self)
          self.id = id
          self.nom = nom

    def run(self):
        print ("Démarrage de " + self.nom)
        if self.id == 1:
            print("Aquisition du verrou 1 par le thread 1")
            verrou1.acquire()
        if self.id == 2:
            print("Aquisition du verrou 2 par le thread 2")
            verrou2.acquire()
        time.sleep(2)
        if self.id == 1:
            print("Attente du verrou 2 par le thread 1")
            verrou2.acquire()
            print("Aquisition du verrou 2 par le thread 1")
            print("Libération du verrou 1 par le thread 1")
            verrou1.release()
        if self.id == 2:
            print("Attente du verrou 1 par le thread 2")
            verrou1.acquire()
            print("Aquisition du verrou 1 par le thread 2")
            print("Libération du verrou 2 par le thread 2")
            verrou2.release()
        print ("Sortie de " + self.nom)

# Création des deux verrous
verrou1 = Lock()
verrou2 = Lock()

# Création des threads
thread1 = myThread(1, "Thread-1", verrou1, verrou2)
thread2 = myThread(2, "Thread-2", verrou1, verrou2)

# Démarrage des threads
thread1.start()
#thread2.start()

print("Sortie du thread principal")

while True:
    pass

Ici, le deuxième thread n'est pas démarré, le Thread 1 doit donc se terminer.

Vous pouvez utiliser ce code dans Thonny comme d'habitude, vous pouvez donc vous déconnecter du serveur.

33) Lancez ce script et vérifiez que le thread se termine bien.

34) Après avoir décommenté la ligne de démarrage du thread 2, relancez ce script et constatez l'interblocage.

Une fois qu'on atteint un interblocage, il est inévitable de devoir arrêter un processus pour sortir de l'interblocage. En général, on cherchera à éviter et prévenir en amont les interblocages en gérant intelligement les ressources, par exemple en utilisant l'algorithme du banquier.

Sources et compléments

Les processus
le chapitre sur les processus d'un wikibook sur GNU-Linux.

Processus Linux
le chapitre de François Goffinet sur les processus.

L'ordonnancement
Page wikipedia sur l'ordonnancement.

L'interblocage
Page wikipedia sur l'interblocage.