Consignes

Le rendu de ce TP se fera uniquement par TPLab et consistera en une archive (zip, tar, etc.) contenant impérativement l'arborescence suivante :

      TP1-votre_login/
         ├─ RAPPORT
         ├─ Makefile
         ├─ question1.c
         ├─ ...
         ├─ test_procesus.c
         ├─ ...
         └─ minishell.c

Attention :

Tout non respect d'une ou plusieurs de ces consignes entrainera automatiquement un retrait de points sur votre note de TP !

Liens utiles

1. Les processus

1.1. Préliminaires

Pour obtenir la liste des processus sur le système, vous pouvez utiliser la command ps. Dans sa forme la plus simple, ps donne uniquement les processus de l'utilisateur courant associé au terminal où est lancé ps.

Certaines options intéressantes sont :

On peut arrêter un processus en lui envoyant un signal d'arrêt avec la commande kill:

$ kill PID

Par défaut, la commande kill envoie le signal TERM (pour terminate). On peut préciser un autre signal. Par exemple,

$ kill -9 PID
$ kill -KILL PID

envoient toutes les deux le signal KILL à un processus.

Sous Linux, vous pouvez afficher la liste des signaux (et leur numéro) avec la commande

$ kill -l

Écrivez un petit programme C qui ne s'arrête pas tout de suite et vérifiez que vous pouvez le voir avec la commande ps.

Remarque : pour empêcher un programme C de se terminer, vous pouvez :

Dans le premier cas, vous devrez arrêter le programme avec un "Control-c", ou bien avec la commande kill...

Dans le deuxième cas, il ne faut pas oublier la ligne #include <stdio.h> au début de votre fichier... Vous pouvez arrêter le programme en appuyant sur la touche "Entrée".

Dans le troisième cas, il ne faut pas oublier la ligne #include <unistd.h>.

1.2. Arborescence des processus, instruction fork

L'appel système (POSIX) fork() permet de dupliquer le processus courant. C'est la valeur de retour de cet appel système qui permet de faire la différence entre le processus de départ (le père) et le nouveau processus (le fils) :

Que doit afficher le programme suivant ?

#include <stdio.h>
#include <unistd.h>

int main() {
  printf("Bonjour,\n");
  fork();
  printf("J'ai fait un 'fork' !\n");
  return 0;
}

Testez et vérifiez que le résultat correspond à vos attentes.

La commande ps affiche les processus par ordre de PID. Il est possible d'afficher les processus en arborescence avec :

  1. Modifiez le programme précédent pour que :

    • le père affiche le PID du processus créé (du fils),

    • le fils affiche un message simple,

    • les processus ne se terminent pas instantanément (avec une boucle while(1); ou un sleep(10)).

  2. Compilez et lancez votre programme en arrière plan :

    $ make
    gcc     question3.c   -o question3
    $ ./question3 &
    Salut, je m'appelle Bob et je suis le père. Mon fils a le PID 185.
    Hej, je suis le fils de Bob.
  3. En utilisant la commande ps, vérifiez que vous avez bien créé 2 processus, et que l'un d'eux a effectivement le PID du fils (affiché par le père).

  4. Vérifiez que vous pouvez tuer le processus père (avec la commande kill depuis le shell) sans que le fils ne disparaisse.

Créez un processus zombie. Vous devez :

1.3. "fork-bomb"

On considère le programme suivant

#include <stdio.h>
#include <unistd.h>

int main() {
  int p;
  int n = 0;
  printf("Bonjour,\n");
  while(++n) {
    sleep(1);
    p = fork();
    if (p > 0) {
      printf("J'ai un nouveau fils: %i!\n", p);
    } else if (p == 0) {
      printf("Je suis le fils n° %i, je me termine!\n", n);
      return 0;
    } else {
      printf("ERREUR lors du fork...\n");
    }
  }
  return 0;
}
  1. Faites un petit schéma pour montrer l'arborescence des processus engendré par ce programme.

  2. Au nème passage dans la boucle, combien de processus sont actifs ?

  3. Au nème passage dans la boucle, combien de processus sont présents dans la table des processus ?

  4. Que va t'il se passer si on attend trop longtemps ? Supprimez la ligne sleep(1) pour confirmer votre conjecture.

"Corrigez" le programme de la question précédente pour éviter de créer un nouveau fils si l'ancien ne s'est pas terminé...

L'appel système wait(NULL) attend que l'un des fils du processus courant se termine et renvoie son PID. Si l'un des fils s'est terminé avant l'appel à wait(NULL), son PID est immédiatement renvoyé. (N'oubliez pas la ligne #include <sys/wait.h>.)

  1. Écrivez un programme qui se comporte de la manière suivante :

    • le processus initial créé un fils et se termine,

    • le fils créé un fils (petit fils du processus initial) et se termine,

    • le petit fils créé un fils (petit petit fils du processus initial) et se termine,

    • ad infinitum

  2. Expliquez pourquoi le programme obtenu ne sature pas la table des processus.

Remarque : la commande killall permet de tuer tous les processus avec un nom donné.

$ killall PROCESS_NAME

On considère le programme suivant.

#include <unistd.h>
int main() {
  while(1) {
    sleep(1);
    fork();
  }
  return 0;
}
  1. Comment va t'il se comporter ?

  2. Faite des petits schémas pour dessiner l'arborescence des processus aux étapes 3 et 4 de la boucle.

(*bonus*)

Expliquez ce que fait l'incantation ésotérique suivante dans un shell:

$ :(){ :|:& };:

1.4. fork et entrées / sorties

Écrivez un programme C qui

Répondez aux questions suivantes :

  1. Dans quel ordre se font les affichages ?

  2. Est-ce que cet ordre est toujours le même ?

N'oubliez pas de décrire comment vous procédez...

Que se passe t'il lorsqu'un processus fait un fork(), et que le père et le fils lisent sur l'entrée standard (avec getchar() par exemple) ?

Décrivez votre méthodologie ainsi que les résultats trouvés.

Le programme suivant n'a pas le comportement attendu...

#include <stdio.h>
#include <unistd.h>

int main() {
    printf("Je vais faire un 'fork'... ");
    int p = fork();
    if (p == 0) {
      printf("Je suis le fils...\n");
    } else {
      printf("Je suis le père...\n");
    }
    return 0;
}

Testez et proposez une explication.

1.5. Ordonnancement des processus et priorités

Comparez l'utilisation du processeur lors de l'exécution des programmes suivants :

int main() {
  while(1);
  return 0;
}
#include <stdio.h>
int main() {
  getchar();
  return 0;
}
#include <unistd.h>
int main() {
  while(1) {
    sleep(1);
  }
  return 0;
}

Expliquez les différences constatées, en particulier entre le premier et le troisième programme.

La commande

$ top

permet d'afficher, en temps réel, les informations relatives au processus

  • la colonne %CPU indique le pourcentage d'utilisation du CPU par le processus,

  • la colonne S indique l'état du processus :

    • R (Running) pour l'état prêt,

    • S (Sleep) pour l'état bloqué,

    • Z (Zombie) pour l'état zombie.

Rappel: vous pouvez lancer plusieurs processus "en même temps" en utilisant

$ CMD1 & CMD2 & CMD3 &
  1. Testez le programme suivant

    #include <stdio.h>
    #include <unistd.h>
    int main() {
        int p = getpid();
        printf("DÉBUT %i\n", p);
        unsigned n = 0x7fffffff;
        while(n--);
        printf("FIN %i\n", p);
        return 0;
    }
    

    et ajustez la valeur de n pour que l'exécution du programme prenne entre 5 et 10 secondes.

  2. Lancez 2 instances de ce programme "en même temps". Quelle est la durée d'exécution totale pour les deux processus ?

    Expliquez ce que vous constatez.

    N'hésitez pas à utilisez la commande top dans un autre terminal pour visualiser les processus...

  3. idem pour 4 ou 8 processus.

  4. Que se passe t'il si on remplace la boucle while(n--); par un sleep(5); ?

On peut lancer une commande en diminuant sa priorité avec

$ nice -n N CMD

N est un nombre inférieur à 20. Plus la valeur de N est grande, plus on diminue la priorité du processus. (L'administrateur a le droit d'utiliser des valeurs de N négatives pour rendre un processus plus prioritaire !)

  1. Lancez deux instances du processus précédent "en même temps", mais en diminuant la priorité d'une des instances.

    Décrivez ce que vous constatez et expliquez.

  2. Idem pour 4 ou 8 instances, dont 3 ou 7 sont moins prioritaires.

2. Un mini shell

L'objectif de cette seconde partie est d'écrire un mini shell en C.

Votre code doit compiler avec les options --std=c99 -Wall -Wextra -pedantic -Werror.

Votre shell consistera en un unique fichier minishell.c qui offrira les fonctionnalité suivantes :

  1. affichage du prompt #,

  2. lecture d'une ligne au clavier, terminée par un saut de ligne,

  3. action correspondante à la ligne,

  4. on recommence à l'étape 1.

En pseudo-code, ça donnerait quelque chose comme :

  while (true) {
    line = getline();
    if (line == TRUC) {
      action_TRUC();
    } else if (line == MACHIN) {
      action_MACHIN();
    } else if (line == BIDULE) {
      action_BIDULE();
    } else {
      action_ERREUR();
    }
  }

Les actions possibles sont :

Cette partie ressemble à un "mini projet" et de nombreux détails sont omis. Vous trouverez la documentation complète des fonctions C (avec leurs prototypes et #include nécessaires) dans les pages de manuel :

$ man fgets

ou

$ man 3 sleep

pour préciser que vous c'est la fonction sleep du langage C qui vous intéresse...

N'hésitez pas à lire, au moins en diagonale, les pages de manuel des fonctions mentionnées dans les questions.

La première version du shell n'interprètera que la commande exit. Dans tous les autres cas, votre shell affichera un message d'erreur...

Fonction C à utiliser :

Remarque : pour simplifier, n'hésitez pas à imposer une limite sur le nombre de caractères autorisés sur une ligne.

Dans la deuxième version du shell, vous devrez interpréter les commandes pwd et cd PATH.

Fonction C à utiliser :

La troisième version du shell lancera un nouveau processus lorsque la ligne n'est pas exit.

On suppose dans ce cas que la ligne consiste uniquement en un chemin d'accès vers un fichier exécutable qu'il faudra lancer dans un nouveau processus. Votre shell devra attendre la fin de ce processus avant de réafficher le prompt et de continuer...

Fonctions C à utiliser :

Travaillez avec l'hypothèse suivante : aucun fichier ou commande ne contient des caractères "étranges" comme : espace, tabulation, guillemet, "<", ">", "|", "&", etc.

La quatrième version du shell lancera un nouveau processus lorsque la ligne n'est pas exit, mais utilisera la suite de la ligne pour spécifier les argument de la commande.

Vous devrez donc découper la ligne en mots et utiliser le premier mot comme chemin d'accès au fichier exécutable, et les mots suivant comme tableau d'arguments (pour la commande execv).

Fonction C à utiliser :

Remarque : n'hésitez pas à imposer une limite sur le nombre d'arguments possibles des commandes,

La cinquième version du shell doit gérer la commande ls, qui appellera une fonction que vous écrirez qui affichera la liste des fichiers / répertoires du répertoire de travail.

Fonctions C à utiliser :

Ajoutez des fonctionnalités (au choix) dans votre shell.

Par exemple :

  1. Utilisation de la variable d'environnement PATH pour chercher les exécutables afin d'éviter d'avoir à taper le chemin absolu des commandes.

    Fonction C à utiliser:

    • getenv,

    • execvp (qu'elle est la différence avec execv ?).

  2. Redirection de la sortie standard dans un fichier avec

    # CMD > FICHIER

    Fonctions C à utiliser:

    • open,

    • dup2,

    • STDOUT_FILENO (constante).

    Vous pouvez imposer que les tokens ">" et "FICHIER" soient les derniers sur la ligne.

  3. Redirection de l'entrée standard avec "< FICHIER", ou redirection de la sortie en mode append avec ">> FICHIER".

    C'est encore mieux si on peut faire plusieurs redirections sur la même ligne...

  4. Lancement d'un processus externe en arrière plan avec

    # CMD &

    Remarque : il n'est pas demandé d'implanter les fonctionnalités bg et fg des shells usuels.

  5. Difficile : redirection de la sortie standard d'une commande vers l'entrée standard d'une autre commande avec

    # CMD1 | CMD2

    Fonctions C à utiliser:

    • pipe,

    • dup2,

    • STDOUT_FILENO et STDIN_FILENO (constantes).

  6. ...