TNSI : Cours Processus en Python⚓︎
Processus Zombie⚓︎
Si le processus fils se termine avant le père, le processus fils passe dans l’état zombi, tant que le père ne se termine pas et qu’il n’a pas récupéré le code de fin du fils. Seul un administrateur système peut le détruire.
Un processus peut être interrompu par la réception d’un signal Exemple: CTRL C envoi le signal n°2 SIGINT CTRL envoi le signal n°3 SIGQUIT On peut envoyer un signal par la commande Shell : Kill n°signal n°processus La liste des signaux peut être obtenu avec la commande : kill -l
Pool de Processus⚓︎
Il peut s’avérer pratique et surtout plus rapide en cas de microprocesseur multi-coeurs de créer un certain nombre de processus appelés Pool
du module multiprocessing
offre un moyen simple de paralléliser l'exécution d'une fonction sur plusieurs valeurs d'entrée, en distribuant ces valeurs entre les processus (parallélisme de données) via une implémentation parallèle de la fonction map()
.
Dans ce cas, l’ordonnanceur les utilise comme bon lui semble. Si l’exécution du processus est rapide, il ne les utilisera peut-être pas tous, mais si l’exécution est longue, il les utilisera tous et les fera exécuter sur différents cœurs (s’il en a plusieurs à sa disposition).
Exp
from multiprocessing import Pool
def f(x):
return x*x
if __name__ == '__main__':
# Ici, 5 est le nombre de processus (workers) à utiliser en parallèle
with Pool(5) as p: # autrement, il faut gérer manuellement la fermeture avec close()
print(p.map(f, [1, 2, 3]))
affiche sur la sortie standard:
[1, 4, 9]
Synchronisation entre processus⚓︎
Parfois les différents processus doivent être synchronisés, notamment lorsqu’ils s’échangent des données où lorsqu’ils partagent les mêmes ressources. Il faut dans ce cas informer l’ordonnanceur (scheduler) de la façon dont les processus doivent s’ordonnancer ou attendre qu’un résultat soit disponible. Plusieurs mécanismes ont été inventés pour que ce soit possible notamment, par exemple, à l’aide de signaux (interruptions logicielles), le verrou ou mutex (exclusion mutuelle), le sémaphore. Pour que des données puissent être échangées entre tâches, les variables globales ne fonctionnent pas (espaces mémoire différents) contrairement aux threads mais des mécanismes tels que « la boite aux lettres », « la mémoire partagée », « les fichiers », « le pipe tube en français », passage de paramètres à une tâche lors de son activation. Nous ne verrons pas ces mécanismes dans ce cours.
Wait et waitid()⚓︎
-
Ces deux fonctions permettent de synchroniser un processus père et son fils. os.wait() attend que l’enfant ait terminé os.waitid(idtype, id, option) peut être plus précise car on peut spécifier l’attente de différents signaux d’un fils ou de tous, par exemple :
*pid = os.fork() → création du fils * Dans le père : idtype = os.P_ALL → Attente de tous les fils
id = pid → processus concerné (si ce processus à des enfants, on les attendra également) option = os.WSTOPPED | os.WEXITED → type de signaux attendus (ici, fin ou arrêt du processus status = os.waitid(idtype, id, option) → status contient l’état du processus fils
wait est bloquant et attend la fin du fils. Il renvoi le pid du fils qui se termine et « etat » contient des infos sur la terminaison du processus (valeur de retour + condition d'arrêt). Si le père n'a pas de processus fils, wait renvoi -1.
- Le père attend le fils Père Fils pid = os.fork()
pid=os.wait() exit(code)
- Le fils se termine avant le père Père Fils pid = os.fork()
pid=wait() exit(code)
- waitpid permet de tester la fin d'un processus particulier en bloquant ou non l'appelant.
- Rôle du paramètre pid pid <-1 tous processus fils dans le groupe | id | pid = -1 tout processus fils pid = 0 tout processus fils du même groupe que l'appelant pid = >1 le processus fils d'identité pid
WIFEXITED | Vrai si le processus fils s'est terminé normalement |
WEXITSTATUS | Fournit le code de retour du fils s'il s'est terminé correctement |
WIFSIGNALED | Vrai si le fils s'est terminé à cause d'un signal |
WTERMSIG | Fournit le n° du signal ayant provoqué la fin du processus |
WIFSTOPPED | Vrai si le processus est stoppé (si waitpid avec WUNTRACED) |
WSTOPSIG | Fournit le n° du signal ayant stoppé le processus |
Verrous et Sections Critiques⚓︎
Une section critique délimite des instructions à exécuter sans interruption. Le verrou un outil simple permettant de réaliser l'exclusion mutuelle sans attente active ni masquage des interruptions pendant toute la durée de la section critique. Un VERROU est une variable d'un type structuré composé d'un booléen et d'une file d'attente gérée le plus souvent en FIFO. On pourrait déclarer: type verrou = structure état : libre, occupé attente: liste de tâches gérées en FIFO end
procédure verrouiller ( var v: verrou) début entrer section critique ( par exemple masquer les it. ) si v.état = libre alors v.état = occupé sinon bloquer la tâche en fin de la file v.attente finsi sortir section critique fin;
procédure déverrouiller ( var v : verrou) début entrer section critique si v.attente non vide alors débloquer la tâche en tête de v.attente sinon v.état = libre finsi sortir section critique fin;
Exemple de figures ..
Chronogramme
Sémaphores⚓︎
Le principe est le même que pour le verrou mais au lieu d’un booléen, un compteur est utilisé. Un sémaphore est une variable d'un type structuré composé d'un entier et d'une file d'attente, généralement gérée en FIFO
type sémaphore = structure compte: entier attente : file de tâches gérée en FIFO end
Lorsque plusieurs processus veulent utiliser une même ressource, il est possible d’utiliser un sémaphore. Un sémaphore contient un ou plusieurs jetons. Cas 1 : 1 seul jeton, la ressource ne doit être accessible que par un seul processus à la fois (exemple : un fichier en écriture). Un processus prend en premier le jeton, les autres doivent attendre qu’il le libère. Cas 2 : plusieurs jetons, la ressource peut être utilisée par plusieurs processus simultanément (exemple : un fichier en lecture). Le nombre de jetons disponible correspond au nombre de processus qui auront accès à la ressources simultanément. S’il n’y a plus de jeton disponible, le processus attend qu’il y en ait un de libre. Initialisation d'un sémaphore: le compte est initialisé à une valeur non négative et la file est initialement vide. Pour agir sur un sémaphore on utilise les deux primitives dénommées classiquement P(s) et V(s) qui sont des opérations indivisibles. P demande d'un sémaphore : attente de la ressource (Passeren = prendre en hollandais), acquire() en Python. V relâchement d'un sémaphore : abandon de la ressource (Vrygeven = libérer en hollandais), release() en Python.
Précautions à prendre:⚓︎
Les interventions sur verrous et sémaphores ne doivent se faire qu'à travers les primitives définies précédemment, jamais directement. Aucune tâche ne doit entrer dans une section critique sans passer par la primitive P correspondante. Les primitives précédentes ne garantissent l'entrée d'une tâche en section critique au bout d'un temps fini, que si la file d'attente est gérée sans priorité, c'est à dire dans l'ordre d'arrivée, ou selon le principe du premier entré, premier sorti.
Interblocage⚓︎
Si une tâche est détruite en cours de section critique, elle risque de bloquer d'autres tâches en attente puisqu'elle ne libère pas la ressource. Il faut alors que l'exécutif détecte que la tâche se trouvait en section critique et libère artificiellement la ressource en exécutant la primitive V. Il peut aussi être utile de s'assurer qu'une tache reste un temps fini en section critique : il faut éviter les boucles dans une section critique.
Différents blocages du système peuvent arriver si les primitives P et V sont mal employées: - blocage définitif d'une ressource par un P() non suivi d'un V(). Ce blocage d'une tâche peut entraîner le blocage complet de toutes les tâches qui attendent la ressource. - inter blocage: si S1 et S2 sont deux sémaphores d'exclusion mutuelle, si deux tâches T1 et T2 peuvent exécuter respectivement:
pour T1 P(S 1 ) ; P(S2) (T1 utilise la ressource S1 et attend S2 pour continuer). pour T2 P(S2) ; P(S1) (T2 utilise la ressource S2 et attend S1 pour continuer).
Différents blocages du système peuvent arriver si les primitives P et V sont mal employées: - blocage définitif d'une ressource par un P() non suivi d'un V(). Ce blocage d'une tâche peut entraîner le blocage complet de toutes les tâches qui attendent la ressource. - inter blocage: si S1 et S2 sont deux sémaphores d'exclusion mutuelle, si deux tâches T1 et T2 peuvent exécuter respectivement:
pour T1 P(S 1 ) ; P(S2) (T1 utilise la ressource S1 et attend S2 pour continuer). pour T2 P(S2) ; P(S1) (T2 utilise la ressource S2 et attend S1 pour continuer). Autre exemple: une tâche doit mettre à jour un fichier sur disque et attend que le lecteur soit libre, celui ci est alloué à une autre tâche qui attend pour le libérer que le fichier soit mis à jour.
Pour éviter l'inter blocage, il faut éviter qu'une tâche ne réquisitionne une ressource alors qu'elle en attend une autre indisponible.
Il existe des méthodes complexes pour détecter les risques d'inter blocage entre tâches basé sur la recherche de chemins critiques dans des arborescences. L'élimination des inter blocages se fait par la destruction partielle ou totale des tâches bloquées.
Watchdog matériel et Logiciel⚓︎
En cas d’interblocage ou de processus qui tournent et dont le fonctionnement semble suspect au noyau, un « watchdog » (chien de garde) logiciel permet d’avertir l’utilisateur de cette suspicion par un message qui demande à l’utilisateur s’il souhaite arrêter le processus par exemple. Sur certain système, en cas de non exécution normal (plantage ou boucle infinie, par exemple), un « watchdog » matériel permet la réinitialisation du système. Ce matériel est présent sur les matériels de sécurité (affichage de l’évolution du parcours des trains, domaine médical, spatial, …).
Sockets⚓︎
Si l’architecture est en client/serveur (local ou distant). La communication entre tâches s’effectue au moyen d’un socket. Le processus serveur écoute en permanence et lorsqu’un client se connecte, le serveur génère un thread qui permet de répondre au client, cette solution permet à plusieurs clients de se connecter simultanément.
Threads⚓︎
Les threads (processus légers) sont différents des processus (processus lourd). Un thread travaille avec un contexte réduit au minimum (registre, priorité d'ordonnancement, masque de signaux et pile d'exécution). Un thread peut accéder en lecture/écriture aux données de son père.