Processus et virtualisation

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

Brain-storming

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 ?

Le processus en bref

Un processus représente l’exécution courante d’un programme. Il contient donc toutes les informations nécessaire à l’exécution du programme.

De l'executable au processus

Mémoire virtuelle

Espace d’adressage du processus (mémoire virtuelle)

Espace d'adressage

Exercice

Créer un programme en C qui:

  • déclare des variables globales
  • déclare des variables locales (e.g. dans une fonction)
  • utilise la fonction malloc pour allouer de la mémoire
  • appelle une fonction autre que la fonction main
  • affiche TOUTES les adresses des objects déclarés ci-dessus (y compris les fonctions et les adresses des pointeurs)
  • attend une entrée utilisateur ou se met en pause

Correction

Click here to download

Exercice

En utilisant la commande pmap:

  • observer les différents segments du processus
  • comparer les adresses des segments avec les adresses des variables de votre programme
  • confirmer la bonne répartition des données dans les segments

Correction

Objectif de la mémoire virtuelle

Grâce la mémoire virtuelle on va pouvoir:

  • définir un espace d’adressage indépendant pour chaque processus;
  • adresser plus de mémoire que la mémoire physique disponible;
  • partager facilement des zones de mémoire entre processus;
  • adresser le contenu de fichiers comme s’il étaient en mémoire.

Virtualisation de la mémoire

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).

Virtualisation

Conversion adr. virtuelle -> adr. physique

Elle est réalisée par le matériel (Memory Management Unit - MMU):

Convertion

Table des pages

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:

  • numéro de page physique;
  • taille d’une page;
  • permissions d’accès;
  • bit «page valide» ou «page présente en RAM»;
  • bit «page sale» (i.e. modifiée depuis sa dernière présence sur disque);

Défaut de page

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:

  • l’accès mémoire est illégal -> le noyaux termine le processus en «segmentation fault» (SIGSEG);
  • La page est présente en mémoire physique , c’est un défaut de page mineur -> il suffit de mettre à jour la table du processus pour la faire pointée sur la page en mémoire physique;
  • La page n’est pas présente en mémoire physique, c’est un défaut de page majeur .

Défaut de page majeur

Pour un défaut de page majeur il faut charger la page manquante:

  • on sauvegarde l’état du processus et on le mets «en attente»;
  • si il n’y a pas de place en mémoire physique on libère une page peu utilisée;
  • on charge la page manquante en mémoire depuis le disque;
  • on mets à jour la table des pages du processus;
  • on charge l’état du processus et on repart de l’instruction ayant provoquée la faute de page (cette fois satisfaite).

A noter que lorsqu’une page est libérée en mémoire physique soit:

  • elle existe déjà sur le disque car elle n’a pas été modifié (i.e. bit «page sale» = 0), dans ce cas il suffit de remplacer cette page physique par la nouvelle
  • elle à été modifiée et est mise en swap pour conserver les modifications.

Verrouillage de la mémoire

Il est possible de demander au noyaux de verrouiller des pages virtuelles en mémoire physique. Cela:

  • évite les défauts de page majeur pour ce processus -> rapidité d’accès;
  • pas de swap, donc moins de persistance de l’information -> sécurité.

						#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 */
					

Exercice

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 ?

Processus

Structure d'un processus

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:

  • l’état du processeur qui l’exécute:
    • les registres accessibles au programme;
    • l’instruction courante (compteur ordinal);
    • les informations de pagination (tables des pages...);
  • son espace mémoire virtuel -> les données et le programme;
  • les ressources dont il dispose;
  • des informations administratives:
    • PID, utilisateur(s), Session ID, Groupe ID;
    • priorités (statique et dynamique);
    • consommation de ressources.

Structure d'un processus

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 */
								/* ... */
							}
						

Création de processus

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.

Création de processus

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:

  • l’enfant a son propre PID et son PPID est égale au PID du parent;
  • pas d’héritage des verrous mémoire et fichiers (mlock, flock).

Création de processus

Le nouveau processus va donc partager des pages avec son processus parent.

Création de processus et mémoire virtuelle

Copy on write

Ces pages seront copiées uniquement lors de modifications de la mémoire. C’est ce que l’on appelle le «copy on write».

Mécanisme de copy on write

Terminaison de processus

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:

  • ferme les descripteurs de fichiers ouverts (inclus STDIN, STDOUT, STDERR);
  • envoi le signal SIGCHLD au parent pour l’informer de la mort de l’enfant;
  • tous les enfants du processus deviennent orphelins;
  • appelle les fonctions enregistrées par atexit (c.f. man).

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
						

Processus zombies

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é.

Eviter les zombies

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éé.

Processus orphelins

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:

  • a été préalablement définit comme processus subreaper (c.f. prctl);
  • est le plus proche ancêtre subreaper de Po;
  • doit géré le décès de ses enfants (c.f. wait).

Le processus 1 (e.g. init, systemd) est un processus subreaper.

Exemple processus orphelins


#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;
}
					

Questions

Un processus orphelin peut-il rester un zombie longtemps

Dans quels cas un processus peut rester un zombie longtemps ?

Execution de processus

Exec*

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:

  • retourne -1 en cas d'erreur (+ errno mis à jour)
  • remplace les segments du processus courant par les segments de l’éxécutable filename (c.f. Fichiers ELF);
  • les paramètres argv et envp sont disponibles dans le main du programme appelé.

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.

Execve: exemple

Les types de fichiers compilés

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

Organization d'un fichier ELF

  • des segments qui:
    • permettent à l'OS de préparer le programme pour son exécution (c.f. exec*);
    • contiennent une ou plusieurs sections;

  • des sections qui:
    • contiennent TOUTES les informations du programme (pas forcément nécessaire à l’exécution – e.g. débogage);
    • sont nécessaires pour la phase de liage;

  • Des entêtes et tables qui:
    • indiquent la position de chaque section;
    • indiquent la position de chaque segment;
    • indiquent la position de la table des sections et de la table des segments.

Fichier ELF - Entête

On peut observer le contenu d’un fichier ELF avec les commandes objdump et readelf.

Entête Table des segments Section .text Section .rodata ... Section .data Section .bss Section .debug ... Table des sections

									#include <elf.h>
									typedef struct {
									//les variables ci-dessous ne sont pas listée dans l’ordre de l’entête
									...
									uint16_t e_type; /* Executable, bibliothèque, objet, ... */
									uint_16 e_machine; /* Intel, HP,...*/
									ElfN_Addr e_entry; /* Première instruction à exécuter par le processus */

									ElfN_Off e_phoff; /* Offset de départ de la table des segments */
									uint16_t e_phentsize; /* Taille d’une entrée dans la table des segments*/
									uint16_t e_phnum; /* nombre d’entrées dans la table des segments */

									ElfN_Off e_shoff; /* Offset de départ de la table des sections */
									uint16_t e_shentsize; /* Taille d’une entrée dans la table des sections*/
									uint16_t e_shnum; /* nombre d’entrées dans la table des sections*/
									...
									} ElfN_Ehdr;
								

Fichier ELF - sections

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

Fichier ELF - segment

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.

Espace virt. proc.

						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

Memory-mapped file (Fichier en mémoire partagée)

Rappel

Nous avons vu que certaines pages de la mémoire virtuelle:

  • ne sont pas présentes en mémoire physique mais réside sur des systèmes de fichiers (swap / fichiers executables)
  • deviennent disponibles au fur et à mesure des fautes de pages
  • sont partagées entre plusieurs processus (e.g. librairies partagées)
  • peuvent être partagées uniquement jusqu'à leur modification ("copy-on-write")

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.

Principe du "file mapping"

Associer le segment (une partie) d'un fichier à un nouveau segments de mémoire partagé (file mapping). Cela permet de:

  • partager des pages (données, instructions) entre plusieurs processus;
  • accéder aux données directement en mémoire (i.e. par pointeurs) plutôt que dans un fichier (i.e. par curseur)

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.

Principe du "file mapping"

Deux processus peuvent partager un segment en y associant des espaces d’adressage virtuel différents:

Shared memory

Le "file mapping" en pratique

Pour associer un fichier à un espace de la mémoire virtuelle du processus on:

  • ouvre le fichier en lecture et/ou écriture pour obtenir un descripteur de fichier fd:
  • int open(const char *pathname, int flags);
  • associe le descripteur de fichier à une zone de la mémoire virtuelle par un appel à:
  • void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
  • pense ensuite à fermer / désassocier la mémoire partagée:
  • int munmap(void *addr, size_t length);
  • ferme le fichier:
  • int close(int fd);

open / close

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);
  • retourne un entier représentant le fichier (descripteur de fichier), soit -1 en cas d'erreur (vérifier 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);
  • 0 en cas de succès, -1 en cas d'erreur (vérifier errno).

mmap


						#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 page
  • length: taille du mapping dans le fichier, complété par des zéros pour remplir une page en mémoire
  • retourne l'adresse virtuelle correspondant au début du segment
File mapping

mmap


						#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;
  • les droits doivent correspondre au mode d’ouverture du fichier.

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.

Questions + Exemple

  • En utilisant la commande strace sur n'importe quel programme, expiquez les premiers appels systèmes
  • Est-ce que les librairies seront chargée immédiatement en mémoire physique ?

Exemple mmap en bonus