Cet article fait partie de notre série de présentations du noyau UNIX en cours.
Dans l'article précédent de cette série, nous avons discuté de la vue d'ensemble du processus UNIX.
Cet article explique en détail les noyaux réentrants, la synchronisation et les sections critiques de l'architecture du noyau UNIX.
Noyaux réentrants
Comme son nom l'indique, un noyau réentrant est celui qui permet à plusieurs processus de s'exécuter en mode noyau à un moment donné et cela sans causer de problèmes de cohérence entre les structures de données du noyau.
Eh bien, nous savons que dans un système à processeur unique, un seul processus peut s'exécuter à un instant donné, mais il peut y avoir d'autres processus bloqués en mode noyau en attente d'exécution.
Par exemple, dans un noyau réentrant, un processus en attente d'un appel "read()" peut décider de libérer du CPU pour un processus en attente d'exécution en mode noyau.
Maintenant, on pourrait se demander pourquoi un noyau est rendu réentrant ? Eh bien, commençons par un exemple où un noyau n'est pas réentrant et voyons ce qui se passe s'il permet à plusieurs processus de s'exécuter en mode noyau.
Supposons qu'un processus s'exécute en mode noyau et accède à une structure de données du noyau et à certaines valeurs globales qui lui sont associées.
- Supposons que le nom du processus soit "A".
- Maintenant, 'A' accède à une variable globale pour voir si la valeur est différente de zéro (afin qu'il puisse faire des calculs, etc.) et juste avant d'essayer d'utiliser cette valeur dans une partie de sa logique, un changement de contexte pour traiter ' B' arrive.
- Maintenant, ce processus "B" essaie d'accéder à la valeur de la même variable globale et la décrémente.
- Un autre changement de contexte se produit et le processus « A » revient en exécution.
- Puisque "A" ne sait pas que "B" a déjà décrémenté la valeur, il essaie à nouveau d'utiliser cette valeur.
- Voici donc le hic, le processus "A" voit deux valeurs différentes de la variable globale car la valeur a été modifiée par un autre processus "B".
Donc, maintenant nous savons pourquoi un noyau doit être réentrant. Une autre question qui peut se poser est comment rendre un noyau réentrant ?
Sur une note de base, les points suivants pourraient être pris en compte pour rendre un noyau réentrant :
- Écrire des fonctions du noyau qui modifient uniquement les variables locales (pile) et ne modifient pas les variables globales ou les structures de données. Ce type de fonctions est également connu sous le nom de fonctions réentrantes.
- Adhérer strictement à l'utilisation des seules fonctions réentrantes dans un noyau n'est pas une solution faisable. Ainsi, une autre technique utilisée est celle des "mécanismes de verrouillage" qui garantissent qu'un seul processus peut utiliser une fonction non réentrante à un moment donné.
À partir des points ci-dessus, il est clair que l'utilisation de fonctions réentrantes et de mécanismes de verrouillage pour les fonctions non réentrantes est au cœur de la création d'un noyau réentrant. Étant donné que l'implémentation de fonctions réentrantes est davantage liée à une bonne programmation, les mécanismes de verrouillage sont liés au concept de synchronisation.
Synchronisation et sections critiques
Comme indiqué ci-dessus dans un exemple, un noyau réentrant nécessite un accès synchronisé aux variables globales et aux structures de données du noyau.
Le morceau de code qui opère sur ces variables globales et structures de données est appelé section critique.
Si un chemin de contrôle du noyau est suspendu (lors de l'utilisation d'une valeur globale ou d'une structure de données) en raison d'un changement de contexte, aucun autre chemin de contrôle ne doit pouvoir accéder à la même valeur globale ou structure de données. Sinon, cela pourrait avoir des effets désastreux.
Si nous regardons en arrière et voyons pourquoi nous avons besoin de synchronisation ? La réponse est d'utiliser en toute sécurité les variables globales du noyau et les structures de données. Eh bien, cela peut également être réalisé grâce à des opérations atomiques. Une opération atomique est une opération qui sera toujours exécutée sans qu'aucun autre processus ne puisse lire ou modifier l'état lu ou modifié au cours de l'opération. Malheureusement, les opérations atomiques ne peuvent pas être appliquées partout. Par exemple, la suppression d'un élément d'une liste chaînée à l'intérieur du noyau ne peut pas être transformée en une opération atomique.
Concentrons-nous maintenant sur la façon de synchroniser les chemins de contrôle du noyau.
Désactivation de la préemption du noyau
La préemption du noyau est un concept dans lequel le noyau permet la suspension/interruption forcée d'une tâche et met en exécution une autre tâche hautement prioritaire qui attendait les ressources du noyau.
En termes plus simples, il s'agit de la commutation de contexte des processus en mode noyau où le processus en cours d'exécution est suspendu de force par le noyau et l'autre processus est mis en exécution.
Si nous suivons la définition, nous réalisons que c'est cette capacité même du noyau (de préempter lorsque les processus sont en mode noyau) qui provoque des problèmes de synchronisation. Une solution au problème consiste à désactiver la préemption du noyau. Cela garantit que le changement de contexte en mode noyau ne se produit que lorsqu'un processus qui s'exécute actuellement en mode noyau libère volontairement le processeur et s'assure que toutes les structures de données du noyau et les variables globales sont dans un état cohérent.
De toute évidence, désactiver la préemption du noyau n'est pas une solution très élégante et cette solution tombe à plat lorsque nous utilisons des systèmes multiprocesseurs car deux processeurs peuvent accéder simultanément à une même section critique.
Désactivation de l'interruption
Un autre mécanisme qui peut être appliqué pour réaliser la synchronisation à l'intérieur du noyau est qu'un processus désactive toutes les interruptions matérielles avant d'entrer dans une région critique et les active après avoir quitté cette région très critique. Encore une fois, cette solution n'est pas une solution élégante car dans les cas où la région critique est grande, les interruptions peuvent être désactivées pendant très longtemps, ce qui va à l'encontre de leur propre objectif d'être une interruption et peut provoquer le gel des activités matérielles.
Sémaphores
Il s'agit de la méthode la plus populaire pour assurer la synchronisation à l'intérieur du noyau.
Il est efficace sur les systèmes monoprocesseurs et multiprocesseurs. Selon ce concept, un sémaphore peut être considéré comme un compteur associé à chaque structure de données et vérifié par tous les threads du noyau lorsqu'ils tentent d'accéder à cette structure de données particulière.
Un sémaphore contient des informations sur la valeur du compteur, une liste des processus en attente d'acquérir le sémaphore (pour accéder à la structure de données) et deux méthodes pour augmenter ou diminuer la valeur du compteur associé à ce sémaphore.
La logique de fonctionnement est la suivante :
- Supposons qu'un processus veuille accéder à une structure de données particulière, il vérifiera d'abord le compteur associé au sémaphore de la structure de données.
- Si le compteur est quelque chose de positif, le processus acquiert Semaphore, décrémente la valeur du compteur, exécute la région critique et incrémente le compteur Semaphore.
- Mais si un processus trouve la valeur du compteur égale à zéro, alors le processus est ajouté à la liste (associée au sémaphore) des processus attendant d'acquérir le sémaphore.
- Maintenant, chaque fois que le compteur devient positif, tous les processus en attente du sémaphore essaient de l'acquérir.
- Celui qui acquiert à nouveau diminue le compteur, exécute la région critique, puis augmente le compteur pendant que les autres processus reviennent en mode d'attente.
Éviter les blocages
Travailler avec un schéma de synchronisation comme les sémaphores a pour effet secondaire les « blocages ».
Prenons un exemple :
- Supposons qu'un processus A acquiert un sémaphore pour une structure de données particulière tandis que le processus B acquiert un sémaphore pour une autre structure de données.
- Maintenant, à l'étape suivante, les deux processus veulent acquérir le sémaphore pour les structures de données qui sont acquises l'un par l'autre, c'est-à-dire que le processus A veut acquérir le sémaphore qui est déjà acquis par le processus B et vice-versa.
- Ce type de situation où un processus attend qu'un autre processus libère une ressource tandis que l'autre attend que le premier libère une ressource est appelé blocage.
- Les interblocages peuvent entraîner un gel complet des chemins de contrôle du noyau.
Ces types de blocages sont plus fréquents dans les conceptions où un grand nombre de verrous du noyau sont utilisés. Dans ces conceptions, il devient extrêmement difficile de déterminer qu'une condition de blocage ne se produirait jamais. Dans les systèmes d'exploitation comme Linux, les blocages sont évités en les acquérant dans l'ordre.