Existe-t-il une relation entre un noyau et un thread utilisateur?
Certains manuels du système d'exploitation indiquaient que "maps un (plusieurs) thread utilisateur à un (plusieurs) thread noyau". Que signifie map ici?
Quand ils disent map, cela signifie que chaque thread du noyau est affecté à un certain nombre de threads en mode utilisateur.
Les threads du noyau sont utilisés pour fournir des services privilégiés aux applications (tels que les appels système). Ils sont également utilisés par le noyau pour garder une trace de tout ce qui s'exécute sur le système, de la quantité de ressources allouées à quel processus et de les planifier.
Si vos applications font un usage intensif des appels système, davantage de threads utilisateur par thread du noyau et vos applications s'exécuteront plus lentement. En effet, le thread du noyau deviendra un goulot d'étranglement, car tous les appels système le traverseront.
D'un autre côté cependant, si vos programmes utilisent rarement des appels système (ou d'autres services du noyau), vous pouvez affecter un grand nombre de threads utilisateur à un thread du noyau sans trop de pénalité en termes de performances, autres que la surcharge.
Vous pouvez augmenter le nombre de threads du noyau, mais cela ajoute des frais généraux au noyau en général, donc bien que les threads individuels soient plus réactifs en ce qui concerne les appels système, le système dans son ensemble deviendra plus lent.
C'est pourquoi il est important de trouver un bon équilibre entre le nombre de threads du noyau et le nombre de threads utilisateur par thread du noyau.
http://www.informit.com/articles/printerfriendly.aspx?p=25075
Implémentation de threads dans l'espace utilisateur
Il existe deux façons principales d'implémenter un package de threads: dans l'espace utilisateur et dans le noyau. Le choix est modérément controversé, et une implémentation hybride est également possible. Nous allons maintenant décrire ces méthodes, ainsi que leurs avantages et inconvénients.
La première méthode consiste à placer le package de threads entièrement dans l'espace utilisateur. Le noyau n'en sait rien. En ce qui concerne le noyau, il gère des processus ordinaires à un seul thread. Le premier avantage, et le plus évident, est qu'un package de threads au niveau utilisateur peut être implémenté sur un système d'exploitation qui ne prend pas en charge les threads. Tous les systèmes d'exploitation appartenaient à cette catégorie, et même maintenant certains le font encore.
Toutes ces implémentations ont la même structure générale, qui est illustrée sur la figure 2-8 (a). Les threads s'exécutent au-dessus d'un système d'exécution, qui est un ensemble de procédures qui gèrent les threads. Nous en avons déjà vu quatre: thread_create, thread_exit, thread_wait et thread_yield, mais généralement il y en a plus.
Lorsque les threads sont gérés dans l'espace utilisateur, chaque processus a besoin de sa propre table de threads privée pour garder une trace des threads dans ce processus. Cette table est analogue à la table de processus du noyau, sauf qu'elle ne garde qu'une trace des propriétés par thread telles que le compteur de programme de chaque thread, le pointeur de pile, les registres, l'état, etc. La table des threads est gérée par le système d'exécution. Lorsqu'un thread est déplacé à l'état prêt ou bloqué, les informations nécessaires pour le redémarrer sont stockées dans la table des threads, exactement de la même manière que le noyau stocke des informations sur les processus dans la table des processus.
Lorsqu'un thread fait quelque chose qui peut le bloquer localement, par exemple, en attendant qu'un autre thread de son processus termine certains travaux, il appelle une procédure système d'exécution. Cette procédure vérifie si le thread doit être mis en état bloqué. Si tel est le cas, il stocke les registres du thread (c'est-à-dire les siens) dans la table des threads, recherche dans le tableau un thread prêt à exécuter et recharge les registres de la machine avec les valeurs enregistrées du nouveau thread. Dès que le pointeur de pile et le compteur de programmes ont été commutés, le nouveau thread reprend vie automatiquement. Si la machine a une instruction pour stocker tous les registres et une autre pour les charger tous, le changement de fil entier peut être effectué en une poignée d'instructions. Faire un changement de thread comme celui-ci est au moins un ordre de grandeur plus rapide que le piégeage vers le noyau et est un argument fort en faveur des packages de threads au niveau utilisateur.
Cependant, il existe une différence clé avec les processus. Lorsqu'un thread est terminé en cours d'exécution pour le moment, par exemple, lorsqu'il appelle thread_yield, le code de thread_yield peut enregistrer les informations du thread dans la table des threads elle-même. De plus, il peut alors appeler le planificateur de threads pour choisir un autre thread à exécuter. La procédure qui enregistre l'état du thread et le planificateur ne sont que des procédures locales, donc les invoquer est beaucoup plus efficace que de faire un appel au noyau. Entre autres problèmes, aucun piège n'est nécessaire, aucun changement de contexte n'est nécessaire, le cache mémoire n'a pas besoin d'être vidé, etc. Cela rend la programmation des threads très rapide.
Les threads de niveau utilisateur présentent également d'autres avantages. Ils permettent à chaque processus d'avoir son propre algorithme de planification personnalisé. Pour certaines applications, par exemple, celles avec un thread de ramasse-miettes, ne pas avoir à se soucier de l'arrêt d'un thread à un moment gênant est un plus. Ils évoluent également mieux, car les threads du noyau nécessitent invariablement un espace table et un espace de pile dans le noyau, ce qui peut être un problème s'il existe un très grand nombre de threads.
Malgré leurs meilleures performances, les packages de threads au niveau utilisateur ont des problèmes majeurs. Le premier d'entre eux est le problème de la mise en œuvre des appels système bloquants. Supposons qu'un thread lit à partir du clavier avant d'appuyer sur une touche. Laisser le thread effectuer l'appel système est inacceptable, car cela arrêtera tous les threads. L'un des principaux objectifs d'avoir des threads en premier lieu était de permettre à chacun d'utiliser des appels de blocage, mais d'empêcher un thread bloqué d'affecter les autres. Avec le blocage des appels système, il est difficile de voir comment cet objectif peut être atteint facilement.
Les appels système peuvent tous être modifiés pour ne pas être bloquants (par exemple, une lecture sur le clavier ne retournerait que 0 octet si aucun caractère n'était déjà mis en mémoire tampon), mais exiger des modifications du système d'exploitation n'est pas attrayant. En outre, l'un des arguments en faveur des threads de niveau utilisateur était précisément qu'ils pouvaient fonctionner avec les systèmes d'exploitation existants. De plus, la modification de la sémantique de lecture nécessitera des modifications de nombreux programmes utilisateur.
Une autre alternative est possible dans le cas où il est possible de dire à l'avance si un appel va bloquer. Dans certaines versions d'UNIX, un appel système, select, existe, qui permet à l'appelant de dire si une lecture potentielle se bloquera. Lorsque cet appel est présent, la procédure de bibliothèque lue peut être remplacée par une nouvelle qui effectue d'abord un appel de sélection, puis ne fait l'appel de lecture que s'il est sûr (c'est-à-dire qu'il ne se bloquera pas). Si l'appel lu se bloque, l'appel n'est pas effectué. Au lieu de cela, un autre thread est exécuté. La prochaine fois que le système d'exécution prendra le contrôle, il pourra vérifier à nouveau si la lecture est désormais sûre. Cette approche nécessite la réécriture de parties de la bibliothèque d'appels système, est inefficace et inélégante, mais il y a peu de choix. Le code placé autour de l'appel système pour effectuer la vérification s'appelle une veste ou un wrapper.
Un peu analogue au problème du blocage des appels système est le problème des défauts de page. Nous les étudierons au Chap. 4. Pour le moment, il suffit de dire que les ordinateurs peuvent être configurés de telle sorte que tout le programme ne soit pas en mémoire principale à la fois. Si le programme appelle ou passe à une instruction qui n'est pas en mémoire, une erreur de page se produit et le système d'exploitation ira chercher l'instruction manquante (et ses voisins) sur le disque. C'est ce qu'on appelle un défaut de page. Le processus est bloqué pendant que l'instruction nécessaire est localisée et lue. Si un thread provoque une erreur de page, le noyau, ne connaissant même pas l'existence des threads, bloque naturellement tout le processus jusqu'à ce que les E/S disque soient terminées, même bien que d'autres threads puissent être exécutables.
Un autre problème avec les packages de threads au niveau de l'utilisateur est que si un thread commence à s'exécuter, aucun autre thread de ce processus ne s'exécutera jamais, sauf si le premier thread abandonne volontairement le CPU. Au sein d'un même processus, il n'y a pas d'interruption d'horloge, ce qui rend impossible la planification des processus à tour de rôle (à tour de rôle). À moins qu'un thread n'entre de plein gré dans le système d'exécution, le planificateur n'aura jamais la moindre chance.
Une solution possible au problème des threads exécutés pour toujours est de demander au système d'exécution de demander un signal d'horloge (interruption) une fois par seconde pour lui donner le contrôle, mais cela aussi est grossier et compliqué à programmer. Les interruptions d'horloge périodiques à une fréquence plus élevée ne sont pas toujours possibles, et même si elles le sont, la surcharge totale peut être importante. En outre, un thread peut également nécessiter une interruption d'horloge, ce qui interfère avec l'utilisation de l'horloge par le système d'exécution.
Un autre argument, et probablement l'argument le plus dévastateur contre les threads de niveau utilisateur, est que les programmeurs veulent généralement des threads précisément dans les applications où les threads se bloquent souvent, comme, par exemple, dans un serveur Web multithread. Ces threads effectuent constamment des appels système. Une fois qu'un piège s'est produit dans le noyau pour effectuer l'appel système, il n'est presque plus nécessaire pour le noyau de changer de threads si l'ancien s'est bloqué, et le fait de le faire élimine la nécessité de faire constamment des appels système sélectionnés qui vérifiez si les appels système lus sont sûrs. Pour les applications qui sont essentiellement entièrement liées au CPU et qui bloquent rarement, quel est l'intérêt d'avoir des threads? Personne ne proposerait sérieusement de calculer les n premiers nombres premiers ou de jouer aux échecs en utilisant des threads car il n'y a rien à gagner en le faisant de cette façon.
Les threads utilisateur sont gérés dans l'espace utilisateur - ce qui signifie que la planification, la commutation, etc. ne sont pas du noyau.
Puisque, en fin de compte, le noyau du système d'exploitation est responsable du changement de contexte entre les "unités d'exécution" - vos threads utilisateur doivent être associés (c'est-à-dire "mapper") à un objet planifiable du noyau - un thread du noyau†1.
Donc, étant donné N threads utilisateur - vous pouvez utiliser N threads du noyau (une carte 1: 1). Cela vous permet de tirer parti du multi-traitement matériel du noyau (fonctionnant sur plusieurs processeurs) et d'être une bibliothèque assez simpliste - en gros juste de reporter la majeure partie du travail au noyau. Cependant, cela rend votre application portable entre les systèmes d'exploitation, car vous n'appelez pas directement les fonctions de thread du noyau. Je crois que les threads POSIX ( PThreads ) est l'implémentation * nix préférée, et qu'elle suit la carte 1: 1 (ce qui la rend pratiquement équivalente à un thread noyau). Cependant, cela n'est pas garanti car il dépend de l'implémentation (une des principales raisons d'utiliser PThreads serait la portabilité entre les noyaux).
Ou, vous ne pouvez utiliser qu'un seul thread du noyau. Cela vous permettrait de fonctionner sur des OS non multitâches, ou d'être complètement en charge de la planification. Windows User Scheduling est un exemple de cette carte N: 1.
Ou, vous pouvez mapper sur un nombre arbitraire de threads du noyau - une mappe N: M. Windows a Fibres , ce qui vous permettrait de mapper N fibres aux threads du noyau M et de les planifier en coopération. Un pool de threads pourrait également en être un exemple - N éléments de travail pour M threads.
†1: Un processus a au moins 1 thread noyau, qui est l'unité d'exécution réelle. De plus, un thread du noyau doit être contenu dans un processus. Les OS doivent planifier le thread pour s'exécuter - pas le processus.
Selon Wikipedia et Oracle , les threads de niveau utilisateur sont en fait dans une couche montée sur les threads du noyau; pas que les threads du noyau exécutent à côté les threads de niveau utilisateur mais que, de manière générale, les seules entités qui sont réellement exécutées par le processeur/OS sont des threads du noyau.
Par exemple, supposons que nous ayons un programme avec 2 threads de niveau utilisateur, tous deux mappés (c'est-à-dire affectés) au même thread du noyau. Parfois, le thread du noyau exécute le premier thread au niveau de l'utilisateur (et il est dit que actuellement ce thread du noyau est mappé au premier thread au niveau de l'utilisateur) et parfois le thread du noyau exécute le deuxième thread de niveau utilisateur. Nous disons donc que nous avons deux threads au niveau utilisateur mappé sur le même thread du noyau.
À titre de clarification :
Le noyau d'un OS est appelé son kernel, donc les threads au niveau du noyau (c'est-à-dire les threads que le noyau connaît et gère) sont appelés threads du noyau, les appels au noyau du système d'exploitation pour les services peuvent être appelés appels au noyau, et .... La seule relation définie entre le noyau choses est qu'ils sont fortement liés au noyau du système d'exploitation, rien de plus.