cours/content/systemes_exploitation/2-processus/index.md

8.9 KiB

title date tags categories
Systèmes d'exploitation : Les processus 2021-09-17
système
appels système
processus
Systèmes d'exploitation
Cours

Les processus sont des instances vivants de programmes. Un programme représente du code binaire stocké sur un support de stockage.

Un processus est composé d'un espace d'adressage en mémoire et d'un contexte d'exécution. Plus d'information est disponible [dans les cours de prog. système]({{<ref "../../progsys/3-processus/index.md">}} "Les processus")

Accès à la mémoire

L'espace d'adressage contient des segments mémoire :

  • le segment de texte / de code : les instructions optimisées par le compilateur, souvent en lecture seule dans les systèmes modernes.
  • le segment data contenant lui même le segment des données initialisées et le BSS (données non-initialisées)
  • le tas, zone de mémoire dynamique gérée par la libc par l'utilisation de malloc() et free(). Le système ne peut détecter un accès en dehors de la plage définie par un malloc(). Lorsque le tas n'a plus d'espace alors la libc effectue un appel système (mais le noyau peut refuser d'allouer)
  • la pile d'exécution, sa taille est de 8MiB maximum sous Linux. Ce segment contient les paramètres des fonctions et leurs variables locales.
  • les librairies partagées mappées à la demande.

Il est possible de voir les espaces de mémoire alloués pour un processus donné :

cat /proc/self/maps
55fee9705000-55fee9707000 r--p 00000000 fe:01 1979024                    /usr/bin/cat
55fee9707000-55fee970c000 r-xp 00002000 fe:01 1979024                    /usr/bin/cat
55fee970c000-55fee970f000 r--p 00007000 fe:01 1979024                    /usr/bin/cat
55fee970f000-55fee9710000 r--p 00009000 fe:01 1979024                    /usr/bin/cat
55fee9710000-55fee9711000 rw-p 0000a000 fe:01 1979024                    /usr/bin/cat
55feead5a000-55feead7b000 rw-p 00000000 00:00 0                          [heap]
7fbcfa322000-7fbcfa344000 rw-p 00000000 00:00 0 
7fbcfa344000-7fbcfa62c000 r--p 00000000 fe:01 1987533                    /usr/lib/locale/locale-archive
7fbcfa62c000-7fbcfa62e000 rw-p 00000000 00:00 0 
7fbcfa62e000-7fbcfa654000 r--p 00000000 fe:01 1969528                    /usr/lib/libc-2.33.so
7fbcfa654000-7fbcfa79f000 r-xp 00026000 fe:01 1969528                    /usr/lib/libc-2.33.so
7fbcfa79f000-7fbcfa7eb000 r--p 00171000 fe:01 1969528                    /usr/lib/libc-2.33.so
7fbcfa7eb000-7fbcfa7ee000 r--p 001bc000 fe:01 1969528                    /usr/lib/libc-2.33.so
7fbcfa7ee000-7fbcfa7f1000 rw-p 001bf000 fe:01 1969528                    /usr/lib/libc-2.33.so
7fbcfa7f1000-7fbcfa7fc000 rw-p 00000000 00:00 0 
7fbcfa80f000-7fbcfa810000 r--p 00000000 fe:01 1969517                    /usr/lib/ld-2.33.so
7fbcfa810000-7fbcfa834000 r-xp 00001000 fe:01 1969517                    /usr/lib/ld-2.33.so
7fbcfa834000-7fbcfa83d000 r--p 00025000 fe:01 1969517                    /usr/lib/ld-2.33.so
7fbcfa83d000-7fbcfa83f000 r--p 0002d000 fe:01 1969517                    /usr/lib/ld-2.33.so
7fbcfa83f000-7fbcfa841000 rw-p 0002f000 fe:01 1969517                    /usr/lib/ld-2.33.so
7ffccf574000-7ffccf595000 rw-p 00000000 00:00 0                          [stack]
7ffccf5c8000-7ffccf5cc000 r--p 00000000 00:00 0                          [vvar]
7ffccf5cc000-7ffccf5ce000 r-xp 00000000 00:00 0                          [vdso]
ffffffffff600000-ffffffffff601000 --xp 00000000 00:00 0                  [vsyscall]

On y voit biens les adresses de début, ceux de fin, les droits (read, write, execute, private)

L'accès par un processus à un espace mémoire invalide donne lieu à la fameuse segmentation fault. Mais il est tout à fait possible de lire et écrire vers une zone non allouée du tas. Par exemple j'initialise un tableau de 10 éléments et le rempli avec une boucle de 15 itérations.

Attributs d'un processus

En plus de l'espace mémoire alloué pour le processus, le noyau stocke en mémoire un ensemble d'attributs : son identitiants (PID), sa priorité, l'UID (réel/effectif), la table des descripteurs de fichiers, la table des signaux, un espace pour sauvegarder les registres (changement de contexte, reprise sur interruption).

Création et vie des processus

Un processus voulant en créer un autre doit faire un appel système fork. Lors du changement de contexte, les registres du processus p0 sont sauvegardés puis remplacés par ceux de p1.

Les signaux sont délivrés au processus lors du passage du noyau à l'exécution.

Processus bloquants

Lorsqu'un processus attends un appel bloquant (par exemple read()) il est muse en sommeil. Lorque l'interruption est lancée, alors le noyau réveille le processus.

Ordonnancement

L'ordonnancement essaye définir un fonctionnement universel visant à organiser l'exécution concurente de processus sur un CPU. Un fonctionnement universel, convenant donc à tous les usages, est impossible à obtenir. Il dépend en effet de l'utilisation qui en est fait : interactif, temps-réel etc.

Dans le cadre d'un système interactif, la réactivité est la caractéristique la plus importante.

Stratégie

Le type de système influe donc sur la stratégie à adopter. nous allons en détailler certaines.

FIFO - First In First Out

Une liste chainée de processus, on exécute le premier jusquà la fin de son exécution ou qu'il soit bloqué puis le second et ainsi de suite.

C'est une technique facile à implémenter, elle est très peu couteuse en temps processeur (le noyau intervient peu, peu de changement de contexte) mais comporte un gand risque de famine : un processus en boucle ne rendrai jamais la main.

Round-Robin

Un temporisateur valable pour tous: le changent de contexte intervient toute les 10ms par exemple. C'est une technique facile à implementer, il y a plus de famine mais on ne gère pas de priorité. S'il y a beaucoup de processus, alors notre éditeut de texte sera moind réactif.

Priorité stricte

Les processus sont triés par priorité et les plus important son exécutés en premier. contrairement au Round-Robin on gère la priorité mais ette technique est discriminatoire. Comment assigner les priorités? Au faciès?

Priorité dynamique

La priorité change au cours de la vie du processus car il change de comportement.

Dans le cas d'une opération de compilation par exemple, le compilateur lit les fichiers sources effectuant beaucoup de read et se bloque régulièrement donc. Ensuite il compile et utilise beaucoup de CPU.

L'ordonnanceur observe donc les métriques du passé pour prévoir l'avenir. Dans l'example du compilateur, le noyau observe que sur les 10ms de temporisation, notre processus s'est bloqué (et change de contexte) au bout de 1ms. Puis losqu'il compile, notre compilateur va rester sur le CPU pour toute sa tempotisation.

Mais comment choisir la bonne priorité en fonction de ces métriques? Tout simplement en choisissant d'abord les processus les plus courts. C'est la stratégie utilisée en général dans les système interactifs.

La stratégie utilisée dans le noyau Linux 2.4

Cette version du noyau Linux, la gestion de l'ordonnancement se fait par l'attribution de crédits. Un processus utilisant l'UC le fait en dépensant des crédits, plus il l'utilise plus il en dépense.

Lorqu'un processus n'a plus de crédit, il ne peut plus utiliser l'UC jusqu'à ce que le noyau en redistribue. Il le fait lorque aucun processus prêt n'a de crédit.

Les processus n'ayant pas dépensé tous ses crédits se voit prélever un "impots":

Crédit = Cn + (Cn-1/2) + (Cn-2/4) + (Cn-3/8) + ...

Dans la limite de 2C.

Et pour les système multi-cœur

Chaque cœur exécute un ordonnanceur de façon asynchrone, la liste de processus peut-être :

  • partagée entre tous les cœurs
  • distribuée par cœur

Il est bon de noter qu'une UC peut envoyer une interruption à un autre UC.

Threads et processus

Un processus est un espace d'adressage plus une pile d'exécution. Un thread est juste un autre flow d'exécution dans le même espace d'adressage. La création de threads (aussi appelés processus légers) est dons plus efficace : il n'y a pas de création d'espace d'adressage.

Dans les noyaux modernes, tout est thread.

Accès concurents à la mémoire

Les threads partagent donc des espace commun de mémoire, il est donc important de gérer des accès concurrent. En effet l'accès aux mêmes cases mémoires par plusieurs threads peut conduire à des fonctionnements arbitraires.

Il est à noter que les accès à la mémoire sont de toute façon atomique: une opération de lecture ou écriture à la fois. Mais ce n'est pas suffisant, un example de code est disponible [dans les cours de prog. système]({{<ref "../../progsys/5_les-processus_legers/index.md">}} "Les processus légers")

Le noyau doit donc mettre en place des primitive de synchronisation.