Files
Yggdrasil/legacy_docs/infotro/PTR/PCO.md
T
Klagarge b78f79e12e docs: move legacy docs to legacy_docs/
Assisted-by: Delta:zai-glm-5.3
2026-10-10 20:28:25 +02:00

21 KiB
Raw Blame History

Programmation concurrente

Un programme concurrent est un programme non séquentiel. Soit l'expression arithmétique suivante : (a+b) * (c+d).

Programmation séquentiel

  1. x = (a+b)
  2. y = (c+d)
  3. x*y

Programmation concurrente

  1. x = (a+b) \qquad y = (c+d)
  2. x*y

La programmation concurrente sur 1 CPU permet d'exploiter pleinement les périphériques parallèlement au processeur. Cela permet d'éviter de bloquer toutes les applications lorsqu'une seule est en attente d'une réponse.

La programmation concurrente sur plusieurs CPU permet en plus un gain de temps.

Switching contexte

Lors d'un switching context, pour passer d'un processus P1 a un processus P2, les actions suivantes sont effectué :

  1. Sauvegarde des registres de P1 sur la stack
  2. Placement de P1 dans la WAITING queue
  3. Sélectionne le premier processus de la READY queue (P2)
  4. Charge les registres de P2
  5. Restaure le contexte utilisateur

Exclusion mutuelle

  • un programme est une entité statique qui contient des processus
  • un processus est une entité dynamique (il s'agit d'une tâche en cours d'exécution)

Les processus ne sont jamais totalement indépendants. Ils partagent des ressources (mémoire, fichiers, périphériques, variable, etc.)

Ceci peut conduire à des erreurs et doit donc être controlé.

  • Section critique : partie du code qui accède à une ressource partagée.
  • Exclusion mutuelle : un seul processus à la fois peut accéder à la section critique.
  • Ressource critique : ressource qui doit être accédé en exclusion mutuelle

On pourrait éviter les problèmes d'exclusion mutuelle si on pouvait assurer que deux processus n'exécute jamais en même temps une section critique. On peut résoudre ce problème à l'aide de mutex ou sémaphore.

Exclusion mutuelle par attente active

type processNum = 1..2;

module CS;                                          /* Section critique */
    defines Enter, Leave;                           /* Primitive */   

    var
        request : array processNum of boolean;
        tour    : processNum;
    procedure Enter(num: processNum);
    begin
        request[num] := true;                       /* Demande d'accès */
        tour := 3 – num;                            /* Autre processus */
        repeat
        /* Attendre que l'autre processus ait fini avec "not request" */
        /* Priorité si les deux processus demandent en même temps à la section critique
           grâce à "tour = num" */
        until (not request[3-num]) or (tour = num)
    end Enter;

    procedure Leave(num: processNum);
    begin
        request[num] := false
    end Leave;

    begin                                           /* Initialisation */
        request[1] := false;
        request[2] := false
    end CS;
        
/***********************************************************************/

process P1;
    uses CS, Console;
begin
    CS.Enter(1);
    Console.Write(”Error on hard disk !”);
    CS.Leave(1);
end P1;

/***********************************************************************/

process P2;
    uses CS, Console;
begin
    CS.Enter(2);
    Console.Write(”Printer not ready !”);
    CS.Leave(2);
end P2;
  • si les processus P1 et P2 ne tentent pas de pénétrer simultanément dans la section critique, la variable request[3-num] est fausse et l'entrée en section critique est autorisée
  • si par contre les processus P1 et P2 tentent de pénétrer simultanément dans la section critique, la variable tour permet d'arbitrer entre les deux demandes. La variable tour (à la ligne 15) indique le numéro du processus élu

Cette méthode peut facilement être généralisée à n processus. Il suffit de faire tour := n+1 – num et request[n+1-num].

Cette solution n'est pas satisfaisante, car elle consomme beaucoup de ressources CPU. En effet, un processus qui attend l'accès à la section critique est en attente active. Il est donc en boucle constante et consomme beaucoup de ressources CPU.

Exclusion mutuelle par masquage des interruptions

Masquer les interruptions pour résoudre le problème de l'exclusion mutuelle. Plus d'interruption => plus de commutation de processus => plus de risque de violer l'exclusion mutuelle.

Le danger avec cette solution est le risque de perte d'information provenant des périphériques. Cela ne peut être une bonne solution que si la section critique est très courte. Il est dangereux de laisser la possibilité de masquer les interruptions à l'utilisateur.

Instruction TAS (TestAndSet)

L'exclusion mutuelle par masquage des interruptions n'est pas possible dans une architecture multiprocesseur. Il est possible de résoudre le problème de l'exclusion mutuelle en utilisant l'instruction TAS. Elle permet de tester et de modifier une variable en une seule instruction assembleur.

Mutex

Le mutex est utilisé pour bloquer l'accès simultané aux sections critiques avec les primitives suivantes :

  • block() : Il laisse passer un seul processus à la fois. Si la section est déjà bloquée, le processus et mis dans une file d'attente.
procedure Block(var v:lock);
begin
  if v.state = OPEN then
    v.state := CLOSE
  else
    block the process in the list "v.waiting"
  end if
end Block;
}
  • release() : Il libère la section critique, si la file d'attente est vide. Sinon, il passe la main au processus suivant.
procedure Release(var v:lock);
begin
  if queue "v.waiting" not empty then
    unblock first process in the list "v.waiting"
  else
    v.state := OPEN
  end if
end Release;
}

Ces procédures sont elles même des sections critiques. Elles sont cepandant très courte et ne contiennent aucunes entrées-sorties. Elles peuvent donc être implémentées à l'aide d'une Exclusion mutuelle par masquage des interruptions ou d'une instruction TAS.

Les Mutex restent un outil primitif. Ils ne s'appliquent pas sur des problèmes plus complexes.

Sémaphore

Le sémaphore est une généralisation du mutex. Il permet de bloquer l'accès à une ressource partagée par plusieurs processus. Il limite l'accès à plusieurs même ressources utilisées par plusieurs processus.

Par exemple : les processus ont besoin d'imprimer et il y a plusieurs imprimantes à disposition. Le sémaphore va limiter l'accès au nombre d'imprimantes disponibles.

Les primitives de sémaphores sont :

  • P (Passeren = passer) appelé aussi wait, up, get ou lock
procedure P(var s:semaphore);
begin
  s.num := s.num – 1;
  if s.num < 0 then
    block the process at the end of the list "s.waiting"
  end if
end P;
  • V (Vrijgeven = libérer) appelé aussi signal, down, release ou unlock
procedure V(var s:semaphore);
begin
  s.num := s.num + 1;
  if s.num <= 0 then
    unblock first process in the list "s.waiting"
  end if
end V;

À l'initialisation, le sémaphore est initialisé au nombre de mêmes ressources disponibles. Ou alors, il est initialisé à 1 pour un mutex. Ou alors, il est initialisé à 0 pour un sémaphore binaire (file d'attente).

Interblocage

L'interblocage est un problème qui peut survenir lorsqu'un ensemble de processus se bloquent mutuellement. On appelle aussi cela un DEADLOCK.

Résolution reader / writter

Le but est qu'il peut avoir plusieurs processus qui puisse lire la même ressource et aucun modifier la ressource. Sinon, un processus peut modifier la ressource et aucun peut lire.

void BeginRead(void) {
	osSemaphoreAcquire(rw, osWaitForever);
    osSemaphoreAcquire(mutexNbReader, osWaitForever);   // ENTER CRITICAL SECTION
	nbReader++;
	if(nbReader == 1) osSemaphoreAcquire(w, osWaitForever);
	osSemaphoreRelease(mutexNbReader);                  // EXIT CRITICAL SECTION
	osSemaphoreRelease(rw);
}

void EndRead(void) {
	osSemaphoreAcquire(mutexNbReader, osWaitForever);   // ENTER CRITICAL SECTION
	nbReader--;
	if(nbReader == 0) osSemaphoreRelease(w);
	osSemaphoreRelease(mutexNbReader);                  // EXIT CRITICAL SECTION
}

void BeginWrite(void) {
	osSemaphoreAcquire(rw, osWaitForever);
	osSemaphoreAcquire(w, osWaitForever);
}

void EndWrite(void) {
	osSemaphoreRelease(w);
	osSemaphoreRelease(rw);
}

Coopération-Synchronisation

La coopération entre processus implique la synchronisation entre eux pour éviter de ressource.

Évènement

L'évènement est un outil permettant de résoudre un problème de synchronisation comme les sémaphores ou les mutex. On peut manipuler les évènements :

  • Wait(event) : Attendre que l'évènement event se produise par la fonction signal(...)
  • Signal(event) : Déclencher l'évènement event
  • Init(event) : Reset l'évènement event (inverse de signal(event))

On peut aussi résoudre un problème de synchronisation entre processus avec un sémaphore initialisé à 0. On peut le comparer à une barrière déclenchable depuis un autre processus.

Les sémaphores ne permettent pas de résoudre tous les problèmes de synchronisation. Trop grand risque de DEADLOCK. Il faut utiliser d'autres outils plus performants.

Le moniteur (monitor)

On peut l'imager avec une classe entièrement protégée par des sémaphores pour l'accès au variable et fonction (procédure).

Le moniteur possède comme primitive :

  • x.wait : bloque le processus jusqu'à ce qu'un x.send soit émis.
  • x.send : débloque le processus qui attend sur un x.wait.
monitor M;
defines DepositeChar, TakeChar;

var
  c : char;
  chAvailable : boolean;
  x : signal;
  
  procedure TakeChar(var ch:char);
  begin
    if not chAvailable then x.wait end if;
    ch := c;
    chAvailable := FALSE
  end TakeChar;

  procedure DepositeChar(ch:char);
  begin
    c := ch;
    chAvailable := TRUE;
    x.send
  end DepositeChar;
 
begin
  chAvailable := FALSE
end M;
process Keyboard;
uses M;
  var cRead: char;
begin
  ...
  Read a character on keyboard
  Put it in the cRead variable
  M.DepositeChar(cRead);
  ...
end Keyboard;

process Screen;
uses M;
  var cWrite: char;
begin
  ...
  M.TakeChar(cWrite);
  Write the character cWrite on the screen
  ...
end Screen;

Producteurs / consommateurs

Le problème survient lorsque le producteur envoie un message au consommateur via une queue. Example

  • Le producteur (Producer) doit être mis en attente s'il veut déposer un message alors que le tampon est plein
  • Le consommateur (Consumer) doit être mis en attente s'il veut prélever un message alors que le tampon est vide
  • Le producteur (Producer) en attente doit être débloqué dès que le consommateur (Consumer) prélève un message
    • à ce moment le tampon n'est plus plein
  • Le consommateur (Consumer) en attente doit être débloqué dès que le producteur (Producer) dépose un message
    • à ce moment le tampon n'est plus vide

On peut résoudre aisément le problème producteurs-consommateurs avec un moniteur ainsi qu'avec un sémaphore.

Solution à chier :
Le producteur attend que le consommateur soit capable de réceptionner le message, ainsi les deux peuvent continuer leurs exécutions (principe de rendez-vous). Ce concept ne possède pas de buffer.

Allocation processeur

État d'un processus :

img_1.png

Le processeur possède une liste de descripteur caractérisant chaque processus. Ces descripteurs de processus qui contiennent le registre de status et un pointeur sur sa stack. Ils sont utilisé pour faire des switching context :

  1. Le noyau va sauvegarder les registres de travail du processus en cours sur la stack.
  2. Il va sauvegarder aussi le registre de status et le pointeur de stack dans la liste de descripteurs.
  3. Il va prendre le descripteur du prochain processus et utiliser ses registres.
  4. Le processus continue jusqu'au nouveau switching context.

Cycle processeur

Un processeur réalise alternativement une suite de :

  • Cycles processeur (CPU burst cycle)
  • Cycles d'entrée-sortie (I/O burst cycle)

Lorsque le temps de cycle processeur est petit et qu'il y a une attente engendrée par les entrées-sorties, il est pertinent de partager le processeur entre plusieurs processus.

Queues d'ordonnancement

Les processus en attente du processeur sont placés dans une liste appelée queue de processus prêts (ready queue). Cette queue peut être gérée comme une file FIFO, une liste de priorité, un arbre, une pile, une liste non-ordonnée, etc.

Cette queue est sous forme d'une liste chaînée qui contient des pointeurs sur les descripteurs de processus. Les processus qui ont besoins d'une ressource sont placés dans une liste d'attente correspondant à la ressource (mutex/sémaphore).

L'ensemble des queues peut être regroupé dans le diagramme de synthèse suivant : synthese_diagram_queue_process.png

Ordonnanceurs

ordonnanceur.png

Ordonnanceur à court terme

  • Sélectionne un nouveau processus toutes les millisecondes.
  • Doit être très rapide
  • Est très simple

Ordonnanceur à long terme

  • Contrôle le degré de multiprogrammation, c'est-à-dire, le nombre de processus en mémoire.
  • Assure que la charge globale du processeur soit répartie équitablement entre les processus réalisant beaucoup d'entrée-sorties et les processus réalisant beaucoup de calculs.
  • S'exécute peu fréquement
  • La complexité de ce dernier augmente fortement.

Le swapping

On peut augmenter la RAM avec de la mémoire virtuelle (swap) pour garder les processus et ne pas les mettre en stockage long termes.

Distributeur de tâches

Il distribue au processeur les processus sélectionnés par l'ordonnanceur court terme.

  • Il charge les registres du processus sélectionné
  • Il change en mode utilisateur depuis le superviseur. Ce dernier servant à modifier les registres.
  • Il met à jour le PC

Critères de performance

  • *Efficacité (CPU utilisation) : Taux d'utilisation du processus
  • Débit (troughtput) : Nombre de processus par unité de temps
  • Temps global (turnaround time) : Temps qui s'écoule entre la soumission du processus et la fin de son exécution
  • Temps d'attente (waiting time) : Temps d'attente d'un processus dans la queue des processus prêts
  • Temps de réponse (response time) : intervalle de temps entre l'heure de soumission et l'heure de la première réponse

Algorithme d'ordonnancement

L'allocateur du processeur doit sélectionner quels processus situés dans la queue des processeurs prêts doivent recevoir le processeur.

FIFO

L'algorithme FIFO est le plus simple. Il sélectionne le premier processus de la queue des processus prêts. Mais, il n'est pas forcément le plus efficace.

Le plus court d'abord

Le processus avec la plus petite suite d'instruction est exécuté en premier. Si deux processus ont le même nombre d'instructions, on peut utiliser un algorithme FIFO pour déterminer le vainqueur. Il est performant, mais c'est compliqué de connaître le nombre d'instructions.

Il nécessite d'éstimer en temps réel la longueur du processus.

\tau_{n+1} = \alpha\cdot t_{n}+(1-\alpha)\cdot \tau_{n}
\tau_{n+1} : prochaine valeur estimée
\alpha : coefficient de pondération 0\leq\alpha\leq 1
t_{n} : valeur mesurée à l'instant n
\tau_{n} précédente valeur estimée pour l'instant n

Priorité

Cet algorithme donne au processeur le processus qui a la plus haute priorité qui est défini par l'utilisateur. On peut rencontrer plusieurs problèmes :

  • Blocage indéfini (indefinite blocking) : on reste bloqué dans un processus de haute priorité.
  • Famine (starvation) : Les processus de basse priorité ne sont jamais exécuté.

Solution : On augmente la priorité dum processus en attente tous les quantums de temps.

Ordonnancement avec réquisition (preemptive)

On réévalue l'ordre des processus dans la queue à chaque tick et le processus élu prend ou garde le processeur.

Tourniquet (round-robin)

Il est basé sur une tranche de temps fixe soit quantum de temps. On donne à chacun des processus ce temps et on passe au processus suivant une fois le temps écoulé. Lorsque le temps est grand, on retrouve en quelque sorte une FIFO, car le temps d'exécution des processus est plus court que le quantum de temps. Si le temps est très petit, le tourniquet est appelé processeur partagé (processor sharing).

Overhead : temps perdu pour changer de processus.

Files multiples (multi-queue)

On utilise cette technique lorsqu'on peut facilement classer les processus en différents groupes. Chaque queue possède son propre algorithme d'ordonnancement. Un ordonnancement entre les queues est réalisé soit par priorité et réquisition, soit en round robin.

Files multiples imbriquées (multi-level feedback queue)

Cette technique permet aux processus d'évoluer dans des queues en fonction du temps d'exécution écoulé.

  • Un processus entrant dans la queue des processus prêts est placé dans la queue 0
  • Si le quantum est écoulé et que la suite d’instruction processeur n’est pas terminée, le processus est placé dans la queue de priorité inférieure

Plus on descend dans les queues, plus on est de priorité basse.

Exemple : Plus on descend dans les queues, plus le quantum devient grand.

Interblocage (DEADLOCK)

Un processus attend indéfiniment pour accéder à une ressource bloquée.

L'interblocage apparaît si les 4 conditions apparaissent simultanément :

  1. L'exclusion mutuelle (mutual exclusion) : Une ressource au minimum n'est pas partageable.
  2. La détention et l'attente (hold and wait) : Un processus qui dispose déjà d'au moins une ressource et qui attend l'acquisistion d'une nouvelle ressource déjà utilisée par un autre processus.
  3. Pas de réquisition (no preemption) : Les ressources obtenues par un processus ne peuvent pas lui être retirées contre son gré
  4. L'attente circulaire (circulare wait) : Il doit y avoir un cycle d'au moins deux processus, chacun d'entre eux attendant une ressource détenue par un autre processus du cycle.

Les interblocages peuvent être décrits plus précisément en utilsant un graphe appelé graphe d'allocation des ressources (ressource allocation graph). interblocage.png

  • Si le graphe d'allocation des ressources ne contient aucun cycle => Aucun processus n'est en interblocage
  • Si le graphe d'allocation des ressources contient un cycle => Un risque d'interblocage existe !!

On peut faire une réduction de graphe pour observer l'état suivant du graphe :

  • Un graphe peut être réduit par un processus Pi, si le processus n'a pas de demande de ressource en cours.
  • Pour faire une réduction, on enlève toutes les allocations du processus.

Complètement réductible : si on arrive à faire des réductions successives jusqu'à ce qu'il n'y a plus de flêches dans le graphe.

On peut résoudre le problème d'interblocage :

  • Prévention : On garantit qu'aucun interblocage ne peut se produire.
    • *Prévenir : Empêcher une des 4 conditions de se produire
    • *Éviter : Éviter dynamiquement en allouant les ressource avec précaution
  • Détection ou guérison : Lorsqu'un interblocage est détecté, on ramène le système dans un état où les processus sont débloqués

En cas d'interblocage, on peut utiliser deux algorithmes pour résoudre ce problème. L'algorithme du banquier pour la prévention et ensuite un algorithme de reprise après interblocage.

L'algorithme de reprise fait soit appel à l'aide d'un opérateur, soit par une séquence automatique qui tue les processus impliqués dans l'interblocage, soit un processus après l'autre jusqu'à ce que l'interblocage soit résolu. Mais la destruction de processus est complexe et dangereuse.

Algorithme du banquier (banker's algorithm)

Cette technique impose de connaître pour chaque processus les ressources dont il aura besoin.

  • Il vérifie avant d'allouer une ressource que le graphe reste complètement réductible.
  • Il alloue une ressource uniquement s'il y a plus de ressources disponibles que demandées.
  • Sinon la demande de ressource est mise en attente.

Exemple :
algo_banquier1.png algo_banquier2.png

Une ressource n'est attribuée que si les ressources demandées sont inférieurs aux ressources disponibles.

(N)Needed < (A)Available

Si aucune rangée (processus) ne satisfait la condition, il y a interblocage.