Devoir Surveillé Systèmes d'Exploitation II

Section 1 - Qui suis-je ? Question 1(a) - Chemins d'exécution multiples Je permets à un système d'exploitation d'avoir simultanément des chemins d'exécution multiples au sein d'un processus unique. Réponse : Le Multithreading (ou les threads). Question 1(b) - Type de parallélisme Je suis un type de parallélisme où des instructions différentes sont exécutées simultanément.

D'après le document Devoir Surveillé Systèmes d'Exploitation II

Cet article a été rédigé automatiquement à partir du document source, puis vérifié avant publication.

Devoir Surveillé Systèmes d'Exploitation II

Document source

Devoir Surveillé Systèmes d'Exploitation II

Programming, Operating Systems, Multithreading · PDF · 7 pages · 2018

Afficher l'aperçu du document

Consulter le document original →

Section 1 - Qui suis-je ?

Question 1(a) - Chemins d'exécution multiples

Je permets à un système d'exploitation d'avoir simultanément des chemins d'exécution multiples au sein d'un processus unique.

Réponse : Le Multithreading (ou les threads).

Question 1(b) - Type de parallélisme

Je suis un type de parallélisme où des instructions différentes sont exécutées simultanément.

Réponse : Le Task parallélisme (ou parallélisme de tâches).

Question 1(c) - Appel système de terminaison

Je suis un appel système qui permet de mettre fin à un thread.

Réponse : pthread_exit

Question 1(d) - Bibliothèque requise

Je suis une bibliothèque à inclure pour utiliser le mutithreading.

Réponse : pthread.h

Question 1(e) - Identité du thread

Je suis un appel système qui permet d'obtenir l'identité du thread appelant.

Réponse : pthread_self

Section 2 - Multithreading

Question 2 - Choix entre fork() et pthread_create()

Pour lancer en parallèle plusieurs traitements d'une même application, vous avez le choix entre les appels systèmes fork() et pthread_create(). Laquelle des deux possibilités choisir ? Justifier votre réponse.

Réponse : Il faut choisir pthread_create(). Justification : La création d'un thread est beaucoup moins coûteuse en ressources que la création d'un processus lourd avec fork(). Les threads partagent le même espace d'adressage (partage de ressources et de mémoire), ce qui rend la communication entre eux plus rapide et évite les lourdes commutations de contexte inhérentes aux processus multiples.

Question 3 - Correction des erreurs de compilation

Il y a 6 erreurs de syntaxe et de logique dans le code fourni qui empêchent sa compilation et son exécution correcte.

Voici les 6 corrections apportées en suivant la solution du document :

  1. Inclusion manquante : Il manque la directive #include <pthread.h> au début du fichier.
  2. Prototype manquant : La fonction fonction_thread est utilisée dans le main avant d'être déclarée. Il faut ajouter son prototype : void * fonction_thread(void * arg);
  3. Erreur de type au retour : La fonction fonction_thread est déclarée avec le type de retour int, mais pour être compatible avec pthread_create, elle doit renvoyer void *.
  4. Erreur de type d'argument : La fonction fonction_thread doit prendre en paramètre un pointeur générique void * arg. Le déréférencement doit ensuite se faire via un cast explicite : int num = *((int*)arg);.
  5. Passage de paramètre erroné : Lors de l'appel à pthread_create, la variable i est passée directement par valeur. Il faut passer son adresse avec un cast explicite : (void*)&i. (Note pédagogique : passer l'adresse du compteur de boucle crée généralement une condition de course "race condition", mais la présence du sleep(1) juste après permet ici d'éviter le problème. Nous respectons le choix de la source).
  6. Arguments manquants pour pthread_join : L'appel pthread_join(thr[i]) est incomplet. Il nécessite un deuxième argument pour récupérer la valeur de retour du thread. On passe NULL : pthread_join(thr[i], NULL);.

Code C corrigé et fonctionnel :

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h> // Correction 1

#define NB_THREADS 5

// Correction 2 : Ajout du prototype
void * fonction_thread(void * arg);

int main(void)
{
    pthread_t thr[NB_THREADS];
    int i;

    for (i = 0; i < NB_THREADS; i++) {
        // Correction 5 : passage de (void*)&i au lieu de i
        if (pthread_create(&thr[i], NULL, fonction_thread, (void*)&i) != 0) {
            fprintf(stderr, "Erreur dans pthread_create\n");
            exit(EXIT_FAILURE); 
        }
        sleep(1); 
    }

    for (i = 0; i < NB_THREADS; i++) {
        // Correction 6 : Ajout de NULL comme second paramètre
        pthread_join(thr[i], NULL); 
    }

    fprintf(stderr, "Thread Main\n");
    return 0;
}

// Correction 3 et 4 : Type de retour void* et argument void*
void * fonction_thread(void * arg)
{
    int num = *((int*)arg);
    fprintf(stderr, "Thread numero %d\n", num);
    pthread_exit(NULL);
}

Section 3 - Concurrence et synchronisation

Question 4 - Attente active vs Sommeil et activation

Comparer les algorithmes d'exclusion mutuelle par attente active et les algorithmes basés sur le sommeil et l'activation.

Réponse :

  • L'attente active (ex: boucle while sur un verrou) force le processus à vérifier continuellement une condition tant qu'elle n'est pas remplie. Cela monopolise le processeur inutilement et gaspille des cycles d'horloge (temps CPU).
  • Le sommeil et l'activation (ex: sémaphores avec files d'attente) placent le processus dans un état bloqué (sommeil) lorsqu'il ne peut pas accéder à la ressource. Le CPU est alors libéré pour exécuter d'autres processus. Le processus endormi sera réveillé (activation) par le système d'exploitation dès que la ressource sera disponible. Cette méthode est beaucoup plus efficace pour la gestion des ressources.

Question 5 - Graphe de précédence

Note : L'image du graphe de précédence est manquante dans le document source. De plus, l'énoncé mentionne l'existence de P5 (avec une erreur de frappe "Process P4 { I5; }") mais la solution officielle fournie par la source omet totalement P5. Nous reconstruisons donc la solution stricte de la source impliquant P1, P2, P3 et P4.

D'après la logique des sémaphores de la solution source, le graphe exige que :

  • L'instruction de P1 (I1) s'exécute avant celles de P2 et P3.
  • Les instructions de P2 (I2) et de P3 (I3) s'exécutent avant celle de P4.

Code de synchronisation corrigé :

PROGRAM P1P2P3P4; \\

var S1, S2, S3 semaphore init 0;

Process P1 { 
    I1; 
    V(S1); 
    V(S2); 
}

Process P2 { 
    P(S1); 
    I2; 
    V(S3); 
}

Process P3 { 
    P(S2); 
    I3; 
    V(S3); 
}

Process P4 { 
    P(S3); 
    P(S3); 
    I4; // Corrigé : la source indique 'I3' par erreur pour P4
}

Question 6 - Ordres d'exécution atomique

Note importante sur les erreurs de la source : Le code fourni pour le Processus B contient une erreur fatale (P(S); c; P(S); d;). Le deuxième appel à P(S) sans V(S) intermédiaire provoque un interblocage (deadlock) immédiat puisque le sémaphore S est initialisé à 1. Par ailleurs, la solution imprimée dans la source (a c-d-a) est corrompue et omet l'instruction b.

Pour que l'exercice ait un sens pédagogique et donne des ordres d'exécution viables, nous devons supposer que le processus B est symétrique au processus A, c'est-à-dire : Process B corrigé : P(S); c; V(S); P(S); d; V(S);

Dans ces conditions, le sémaphore S=1 garantit l'exclusion mutuelle pour chaque bloc. Les instructions protégées (a, b, c, d) peuvent s'entrelacer, mais l'ordre interne des processus (a avant b, et c avant d) doit être respecté.

Les 6 ordres d'exécution possibles sont :

  1. a - b - c - d
  2. a - c - b - d
  3. a - c - d - b
  4. c - a - b - d
  5. c - a - d - b
  6. c - d - a - b

Méthode

Pour réussir ce type d'épreuve sur les systèmes d'exploitation :

  1. Multithreading : Assurez-vous de bien maîtriser l'API POSIX (pthread). Retenez par cœur les signatures des fonctions principales (pthread_create, pthread_join, pthread_exit) ainsi que le besoin absolu de manipulations de pointeurs (void *) pour passer et récupérer des arguments. Soyez très attentif aux variables partagées et aux conditions de course dans les boucles (comme le passage de l'adresse du compteur i).
  2. Synchronisation : Lors de l'analyse d'un graphe de précédence, rappelez-vous qu'une flèche entre deux processus Px et Py se traduit par un sémaphore initialisé à 0. Px effectue un V(S) à la fin de son exécution, et Py effectue un P(S) avant de démarrer.
  3. Analyse de code (Concurrence) : Tracez toujours l'état de la valeur de votre sémaphore mentalement ou sur un brouillon. Si un processus effectue deux opérations P() successives sur un sémaphore d'exclusion mutuelle (initialisé à 1) sans opération V() intermédiaire, identifiez et signalez immédiatement l'interblocage (deadlock).

Partager

Commentaires

Aucun commentaire pour le moment. Posez la première question.

Les commentaires sont relus avant publication. Votre e-mail n'est jamais affiché.

← Toutes les révisions