Définition

Un processus est une instance dynamique d’un programme en cours d’exécution, qui se matérialise par une image mémoire évolutive.

Programme ou fichier binaire : Représentation statique du code :

  • RĂ©sultat de la compilation, il contient les instructions Ă  exĂ©cuter dans un ordre prĂ©cis.
  • Son contenu est immuable tant qu’il n’est pas modifiĂ©.
  • Les variables sont dĂ©clarĂ©es, mais n’ont pas de valeur dĂ©finie avant l’exĂ©cution.
  • Les fichiers ne sont pas ouverts et ne contiennent pas de donnĂ©es spĂ©cifiques Ă  une exĂ©cution.

Processus : Instance dynamique du programme :

  • Chargement du fichier binaire en mĂ©moire, allouant les ressources nĂ©cessaires Ă  son exĂ©cution.
  • Initialisation des variables : Les variables dĂ©clarĂ©es dans le programme reçoivent une valeur initiale (souvent une valeur par dĂ©faut).
  • Allocation dynamique de la mĂ©moire : Les tableaux, les structures de donnĂ©es et d’autres objets sont créés en mĂ©moire Ă  la demande.
  • Ouverture et manipulation de fichiers : Les fichiers sont ouverts, lus, Ă©crits et fermĂ©s au cours de l’exĂ©cution.
  • État Ă©volutif : L’état du processus change constamment au cours de son exĂ©cution, en fonction des instructions exĂ©cutĂ©es et des donnĂ©es traitĂ©es.
  • Variables et Ă©tat : Les variables stockent les valeurs intermĂ©diaires des calculs et reflètent l’état du processus Ă  un instant donnĂ©.
  • Nature dynamique : Le processus est une entitĂ© dynamique qui naĂ®t, Ă©volue et meurt au cours de l’exĂ©cution du programme.

État d’un processus

Durant son exécution, un processus peut-être dans un des trois états suivants : (principaux états, valables pour tous les OS)

  • Actif: Un processeur lui a Ă©tĂ© attribuĂ©. Ce dernier exĂ©cute une partie de son code.
  • Activable : Il est prĂŞt Ă  ĂŞtre exĂ©cutĂ©, il dispose de toutes les ressources nĂ©cessaires hormis un processeur.
  • En attente/bloqué : Un Ă©vènement extĂ©rieur, ou une ressource est nĂ©cessaire Ă  son exĂ©cution

L’ordonnancement des programmes/ressources

L’ordonnancement des ressources et des processus est une opération fondamentale dans les systèmes informatiques, particulièrement dans ceux gérant plusieurs tâches simultanément. Son objectif principal est d’optimiser l’utilisation des ressources disponibles, qu’il s’agisse du processeur, de la mémoire ou d’autres périphériques, afin de maximiser la performance globale du système et de répondre aux besoins de tous les utilisateurs ou processus.

En effet, un système informatique doit arbitrer entre les différentes demandes de ressources, car celles-ci sont limitées. L’ordonnancement permet de déterminer quel processus sera exécuté à un instant donné, pendant combien de temps, et dans quel ordre les différentes tâches seront traitées. Cette décision est cruciale, car elle influence directement la réactivité du système, le temps d’exécution des tâches et l’équité entre les différents utilisateurs.

Les algorithmes d’ordonnancement varient en complexité et en objectifs. Certains privilégient la minimisation du temps d’attente moyen, d’autres la maximisation du débit, ou encore la garantie de temps de réponse pour certaines tâches critiques. Le choix de l’algorithme dépend des caractéristiques du système, des types de tâches à exécuter et des contraintes spécifiques.

Transitions d’états

  • Actif → Activable : Le processus cède temporairement le processeur, souvent en raison d’un partage du temps de calcul.
  • Actif → Bloqué : Le processus est suspendu en attendant la rĂ©alisation d’une condition extĂ©rieure, comme la fin d’une opĂ©ration d’EntrĂ©es sorties (I.O), la rĂ©ception d’un Signal ou l’obtention d’une ressource (mĂ©moire).
  • BloquĂ© → Activable : La condition d’attente du processus est satisfaite, il peut donc reprendre son exĂ©cution.

Création de processus

La création d’un processus est une opération dynamique qui permet de générer une nouvelle instance d’un programme en cours d’exécution. Cette opération, généralement réalisée à l’aide d’une primitive système dédiée (comme fork() sous Linux).

À la création d’un processus, le système d’exploitation entreprend plusieurs actions essentielles.

Tout d’abord, il alloue dynamiquement une zone de mémoire pour stocker le code, les données et la pile du nouveau processus. Ensuite, il crée un descripteur de processus, une structure de données qui contient des informations cruciales telles que l’identifiant du processus, son état actuel, ses registres et les ressources qui lui sont allouées.

Ce descripteur sert à gérer le processus tout au long de son cycle de vie. Enfin, le système d’exploitation insère le processus dans une file d’attente, généralement appelée liste des processus prêts, afin qu’il puisse être sélectionné par l’ordonnanceur pour être exécuté sur le processeur.

Terminaison

Il existe deux façons de terminer un programme : Son destin peut être de s’éteindre paisiblement après avoir exécuté son code, ou d’être abruptement interrompu en cas de problème ou de Signal.

Lorsqu’un processus termine son exécution, le système d’exploitation procède à un nettoyage systématique : les blocs de mémoire, les fichiers ouverts, les périphériques réservés et autres ressources sont libérés, rendant ces ressources disponibles pour d’autres processus. De plus, le descripteur de processus, qui contenait toutes les informations relatives au processus, est détruit, marquant ainsi sa disparition définitive du système.

Signal

Un signal, dans le contexte des systèmes d’exploitation comme Linux, est une forme de communication asynchrone entre processus. C’est un peu comme une interruption logicielle qui vient perturber le flux normal d’exécution d’un processus pour signaler un événement particulier.

Utilité

Les signaux servent Ă  :

  • Notifier un Ă©vĂ©nement: Par exemple, un signal peut indiquer Ă  un processus qu’il a reçu une interruption clavier (Ctrl+C), qu’un fichier est devenu accessible ou qu’une erreur s’est produite.
  • Terminer un processus: Certains signaux sont utilisĂ©s pour demander Ă  un processus de se terminer de manière ordonnĂ©e ou de manière abrupte.
  • Suspendre ou reprendre un processus: Les signaux peuvent ĂŞtre utilisĂ©s pour mettre un processus en pause ou pour le redĂ©marrer.

Les principaux signaux

Voici une liste non exhaustive des principaux signaux :

  • SIGTERM: Demande de terminaison normale.
  • SIGINT: Interruption par le clavier (Ctrl+C).
  • SIGKILL: Terminaison immĂ©diate (ne peut pas ĂŞtre interceptĂ©e).
  • SIGSEGV: Erreur de segmentation (accès mĂ©moire illĂ©gal).
  • SIGABRT: Appel Ă  la fonction abort().
  • SIGALRM: Expiration d’une alarme.
  • SIGCHLD: Terminaison d’un processus enfant.

La réaction du processus

Un processus peut :

  • Ignorer le signal: Le signal n’a aucun effet.
  • ExĂ©cuter un gestionnaire de signal: Le processus exĂ©cute une fonction spĂ©cifique pour traiter le signal.
  • Terminer: Le processus s’arrĂŞte.

Les fonctions signal() et sigaction() permettent de définir le comportement d’un processus face à un signal donné. Par exemple, on peut utiliser ces fonctions pour créer un gestionnaire de signal qui sera exécuté lorsque le processus recevra un SIGINT ou d’autres signaux.

Agir sur les processus

Tuer le process

Pour tuer un processus, on peut utiliser la méthode kill:

int kill(pid_t pid, int sig);

Arborescence des processus

Le noyau Linux crée une structure arborescente de processus, où chaque nœud représente un processus. Le processus racine, init, est le point de départ de cette arborescence. Chaque processus, à l’exception de init, est lié à un processus parent par son PPID, formant ainsi une relation parent-enfant entre les processus.

L’organisation hiérarchique des processus sous Linux permet de tracer la lignée de chaque processus, depuis sa création jusqu’à sa terminaison. Le PID unique identifie de manière univoque chaque processus dans cette arborescence, tandis que le PPID établit le lien de parenté avec le processus créateur.

Le PID et le PPID sont deux attributs fondamentaux de chaque processus sous Linux. Le PID sert à identifier de manière unique un processus, tandis que le PPID établit le lien de filiation avec le processus parent, permettant ainsi de reconstituer l’arborescence des processus.

Caractéristiques d’un processus

PID & PPID

#include <unistd.h>
 
pid_t getpid(void); // Renvoie le PID du processus appelant
pid_t getppid(void); // Renvoie le PPID du processus appelant

Propriétaire

Le propriétaire d’un processus est l’utilisateur qui a lancé le processus. Il existe deux notions de propriétaire :

  • PropriĂ©taire rĂ©el (UID rĂ©el) : C’est l’utilisateur qui a effectivement lancĂ© le processus. Il dĂ©tient les droits les plus Ă©levĂ©s sur le processus.
  • PropriĂ©taire effectif (UID effectif) : C’est l’utilisateur dont les droits sont utilisĂ©s pour vĂ©rifier les autorisations d’accès aux ressources. Il peut ĂŞtre diffĂ©rent du propriĂ©taire rĂ©el en cas d’utilisation de mĂ©canismes de changement d’utilisateur (par exemple, sudo).
#include <unistd.h>
 
uid_t getuid(void);  // Renvoie l'UID réel du processus
uid_t geteuid(void); // Renvoie l'UID effectif du processus
gid_t getgid(void);  // Renvoie le GID réel du processus
gid_t getegid(void); // Renvoie le GID effectif du processus

L’égalité entre UID réel et effectif (et GID réel et effectif) n’est pas toujours une évidence. En effet, le système d’exploitation Linux offre des mécanismes pour modifier temporairement ces identifiants, offrant ainsi une flexibilité accrue en termes de gestion des droits d’accès.

Recouvrement de processus

Fork : Duplique un processus existant, créant ainsi un nouveau processus fils qui est une copie quasi-conforme du processus père.

Système d’appel exec : Permet de remplacer le code et les données d’un processus en cours d’exécution par ceux d’un nouveau programme.

Caractéristiques conservées et modifiées lors d’un exec :

  • ConservĂ©es : PID du processus parent (PPID), UID, GID, descripteurs de fichiers (sauf ceux ouverts avec le flag O_CLOEXEC), signaux en attente.
  • ModifiĂ©es : Code du processus, donnĂ©es, PID (le nouveau processus reçoit un nouveau PID), arguments de ligne de commande, variables d’environnement.

La primitive de recouvrement : execvr

#include <unistd.h>
 
int execve(const char* filename, char* const argv[], char* const envp[]);

Fonctionnement : Charge et exécute le programme binaire spécifié par filename. Le processus appelant est remplacé par le nouveau processus. L’exécution commence à la fonction main du nouveau programme.

Paramètres :

  • filename : Chemin absolu ou relatif vers le fichier binaire Ă  exĂ©cuter.
  • argv : Tableau de chaĂ®nes de caractères reprĂ©sentant les arguments de la ligne de commande. Le premier Ă©lĂ©ment argv[0] est gĂ©nĂ©ralement le nom du programme. Le dernier Ă©lĂ©ment doit ĂŞtre un pointeur nul.
  • envp : Tableau de chaĂ®nes de caractères de la forme “nom=valeur” reprĂ©sentant les variables d’environnement du nouveau processus. Le dernier Ă©lĂ©ment doit ĂŞtre un pointeur nul.

Retour :

  • -1 en cas d’échec
  • En cas de succès, la fonction ne retourne pas ! (le code a Ă©tĂ© remplacĂ©)

Exemple :

Exécution de ls -l avec le nom de fichier passé en argument.

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
 
int main(int argc, char *argv[]) {
    if (argc != 2) {
        fprintf(stderr, "Usage: %s fichier\n", argv[0]);
        return 1;
    }
 
    char *arg[4];
    
    // Initialiser les arguments
    arg[0] = "ls";
    arg[1] = "-l";
    arg[2] = argv[1]; // Le nom du fichier
    arg[3] = NULL;
 
    // Exécution de 'ls -l <fichier>'
    if (execve("/bin/ls", arg, environ) == -1) {
        perror("execve");
        free(arg);
        return 1;
    }
 
    // Ce code ne sera jamais atteint si execve réussit
    free(arg);
    return 0;
}

Variantes de execve:

  • avec un p: la variable PATH est utilisĂ© pour trouver l’exĂ©cutable
  • avec un e: On peut spĂ©cifier l’environnement.
CommandeSignatureUtilisation
execlint execl(const char* path, const char* arg, ... /* (char*) NULL */)arguments passés par liste d’arguments
execlpint execlp(const char* file, const char* arg, ...)
execle…
execv…arguments passés avec un tableau (ou vecteur)
execvp…
execvpe…

Redirections

Les redirections permettent de rediriger les flux standards (voir Entrées sorties (I.O)) depuis/vers d’autres fichiers/flux.

Cela peut se faire de deux manières :

Par ouverture d’un fichier en l’associant à un descripteur de haut-niveau

Avec :

FILE* freopen(const char* path, const char* mode, FILE* stream);

Cette méthode permet d’ouvrir un fichier (comme avec fopen) mais nécessite la fermeture du descripteur stream donné (qui doit être compatible avec le mode d’ouverture : lecture ou écriture) une fois les opérations sur le fichier terminées.

Exemple :

File* redir;
 
redir = freopen("/tmp/sortie.txt", "w", stdout);
if (redir == NULL){
	// Erreur
}
 
printf("Coucou");

Le descripteur stream est fermé si nécessaire

En modifiant le descripteur bas-niveau

Cela se fait avec les primitives dup() ou dup2()

C’est la méthode la plus générale qui consiste à dupliquer un descripteur existant. Cette méthode fonctionne quelle que soit la sortie du descripteur (fichier, tubes (pipelines), socket réseau…,).

Le principe général est le suivant :

  1. Création d’un descripteur avec open()
  2. Fermeture du descripteur Ă  rediriger
  3. Duplication du descripteur valide avec dup()
  4. Fermeture du descripteur utilisé pour la redirection.

Remarques

  1. L’étape 1 est inutile si le descripteur existe déjà (ex. tubes (pipelines), socket réseau…,)
  2. L’étape 4 est facultative
  3. Les étapes 2 et 3 peuvent être combinées en utilisant la méthode dup2()

Primitives :

#include <unistd.h>
 
int dup(int oldfd);
int dup2(int oldfd, int newfd);

dup: Duplique le descripteur oldfd dans le premier descripteur disponible.

dup2: Duplique oldfd dans le descripteur newfd qui est fermé si besoin.

Exemple:

int fd = creat("/tmp/sortie.txt", "0640"); // (1)
close(STDOUT_FILENO) // (2)
dup(fd); // 3
close(fd); // 4

Remarques

Les redirections restent en place après un fopen/exec, (sauf si le fichier a été ouvert avec O_CLOEXEC). C’est par exemple utilisé par le shell.