CentraleSupélecDépartement informatique
Plateau de Moulon
3 rue Joliot-Curie
F-91192 Gif-sur-Yvette cedex
3IF1020 - Programmation système - TP n°1

Table des matières

  • Date de la séance de TP : vendredi 4 octobre 2024 - 13h45
  • Date limite de prise en compte de votre version sur votre dépôt GitLab : dimanche 20 octobre 2024 - 23h59

Utiliser les fichiers du dossier ps_tp1 à la racine de votre dépôt GitLab, seul le contenu de ce dossier sera pris en compte pour l'évaluation de votre travail sur ce TP.

Les noms des fichiers à utiliser sont indiqués dans ce sujet.

Vous devez respecter les add, commit & push mentionnés dans le sujet. Cela ne vous interdit bien sûr pas de corriger une réponse déjà fournie dans un commit ultérieur.


Ces exercices ont pour objectif de mettre en œuvre une partie des éléments présentés en cours en vous faisant utiliser des appels systèmes classiques d'un système d'exploitation de type Unix.

Avec un système Windows, l'environnement WSL devrait permettre de faire cet exercice (non testé). En revanche, MSYS2 ne peut pas être utilisé. Si vous n'êtes pas familier avec ces aspects techniques, utilisez MyDocker en demandant cet environnement.

Les programmes seront écrits en C.

1. Un programme, un processus (1 point)

1.1. Générateur de nombre aléatoires

  • Ouvrir deux fenêtres Terminal que nous appelleront Term1 et Term2.
  • Écrire dans le fichier server_1.c un programme C contenant un main() qui :
    • affiche un message
    • puis, dans une boucle infinie :
      • affiche son id (numéro de processus, utilisez getpid()), celui de son père (getppid()), ainsi que celui de son groupe (getpgrp())
      • affiche un nombre aléatoire entre 0 et 99 obtenu avec rand()
      • se met en pause pendant une seconde grâce à sleep()
    • affiche de nouveau un message (après la boucle)
    • retourne le code EXIT_SUCCESS indiquant un fonctionnement nominal.

getpid() est souligné car c'est un lien vers la page de documentation de cette fonction : vous devez prendre l'habitude d'aller consulter cette documentation, ainsi que celle des autres fonctions mentionnées dans le sujet.

Les pages de documentation des appels systèmes peuvent parfois être très techniques et compliquées à comprendre ; ce n'est pas le cas de getpid(), mais c'est le cas de sigaction() que vous allez utiliser juste après. Il ne faut pas chercher à tout lire et tout comprendre pour utiliser une telle function une première fois : survolez la documentation, identifiez les points importants (au début la plus part du temps) pour votre besoin, le cas échéant cherchez des exemples sur Internet.


Toutes les fonctions systèmes utiles pour cet exercice sont mentionnées dans le sujet. Si vous en utilisez d'autres, justifiez le.

  • Dans Term1, compiler et exécuter ce programme.
  • Dans Term2, taper la commande ps aj. Identifier votre processus dans la liste, quel est son processus père ? Que constatez-vous pour les numéros de groupe ?
  • Dans Term1, arrêter le programme en utilisant la combinaison de touches CTRL-C, puis vérifier dans Term2 qu'il n'apparait plus dans la liste affichée par la commande ps aj ; dans Term1, relancer le.
  • Dans Term2, taper kill idid est le numéro de processus de votre programme (kill est la commande, id son argument). Vérifier qu'il s'est arrêté.

Les réponses aux questions posées doivent être en commentaires dans le fichier source.


add, commit & push


1.2. Arrêt sur un signal

  • Recopier votre programme dans le fichier server_2.c.
  • Modifier votre boucle pour qu'elle ne s'exécute que tant qu'une variable booléenne globale running soit vraie (cette variable doit être vraie au démarrage de votre programme). Définir une fonction qui reçoit un numéro de signal (un int), l'affiche, affiche un message et met cette variable running à faux. Installer cette fonction, en utilisant sigaction(), pour qu'elle soit automatiquement exécutée sur réception du signal SIGINT.

Une fonction installée avec sigaction() pour un signal donné le reste tant que vous n'installez pas une autre fonction pour ce même signal.

Votre variable running doit être déclarée volatile : elle va en effet être modifiée par votre fonction de manière asynchrone par rapport au déroulement du programme principal, ce mot clef impose au compilateur d'aller systématiquement chercher la valeur de cette variable dans la mémoire centrale, donc de ne pas la conserver dans un registre pour des raisons d'optimisation.


La documentation de sigaction() précise que cette fonction retourne -1 en cas d'erreur et positionne la variable globale errno avec le code d'erreur. Une bonne pratique dans ce cas est d'utiliser perror() pour afficher un message avant de terminer le processus avec la fonction exit() utilisant le code de retour EXIT_FAILURE.

De manière générale, il est important en programmation système de n'ignorer aucun signalement d'erreur.


  • Dans Term1, compiler et exécuter ce programme, arrêter le en utilisant CTRL-C, vérifier que le message de votre fonction est affiché.
  • Dans Term1, exécuter le de nouveau ; dans Term2, utiliser la commande kill avec l'option -s INT (l'option doit être indiquée avant l'argument). Est-ce que le message a été affiché ? Recommencer en utilisant la commande kill sans option. Est-ce que le message a été affiché ? Modifier votre programme pour que le message soit affiché aussi dans ce cas (le message doit continuer à s'afficher avec l'option -s INT).
  • Dans Term1, exécuter le de nouveau ; dans Term2, utiliser la commande kill avec l'option -s KILL (plus connue sous le nom kill -9). Est-ce que le message a été affiché ? Peut-on faire en sorte qu'il le soit ? Que se passe-t-il si vous donnez comme argument à la commande kill (sans, puis avec l'option -s KILL) le numéro du processus père ?
  • Modifier votre fonction afin de ne plus modifier la variable running. Est-ce que maintenant votre programme s'arrête via un CTRL-C ? un kill ? un kill -9 ? Remettre la modification de la variable running dans votre fonction.

1.3. Message de fin

  • Ajouter dans votre programme une fonction qui affiche un message. Installer cette fonction au début de main(), en utilisant atexit(), pour qu'elle soit automatiquement exécutée à la fin du programme. Tester si cette fonction est activée quand le programme est arrêté via un CTRL-C, un kill et un kill -9.

La remarque faite ci-dessus pour sigaction() est aussi valable pour atexit().


add, commit & push


2. Un programme, deux processus (1 point)

2.1. Même code

  • Recopier votre programme dans le fichier server_3.c.
  • Dupliquer votre processus en utilisant fork(). Les 2 processus exécutent le même programme, faites en sorte que les messages précisent s'ils sont émis par le père ou par le fils (mais éviter de dupliquer du code !)
  • Que constatez-vous pour les numéros de groupe ? Est-ce que les 2 processus s'arrêtent avec CTRL-C ? Pouvez-vous expliquer pourquoi ?
Besoin d'aide ?

Consulter par exemple la page de getpgid().


add, commit & push


  • Relancer votre programme. Utiliser ps aj dans Term2 pour les visualiser. Utiliser kill pour arrêter le processus fils puis ps aj de nouveau : que remarquez-vous ?
  • Maintenant, tuer le père, est-ce que le fils est toujours visible ?
  • Modifier votre code pour que le père s'arrète quand il détecte (via un signal) que le fils s'est arrêté.
  • Relancer votre programme et commencer cette fois par tuer le père : qu'observez-vous ?
  • Tuer le fils.
  • Modifier votre programme pour que le père, quand il est arrêté par un signal, arrête le fils en lui envoyant un signal.

add, commit & push


2.2. Un serveur, un client, un tube

Le père et le fils vont maintenant exécuter un code différent : au lieu d'afficher les nombres, le père va les communiquer (en tant que nombres - int -, pas sous forme de chaînes de caractères, et sans utiliser une valeur particulière pour indiquer la fin de la communication) au fils qui, lui, les affichera. La communication se fera via un « tube ».

  • Recopier votre programme dans le fichier server_4.c.
  • Supprimer l'arrêt du fils par le père et celui du père par le fils : les 2 processus vont maintenant communiquer par un tube, ils seront ainsi en mesure de détecter l'arrêt de l'autre.
  • Créer un tube en utilisant pipe() et modifier le code pour réaliser le comportement demandé. Vous utiliserez read() pour lire et write() pour écrire dans le tube.

Vous connaissez le principe DRY ? Ici, le code commun au père et au fils ne doit pas être dupliqué !


  • Vérifier que les processus s'arrêtent avec CTRL-C, ainsi qu'avec un kill sur le fils ou sur le père.

N'oubliez pas que les extrémités d'un tube doivent être fermées (avec close()) quelle que soit la façon dont le processus se termine (sauf bien sûr sur un kill -9) et que read() et write() retournent une valeur.


  • Comparer les messages affichés quand on arrête le père en premier, et ceux quand on arrête le fils en premier. Faire en sorte que le père affiche aussi son message de fin quand on arrête le fils en premier.
Besoin d'aide ?

Le parent reçoit un signal quand le tube est fermé par le fils.


add, commit & push


3. Deux programmes, deux processus (1 point)

3.1. Un serveur, un client, une fifo

Les deux processus seront maintenant des programmes différents (un serveur server_5.c qui envoie des nombres, pas des caractères, à un client client_5.c). Pour communiquer, ils utiliseront un « tube nommé » ou fifo.

  • Créer une fifo nommée à l'aide de la commande shell mkfifo.
  • En reprenant votre code précédent, écrire deux programmes distincts qui communiquent via la fifo. Celle-ci sera ouverte en utilisant open() (en écriture seule par le serveur, en lecture seule par le client). Dès qu'un des deux processus sera arrêté, y compris par un kill -9, l'autre devra s'arrêter aussi (en affichant ses messages d'arrêt).

add, commit & push


4. Partie avancée (1 point)

4.1. Un serveur, un client, le réseau

Les deux processus sont toujours des programmes différents (un serveur server_6.c qui envoie des nombres à un client client_6.c), mais, pour communiquer, ils utiliseront une connexion réseau.

  • En reprenant votre code précédent, écrire deux programmes distincts qui communiquent via TCP.
    • Le serveur commencera par créer une extrémité de communication avec socket() en utilisant le domaine IPv4, une communication de type flux et le protocole associé par défaut. Il ne faudra pas oublier de faire les conversions entre la représentation machine et la représentation réseau à l'aide des fonctions htonl() et htons(). En utilisant bind(), le serveur associera ce socket à une adresse IPv4 de socket sur toutes les interfaces locales et en utilisant un numéro de port non privilégié qu'il faudra choisir. Il pourra alors attendre, en utilisant listen(), la demande d'ouverture de connexion par le client, l'accepter avec accept() et enfin lui envoyer des nombres aléatoires avec write().
    • Le client créera aussi une extrémité de communication du même type. Il utilisera connect() pour se connecter au serveur sur l'adresse de loopback, et pourra ainsi recevoir les nombres générés par le serveur avec read().

La description de ce qu'il faut faire est volontairement succincte afin de vous inciter le cas échéant à utiliser Internet pour vous aider.


add, commit & push