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/
         ├─ Makefile
         ├─ question1.c
         ├─ question2.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. Arborescence des processus, instruction fork

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 :

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

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

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 initial (le père) et le nouveau processus (le fils) :

  1. Écrivez un programme avec un fork où :

    • 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. (Vous pouvez par exemple utiliser une boucle while(1); ou un sleep(10)).

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

    $ make
    gcc     question1.c   -o question1
    $ ./question1 &
    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) et que le fils est adopté par init.

  5. Vérifiez que vous pouvez tuer le processus fils (avec la commande kill depuis le shell) et qu'il reste sous forme de processus zombie.

1.2. "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.

  5. "Corrigez" le programme 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.

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

    $ killall <PROCESS_NAME>

    mais elle risque de ne pas fonctionner si les processus apparaissent trop rapidement.

  • la commande kill permet aussi de tuer tous les processus d'un même groupe : il suffit de remplacer PID par -PGID. (N'oubliez pas le signe - devant le numéro de groupe !).

    Pour récupérer ce numéro de groupe, vous pouvez :

    • utiliser l'appel système getpgrp(),

    • récupérer le PID du processus initial avec getpid() (il est égal au numéro de groupe de ses sous-processus),

    • utiliser la commande

      ps -o pid,pgid,tty,time,comm x

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.

Une variante de ce programme à exécuter dans un shell est

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

1.3. 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 terminal bufferise lui aussi les entrées. Pour changer ce comportement, vous pouvez faire

$ stty -icanon
...
...
...
$ stty icanon

1.4. Ordonnancement des processus et priorités

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 2 et 5 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, ou plus.

  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 d'un 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.

Il n'est pas nécessaire de gérer un historique des commandes tapées par l'utilisateur, ou l'utilisation des touches "flèches". Pour obtenir ces fonctionnalités automatiquement, vous pouvez utiliser l'utilitaire rlwrap et lancer votre shell avec

$ rlwrap ./minishell

(Attention, l'utilitaire rlwrap n'est pas installé par défaut...)

La première commande à interpréter est 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 (raisonnable) sur le nombre de caractères autorisés sur une ligne.

Interprétez maintenant les commandes pwd et cd <PATH>.

Fonction C à utiliser :

  • La commande chdir gère automatiquement les chemins relatifs au répertoire courant.

  • N'oubliez pas de faire un minimum de gestion d'erreur en affichant un message lorsqu'une commande ne fonctionne pas.

"exit" ou "pwd" sont des commandes internes au shell ("builtin" en anglais). Le shell permet également de lancer de nouveaux processus en exécutant un fichier. Vous devez maintenant gérer ceci.

  1. On suppose dans ce cas que la ligne contient uniquement 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 :

    • fork,

    • execl pour exécuter un nouveau programme,

    • wait ou waitpid.

    Les commandes de la famille de execl gèrent automatiquement les chemins absolus et relatifs.

    Pour tester, vous pouvez soit exécuter des commandes système (avec un chemin absolu complet), soit tester avec des commandes locales (avec un chemin relatif) :

    # /bin/date
    Thu Nov 7 15:39:15 CET 2019
    # ./question1
    Salut, je m'appelle Bob et je suis le père. Mon fils a le PID 217.
    Hej, je suis le fils de Bob.
  2. La plupart des programmes acceptent des arguments sur la ligne de commandes. 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 :

    • strtok pour découper la ligne,

    • execv, pour remplacer le execl de la question précédente (faites attention à bien initialiser les premières et dernières cases de l'argument argv !).

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

    Vous pouvez par exemple tester la commande printf:

      # /usr/bin/printf %i+%i=%i 1 2 1+2
      1+2=3

    (Attention à ne pas rajouter d'espace pour que les arguments soient bien lus correctement avec strtok.)

La commande ls est une commande externe (fichier /bin/ls sur mon système). Nous allons en programmer une version simple interne : lorsque la ligne commence par ls, votre code 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. Possibilité de quitter le shell avec Ctrl-d, qui ferme l'entrée standard du programme.

  2. Gestion des variables d'environnement $PWD et $OLDPWD et de l'abréviation cd - pour aller au répertoire précédent, cd pour aller au répertoire $HOME de l'utilisateur.

    Fonctions C pertinentes :

    • getenv,

    • setenv.

  3. Utilisation de la variable d'environnement PATH pour chercher les exécutables afin d'éviter d'avoir à taper le chemin absolu des commandes. (Regardez dans la page de manuel de execv pour voir comment faire.)

  4. 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.

  5. 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...

  6. 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.

  7. Plus 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).

  8. ...