Guillaume Chanel
Pour vous qu’est-ce qu’un processus?
Quelles informations contient-il?
Où réside ces informations en mémoire?
Que permet les systèmes multi-processus ?
Un processus représente l’exécution courante d’un programme. Il contient donc toutes les informations nécessaire à l’exécution du programme.
Créer un programme en C qui:
Click here to download
En utilisant la commande pmap
:
Grâce la mémoire virtuelle on va pouvoir:
L’espace d’adressage est divisé en pages (en général de 4Ko). Une page virtuelle peut être associée à une page de mémoire vive (page valide) ou morte (page invalide).
Elle est réalisée par le matériel (Memory Management Unit - MMU):
Une table des pages existe pour chaque processus.
Chaque table est maintenue par le système (i.e. Linux, MacOSX, Windows, etc…) et utilisée par le MMU.
Quelques informations généralement contenues dans une entrée de la table:
Un défaut de page arrive lorsque le MMU ne peut pas satisfaire une demande de page car elle n’est pas référencée dans la table du processus (bit «page valide» = false).
Il y a alors 3 cas possibles:
Pour un défaut de page majeur il faut charger la page manquante:
A noter que lorsqu’une page est libérée en mémoire physique soit:
En utilisant la commande pmap -X
sur le processus précédent, observer et expliquer les champs Size, RSS, PSS et Swap
Comment ces champs evoluent-t-ils lorsque plusieurs processus identiques sont lancés ?
Que faudrait-il faire pour que la champ Swap commence à augmenter ?
Pourquoi dans certain cas Size est différent de RSS mais Swap vaut 0 ?
Un processus est identifié grâce à son PID (Process ID). Il est unique pour chaque processus mais un PID libéré peut être réutilisé.
Chaque processus est décrit par son contexte:
Dans le noyaux Linux (3.7.10) un processus est définit par la structure task_struct (/usr/src/linux/include/linux/sched.h).
struct task_struct {
/* ... */
/* PID du processus */
pid_t pid;
/* Description de la mémoire virtuelle + table de page */
struct mm_struct *mm;
/* Etat du CPU / registres (specifique à la platforme) */
struct thread_struct thread;
/* Information sur l'ordonnancement du processus */
struct sched_info sched_info;
/* Contient notament la table des descripteur de fichier ainsi
qu'une liste des descripteurs "close on-exec" */
struct files_struct *files;
/* ... (+ de 350 lignes au total) */
};
/* Dans /usr/src/linux/include/linux/mm_types.h */
struct mm_struct {
/* ... */
unsigned long start_code, end_code, start_data, end_data; /* segments de code / données */
unsigned long start_brk, brk, start_stack; /* segment du tas et de la pile */
/* ... */
}
Lors du démarrage du système, le processus systemd (ou init) est créé par le noyau. Il est donc le premier processus et porte le PID 1.
Tous les autres processus sont créés par un appel à la fonction fork. Chaque
processus a donc un parent (excepté systemd, c.f. commande pstree
).
#include <unistd.h>
#include <sys/types.h>
pid_t fork(void); // Crée un nouveau processus enfant
pid_t getpid(void); // retourne le PID du processus
pid_t getppid(void); // retourne le PID du parent
Cette fonction crée un nouveau processus qui est une réplique du processus parent (e.g. copie de la table des pages, état du processeur, descripteurs de fichier, etc...), et va continuer son exécution à partir du fork.
La fonction fork retourne 0 pour le processus enfant, le PID de l’enfant dans le processus parent, -1 en cas d’erreur.
L’implémentation d’un fork peut donc ce faire de la manière suivante:
#include <unistd.h>
pid_t pid = fork()
if(pid > 0) {
// Code du parent
}
else if(pid == 0){
// Code de l’enfant
}
else // Error
Le processus enfant n’est pas une réplique exacte du parent (see man fork), notamment:
La table des descripteurs de fichier est copiée
En conséquence, que ce passe-t-il si le processus parent écrit sur le même descripteur que le processus enfant ?
Le nouveau processus va donc partager des pages avec son processus parent.
Ces pages seront copiées uniquement lors de modifications de la mémoire. C’est ce que l’on appelle le «copy on write».
La fonction exit permet de terminer un processus à tout moment:
exit(int status);
Il existe deux constantes souvent utilisées EXIT_SUCCESS et EXIT_FAILURE.
Avant de terminer le processus la fonction exit:
Il existe d’autres fonction pour terminer un programme:
void _exit(int status); // appel système direct, sans appel aux fonction enregistrées avec atexit
void abort(void); // génération d’un core dump
Lorsqu’un processus se termine, le noyau garde certaines informations de la
task_struct
(pid, statut de terminaison, etc...). On dit alors que le
processus est un zombie.
Ces information sont conservées en mémoire tant que le parent du processus n’y a pas accédé.
Dans le cas ou le parent du processus est mort c’est le processus 1 (systemd
ou init) qui va se charger de détruire la task_struct
.
Lorsqu’un processus effectue un fork il doit donc prendre soins d’éviter les zombies en appelant une des fonctions suivantes:
#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int *status);
pid_t waitpid(pid_t pid, int *status, int options);
Ces fonctions permettent d’attendre la terminaison d’un enfant pour récupérer son statut. Si un enfant est déjà terminé (i.e. est un zombie), ces fonctions retournent immédiatement.
Plusieurs macros permettent de tester le statut de retour (c.f. man wait) dont:
WIFEXITED(status)
: indique si l’enfant c’est terminé normalement;WCOREDUMP(status)
: indique si un core dump de l’enfant a été créé.Un processus orphelin peut-il rester un zombie longtemps
Dans quels cas un processus peut rester un zombie longtemps ?
L’execution d’un nouveau programme ce fait par les fonctions exec*
, dont:
#include <unistd.h>
int execve(const char *filename, char *const argv[], char *const envp[]);
Cette fonction ne retourne pas de valeur en cas de succès mais elle:
Si filename est un script, le shell correspondant est chargé et le fichier executé par le shell.
C'est donc cette fonction qui se charge de construire l'espace de mémoire virtuel d'un processus à partir du fichier executable.
Système | Nom | Commentaires |
---|---|---|
MSDOS / Windows | COM | Exécutable très limité, n’est quasi plus utilisé |
PE (Portable Executable) |
Fichiers exécutables: .EXE Librairies partagées : .DLL ActiveX: .OCX |
|
OS X | Mach-O | Apps., frameworks, bib., etc. |
Unix/Linux | a.out | Format original des objets et exécutable Unix, non adapté au librairies partagées |
COFF (Common Object File Format) | Ancien format des objets et exécutable Unix, non adapté au librairies partagées | |
ELF (Executable and Linkable Format) |
Fichiers Exécutables: .o Librairies partagées: .so Fichiers core (coredump) Utilisable sur plusieurs plateformes |
exec*
);On peut observer le contenu d’un fichier ELF avec les commandes objdump
et readelf
.
La table des sections permet de définir les sections dans le fichier. Une section peut contenir des informations de liage, du code, des données.
typedef struct {
...
uint32_t sh_name; /*Index spécifiant le nom de la section (.text, .data, etc.)*/
ElfN_Addr sh_addr; /* Adresse de la section en mémoire virtuelle */
ElfN_Off sh_offset; /* Offset de la section dans le fichier ELF*/
uintN_t sh_size; /* Taille de la section */
...
} ElfN_Shdr;
Exercice: ajouter les flêches
La table des segments (program header) permet de regrouper les sections en plusieurs segments. Ces segments peuvent être chargés en mémoire virtuelle lors de l'exécution.
typedef struct {
uint32_t p_type; /* if == PT_LOAD -> le segment doit être placé en mémoire */
ElfN_Off p_offset; /* Offset du segment dans le fichier */
uintN_t p_filesz; /* Taille du segment dans le fichier*/
ElfN_Addr p_vaddr; /* Adresse où charger le segment en mémoire virtuelle */
uint32_t p_memsz; /* Taille du segment en mémoire, si >= p_filesz, complété par des 0 */
uintN_t p_flags; /* Exec, write, read */
...
} ElfN_Phdr;
Exercice (ensemble): représenter comment ces informations permettent de définir l'espace de mémoire virtuelle du processus