System Calls, File, and Process Management

1 carte

The document covers the fundamentals of operating systems, including system calls, file management, process management, and signals. It explains concepts like kernel mode vs. user mode, process control blocks, process states, scheduling algorithms, process creation (fork), program execution (exec), and signal handling. File system concepts such as inodes, hard links, symbolic links, and file descriptors are also detailed. The document uses examples in C to illustrate these concepts, particularly within the context of Unix-like systems.

1 carte

Réviser
La répétition espacée te présente chaque carte au moment optimal pour la mémoriser durablement, en espaçant les révisions de façon croissante.
Question
Réponse

Programmation Systèmes et Réseaux : Fondements, Appels Système, Gestion des Fichiers et Processus

Ce cours explore les mécanismes internes des systèmes d'exploitation de type Unix/Linux, en se concentrant sur la gestion des fichiers, les entrées/sorties de bas niveau et la gestion des processus. L'objectif est de comprendre comment Unix traite l'information de manière universelle et performante, ainsi que la mécanique intime de la création, de l'exécution et de l'extinction des processus.

1. Fondements du Système d'Exploitation

1.1 Composants d'un système informatique

Un système informatique est structuré en quatre composants principaux :

  • Matériel : Constitué de l'unité centrale (CPU), de la mémoire (RAM) et des périphériques d'entrée/sortie (E/S). Ces éléments fournissent les ressources de calcul fondamentales.
  • Système d'exploitation (OS) : Un ensemble de programmes qui gère et contrôle le matériel, coordonnant son utilisation entre les diverses applications et utilisateurs.
  • Logiciels d'application : Des programmes comme les traitements de texte, tableurs ou navigateurs web, qui utilisent les ressources matérielles gérées par l'OS pour résoudre des problèmes spécifiques aux utilisateurs.
  • Utilisateur : L'individu qui interagit avec les logiciels et le système pour accomplir des tâches.

1.2 Qu'est-ce qu'un Système d'Exploitation ?

Le système d'exploitation est la pierre angulaire de tout ordinateur, agissant comme un ensemble de programmes qui gère les ressources matérielles et logicielles. Il offre une interface essentielle entre l'utilisateur et le matériel, permettant l'exécution d'applications et la gestion de ressources cruciales comme les fichiers, la mémoire et les processus.

Un système d'exploitation typique comprend plusieurs couches :

  • Le noyau (Kernel) : Le cœur du système.
  • Les bibliothèques systèmes : Des collections de fonctions qui abstrait les opérations de bas niveau, comme la libc sous Linux (fonctions C standards) ou la WinAPI sous Windows.
  • Les services système : Des démons ou services en arrière-plan qui gèrent des fonctions comme les utilisateurs, l'impression, la mise en réseau, etc.
  • L'interface utilisateur : Peut être graphique (GUI - Graphical User Interface) ou en ligne de commande (CLI - Command Line Interface).
  • Les utilitaires et applications de base : Des outils fondamentaux comme les explorateurs de fichiers, éditeurs de texte, gestionnaires de tâches.

1.3 Services offerts par un Système d'Exploitation

Un OS fournit un environnement pour l'exécution des programmes en offrant divers services :

  • Exécution de programmes : Chargement en mémoire, exécution, et gestion de l'état des programmes.
  • Opérations d'E/S : Gestion de l'accès aux périphériques.
  • Systèmes de fichiers : Organisation, stockage et récupération des données.
  • Communication : Permettre aux processus de communiquer entre eux (IPC) ou avec des systèmes distants.
  • Allocation de ressources : Distribution équitable des ressources (CPU, mémoire, E/S) entre les processus.
  • Détection d'erreurs : Identification et réaction aux erreurs matérielles ou logicielles.
  • Protection et sécurité : Garantir l'intégrité du système et la confidentialité des données.
  • Interfaces utilisateur : GUI, écrans tactiles, ligne de commande.

1.4 Distinction entre Noyau (Kernel) et Système d'Exploitation (OS)

Bien que souvent utilisés de manière interchangeable, le noyau et le système d'exploitation ont des rôles et des portées distincts :

Caractéristique Noyau (Kernel) Système d'exploitation (OS)
Définition Composant central qui gère le matériel et les ressources les plus essentielles. Ensemble de logiciels qui inclut le noyau et d'autres composants pour interagir avec l'ordinateur.
Rôle principal Gestion des ressources système (CPU, mémoire, périphériques) et communication directe avec le matériel. Fournir une interface utilisateur, des services aux applications, et un environnement complet.
Exécution Fonctionne en mode noyau (privilégié). Fonctionne principalement en mode utilisateur (avec accès restreint au matériel), sauf pour les tâches déléguées au noyau.
Exemples Linux Kernel, Windows NT Kernel, XNU (macOS). Windows, macOS, Ubuntu, Android.

Le noyau est la partie la plus critique et la plus basse de l'OS, mais à lui seul, il ne constitue pas un système d'exploitation complet utilisable par un être humain. Un OS ajoute les couches supérieures (bibliothèques, services, interface) pour rendre machine opérationnelle.

1.5 Modes d'exécution : Espace Utilisateur vs Espace Noyau

Pour des raisons de stabilité et de sécurité, les systèmes d'exploitation modernes (comme Linux/Unix) divisent l'exécution du code en deux modes avec des niveaux de privilège différents :

  • Espace Noyau (Kernel Mode / Kernel Space) :
    • Privilèges : Totaux. Le code exécuté ici a un accès direct et sans restriction à l'ensemble du matériel (CPU, RAM, disques, cartes réseau) et à toutes les instructions du processeur (y compris celles critiques pour la gestion mémoire ou les interruptions). C'est le "sanctuaire" du système.
    • Résidents : Le noyau lui-même, les pilotes de périphériques (drivers) et les modules chargés dynamiquement.
    • Risque : Une erreur dans l'espace noyau (bug, corruption mémoire) entraîne un plantage de l'ensemble du système (Kernel Panic sous Linux, Blue Screen of Death sous Windows). Il n'y a aucune couche de protection.
    • Analogie matérielle : Correspond à l'anneau de protection Ring 0 sur les architectures x86.
  • Espace Utilisateur (User Mode / User Space) :
    • Privilèges : Restreints. Le code exécuté ici est isolé et ne peut accéder qu'à sa propre zone mémoire. Il est strictement interdit d'accéder directement au matériel ou à la mémoire d'autres processus ou du noyau.
    • Résidents : Les applications (navigateurs, éditeurs de texte), les serveurs (Apache, Nginx) et tous les programmes utilisateur.
    • Sécurité : Si un programme plante dans l'espace utilisateur (par exemple, tentative d'accès à une zone mémoire interdite), le noyau intercepte l'erreur et met fin uniquement à ce processus (Segmentation Fault). Le reste du système reste stable et fonctionnel.
    • Analogie matériere : Correspond à l'anneau de protection Ring 3 sur les architectures x86.

Le tableau suivant résume les différences clés :

Caractéristique Espace Utilisateur (User Mode) Espace Noyau (Kernel Mode)
Accès Matériel Interdit (doit demander la permission via des appels système) Direct et Total
Mémoire Virtuelle, isolée par processus Accès à toute la mémoire physique
Crash Tue le processus fautif (Segfault), le système continue de fonctionner. Plante le système entier (Kernel Panic).
Instructions CPU Sous-ensemble limité d'instructions (non privilégiées) Jeu d'instructions complet (privilégiées et non privilégiées)

1.6 Le langage C : Compilation et Exécution

Le langage C est historiquement lié à la programmation système, ayant été conçu pour écrire des systèmes d'exploitation comme Unix. Il est donc particulièrement adapté pour interagir avec les fonctionnalités de bas niveau du système.

  • Compilateur : Transforme le code source écrit en C (fichiers .c) en fichier objet (code machine non lié).
  • Éditeur de liens (Linker) : Regroupe les fichiers objets et les bibliothèques nécessaires pour créer un fichier exécutable.

Sous Linux, le compilateur C standard est gcc (GNU Compiler Collection), intégré dans pratiquement toutes les distributions.

Compilation et Exécution en Ligne de Commande :

Pour compiler un fichier source nom_fichier_source.c :

gcc nom_fichier_source.c

Cette commande crée par défaut un exécutable nommé a.out.

Pour spécifier un nom d'exécutable personnalisé, utilisez l'option -o :

gcc -o nom_fichier_executable nom_fichier_source.c

Pour exécuter le fichier compilé :

./nom_fichier_executable
Exemple : Programme "Hello World!"

Fichier hello.c :

#include <stdio.h>

int main(void) {
    printf("Hello world!\n");
    return 0;
}

Compilation : gcc -o hello hello.c

Exécution : ./hello

Sortie : Hello world!

1.7 Gestion des Erreurs en Programmation Système

La gestion robuste des erreurs est primordiale en programmation système pour comprendre et réagir aux défaillances.

  • La variable globale errno :
    • Définition : Variable globale (un entier) définie dans <errno.h>. Elle est automatiquement définie par les appels système ou les fonctions de la bibliothèque C pour indiquer la cause d'une erreur lorsqu'une fonction échoue (généralement son retour sera -1).
    • Valeurs : Chaque valeur de errno correspond à un code d'erreur standardisé (ex: pour "Permission denied", pour "No such file or directory", pour "Invalid argument", pour "Out of memory").
  • La fonction perror() :
    • Utilisation : Affiche un message d'erreur lisible par l'utilisateur sur stderr, basé sur la valeur actuelle de errno.
    • Syntaxe :
      void perror(const char *s);
      est une chaîne de caractères personnalisée affichée avant le message d'erreur système.
    • Exemple :
      if (open("non_existant.txt", O_RDONLY) == -1) {
          perror("Erreur lors de l'ouverture");
      }
    • Sortie typique : Erreur lors de l'ouverture: No such file or directory (si errno vaut ).

1.8 Appels Système et Fonctions Clés

Les appels système sont l'interface directe entre les programmes utilisateur et le noyau. Voici quelques catégories d'appels système fondamentaux :

  • Gestion des fichiers : open(), read(), write(), close() pour les opérations de base sur les fichiers.
  • Gestion des processus : fork() pour créer de nouveaux processus, exec() pour exécuter un nouveau programme, exit() pour terminer un processus, wait() pour attendre la terminaison d'un processus enfant.
  • Gestion de la mémoire : mmap() pour mapper des fichiers ou des périphériques en mémoire, brk() pour ajuster la taille du segment de données.
  • Réseau : socket() pour créer des points de communication réseau, connect() pour établir une connexion, send() et recv() pour envoyer et recevoir des données.

1.9 Le concept "Tout est un fichier" sous Linux

Sous Linux, une philosophie clé est que "tout est un fichier". Cela ne signifie pas que tout est littéralement un fichier binaire sur un disque, mais que le système d'exploitation expose et interagit avec la plupart des ressources du système comme si elles étaient des fichiers. Cette abstraction unifiée apporte des avantages majeurs :

  • Simplification de l'accès : Ouvrir un fichier texte, un disque dur, un périphérique série ou une interface réseau utilise les mêmes appels système (open(), read(), write(), close()).
  • Unification des outils : Les commandes et outils universels (comme cat, echo, cp, grep) peuvent être utilisés indifféremment pour manipuler des fichiers classiques, des périphériques ou des processus.
  • Facilite l'automatisation et le scripting : Rend le développement de scripts puissant et cohérent.

Exemples de "fichiers" sous Linux :

  • Fichiers classiques : Textes, binaires, images, etc.
  • Répertoires : Sont en réalité des fichiers spéciaux contenant une liste d'entrées (noms de fichiers et leurs numéros d'inode).
  • Périphériques matériels : Exposés sous /dev (ex: /dev/sda pour un disque dur, /dev/ttyS0 pour un port série, /dev/audio pour une carte son).
  • Processus : Via des fichiers virtuels dans /proc (ex: /proc/1234/cmdline pour la commande d'un processus avec PID 1234).
  • Interfaces du noyau : Via /sys, permettant de configurer ou d'interroger le noyau.
  • Flux d'entrée/sortie standards : stdin, stdout, stderr.
  • Sockets Unix et canaux nommés (FIFO) : Mécanismes de communication inter-processus.

1.10 Système de Fichiers Unix et Inodes

Un système de fichiers est la structure logique et l'ensemble de règles qui régissent l'organisation, le stockage et la récupération des données sur un support de stockage (disque dur, SSD, clé USB, etc.). Ses fonctions incluent :

  • Organisation de l'information : Création d'arborescences (répertoires, sous-répertoires, fichiers).
  • Gestion de l'allocation de blocs : Attribution et libération des blocs de données sur le support physique.
  • Cohérence des données : Mécanismes de journalisation et de vérification pour prévenir la corruption.
  • Gestion des permissions : Contrôle d'accès basé sur l'utilisateur (), le groupe () et les droits (lecture, écriture, exécution).
  • Optimisation de l'accès : Adaptations pour différents types de supports de stockage.
Inodes (Index Nodes)

Pour l'utilisateur, un fichier est un nom (ex: rapport.txt). Pour le système Unix, un fichier est identifié par un numéro unique appelé inode. Un inode est une structure de données stockée sur le disque qui contient l'ensemble des métadonnées du fichier, à l'exception de son nom.

L'inode ne contient pas le nom du fichier. Le nom du fichier est une simple entrée dans un répertoire, qui associe une chaîne de caractères (le nom) à un numéro d'inode.

Ce que contient un inode :

  • Type de fichier : Indique s'il s'agit d'un fichier régulier, d'un répertoire, d'un lien symbolique, d'un périphérique, d'une socket, etc.
  • Permissions : Droits d'accès (lecture, écriture, exécution) pour le propriétaire, le groupe et les autres.
  • Propriétaire : Identifiant de l'utilisateur () et du groupe ().
  • Taille du fichier : En octets.
  • Dates : Date de création, de dernière modification, de dernier accès.
  • Compteur de liens : Le nombre de liens physiques (noms de fichiers) qui pointent vers cet inode.
  • Pointeurs vers les blocs de données : Le contenu réel du fichier est stocké dans des blocs de données sur le disque, et les pointeurs dans l'inode indiquent où trouver ces blocs.

Pour visualiser les numéros d'inodes, on utilise la commande ls -li :

ghost@DESKTOP-S9DICMD:~$ ls -li
total 4
46412 -rw-r--r-- 1 ghost ghost 14 Nov 25 17:31 note.txt

Dans cet exemple, 46412 est le numéro d'inode du fichier note.txt.

1.11 Liens Physiques (Hard Links) et Liens Symboliques (Soft Links / Symlinks)

La séparation entre le nom du fichier et son inode permet à Unix d'avoir plusieurs noms pour les mêmes données.

Lien Physique (Hard Link)
  • Mécanisme : Un lien physique est un deuxième nom (ou plus) donné au même inode existant. Créer un lien physique, c'est ajouter une nouvelle entrée de répertoire qui associe un nouveau nom au numéro d'inode d'un fichier déjà existant. Le compteur de liens de l'inode est incrémenté.
  • Conséquence : Tous les liens physiques d'un même inode sont indiscernables. Une modification via l'un est visible via tous les autres, car ils partagent les mêmes données sous-jacentes.
  • Suppression : La suppression d'un lien physique réduit le compteur de liens de l'inode. Les données ne sont libérées du disque (et l'inode n'est pas réutilisé) que lorsque le compteur de liens atteint zéro.
  • Limitations :
    • Impossible de créer un lien physique vers un répertoire (pour éviter des boucles infinies dans le système de fichiers).
    • Impossible de créer des liens physiques entre des systèmes de fichiers différents ou des partitions différentes, car les inodes sont uniques par système de fichiers.
  • Utilisation typique : Avoir un fichier accessible depuis plusieurs chemins sans dupliquer les données, créer des sauvegardes incrémentales "sans coût" d'espace, ou gagner de l'espace disque en évitant la duplication de contenu.
ln fichier_original lien_physique
Lien Symbolique (Soft Link / Symlink ou Lien Doux)
  • Mécanisme : Un lien symbolique est un nouveau fichier à part entière, avec son propre inode, dont le contenu est le chemin d'accès (absolu ou relatif) vers un autre fichier ou répertoire. C'est l'équivalent d'un "raccourci" sous Windows.
  • Conséquence : Si le fichier ou répertoire cible est supprimé ou déplacé, le lien symbolique devient "cassé" (dangling link), car il pointe vers un chemin qui n'existe plus.
  • Avantages :
    • Peut pointer vers des répertoires.
    • Peut traverser les limites des systèmes de fichiers (différentes partitions).
    • Permet de pointer vers des fichiers qui n'existent pas encore (créer un lien, puis créer le fichier).
  • Utilisation typique : Simplifier l'accès à des emplacements complexes, déplacer des données sans rompre les dépendances des applications, partager un même dossier entre plusieurs programmes (ex: versions de logiciels), synchronisation de dossiers.
ln -s cible lien_symbolique
Comparaison Liens Physiques vs Liens Symboliques :
Caractéristique Lien Physique (Hard) Lien Symbolique (Soft)
Inode Partage le même inode que la cible. A son propre inode unique.
Taille Celle du fichier original (n'occupe pas d'espace supplémentaire pour les données). Minuscule (taille en octets du chemin du fichier cible).
Suppression cible Le lien reste valide (les données sont conservées tant qu'il reste au moins un lien). Le lien est cassé (devient un "dangling link").
Portée Uniquement sur la même partition (même système de fichiers). Peut traverser les partitions et pointer vers des répertoires.
Cible Fichiers uniquement. Fichiers ou répertoires.

1.12 Descripteurs de Fichiers (File Descriptors - FD)

Alors que l'inode représente le fichier sur le disque, le descripteur de fichier est sa représentation en mémoire vive (RAM) pour un processus spécifique. C'est une abstraction clé d'Unix.

Lorsqu'un programme ouvre un fichier (via l'appel système open()), le noyau ne lui donne pas un accès direct aux secteurs du disque. Au lieu de cela :

  1. Le noyau crée une entrée dans une table interne qui suit l'état de cette ouverture spécifique (position actuelle, mode d'accès, pointeur vers l'inode sous-jacent).
  2. Il renvoie au programme un entier non négatif. Cet entier est le Descripteur de Fichier (FD).
  3. Pour toutes les opérations ultérieures (lecture, écriture, fermeture), le programme utilisera cet entier (ex: 3) pour indiquer au noyau quel flux de données il souhaite manipuler.
Descripteurs de Fichiers Standards (FDs Standards)

Par convention, chaque processus démarre avec trois descripteurs de fichiers déjà ouverts :

  • 0 (STDIN) : L'entrée standard, généralement connectée au clavier du terminal.
  • 1 (STDOUT) : La sortie standard, généralement connectée à l'écran du terminal.
  • 2 (STDERR) : La sortie d'erreur standard, également connectée à l'écran du terminal, mais séparée de pour permettre des redirections indépendantes (ex: afficher le résultat à l'écran mais enregistrer les erreurs dans un fichier).
Importance des Descripteurs de Fichiers :
  • Quotas et fuites de descripteurs : Les systèmes ont une limite sur le nombre de fichiers qu'un processus peut ouvrir (visible avec ulimit -n). Si un programme n'appelle pas close() pour libérer ses descripteurs, il peut atteindre cette limite et empêcher d'autres opérations, menant à une "fuite de descripteurs" (file descriptor leak).
  • Redirections : Le shell (bash, zsh, etc.) utilise la manipulation des FDs pour implémenter les redirections. Quand vous tapez ls > fichier.txt, le shell ferme le FD 1 () et le remplace par un FD pointant vers fichier.txt. Le programme ls écrit dans le FD 1 sans savoir que ce n'est plus l'écran, mais un fichier.

1.13 Entrées/Sorties Bas Niveau (Appels Système) vs. Haut Niveau (Bibliothèque Standard C)

En programmation système, il existe deux approches principales pour les opérations d'E/S : les appels système directs et les fonctions de la bibliothèque standard C.

Bibliothèque Standard C (std.io) - Haut Niveau

Les fonctions comme fopen(), fread(), fwrite(), fprintf() sont des fonctions de haut niveau qui manipulent des flux ( Stream). La différence majeure réside dans la bufferisation :

  • Bufferisation : La bibliothèque standard C (libc) maintient des tampons (buffers) en espace utilisateur. Les données ne sont pas écrites immédiatement sur le disque à chaque appel fprintf() ou fwrite(). Elles sont d'abord stockées dans ces tampons. L'écriture réelle sur le périphérique sous-jacent ne se déclenche que lorsque le tampon est plein, un caractère de nouvelle ligne \n est rencontré (pour les terminaux), ou fflush() est appelé.
  • Objectif : Minimiser le nombre d'appels système (qui sont coûteux car ils impliquent une commutation de mode utilisateur vers noyau) pour améliorer les performances, surtout sur de petits volumes de données.
  • Inconvénients : Moins de contrôle fin, peut causer des problèmes de synchronisation si les données ne sont pas explicitement vidées.
#include <stdio.h>

int main() {
    FILE *fp = fopen("mon_fichier.txt", "w");
    fprintf(fp, "Hello "); // Peut rester en tampon
    fprintf(fp, "world!\n"); // Force potentiellement l'écriture
    fclose(fp);
    return 0;
}
Appels Système (Syscalls) - Bas Niveau

Les fonctions comme open(), read(), write(), close() sont des appels système de bas niveau. Elles manipulent directement des Descripteurs de Fichiers (des entiers de type int).

  • Non-bufferisé : Chaque appel à write() (sauf si le noyau bufferise pour son propre compte) déclenche immédiatement une commutation de mode (User Kernel) et une tentative d'écriture sur le périphérique.
  • Objectif : Contrôle précis sur les opérations d'E/S (essentiel pour les pilotes de périphériques, les sockets réseau ou les fichiers spéciaux), atomicité des opérations, ou manipulation de très gros blocs de données où la bufferisation de libc pourrait ne pas être optimale.
  • Inconvénients : Plus d'appels système peuvent signifier des performances moindres pour de petites écritures fréquentes.

Les fichiers d'en-tête requis pour les appels système de bas niveau sont :

  • #include <sys/types.h> : Définit les types système de base (ex: ssize_t, off_t).
  • #include <sys/stat.h> : Fournit les fonctions et structures pour gérer les attributs de fichiers.
  • #include <fcntl.h> : Déclare les constantes et fonctions pour l'ouverture et le contrôle des fichiers.
  • #include <unistd.h> : Regroupe les appels système POSIX (lecture, écriture, fermeture, etc.).
Primitives Fondamentales des E/S Bas Niveau :
open() : Ouverture ou Création de Fichiers

Transforme un chemin de fichier en un descripteur de fichier (). Retourne le FD (entier positif) en cas de succès, -1 en cas d'erreur (et initialise errno).

int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode); // Pour O_CREAT
  • pathname : Chemin vers le fichier à ouvrir ou créer.
  • flags : Mode d'accès et options, combinables avec l'opérateur | (OU binaire). Exemples :
    • O_RDONLY : Lecture seule.
    • O_WRONLY : Écriture seule.
    • O_RDWR : Lecture et écriture.
    • O_TRUNC : Tronque (vide) le fichier s'il existe et est ouvert en écriture.
    • O_APPEND : Place le curseur à la fin du fichier avant chaque écriture (utile pour les logs).
    • O_CREAT : Crée le fichier s'il n'existe pas. Nécessite le troisième argument mode pour définir les permissions.
    • O_EXCL : Utilisé avec O_CREAT, garantit que le fichier n'existe pas. Si le fichier existe, open() échoue.
  • mode : Permissions du fichier, spécifiées en octal (ex: 0644 pour ). N'est utilisé que si O_CREAT est spécifié.
int fd = open("log.txt", O_WRONLY | O_CREAT | O_APPEND, 0644);
if (fd == -1) {
    perror("Erreur open");
    // Gérer l'erreur
}
read() : Lecture de Données

Lit des données brutes depuis le fichier associé à fd vers un tampon mémoire.

ssize_t read(int fd, void *buf, size_t count);
  • fd : Le descripteur de fichier obtenu via open().
  • buf : Pointeur vers la zone mémoire (tampon) où les données lues seront stockées.
  • count : Nombre maximal d'octets à lire.
  • Retour () :
    • : Nombre d'octets réellement lus (peut être inférieur à count si la fin du fichier est atteinte ou si une interruption se produit).
    • 0 : Fin de fichier () atteinte.
    • -1 : Erreur.
write() : Écriture de Données

Copie des données depuis une zone mémoire vers le fichier associé à fd.

ssize_t write(int fd, const void *buf, size_t count);
  • fd : Le descripteur de fichier.
  • buf : Pointeur vers la zone mémoire source contenant les données à écrire.
  • count : Nombre d'octets à écrire.
  • Retour () :
    • : Nombre d'octets réellement écrits (peut être inférieur à count si une erreur survient, si pas assez d'espace, ou si une interruption se produit).
    • -1 : Erreur.
close() : Fermeture du Descripteur de Fichier

Libère le descripteur de fichier dans le noyau. C'est essentiel pour éviter les fuites de ressources.

int close(int fd);
  • fd : Le descripteur de fichier à fermer.
  • Retour : 0 en cas de succès, -1 en cas d'erreur.
Exemple : Copie de fichier (cp simplifié)

Cet exemple illustre une copie de fichier en utilisant les appels système de bas niveau, avec une gestion simple des erreurs. Il met en évidence la boucle classique de lecture/écriture.

#include <fcntl.h>      // Pour open(), O_RDONLY, O_WRONLY, O_CREAT, O_TRUNC
#include <unistd.h>     // Pour read(), write(), close()
#include <stdio.h>      // Pour perror()
#include <stdlib.h>     // Pour exit()

#define BUFFER_SIZE 1024

int main() {
    int fd_src, fd_dst;
    ssize_t n_read, n_write;
    char buffer[BUFFER_SIZE];

    // 1. Ouverture du fichier source en lecture seule
    fd_src = open("source.txt", O_RDONLY);
    if (fd_src == -1) {
        perror("Erreur ouverture source");
        exit(EXIT_FAILURE);
    }

    // 2. Ouverture/Création du fichier destination en écriture seule.
    //    Si dest.txt existe, il est tronqué. Permissions 0644 (rw-r--r--).
    fd_dst = open("dest.txt", O_WRONLY | O_CREAT | O_TRUNC, 0644);
    if (fd_dst == -1) {
        perror("Erreur ouverture destination");
        close(fd_src); // Nettoyage : fermer le fichier source avant de quitter
        exit(EXIT_FAILURE);
    }

    // 3. Boucle de copie : lire par blocs et écrire
    while ((n_read = read(fd_src, buffer, BUFFER_SIZE)) > 0) {
        n_write = write(fd_dst, buffer, n_read); // Écrire exactement ce qui a été lu

        if (n_write != n_read) {
            perror("Erreur d'écriture : tous les octets n'ont pas été écrits");
            close(fd_src);
            close(fd_dst);
            exit(EXIT_FAILURE);
        }
    }

    // Vérifier si la boucle s'est terminée à cause d'une erreur de lecture
    if (n_read == -1) {
        perror("Erreur de lecture lors de la copie");
        close(fd_src);
        close(fd_dst);
        exit(EXIT_FAILURE);
    }

    // 4. Fermeture propre des descripteurs
    close(fd_src);
    close(fd_dst);

    return EXIT_SUCCESS;
}
lseek() : Déplacement dans le Fichier (Accès Aléatoire)

Contrairement aux flux (streams) souvent séquentiels, les descripteurs de fichiers permettent un déplacement arbitraire du "curseur" interne (offset). Cela permet l'accès aléatoire aux données.

off_t lseek(int fd, off_t offset, int whence);
  • fd : Le descripteur de fichier.
  • offset : Le décalage en octets. Peut être positif (avancer) ou négatif (reculer).
  • whence : Le point de référence pour le décalage :
    • SEEK_SET : offset est la position absolue à partir du début du fichier.
    • SEEK_CUR : offset est relatif à la position actuelle du curseur.
    • SEEK_END : offset est relatif à la fin du fichier (souvent utilisé avec un offset de 0 pour aller à la fin, ou négatif pour remonter).
  • Retour () : La nouvelle position du curseur (en octets depuis le début du fichier) en cas de succès, ou -1 en cas d'erreur.
Exemples d'utilisation de lseek() :
// Aller au 10ème octet du fichier (position 9, car 0-indexé)
lseek(fd, 9, SEEK_SET); // Ou 10 si on considère 1er octet comme 1

// Avancer de 5 octets depuis la position actuelle
lseek(fd, 5, SEEK_CUR);

// Aller à la toute fin du fichier
lseek(fd, 0, SEEK_END);

// Récupérer la taille totale d'un fichier sans le lire :
// Déplacer le curseur à la fin, puis le ramener à la position d'origine si nécessaire
off_t file_size = lseek(fd, 0, SEEK_END);
// Après cela, le curseur est à la fin du fichier. Si on veut continuer à lire/écrire
// depuis le début ou la position précédente, il faut appeler lseek à nouveau.

1.14 Duplication de Descripteurs de Fichiers : dup() et dup2()

La capacité à manipuler et dupliquer les descripteurs de fichiers est fondamentale pour la redirection d'E/S et la communication inter-processus sous Unix.

Chaque processus possède sa propre table de descripteurs de fichiers. Par défaut, pour un programme lancé via le shell :

  • FD 0 () pointe vers le clavier.
  • FD 1 () pointe vers l'écran (terminal).
  • FD 2 () pointe également vers l'écran (terminal).

Une redirection consiste à modifier cette table interne pour qu'un FD (par exemple, FD 1) pointe vers une autre ressource (un fichier, un pipe, une socket réseau), sans que le programme n'ait à le savoir ou à le modifier.

Les fonctions dup() et dup2(), définies dans <unistd.h>, permettent de dupliquer ces descripteurs.

dup() : Dupliquer un Descripteur

Crée une copie d'un descripteur de fichier existant. Le nouveau descripteur pointera vers la même ouverture de fichier (même inode, même offset de lecture/écriture, mêmes flags d'état).

int dup(int oldfd);
  • oldfd : Le descripteur de fichier existant à dupliquer.
  • Retour : Retourne le plus petit numéro de descripteur libre disponible pour la copie en cas de succès, -1 en cas d'erreur.
int old_stdout = dup(1); // Sauvegarde STDOUT original
// ... modifier STDOUT ...
dup2(old_stdout, 1); // Restaurer STDOUT original
dup2() : Dupliquer vers un Descripteur Spécifique

C'est la fonction la plus utilisée pour la redirection. Elle permet de forcer la copie du descripteur oldfd vers un numéro de descripteur spécifique newfd.

int dup2(int oldfd, int newfd);
  • oldfd : Le descripteur existant à dupliquer.
  • newfd : Le numéro du descripteur que l'on souhaite réaffecter.
  • Comportement :
    • Si newfd est déjà ouvert, dup2() le ferme silencieusement avant de le réaffecter à oldfd.
    • Il copie la sémantique de oldfd (l'ouverture de fichier sous-jacente) vers newfd.
    • Après l'appel, oldfd et newfd pointent vers la même structure de fichier (même inode, même offset).
  • Retour : Retourne newfd en cas de succès, -1 en cas d'erreur.

Pourquoi dup() et dup2() sont essentiels :

  • Construction des shells (redirections >, <).
  • Implémentation des pipelines Unix (commande1 | commande2).
  • Mise en place de la communication inter-processus (IPC) via des pipes ou des sockets.
Application pour les Réseaux (Exemple Rudimentaire de Serveur)

Les fonctions dup2() sont cruciales en programmation réseau. Pour un serveur qui doit exécuter une commande et rediriger ses E/S vers un client distant (comme Telnet ou SSH) :

#include <unistd.h> // Pour dup2(), exec*()
#include <sys/socket.h> // Pour les sockets (non montré mais implicite)

// Supposons que client_sock est un descripteur de socket connecté au client
int client_sock = accept(...); // Supposons qu'une connexion a été acceptée

// 1. Rediriger l'entrée standard (FD 0) pour qu'elle vienne de la socket client
dup2(client_sock, 0); // L'entrée du programme désormais lue depuis le réseau

// 2. Rediriger la sortie standard (FD 1) pour qu'elle parte vers la socket client
dup2(client_sock, 1); // La sortie du programme désormais écrite vers le réseau

// 3. Rediriger la sortie d'erreur (FD 2) pour qu'elle parte vers la socket client
dup2(client_sock, 2); // Les erreurs du programme désormais écrites vers le réseau

// Optionnel mais recommandé : fermer le descripteur original de la socket,
// car il est maintenant dupliqué sur 0, 1 et 2.
close(client_sock);

// 4. Lancer un shell ou une commande.
// Le shell (ou le programme) s'exécutera et "croira" travailler avec un terminal local.
// Tout printf() ira sur la socket, tout scanf() lira de la socket.
execl("/bin/sh", "sh", NULL);
perror("exec failed"); // Si exec échoue
exit(EXIT_FAILURE);

2. Gestion des Processus et des Signaux

2.1 Piliers de la Gestion des Processus

La gestion des processus est fondamentale pour la stabilité et la performance d'un système d'exploitation. Elle concerne la naissance, l'exécution, la duplication et la terminaison des programmes.

2.2 Qu'est-ce qu'un Processus ?

Un processus est une instance d'un programme en cours d'exécution. Il représente une entité dynamique qui possède ses propres ressources, ce qui permet à plusieurs programmes de s'exécuter de manière concurrente sur un même système. Un processus se distingue d'un programme (fichier binaire statique) par son caractère dynamique et ses ressources allouées.

Caractéristiques clés d'un processus :

  • Espace d'adressage virtuel : Chaque processus dispose de son propre espace mémoire virtuel, isolé des autres, assurant l'indépendance et la protection des données.
  • Registres CPU et Contexte d'exécution : Le processus garde son propre état du processeur (valeurs des registres, compteur ordinal) et d'autres informations pour reprendre l'exécution après une interruption.
  • Identifiant de Processus (PID) : Un numéro unique (entier positif) attribué à chaque processus par le système pour le gérer et le suivre.
  • Hiérarchie de processus : Les processus sont souvent organisés en une structure parent-enfant. Un processus parent crée des processus enfants, qui peuvent à leur tour créer les leurs.
  • Descripteurs de fichiers : Chaque processus a sa propre table de descripteurs de fichiers ouverts.
Comparaison : Programme vs Processus
Concept Programme Processus
Nature Statique (fichier binaire exécutable sur le disque, ex: .exe, binaire ELF). Dynamique (entité active en mémoire RAM, avec ses ressources et son état d'exécution).
Localisation Disque Dur (stockage secondaire). Mémoire Vive (RAM) + Registres CPU (lors de son exécution).
Durée de vie Permanente (tant qu'il n'est pas effacé). Temporaire (de sa création à sa terminaison).
Exemple Le fichier /bin/ls. Lorsque vous saisissez ls dans le terminal, une instance de ce programme est exécutée en tant que processus.

2.3 Structure de l'Espace d'Adressage d'un Processus

Lorsqu'un processus démarre, le système d'exploitation lui alloue un espace d'adressage virtuel, structuré en plusieurs segments logiques :

  • Segment de texte (Code) :
    • Contient les instructions machines (le code binaire) du programme.
    • Souvent marqué en lecture seule pour éviter les modifications accidentelles (sécurité) et permettre le partage entre plusieurs processus exécutant le même programme.
  • Segment de données (Data) :
    • Contient les variables globales et statiques.
    • Divisé en deux sous-sections :
      • Data (initialisées) : Variables globales ou statiques qui sont initialisées avec une valeur non nulle dans le code (ex: int y = 10;).
      • BSS (Block Started by Symbol - non initialisées) : Variables globales ou statiques qui ne sont pas explicitement initialisées ou initialisées à zéro (ex: int x;). Ces variables sont initialisées à zéro par le système au chargement du programme.
  • Tas (Heap) :
    • Zone de mémoire pour l'allocation dynamique de mémoire durant l'exécution (fonctions malloc(), calloc(), realloc() en C, new en C++).
    • Grandit généralement vers les adresses mémoires plus hautes.
    • Géré explicitement par le programmeur (ou via un ramasse-miettes).
  • Pile (Stack) :
    • Zone de mémoire utilisée pour stocker les variables locales des fonctions, les paramètres de fonctions, les adresses de retour d'appel de fonction, et sauvegarder les registres lors des appels de fonctions.
    • Fonctionne sur le principe LIFO (Last In, First Out).
    • Grandit généralement vers les adresses mémoires plus basses (sur la plupart des architectures).
    • Gérée automatiquement par le compilateur et le système.

2.4 Process Control Block (PCB) et Commutation de Contexte

Le système d'exploitation gère chaque processus via une structure de données interne appelée Process Control Block (PCB), qui agit comme la "carte d'identité" du processus. Le PCB contient toutes les informations nécessaires à la gestion d'un processus :

  • PID (Process ID) : Identifiant unique du processus.
  • PPID (Parent PID) : Identifiant du processus parent.
  • Compteur Ordinal (Program Counter - PC) : Adresse de la prochaine instruction à exécuter.
  • Registres du processeur : État des registres généraux, registres de segments, etc.
  • État du processus : Ready, Running, Waiting/Blocked, Terminated, etc.
  • Informations d'ordonnancement : Priorité, temps CPU accumulé.
  • Descripteurs de fichiers : Liste des fichiers ouverts par le processus.
  • Informations de gestion mémoire : Pointeur vers la table des pages ou le PCB.
  • Informations de comptabilité : Temps CPU, limites de ressources.
Commutation de Contexte (Context Switching)

Pour donner l'illusion que plusieurs programmes s'exécutent "simultanément" (multitâche) sur un processeur qui ne peut exécuter qu'une seule instruction à la fois, le système d'exploitation effectue des commutations de contexte. Une commutation de contexte est l'opération de sauvegarde de l'état d'un processus en cours d'exécution et de restauration de l'état d'un autre processus, permettant ainsi au CPU de basculer entre eux.

Le contexte est l'ensemble des informations instantanées nécessaires pour reprendre l'exécution d'un processus exactement là où elle a été interrompue. Cela inclut :

  • L'état du processus (Running, Ready, Blocked).
  • Les valeurs des registres CPU.
  • Le compteur ordinal.
  • L'entrée du processus dans la table des processus du noyau.
  • L'état de son espace d'adressage virtuel (pile, zones de code et de données).
  • L'état de ses descripteurs de fichiers ouverts.

NB : Le noyau lui-même et ses variables ne font partie du contexte d'aucun processus. Le noyau exécute son propre code en mode privilégié, orthogonalement aux processus utilisateur.

2.5 États d'un Processus et Transitions

Un processus ne démarre pas et ne se termine pas instantanément. Il passe par différents états au cours de sa vie, en fonction des événements externes et des décisions de l'ordonnanceur du noyau.

États Principaux :
  • Processus Prêt (Ready) :
    • Le processus est chargé en mémoire et a toutes les ressources nécessaires pour s'exécuter, à l'exception du processeur.
    • Il est en attente dans une file gérée par l'ordonnanceur.
    • Transition vers cet état : Après sa création, ou lorsqu'un processus bloqué est débloqué.
  • Processus Élu (Running) :
    • Le processus est en cours d'exécution sur le CPU. C'est l'état actif.
    • Il peut quitter cet état pour plusieurs raisons :
      1. Il termine son exécution (volontairement avec exit() ou naturellement).
      2. Il est interrompu par le système d'exploitation (préemption).
      3. Il demande une ressource indisponible (E/S, sémaphore) et passe à l'état bloqué.
  • Processus Bloqué (Blocked / Waiting) :
    • Le processus ne peut pas s'exécuter car il attend un événement externe (lecture disque, réception d'un paquet réseau, frappe au clavier, attente d'un enfant, etc.).
    • Il est mis en attente jusqu'à ce que l'événement se produise.
    • Une fois l'événement déclenché, le processus retourne à l'état Prêt (il ne reprend pas directement le CPU).
  • Processus Nouveau (New) : Le processus est en cours de création.
  • Processus Terminé (Terminated) : Le processus a fini son exécution mais son entrée dans la table des processus n'a pas encore été libérée par son parent. Il peut être dans un état "zombie".
Transitions entre États :
Transition Direction Cause / Explication
Admission New → Ready Le système accepte le processus et le met en file d'attente pour l'exécution.
Élection (Dispatch) Ready → Running L'ordonnanceur choisit ce processus pour l'allouer au CPU.
Réquisition (Timeout) Running → Ready Le processus a épuisé son temps de parole (quantum) ou un processus plus prioritaire est arrivé. C'est le principe du multitâche préemptif.
Appel Bloquant Running → Blocked Le processus effectue un appel système qui nécessite une ressource externe lente (ex: scanf(), read() sur disque, wait()). Le CPU n'attend pas et passe à un autre processus.
Déblocage (Wakeup) Blocked → Ready L'événement attendu s'est produit (touche pressée, données disponibles sur disque ou réseau). Le processus retourne dans la file d'attente des processus prêts (il ne reprend pas directement le CPU).
Sortie / Terminaison Running → Terminated Le processus appelle exit() ou reçoit un signal fatal (ex: Segmentation Fault).

2.6 Ordonnancement des Processus

L'ordonnancement (scheduling) est la fonction du système d'exploitation qui gère le partage du CPU entre les divers processus en attente d'exécution. Les objectifs de l'ordonnanceur varient selon le point de vue :

  • Point de vue Système :
    • Maximiser l'utilisation du CPU.
    • Maximiser le débit (nombre de tâches complétées par unité de temps).
  • Point de vue Utilisateur :
    • Minimiser la latence.
    • Minimiser le temps de complétion d'une tâche (durée entre l'arrivée et la fin).
    • Minimiser le temps de réponse (durée entre l'arrivée et le début de son exécution).
    • Minimiser l'attente (durée passée à attendre le CPU).
Ordonnancement Préemptif vs Non Préemptif

La capacité de l'ordonnanceur à interrompre un processus en cours d'exécution détermine son type :

  • Ordonnancement Non Préemptif :
    • Interdiction de réquisition : Une fois qu'un processus se voit allouer le CPU, il le conserve jusqu'à ce qu'il termine son exécution ou se bloque volontairement. Le système d'exploitation ne peut pas l'interrompre de force.
    • Implémentation : Plus simple à implémenter.
    • Utilisation : Historiquement dans certains systèmes batch, ou dans des systèmes temps réel et applications critiques où les interruptions inattendues peuvent être problématiques.
    • Inconvénients :
      • Temps de réponse élevé : Un processus long peut monopoliser le CPU, faisant attendre très longtemps les autres processes, notamment les processus interactifs.
      • Manque de réactivité : Le système peut sembler figé.
  • Ordonnancement Préemptif :
    • Interruption possible : Le système d'exploitation peut interrompre un processus en cours d'exécution à tout moment (par exemple, après un "quantum" de temps alloué, ou si un processus de plus haute priorité devient prêt).
    • Implémentation : Plus complexe, nécessite la gestion des interruptions et des changements de contexte.
    • Utilisation : Majorité des systèmes d'exploitation modernes (Unix, Linux, Windows, macOS) qui gèrent le multitâche interactif.
    • Avantages :
      • Réactivité élevée : Permet de donner le CPU rapidement aux processus (interactifs ou prioritaires) qui en ont besoin.
      • Partage équitable : Assure un partage plus équitable des ressources CPU.
    • Inconvénients :
      • Surcharge (overhead) : Les interruptions fréquentes et les changements de contexte augmentent la charge de travail du système.
      • Complexité : Plus difficile à implémenter et à gérer.
Comparaison Ordonnancement Non Préemptif vs Préemptif :
Critère Ordonnancement Non Préemptif Ordonnancement Préemptif
Interruption Impossible (le processus garde le CPU jusqu'à ce qu'il se bloque ou termine) Possible (le système peut interrompre un processus en cours d'exécution)
Réactivité Faible, surtout avec des processus longs Élevée, meilleure pour les systèmes interactifs
Complexité Plus simple à implémenter Plus complexe à gérer (nécessite gestion des interruptions et contextes)
Utilisation Systèmes temps réel (parfois), applications critiques spécifiques Systèmes d'exploitation multitâches généraux, interactifs
Famine (Starvation) Possible si un processus long bloque tous les autres indéfiniment. Peut être évité avec des algorithmes adaptés (ex: vieillissement des priorités).
Surcharge (Overhead) Faible, car peu de changements de contexte. Élevée, en raison des interruptions et changements de contexte fréquents.
Politiques d'Ordonnancement Courantes :

Ces politiques sont des algorithmes qui décident quel processus, parmi ceux qui sont prêts, obtiendra le CPU.

  • FIFO (First-Come, First-Serve - Premier Arrivé, Premier Servi) :
    • Politique non préemptive.
    • Les processus sont exécutés dans l'ordre de leur arrivée dans la file d'attente.
    • Simple, mais peut entraîner de longs temps d'attente pour les petites tâches si une longue tâche arrive en premier.
  • SJF (Shortest Job First - Plus Court d'abord) :
    • Non préemptif.
    • Le CPU est alloué au processus dont le temps d'exécution estimé est le plus court.
    • Optimise le temps d'attente moyen, mais difficile à implémenter car le temps d'exécution est rarement connu d'avance.
  • SRTF (Shortest Remaining Time First - Temps Restant le Plus Court d'abord) :
    • Version préemptive du SJF.
    • L'ordonnanceur choisit le processus dont le temps d'exécution restant est le plus court. Si un nouveau processus arrive avec un temps restant plus court que le processus en cours, ce dernier est préempté.
  • RR (Round Robin - Par Tourniquet) :
    • Politique préemptive par excellence pour les systèmes multitâches.
    • Chaque processus se voit allouer une petite tranche de temps CPU fixe appelée quantum.
    • Après le quantum, le processus est préempté et remis à la fin de la file d'attente des processus prêts.
    • Idéal pour les systèmes interactifs car il assure une bonne réactivité.
  • PS (Priority Scheduling - Par Priorités Constantes) :
    • Chaque processus se voit attribuer un niveau de priorité.
    • L'ordonnanceur choisit le processus de plus haute priorité prêt à s'exécuter.
    • Peut être préemptif ou non préemptif.
    • Risque de famine (starvation) si des processus de basse priorité ne sont jamais exécutés à cause d'un flux constant de processus de haute priorité. Le "vieillissement" (aging) est une technique pour augmenter la priorité des processus en attente.

2.7 Cycle de Vie d'un Processus sous Unix : fork() et exec()

Sous Unix/Linux, la création d'un nouveau programme ne se fait pas de A à Z en une seule étape, mais en un processus en deux phases distinctes :

  1. Duplication (fork()) : Un processus existant (le parent) se clone pour créer un nouveau processus (le fils) qui est une copie presque exacte.
  2. Mutation (exec()) : Le processus fils remplace son propre code et ses données par un nouveau programme à exécuter.
La fonction fork()

La fonction fork() est l'appel système central pour la création de processus. Elle ne lance pas un nouveau programme, mais crée une copie exacte du processus appelant.

#include <unistd.h>   // Pour fork()
#include <sys/types.h> // Pour pid_t

pid_t fork(void);
  • Mécanisme : Le processus appelant est le processus parent. Le nouveau processus créé est le processus fils (child).
  • Ce qui est copié du parent au fils :
    • L'intégralité de l'espace d'adressage virtuel (segments de Code, Données, Tas, Pile). Conceptuellement, le fils est un duplicata du parent.
    • Les descripteurs de fichiers ouverts (parent et fils partagent les mêmes ouvertures de fichiers, chacun ayant sa propre copie du descripteur).
    • L'environnement du processus.
  • Ce qui change / est unique pour le fils :
    • Le PID (Process ID) : Le fils reçoit un nouveau PID unique.
    • Le PPID (Parent Process ID) : Le fils a le PID de son parent comme PPID.
    • Le compteur de temps CPU (remis à zéro pour le fils).
    • La valeur de retour de fork().
  • Valeurs de retour de fork() : fork() est unique car elle retourne deux fois (une fois dans le parent, une fois dans le fils) avec des valeurs différentes :
    • Dans le processus parent : Retourne le PID du processus enfant créé.
    • Dans le processus enfant : Retourne 0.
    • En cas d'erreur : Retourne -1 (errno est défini). Cela se produit généralement si le système manque de ressources (PIDs, mémoire).
Fonctions d'Information sur les Processus :
  • pid_t getpid(void); : Retourne le PID du processus appelant.
  • pid_t getppid(void); : Retourne le PPID (PID du parent) du processus appelant.
  • uid_t getuid(void); : Retourne l'UID (User ID) du processus appelant.
  • gid_t getgid(void); : Retourne le GID (Group ID) du processus appelant.
Exemple d'utilisation de fork() :

Ce programme illustre comment fork() crée deux processus distincts qui exécutent la même suite d'instructions, mais divergent en fonction de la valeur de retour de fork().

#include <stdlib.h>  // Pour EXIT_SUCCESS et EXIT_FAILURE
#include <stdio.h>   // Pour fprintf()
#include <unistd.h>  // Pour fork(), getpid(), getppid()
#include <errno.h>   // Pour perror() et errno
#include <sys/types.h> // Pour pid_t

int main (void)
{
    pid_t pid_fils = fork(); // Création d'un processus

    if (pid_fils == -1) {
        // Cette branche est exécutée UNIQUEMENT SI fork() échoue
        fprintf(stderr, "fork() impossible, errno=%d\n", errno);
        perror("Probleme fork"); // Afficher le message d'erreur système
        return EXIT_FAILURE;
    }

    if (pid_fils == 0) {
        // Cette branche est exécutée UNIQUEMENT par le processus fils
        fprintf(stdout, "Fils : PID=%d, PPID=%d\n", (int) getpid(), (int) getppid());
        printf("Le fils continue son execution ici.\n");
        // Le fils peut maintenant faire des choses différentes, comme execve
        return EXIT_SUCCESS;
    }
    else {
        // Cette branche est exécutée UNIQUEMENT par le processus père
        fprintf(stdout, "Pere : PID=%d, PID fils=%d\n", (int) getpid(), (int)pid_fils);
        printf("Le pere continue son execution ici.\n");
        // Le père peut attendre le fils ou continuer en parallèle
        return EXIT_SUCCESS;
    }
}

Exécution et sortie typique :

ghostSUESKTOP-SSDICMD:~/cours_prog_system/chap2$ ./exemple1
Pere : PID=1061, PID fils=1062
Le pere continue son execution ici.
Fils : PID=1062, PPID=1061
Le fils continue son execution ici.

Notez que l'ordre des messages (parent puis fils ou vice-versa) peut varier car les deux processus s'exécutent de manière concurrente.

Après fork(), les deux processus sont identiques en apparence, partageant le même état. L'étape suivante, exec(), est nécessaire pour que le processus fils devienne un programme différent.

2.8 Terminaison des Processus : exit() et Statuts de Retour

Un processus se termine normalement de deux manières principales :

  • En atteignant la fin de la fonction main().
  • En appelant explicitement la fonction exit().
La fonction exit()
#include <stdlib.h> potentiellement
void exit(int status);
  • Fonction : Provoque la terminaison immédiate et normale du processus appelant. Elle libère les ressources allouées au processus (mémoire, descripteurs de fichiers ouverts).
  • Statut : Un entier (entre 0 et 255) est renvoyé au processus père. Par convention :
    • 0 ou EXIT_SUCCESS : Indique un succès.
    • Toute autre valeur (ex: EXIT_FAILURE) : Indique une erreur ou un échec.
  • Signal SIGCHLD : Lorsque le fils se termine, le noyau envoie un signal à son processus père. Le fils ne disparaît pas immédiatement. Il reste dans un état intermédiaire (zombie) pour permettre à son père de récupérer son statut de sortie.
  • Différence avec return de main() : exit() termine le programme où qu'il soit appelé dans le code, sans revenir à l'appelant. return de main() équivaut à exit() avec la valeur de retour spécifiée.
Exemple d'utilisation de exit() :
#include <stdlib.h>
#include <stdio.h>

void fonction_critique() {
    printf("Erreur fatale détectée !\n");
    exit(EXIT_FAILURE); // Termine TOUT le processus immédiatement
}

int main() {
    fonction_critique();
    printf("Cette ligne ne sera jamais affichée.\n"); // Cette ligne n'est jamais atteinte
    return 0; // Ce return n'est jamais exécuté
}

Exécution :

ghost@DESKTOP-S9DICMD:~/cours_prog_system/chap2$ ./exemple3
Erreur fatale détectée !

2.9 Pathologies des Processus : Zombie et Orphelin

Dans le modèle Unix, la gestion des processus fils par le processus parent est une responsabilité cruciale pour la bonne santé du système.

Processus Zombie (défunt)
  • Définition : Un processus zombie (ou defunct) est un processus qui a terminé son exécution (il a appelé exit() ou terminé sa fonction main()), mais qui occupe encore une entrée dans la table des processus du système.
  • Cause : Le noyau conserve le PID et le statut de terminaison du fils en attendant que le processus père vienne les récupérer via un appel système wait() ou waitpid(). Si le père est occupé, mal programmé ou ignore son devoir, le fils reste un zombie.
  • Danger : Un zombie lui-même ne consomme ni CPU, ni RAM (son code et ses données sont libérés). Cependant, il consomme une entrée dans la table des processus (un PID). Le nombre total de PIDs est limité par le système. Une accumulation massive de zombies (par exemple, par un serveur mal codé qui fork pour chaque requête mais n'appelle jamais wait()) peut épuiser les PIDs disponibles, empêchant la création de nouveaux processus et bloquant le système.
  • Détection : Dans la commande ps -el ou top, il apparaît avec l'état Z et la mention <defunct>.
Processus Orphelin
  • Définition : Un processus orphelin est un processus enfant dont le processus père s'est terminé avant lui.
  • Le Problème : Si le père est mort, il n'y a plus personne pour récupérer le code de retour du fils quand ce dernier mourra. Cela pourrait entraîner la création de zombies "éternels".
  • La Solution du Système (Adoption) : Le noyau détecte automatiquement la mort du processus parent. Il réassigne alors le processus fils orphelin au processus init (PID 1) ou systemd sur les systèmes modernes.
  • Rôle de init (ou systemd) : Les processus init/systemd sont spécialement conçus pour exécuter des appels wait() en permanence sur leurs processus enfants (y compris ceux qu'ils ont adoptés). Ainsi, un orphelin ne reste jamais zombie très longtemps. init est le "grand père de tous les processus", il est le garant de la collecte des statuts de sortie des orphelins.
Outils de Nettoyage : wait() et waitpid()

Pour éviter les processus zombies, un processus père doit attendre la terminaison de ses fils. Cela permet deux choses :

  1. Synchroniser l'exécution : Le père peut attendre que le fils ait terminé certaines tâches.
  2. Récupérer l'état du fils : Obtenir le code de retour du fils (succès/échec) et ainsi libérer son entrée de la table des processus.
Primitive wait()

Bloque le processus père jusqu'à ce que l'un de ses fils se termine. Débloqué, il recueille le statut de terminaison du fils et libère son entrée.

#include <sys/wait.h> // Pour wait(), WIFEXITED, WEXITSTATUS, WIFSIGNALED

pid_t wait(int *status);
  • status : Pointeur vers un entier où le système stockera des informations codées sur la cause de la mort du fils (succès, erreur, tué par un signal, etc.). Si NULL, le statut est ignoré.
  • Retour :
    • Le PID du fils qui vient de mourir en cas de succès.
    • -1 en cas d'erreur (ex: pas de fils à attendre) ou d'interruption par un signal.
  • Décodage du status : L'entier status est un champ de bits. Utilisez des macros pour l'interpréter :
    • WIFEXITED(status) : Vrai si le fils a terminé normalement (via exit() ou return de main()).
    • WEXITSTATUS(status) : Si WIFEXITED est vrai, extrait le code de retour passé à exit().
    • WIFSIGNALED(status) : Vrai si le fils a été terminé par un signal non intercepté.
    • WTERMSIG(status) : Si WIFSIGNALED est vrai, retourne le numéro du signal qui a tué le fils.
int status;
pid_t child_pid = wait(&status);

if (WIFEXITED(status)) {
    printf("Fils %d terminé avec le statut %d\n", child_pid, WEXITSTATUS(status));
} else if (WIFSIGNALED(status)) {
    printf("Fils %d tué par le signal %d\n", child_pid, WTERMSIG(status));
}
Primitive waitpid()

Plus flexible que wait(), elle permet d'attendre un fils spécifique ou d'attendre sans bloquer.

#include <sys/wait.h>

pid_t waitpid(pid_t pid, int *status, int options);
  • pid : Le type de processus fils à attendre :
    • : Attend le fils ayant ce PID précis.
    • -1 : Attend n'importe quel fils (comportement comme wait()).
    • 0 : Attend n'importe quel fils dans le même groupe de processus que le père.
    • : Attend n'importe quel fils dont le groupe de processus est égal à la valeur absolue de pid.
  • status : Identique à wait().
  • options : Modifie le comportement bloquant :
    • 0 : Comportement bloquant standard (bloque le père jusqu'à ce qu'un fils corresponde aux critères pid et termine).
    • WNOHANG : Rend l'appel non bloquant. Si aucun fils ne correspond aux critères pid et n'a terminé, waitpid() retourne 0 immédiatement. Très utile pour les serveurs ou les programmes qui ne peuvent pas se permettre de rester bloqués en attendant un fils.
  • Retour :
    • Le PID du fils qui a terminé (si pid > 0 et correspond).
    • 0 si WNOHANG est spécifié et qu'aucun fils n'a terminé.
    • -1 en cas d'erreur.

2.10 Signaux : Interruption Asynchrone des Processus

Jusqu'à présent, nous avons surtout considéré un flux d'exécution linéaire. Les signaux brisent cette linéarité en fournissant un mécanisme de communication asynchrone entre le noyau (ou d'autres processus) et un processus en cours d'exécution.

Un signal est une interruption logicielle envoyée par le noyau (ou un autre processus) à un processus cible pour lui notifier un événement important.

  • Asynchrone : Le processus ne sait pas quand le signal va arriver. Il peut être en train de calculer, de lire un fichier, ou d'être endormi ; le signal l'interrompra immédiatement (si le signal peut être intercepté).
  • Comportement par défaut : Pour la plupart des signaux, si le programme n'a pas défini de gestionnaire spécifique, le noyau termine par défaut le processus.
Un signal peut servir à :
  • Interrompre un processus (ex: Ctrl+C).
  • Le terminer proprement ou de force.
  • Suspendre ou reprendre son exécution.
  • L'avertir qu'un événement externe important est survenu (ex: fin d'un processus fils - , expiration d'un timer - , changement de terminal - ).
Signaux Critiques pour la Gestion de Service :

Parmi la trentaine de signaux existants (liste complète avec kill -l ou man 7 signal), cinq sont fondamentaux :

Signal Nom Code Rôle et Analogie
SIGHUP Hangup 1 À l'origine, envoyé lorsqu'un terminal se déconnectait (hang up). Aujourd'hui, il est la convention standard pour demander aux démons (serveurs Apache, Nginx, etc.) de recharger leur configuration sans s'arrêter.
SIGINT Interrupt 2 L'interruption "polie". Déclenché par Ctrl+C. C'est l'utilisateur qui demande au programme d'arrêter ce qu'il fait. Le programme peut l'intercepter, effectuer un nettoyage avant de sortir, ou même l'ignorer.
SIGQUIT Quit 3 L'arrêt brutal avec trace. Déclenché par Ctrl+\. Similaire à mais plus sévère. Il force l'arrêt et demande au noyau de générer un fichier coredump pour le débogage (snapshot de la mémoire du processus au moment du crash).
SIGTERM Terminate 15 La demande d'arrêt. C'est le signal envoyé par défaut par la commande kill (ex: kill PID). C'est une demande formelle au processus de s'arrêter proprement, en sauvegardant les données et fermant les connexions. Le processus peut l'intercepter.
SIGKILL Kill 9 L'arrêt de mort immédiat. Ce n'est pas une demande, c'est un ordre direct du noyau. Le processus ne peut ni l'ignorer, ni l'intercepter. Il est utilisé comme dernier recours pour tuer un processus récalcitrant (ex: kill -9 PID).

2.11 Interception et Gestion des Signaux (Handling)

Un programme peut définir une stratégie pour réagir à un signal :

  1. Ignorer le signal : Le processus ne réagit pas (sauf pour et ).
  2. Intercepter le signal : Exécuter une fonction dédiée (un gestionnaire ou handler de signal) lorsque le signal est reçu.
  3. Laisser l'action par défaut : Le système d'exploitation applique son action par défaut (souvent, la terminaison du processus).
La primitive signal()

Bien que sigaction() soit la norme moderne, plus robuste et recommandée pour les applications sérieuses, signal() est plus simple pour comprendre le concept d'interception.

#include <signal.h>

typedef void (*sighandler_t)(int); // Type pour une fonction de gestionnaire de signal
sighandler_t signal(int signum, sighandler_t handler);
  • signum : Le numéro du signal à gérer (ex: , ).
  • handler :
    • La fonction à appeler lorsque le signal est reçu. Cette fonction doit prendre un int (le numéro du signal) en argument et ne rien retourner (void).
    • Deux macros spéciales :
      • SIG_IGN : Pour ignorer le signal.
      • SIG_DFL : Pour restaurer le comportement par défaut du signal.
  • Retour : Retourne le gestionnaire de signal précédent pour signum, ou SIG_ERR en cas d'erreur.
Exemple : Rendre un Processus "Immortel" (ou presque)

Ce programme démonstratif intercepte le signal (Ctrl+C). Il ne se terminera pas quand l'utilisateur appuie sur Ctrl+C, mais seulement avec kill -9.

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h> // Pour sleep(), getpid()
#include <signal.h> // Pour signal(), SIGINT

// Le gestionnaire de signal (Handler)
void attrap_signal(int sig) {
    printf("\n[Signal] Haha ! J'ai intercepté le signal %d (SIGINT).\n", sig);
    // On ne fait PAS de exit() ici, donc le programme reprend son cours normal
}

int main() {
    // 1. Installation du gestionnaire pour SIGINT
    // Quand SIGINT est reçu, la fonction attrap_signal sera exécutée
    signal(SIGINT, attrap_signal);

    printf("Essayez de faire Ctrl+C pour me tuer...\n");
    printf("Pour vraiment me tuer, ouvrez un terminal et tapez : kill -9 %d\n", getpid());

    // 2. Boucle infinie pour que le programme reste actif et attende les signaux
    while(1) {
        printf("Je travaille...\n");
        sleep(5); // Pause le processus pendant 5 secondes
    }

    return 0; // Cette ligne n'est jamais atteinte dans une boucle infinie
}

Analyse du comportement :

  1. Lorsque l'utilisateur fait Ctrl+C :
    • Le noyau détecte l'action et envoie un signal au processus.
    • Le thread principal de main() (actuellement dans sleep(5)) est interrompu.
    • La fonction attrap_signal() est exécutée.
    • Une fois attrap_signal() terminée, le processus main() reprend son exécution exactement là où il s'était arrêté (dans sleep(), qui peut reprendre ou se terminer prématurément). Le programme ne se termine pas.
  2. Lorsque l'utilisateur fait kill -9 [PID] :
    • Le noyau reçoit l'ordre .
    • Le noyau ne consulte pas le programme cible. Il retire brutalement le processus de la mémoire et libère ses ressources.
    • Le programme s'arrête sans que attrap_signal() ne soit appelée, et sans afficher de message.
Pourquoi SIGKILL ne peut-il pas être intercepté ?

C'est une sécurité fondamentale des systèmes Unix, garantissant l'intégrité et l'administrabilité du système :

  • Si un programmeur pouvait intercepter , un virus, un programme malveillant ou simplement un programme buggé pourrait refuser l'ordre de terminaison, disant "Non merci, je continue".
  • L'administrateur système perdrait alors le contrôle de la machine, incapable d'arrêter ce processus. La seule solution serait un redémarrage physique (électrique) du serveur, ce qui peut entraîner des pertes de données et des temps d'arrêt importants.
  • est l'arme absolue de l'administrateur : c'est le noyau qui exécute l'ordre de mort, pas le processus qui "se suicide" (qui peut donc refuser). Il garantit que tout processus peut être arrêté.

2.12 Envoyer des Signaux depuis un Programme

Un processus peut également envoyer des signaux à d'autres processus (à condition d'avoir les droits nécessaires, généralement être le propriétaire du processus cible ou root).

La fonction kill() est utilisée pour envoyer des signaux :

#include <sys/types.h> // Pour pid_t
#include <signal.h>    // Pour kill(), les constantes SIG*

int kill(pid_t pid, int sig);
  • pid : Le PID du processus cible auquel envoyer le signal.
    • : Envoie le signal au processus dont le PID est .
    • 0 : Envoie le signal à tous les processus dans le groupe de processus du processus appelant.
    • -1 : Envoie le signal à tous les processus pour lesquels le processus appelant a les privilèges d'envoyer un signal (sauf les processus init et systemd).
    • : Envoie le signal à tous les processus du groupe de processus dont l'ID est la valeur absolue de .
  • sig : Le numéro du signal à envoyer (ex: , ). Si est 0, aucun signal n'est envoyé, mais la fonction vérifie si le processus cible existe et si l'appelant a la permission d'envoyer des signaux.
  • Retour : 0 en cas de succès, -1 en cas d'erreur.
// Exemple : Envoyer SIGTERM à un processus spécifié par un PID
pid_t target_pid = 12345;
if (kill(target_pid, SIGTERM) == -1) {
    perror("Erreur lors de l'envoi du signal SIGTERM");
} else {
    printf("Signal SIGTERM envoyé au processus %d.\n", target_pid);
}

Conclusion et Points Clés

La maîtrise de la programmation système sous Unix/Linux implique une compréhension approfondie de plusieurs concepts fondamentaux :

  • Le rôle central du noyau et la distinction cruciale entre espace utilisateur et espace noyau pour la sécurité et la stabilité du système.
  • La philosophie "Tout est un fichier" qui unifie l'accès aux ressources via les inodes et les descripteurs de fichiers.
  • L'importance des appels système de bas niveau (open(), read(), write(), close(), lseek()) pour un contrôle précis des E/S.
  • Les mécanismes de redirection d'E/S via dup() et dup2(), essentiels pour les shells et la programmation réseau.
  • La dynamique des processus, de leur création par fork() et exec(), à leur terminaison par exit().
  • La gestion des processus zombies et orphelins, et l'importance des appels wait() et waitpid() pour la propreté du système.
  • Le concept des signaux comme moyen de communication asynchrone et de contrôle des processus, avec la distinction des signaux tels que , , et l'incontournable .

Ces concepts sont les fondations sur lesquelles sont construits tous les systèmes d'exploitation modernes et sont indispensables pour développer des applications performantes, robustes et sécurisées, en particulier dans les domaines des systèmes et réseaux.

Programmation Systèmes et Réseaux : Fondements, Fichiers, Processus et Signaux

Ce document explore les mécanismes fondamentaux des systèmes d'exploitation Unix/Linux, la gestion des fichiers à bas niveau, et la manipulation des processus et signaux en programmation système (Langage C).

Objectif : Maîtriser la mécanique interne d'Unix pour une programmation système universelle et performante.

1. Fondements et Système d'Exploitation

Un système informatique est composé de quatre éléments clés :

  • Matériel (CPU, RAM, Périphériques E/S) : Ressources de base.

  • Système d'Exploitation (OS) : Gère le matériel et coordonne les ressources.

  • Logiciels : Utilisent les ressources pour résoudre les problèmes.

  • Utilisateur : Exploite les logiciels.

Le système d'exploitation fournit une interface entre l'utilisateur et le matériel, gérant fichiers, mémoire, processus.

Composants du Système d'Exploitation :

  • Noyau (Kernel) : Composant central, gère le matériel et les ressources.

  • Bibliothèques systèmes (, WinAPI)

  • Services système (gestion utilisateurs, impression, réseau)

  • Interface utilisateur (GUI ou CLI)

  • Utilitaires et applications de base

Noyau (Kernel) vs Système d'Exploitation (OS) :

Caractéristique

Noyau (Kernel)

Système d'exploitation (OS)

Définition

Composant central qui gère le matériel et les ressources.

Ensemble de logiciels incluant le noyau et d'autres composants.

Rôle principal

Gestion des ressources système et communication avec le matériel.

Fournir une interface utilisateur et des services aux applications.

Exécution

Mode noyau (privilégié).

Mode utilisateur (restreint).

Modes d'exécution : Espace utilisateur vs Espace noyau

Pour la stabilité et la sécurité, le CPU ne traite pas toutes les instructions avec les mêmes privilèges.

  • Espace noyau (Kernel Mode - Ring 0) :

    • Privilège : Total. Accès direct au matériel.

    • Résidence : Noyau, pilotes de périphériques, modules.

    • Risque : Erreur fatale = plantage système (Kernel Panic).

  • Espace utilisateur (User Mode - Ring 3) :

    • Privilège : Restreint. Accès à sa propre zone mémoire, indirect au matériel.

    • Résidence : Applications utilisateur (navigateurs, éditeurs, serveurs).

    • Sécurité : Erreur = processus tué (Segmentation Fault), le système continue.

Le Langage C : Compilation et Exécution

Le C est le langage de prédilection pour la programmation système (développement d'OS comme Unix).

  • Compilateur : Transforme le code source en fichier objet. Sous Linux, gcc est standard.

  • Éditeur de liens (linker) : Regroupe les fichiers objets en un exécutable.

  • Compilation minimale : gcc nom_fichier_source.c (crée `a.out`).

  • Nom spécifique : gcc -o nom_executable nom_fichier_source.c.

  • Exécution : ./nom_executable.

Gestion des Erreurs en C (Programmation Système)

La gestion des erreurs est primordiale.

  • Variable globale :

    • Indique le code d'erreur ( généralement) après l'échec d'une fonction système.

    • Définie dans .

    • Exemples : EACCES, ENOENT.

  • Fonction :

    • Affiche un message d'erreur lisible basé sur la valeur de .

    • Usage : perror("Mon message");

    • Exemple de sortie : "Mon message: No such file or directory".

2. Gestion des Fichiers et Entrées/Sorties Bas Niveau

« Sous Linux, tout est un fichier » : pratiquement toutes les ressources sont représentées comme des fichiers.

  • Fichiers classiques (texte, binaire)

  • Répertoires

  • Périphériques matériels ()

  • Processus ()

  • Interfaces du noyau ()

  • Flux d'entrée/sortie (stdin, stdout, stderr)

  • Sockets Unix, canaux nommés (FIFO)

Avantages : unification de l'accès aux ressources et des outils, automatisation facilitée.

Système de Fichiers Unix

Structure logique et règles d'organisation des données sur un support.

  • Organisation de l'information (arborescences, dossiers)

  • Gestion de l'allocation de blocs

  • Cohérence des données (journalisation, vérification)

  • Permissions (UID, GID, droits)

  • Optimisation de l'accès

Inodes (Index Nodes)

  • Pour l'utilisateur : un nom (rapport.txt). Pour le système : un numéro unique d'inode.

  • Structure de données sur le disque contenant les métadonnées du fichier (PAS le nom !).

  • Contenu d'un inode :

    • Type de fichier (régulier, répertoire, lien, socket...).

    • Permissions (rwx) et propriétaire (UID, GID).

    • Taille, dates (création, modif, accès).

    • Compteur de liens (nombre de noms pointant vers cet inode).

    • Pointeurs vers les blocs de données du contenu réel.

  • Visualiser les inodes : ls -li.

Liens (Links)

Permettent d'avoir plusieurs noms pour les mêmes données.

  • Lien Physique (Hard Link) :

    • Deuxième nom donné au même inode.

    • Le compteur de liens de l'inode augmente (ex: ).

    • Indiscernables : modifier l'un modifie l'autre (même donnée).

    • Suppression : Les données ne sont effacées que quand le compteur de liens atteint .

    • Limitations : Impossible vers répertoires ou entre partitions différentes.

    • Utile pour : accès multiples sans duplication, sauvegardes, gain de place.

  • Lien Symbolique (Soft Link / Symlink) :

    • Nouveau fichier (avec son propre inode) qui contient le chemin vers un autre fichier (raccourci).

    • Conséquence : Si le fichier original est supprimé, le lien symbolique est "cassé" (dangling link).

    • Avantage : Peut pointer vers répertoires et traverser les systèmes de fichiers.

    • Utile pour : simplification d'accès, déplacement de données, partage, synchronisation.

Caractéristique

Lien Physique (Hard)

Lien Symbolique (Soft)

Inode

Partage le même inode que la cible

A son propre inode unique

Taille

Celle du fichier original

Minuscule (taille du chemin texte)

Suppression cible

Le lien reste valide (données conservées si le compteur n'est pas à 0)

Le lien est cassé

Portée

Même partition uniquement

Peut traverser les partitions

Descripteurs de Fichiers (File Descriptors - FD)

  • Représentation d'un fichier en mémoire vive (RAM) pour un processus.

  • Quand on ouvre un fichier avec open(), le noyau renvoie un entier positif (FD).

  • Le programme utilise cet entier pour interagir avec le fichier.

  • FD standards (ouverts par défaut pour chaque processus) :

    • 0 (stdin) : Entrée standard (clavier).

    • 1 (stdout) : Sortie standard (écran).

    • 2 (stderr) : Sortie d'erreur (écran).

  • Importance :

    • Quotas : Limite de FD ouverts. Oublier de fermer (close()) entraîne une fuite de descripteurs.

    • Redirections : Modification de la table des FD pour que FD 1 (par ex.) pointe vers un fichier au lieu de l'écran.

Entrées / Sorties Bas Niveau (Appels Système)

Contrôle direct et précis sur le matériel, à l'opposé des fonctions C de haut niveau.

  • Bibliothèque standard (stdio.h) - Haut niveau :

    • Fonctions : fopen(), fread(), fprintf().

    • Utilise des flux (FILE *) et la bufferisation en espace utilisateur.

    • Objectif : minimiser les appels système pour améliorer les performances (sur de petits volumes de données).

  • Appels système (unistd.h, fcntl.h) - Bas niveau :

    • Fonctions : open(), read(), write(), close().

    • Manipulent des descripteurs de fichiers (FD).

    • Non-bufferisé : chaque appel déclenche un changement de mode (User Kernel) et une écriture immédiate.

    • Objectif : contrôle précis (pilotes, sockets, gros blocs de données).

Primitives Fondamentales pour E/S Bas Niveau :

Inclusions requises :

  • #include : Types système de base.

  • #include : Attributs de fichiers.

  • #include : Ouverture et contrôle de fichiers.

  • #include : Appels système POSIX (lecture, écriture, fermeture).

Commandes Essentielles :

  • int open(const char *pathname, int flags, [mode_t mode]) :

    • Ouvre ou crée un fichier.

    • Retourne le FD () ou en cas d'erreur.

    • flags (OR binaire) : O_RDONLY, O_WRONLY, O_RDWR, O_TRUNC, O_APPEND, O_CREAT.

    • mode : Permissions en octal si O_CREAT est utilisé (ex: 0644 pour rw-r--r--).

  • ssize_t read(int fd, void *buf, size_t count) :

    • Lit des données du fichier vers un tampon (buf).

    • Retourne : nombre d'octets lus (), (EOF), (erreur).

  • ssize_t write(int fd, const void *buf, size_t count) :

    • Écrit des données depuis un tampon vers le fichier.

    • Retourne : nombre d'octets écrits ou (erreur).

  • int close(int fd) :

    • Ferme le descripteur de fichier et libère la ressource (essentiel).

  • off_t lseek(int fd, off_t offset, int whence) :

    • Déplace le curseur interne de lecture/écriture (accès aléatoire).

    • offset : Décalage en octets.

    • whence : Point de référence (SEEK_SET - début, SEEK_CUR - actuel, SEEK_END - fin).

    • Utile pour : aller à un endroit précis, chercher la fin, récupérer la taille du fichier (lseek(fd, 0, SEEK_END)).

Duplication de Descripteurs de Fichiers (dup, dup2)

Essentiel pour gérer les redirections et la communication inter-processus.

  • int dup(int oldfd) :

    • Crée une copie d'un FD existant.

    • Utilise le plus petit FD libre disponible pour la copie.

    • Le nouveau FD pointe vers la même ouverture de fichier.

  • int dup2(int oldfd, int newfd) :

    • Force la copie sur un numéro de FD précis ou le ferme d'abord.

    • Si newfd est déjà ouvert, dup2 le ferme silencieusement.

    • Crucial pour les Sockets : rediriger stdin/stdout/stderr vers une socket réseau (le shell lance des commandes qui "croient" interagir avec un terminal local alors qu'elles communiquent sur le réseau).

3. Gestion des Processus et des Signaux

Un processus est une instance d'un programme en cours d'exécution.

  • Entité dynamique avec ses propres ressources.

  • Espace d'adressage : mémoire virtuelle isolée.

  • État du processus : différents états durant l'exécution.

  • Contexte d'exécution : informations pour reprendre l'exécution (registres, compteur ordinal).

  • PID (Process Identifier) : Numéro unique pour gérer et suivre le processus.

  • Hiérarchie : Parent/Enfant.

Programme vs Processus :

Concept

Programme

Processus

Nature

Statique (fichier binaire sur disque)

Dynamique (actif en RAM)

Localisation

Disque Dur

Mémoire Vive (RAM) + Registres CPU

Durée de vie

Permanente

Temporaire (de la création à la terminaison)

Espace d'Adressage d'un Processus :

Mémoire virtuelle découpée en segments logiques :

  • Texte (Code) : Instructions machines (souvent Lecture Seule).

  • Données (Data) : Variables globales et statiques (Data initialisées, BSS non initialisées).

  • Tas (Heap) : Allocation dynamique (malloc), grandit vers les adresses hautes.

  • Pile (Stack) : Variables locales, paramètres, adresses de retour. LIFO, grandit vers les adresses basses.

Process Control Block (PCB)

La "carte d'identité" du processus, gérée par l'OS. Contient :

  • PID (identifiant unique), PPID (identifiant du parent).

  • Compteur Ordinal (adresse prochaine instruction).

  • État du processus (Ready, Running, Waiting/Blocking).

  • Descripteurs de fichiers ouverts.

Commutation de Contexte

Illusion de multitâche : l'OS bascule rapidement entre processus.

  • Contexte : Ensemble d'informations nécessaires pour reprendre l'exécution d'un processus après interruption.

  • Inclut : état du processus, variables, table des processus, espace utilisateur, pile, code, données.

  • Le noyau et ses variables ne font partie d'aucun contexte de processus.

États d'un Processus :

  • Prêt (Ready) : Prêt à s'exécuter, attend simplement le CPU dans une file d'attente.

  • Élu (Running) : En cours d'exécution sur le CPU. Peut passer à Prêt, Bloqué ou Terminé.

  • Bloqué (Blocked/Waiting) : Ne peut pas s'exécuter, attend un événement extérieur (E/S, ressource). Reviendra à Prêt après l'événement.

Transitions entre États :

Transition

Direction

Cause / Explication

Admission

New Ready

Le système accepte le processus.

Élection (Dispatch)

Ready Running

L'ordonnanceur choisit le processus pour le CPU.

Réquisition (Timeout)

Running Ready

Fin du temps alloué (quantum) ou processus plus prioritaire (multitâche préemptif).

Appel Bloquant

Running Blocked

Demande de ressource lente (scanf, read, wait).

Déblocage (Wakeup)

Blocked Ready

L'événement attendu s'est produit.

Sortie

Running Terminated

Le processus appelle exit() ou reçoit un signal fatal.

Ordonnancement (Scheduling)

Gestion du partage du processeur entre les processus.

  • Objectifs Système : Maximiser CPU et débit.

  • Objectifs Utilisateur : Minimiser latence, temps de complétion, temps de réponse, attente.

Ordonnancement Préemptif vs Non Préemptif :

  • Non Préemptif :

    • Impossible d'interrompre le processus élu. Il garde le CPU jusqu'à fin ou blocage.

    • Plus simple à implémenter, mais réactivité faible (temps de réponse élevé).

    • Utilisé dans systèmes temps réel.

  • Préemptif :

    • Le système peut interrompre à tout moment (priorité, temps écoulé).

    • Plus complexe à gérer, mais réactivité élevée.

    • Utilisé dans systèmes multitâches interactifs.

    • Surcharge plus élevée due aux changements de contexte fréquents.

Politiques d'Ordonnancement :

  • FIFO (First-Come First-Serve) : Premier arrivé, premier servi (sans réquisition).

  • SJF (Shortest Job First) : Le plus court d'abord (non préemptif).

  • SRTF (Shortest Remaining Time First) : Variante préemptive du SJF.

  • RR (Round Robin) : Tourniquet avec un quantum de temps.

  • PS (Priority Scheduling) : Élu = processus de plus forte priorité constante.

Cycle de Vie d'un Processus (Unix/Linux)

Création en deux étapes :

  1. Duplication (fork) : Clonage du processus actuel.

  2. Mutation (exec) : Remplacement du code du clone par un nouveau programme.

La fonction :

  • pid_t fork(void); (déclarée dans et ).

  • Crée une copie exacte (processus fils) du processus appelant (processus parent).

  • Ce qui est copié : espace mémoire (Code, Tas, Pile), descripteurs de fichiers, environnement.

  • Ce qui change :

    • Fils a un nouveau PID.

    • Fils a le PID du parent comme PPID.

    • Compteur de temps CPU remis à zéro.

  • Valeurs de retour :

    • Parent : retourne le PID du fils.

    • Fils : retourne .

    • Erreur : retourne .

Autres fonctions utiles :

  • pid_t getpid(void); : Retourne le PID du processus appelant.

  • pid_t getppid(void); : Retourne le PPID du processus appelant.

  • pid_t getuid(void); : Retourne l'UID du processus appelant.

  • pid_t getgid(void); : Retourne le GID du processus appelant.

Terminaison des Processus (exit()) :

  • Un processus se termine en atteignant la fin de main() ou en appelant explicitement exit().

  • void exit(int status); :

    • Libère les ressources et envoie un signal SIGCHLD au parent.

    • status : pour succès, autre pour erreur.

  • Important : le processus reste un zombie jusqu'à ce que le parent lise son statut.

Pathologies des Processus : Zombie, Orphelin

Le modèle Père/Fils implique une responsabilité parentale.

  • Processus Zombie (defunct) :

    • A terminé son exécution (exit() appelé) mais occupe encore une entrée dans la table des processus.

    • Cause : Le parent n'a pas encore appelé wait() pour récupérer le statut du fils.

    • DANGER : Consomme un PID. Accumulation = impossible de créer de nouveaux processus.

    • Détection : État Z dans ps -el ou top.

  • Processus Orphelin :

    • Le père s'est terminé avant lui.

    • Solution : Le noyau ré-assigne automatiquement le fils au processus init (PID 1).

    • Le rôle d'init : Exécute des wait() en permanence sur ses fils adoptifs pour éviter les zombies.

Outils de Nettoyage : wait() et waitpid()

Permettent au parent d'attendre la terminaison d'un fils et de récupérer son état de sortie.

  • Primitive wait() :

    • pid_t wait(int *status);

    • Bloque le père jusqu'à ce qu'un de ses fils se termine.

    • Retourne le PID du fils ou (erreur).

    • status : Entier codé en bits. Utiliser des macros pour décoder : WIFEXITED(), WEXITSTATUS(), WIFSIGNALED().

  • Primitive waitpid() :

    • pid_t waitpid(pid_t pid, int *status, int options);

    • Plus flexible : peut attendre un fils spécifique ou être non-bloquant.

    • pid : (PID précis), (n'importe quel fils, comme wait()).

    • options :

      • 0 : Bloquant.

      • WNOHANG : Non-bloquant, retourne si aucun fils n'est terminé (utile pour serveurs).

Signaux

Interruption logicielle asynchrone envoyée par le noyau (ou un processus) pour notifier un événement.

  • Asynchrone : Le processus est interrompu n'importe quand.

  • Comportement par défaut : Souvent, le noyau tue le processus.

  • Utilité : Interrompre, terminer, suspendre, reprendre, avertir d'événements externes.

Signaux Critiques (Exemples) :

Signal

Nom

Code

Rôle et Analogie

SIGHUP

Hangup

1

Recharger la configuration (pour démons), sans s'arrêter.

SIGINT

Interrupt

2

Interruption polie (Ctrl+C). Le programme peut nettoyer avant de sortir.

SIGQUIT

Quit

3

Arrêt brutal avec trace (Ctrl+\). Génère un coredump pour débogage.

SIGTERM

Terminate

15

Demande d'arrêt propre (kill par défaut). Le processus peut sauvegarder, fermer.

SIGKILL

Kill

9

Arrêt immédiat, non interceptable. Ordre du noyau. Arme absolue de l'administrateur.

Interception et Gestion des Signaux (signal()) :

Un programme peut définir une stratégie :

  1. Ignorer le signal (via SIG_IGN).

  2. Intercepter avec une fonction dédiée (gestionnaire/handler).

  3. Laisser l'action par défaut (souvent terminaison).

  • sighandler_t signal(int signum, sighandler_t handler); (inclure ).

  • signum : Numéro du signal (ex: SIGINT).

  • handler : Fonction à appeler, ou SIG_IGN, SIG_DFL.

  • SIGKILL () est non interceptable car c'est une sécurité fondamentale des systèmes Unix.

Lancer un quiz

Teste tes connaissances avec des questions interactives