Guillaume Chanel
Cours système d'exploitation by Guillaume Chanel, Jean-Luc Falcone and University of Geneva is licensed under CC BY-NC-SA 4.0
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:
Il est possible de demander au noyaux de verrouiller des pages virtuelles en mémoire physique. Cela:
#include <sys/mman.h>
int mlock(const void *addr, size_t len);
int munlock(const void *addr, size_t len);
/* Verrouille / déverrouille les pages incluant les adresses allant de addr à (addr + len) */
int mlockall(int flags);
int munlockall(void);
/* Verrouille / déverrouille TOUTES les pages virtuelles du processus
flags = MCL_CURRENT -> seulement les pages actuellement en mémoire virtuelle
flags = MCL_FUTURE -> aussi les pages futures */
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 descripteur "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 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é init, 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:
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é.
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éé.Si un le parent d'un processus Po meurt avant que Po soit terminé, alors Po deviens un processus orphelin.
Comme les processus linux doivent avoir un parent, Po est reparenté à un processus spécial qui:
prctl
);wait
).Le processus 1 (e.g. init
, systemd
) est un processus subreaper.
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
int main(void)
{
pid_t pid; //Pour sauver le retour de la fonction fork
//Creation d'un nouveau processus
pid = fork();
//Depedant du retour soit on est dans le père, dans le fils ou retour erreur
if(pid == 0) { // fils
printf("Je suis %d fils de %d ET j'attends 20 secondes\n", getpid(), getppid());
sleep(20);
printf("Je suis %d fils de %d ET je meurt\n", getpid(), getppid());
}
else if(pid > 0) { //père
printf("Je suis %d père de %d ET j'attend 10 seconds\n", getpid(), pid);
sleep(10);
printf("Je suis %d père de %d ET je meurt\n", getpid(), pid);
}
else //erreur
OnError("Could not fork\n");
return 0;
}
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
Nous avons vu que certaines pages de la mémoire virtuelle:
Nous allons voir un appel système qui permet d'associer un segment de mémoire virtuelle à un segment de fichier. Cet appel système est par exemple utilisé pour charger les librairies partagées.
Associer le segment (une partie) d'un fichier à un nouveau segments de mémoire partagé (file mapping). Cela permet de:
Un tel fichier ne sera pas chargé intégralement en mémoire mais page par page au fur et a mesure des fautes de page du processus.
Deux processus peuvent partager un segment en y associant des espaces d’adressage virtuel différents:
Pour associer un fichier à un espace de la mémoire virtuelle du processus on:
fd
:int open(const char *pathname, int flags);
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
int munmap(void *addr, size_t length);
int close(int fd);
On peut ouvrir un fichier avec l'appel système suivant:
int open(const char *pathname, int flags);
pathname
est le nom du fichier;flags
est un champ de bit indiquant le mode d'accès au fichier (O_RDONLY, O_WRONLY, O_RDWR
);errno
).On doit fermer un fichier avec l'appel système suivant:
int close(int fd);
fd
est l'entier représentant le fichier (descripteur de fichier);errno
).
#include <sys/mman.h>
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
fd
: entier représentant le fichier (file descriptor);addr
: adresse d‘un début de page (ajustée automatiquement), si NULL l'adresse est choisie automatiquement;offset
: début du mapping dans le fichier, doit être multiple de la taille d'une pagelength
: taille du mapping dans le fichier, complété par des zéros pour remplir une page en mémoire
#include <sys/mman.h>
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
prot
, bit field définissant la protection des pages partagées:
PROT_READ / PROT_WRITE / PROT_EXEC
autorise respectivement la lecture, l’écriture et l’exécution;PROT_NONE
aucun droit, utilisé pour réserver des pages;flags
, bit field utilisé pour les options suivantes:
MAP_SHARED
: la zone est partagée entre les processus / fichiers toute modification sera reportée aux autres processus et dans le fichier;MAP_PRIVATE
: copy-on-write, si un processus modifie le contenu il crée sa propre copie des pages et le fichier ne sera pas modifié;MAP_ANONYMOUS
: pas d’association avec un fichier, la mémoire est initialisée à 0 (fd et offset sont ignorés) et partageable uniquement avec ses enfants.strace
sur n'importe quel programme, expiquez les premiers appels systèmes