Système d'exploitation

TBD suite des principes systèmes où on rentre plus dans les détails.

Mémoire

Process

Un process est l'unité de base d'un programme. Un process est un ensemble d'instruction exécutées par le système d'exploitation. Tout process est la propriété le l'utilisateur du système qui l'a exécuté.

Noyau

Le noyau est partie intégrante de tout process. Il est toujours là et s'exécute de temps en temps pour effectuer ses tâches.

Multi Process

Les systèmes d'exploitation permettent tous d'exécuter plusieurs threads de façon concurrente :

Ceci peut se passer même si l'ordinateur ne possède qu'un seul core. La quasi totalité des ordinateurs actuellement sont multi-core, ce qui permet même d'exécuter des threads de façon parallèle.

Chaque thread sur une machine peut–être dans 3 états distincts :

Le principe du multi-processing est est simple. Prenons l'exemple de 2 threads (A et B) à exécuter sur un unique core. Lorsque le noyau lance l'exécution du premier thread, il va demander au processeur de se faire réveiller au bout de 10ms par une interruption. Le thread A s'exécute donc pendant 10ms avant qu'une interruption ne rappelle le noyau qui va pouvoir stopper le thread A (qu'il va placer en mode activable) et réactiver le thread B (il passe de activable à activé). Une fois ceci fait, le noyau se rendort pour 10ms (via une interruption) et le cycle continue.

Si l'ordinateur possède plusieurs core, le noyau choisi sur quel lancer le thread mais le principe est le même : il se réveille à intervalles déterminés pour gérer l'activation et la désactivation des threads.

Cette activation/ désactivation s'appelle le context switching et n'est pas immédiate, il faut en effet s'assurer que le thread B n'endommage pas l'exécution du thread A.

Context switching

L'exécution d'un thread dépend :

Si l'on exécuter un thread B sur un core qui exécute déjà un thread A il faut :

S'il est facile de sauver/restaurer tout les registres d'un core (il n'y en a pas beaucoup et sont de petite taille) il n'est pas possible de sauver toute la mémoire.

Même si on ne considère que la mémoire effectivement utilisée par le process (les segments de code, données, pile, tas et bibliothèques partagées) il y en a beaucoup trop pour que le transfert vert le disque dur ne soit pas prohibitif niveau temps. En revanche, ces segments sont loin de remplir toute la mémoire : il y a (normalement) la place de contenir les segments utilisées pour deux threads

L'idée est donc de trouver un moyen efficace en temps pour :

Pour cela, on commence par découper toute la mémoire en pages (habituellement de 4KiB) et d'avoir une correspondance entre une mémoire logique, celle que voit le thread et la mémoire physique, en RAM.

On (le noyau) conserve pour chaque thread la correspondance des adresses physiques pour chaque page utilisée par ses segments :

mémoire physique  process A    process B    segments
      a              a           a           noyau
      b              f           b           pile
      c                          e
      d
      e
      f              c           c           bibliothèques partagées
      g
      h              d           k           tas
      j
      j              h           g           data
      l              i           j           code
      0

On voit que le process A et B partagent certaines pages (le code du noyau et les bibliothèques partagées), et qu'il reste de la place pour au moins un troisième process en mémoire.

Lorsque la mémoire RAM physique ne suffit plus pour stocker toutes les données de tous les process, le système d’exploitation possède une partie spécifique du disque dur appelée swap qui permet de transférer de la RAM au disque dur et réciproquement si nécessaire.

Avec cette astuce de mémoire logique (vous verrez aussi parfois le terme de virtuelle) et physique, changer de contexte d'exécution pour arrêter un thread et en activer un autre est très rapide ! Encore pus rapide si les deux threads font partie du même process.

Ordonnancement

Si on doit exécuter $n$ thread sur $p < n$ cores il faut :

Comme chaque thread possède le code du noyau et que chaque core possède un thread : le noyau est présent sur chaque core. Il est ainsi possible pour le noyau :

Le noyau peut donc choisir :

  1. quel thread exécuter sur chaque core
  2. quel thread activer par core à un moment donné
  3. combien de temps laisser activé un thread avant d'en activer un autre

Exemple

Un exemple simple d’ordonnancement avec des entrées sorties qui ralentissent l'exécution des process : https://www.youtube.com/watch?v=sxSv4ZdQTOQ

TBD : à expliciter.

Core et thread

Le noyau possède pour chaque core une liste de threads qu'ils gère et il peut décider de déplacer un thread d'un core à un autre pour garantir une charge de travail équilibrée entre les différents core. Pour cela, le noyau possède un thread par core dont le but est de migrer si nécessaire un thread d'un core à un autre.

Choisir quel thread exécuter sur quel core pour minimiser le temps total d'exécution est un problème difficile, le noyau a donc des heuristiques de choix simples pour pouvoir décider souvent plutôt qu'optimalement.

Le problème dérive du problème partition qui est un problème NP-complet, (c'est à dire universel) : on ne connaît pas d'autre algorithme que de tester toutes les possibilités.

Pour s'en convaincre, prenons une machine à 2 cores et $n$ processus dont on connaît les temps d'exécution. Minimiser le temps d'exécution des $n$ process revient à séparer les $n$ en deux groupe dont la somme des temps d'exécution est égale.

Activation et désactivation d'un thread

Plusieurs possibilités existent, chacune avec leurs avantages et inconvénient :

Les algorithmes basé sur la priorité doivent pouvoir rapidement :

Pour cela, on utilise une file de priorité implémentée sous la forme d'un arbre de recherche rouge-noir ce qui permet de garantir des opérations en $\mathcal{O}(\log(N))$.

Voir ce document pour le scheduler par défaut de Linux.

Temps d'activation

Voir https://opensource.com/article/19/2/fair-scheduling-linux pour le scheduler actuel de Linux.

L'algorithme utilisé actuellement (CFS) procède ainsi. Dans un temps donné T, s'il y a $N$ thread à exécuter, on allouera $T/N$ temps à tout le monde.

On s'assure en plus que ce temps est supérieur au temps nécessaire pour le context switching, ce qui assure que tout thread pourra au moins être exécuté 1 cycle.

Création de process/thread

L'ensemble des process est organisé en arbre, à partir du premier process lancé par le noyau.

Un process est composé :

Création de thread

Il suffit d'allouer une page pour sa pile et créer un contexte d'exécution en associant une valeur au pointeur de pile et au pointeur d'instruction (la première instruction du thread, une fonction du code).

Création de process

La création d'un nouveau process se fait par duplication d'un autre process. C'est un appel système.

Le nouveau process possède :

Ce qui différencie le nouveau process de l'ancien est que le retour du syscall de duplication vaut :

Pour que la duplication des pages mémoires ne soit pas longue, on utilise la technique du copy on write : on garde la même page pour les deux process, on ne duplique la page mémoire que si un des deux process veut modifier la page.

Les process sont ainsi organisé en arbre. Chaque process ayant un parent, à part le premier process exécuté par le noyau dont le but est de créer tous les démons et de finir par le process de login.

Lorsqu'un process se termine (ou est tué par un signal) il commence par terminer ses enfants (en leur envoyant un signal de fin).

un exemple en python de l'utilisation de fork() pour dupliquer un process.

Système de fichiers

La mémoire permet de stocker et gérer l'information nécessaire pour l'exécution de process. Il est cependant nécessaire d'avoir un espace de stockage permanent pour stocker les programmes ou les données lorsque l'ordinateur est éteint.

On stocke ces informations sous la forme de fichiers organisés en arborescence.

Démarrage de l'ordinateur

Les différentes étapes du chargement d'un système d'exploitation

  1. boot de l'ordinateur
  2. exécution d'un chargeur d'amorçage (bootloader)
  3. charge le noyau
    1. vérification du matériel
    2. vérification des sous-systèmes : réseau, ...
  4. passage en user mode puis charge les démons et les interfaces
  5. login

Bibliographie