Je veux apprendre et combler les lacunes de mes connaissances à l'aide de cette question.
Ainsi, un utilisateur exécute un thread (au niveau du noyau) et il appelle maintenant yield
(un appel système je présume). Le planificateur doit maintenant enregistrer le contexte du thread actuel dans le TCB (qui est stocké quelque part dans le noyau) et choisir un autre thread à exécuter et charge son contexte et passer à son CS:EIP
. Pour affiner les choses, je travaille sur Linux fonctionnant sur l'architecture x86. Maintenant, je veux entrer dans les détails:
Donc, nous avons d'abord un appel système:
1) La fonction wrapper pour yield
poussera les arguments d'appel système sur la pile. Appuyez sur l'adresse de retour et déclenchez une interruption avec le numéro d'appel système inséré dans un registre (disons EAX
).
2) L'interruption change le mode CPU de l'utilisateur au noyau et passe à la table des vecteurs d'interruption et de là à l'appel système réel dans le noyau.
3) Je suppose que le planificateur est appelé maintenant et maintenant il doit enregistrer l'état actuel dans le TCB. Voici mon dilemme. Depuis, le planificateur utilisera la pile du noyau et non la pile utilisateur pour effectuer son opération (ce qui signifie que les SS
et SP
doivent être modifiés) comment stocke-t-il l'état de l'utilisateur sans la modification des registres dans le processus. J'ai lu sur les forums qu'il existe des instructions matérielles spéciales pour l'enregistrement de l'état, mais comment le planificateur y accède-t-il et qui exécute ces instructions et quand?
4) Le planificateur stocke maintenant l'état dans le TCB et charge un autre TCB.
5) Lorsque le planificateur exécute le thread d'origine, le contrôle revient à la fonction wrapper qui efface la pile et le thread reprend.
Questions secondaires: le planificateur s'exécute-t-il en tant que thread uniquement noyau (c'est-à-dire un thread qui ne peut exécuter que du code noyau)? Existe-t-il une pile de noyau distincte pour chaque thread de noyau ou chaque processus?
À un niveau élevé, il y a deux mécanismes distincts à comprendre. Le premier est le mécanisme d'entrée/sortie du noyau: il fait basculer un seul thread en cours d'exécution du code en mode utilisateur vers le code du noyau en cours d'exécution dans le contexte de ce thread, et inversement. Le second est le mécanisme de changement de contexte lui-même, qui bascule en mode noyau de s'exécuter dans le contexte d'un thread à un autre.
Ainsi, lorsque le thread A appelle sched_yield()
et est remplacé par le thread B, ce qui se passe est:
Chaque thread utilisateur possède à la fois une pile en mode utilisateur et une pile en mode noyau. Lorsqu'un thread entre dans le noyau, la valeur actuelle de la pile en mode utilisateur (SS:ESP
) Et du pointeur d'instruction (CS:EIP
) Sont enregistrés dans la pile en mode noyau du thread, et le processeur passe à la pile en mode noyau - avec le mécanisme syscall int $80
, cela est fait par le CPU lui-même. Les valeurs de registre et les indicateurs restants sont ensuite également enregistrés dans la pile du noyau.
Lorsqu'un thread retourne du noyau en mode utilisateur, les valeurs de registre et les drapeaux sont extraits de la pile en mode noyau, puis la pile en mode utilisateur et les valeurs du pointeur d'instruction sont restaurées à partir des valeurs enregistrées sur la pile en mode noyau.
Lorsqu'un thread change de contexte, il appelle le planificateur (le planificateur ne s'exécute pas en tant que thread séparé - il s'exécute toujours dans le contexte du thread actuel). Le code du planificateur sélectionne un processus à exécuter ensuite et appelle la fonction switch_to()
. Cette fonction change essentiellement les piles du noyau - elle enregistre la valeur actuelle du pointeur de pile dans le TCB pour le thread actuel (appelé struct task_struct
Sous Linux), et charge un pointeur de pile précédemment enregistré à partir du TCB pour le fil suivant. À ce stade, il enregistre et restaure également un autre état de thread qui n'est généralement pas utilisé par le noyau - des choses comme les registres à virgule flottante/SSE. Si les threads en cours de commutation ne partagent pas le même espace de mémoire virtuelle (c'est-à-dire qu'ils sont dans des processus différents), les tables de pages sont également commutées.
Ainsi, vous pouvez voir que l'état de mode utilisateur principal d'un thread n'est pas enregistré et restauré au moment du changement de contexte - il est enregistré et restauré dans la pile de noyau du thread lorsque vous entrez et quittez le noyau. Le code de changement de contexte n'a pas à se soucier de clobber les valeurs de registre en mode utilisateur - celles-ci sont déjà sauvegardées en toute sécurité dans la pile du noyau à ce stade.
Ce que vous avez manqué à l'étape 2, c'est que la pile passe de la pile de niveau utilisateur d'un thread (où vous avez poussé les arguments) à la pile de niveau protégé d'un thread. Le contexte actuel du thread interrompu par l'appel système est en fait enregistré sur cette pile protégée. À l'intérieur de l'ISR et juste avant d'entrer dans le noyau, cette pile protégée est à nouveau basculée vers la pile de noyau dont vous parlez. Une fois à l'intérieur du noyau, les fonctions du noyau telles que les fonctions du planificateur finissent par utiliser la pile du noyau. Plus tard, un thread est élu par le planificateur et le système retourne à l'ISR, il repasse de la pile du noyau à la pile de niveau protégé du thread nouvellement élu (ou l'ancien si aucun thread de priorité supérieure n'est actif), qui contient finalement le nouveau contexte de thread. Par conséquent, le contexte est restauré automatiquement à partir de cette pile par du code (en fonction de l'architecture sous-jacente). Enfin, une instruction spéciale restaure les derniers registres sensibles tels que le pointeur de pile et le pointeur d'instruction. De retour au pays des utilisateurs ...
Pour résumer, un thread a (généralement) deux piles, et le noyau lui-même en a une. La pile du noyau est effacée à la fin de chaque entrée de noyau. Il est intéressant de noter que depuis 2.6, le noyau lui-même est threadé pour un certain traitement, donc un thread noyau a sa propre pile de niveau protégé à côté de la pile noyau générale.
Quelques ressources:
J'espère que cette aide!
Le noyau lui-même n'a aucune pile. Il en va de même pour le processus. Il n'a pas non plus de pile. Les threads ne sont que des citoyens du système qui sont considérés comme des unités d'exécution. Pour cette raison, seuls les threads peuvent être planifiés et seuls les threads ont des piles. Mais il y a un point que le code en mode noyau exploite fortement - à chaque instant, le système fonctionne dans le contexte du thread actuellement actif. En raison de ce noyau lui-même peut réutiliser la pile de la pile actuellement active. Notez qu'un seul d'entre eux peut exécuter au même moment du code noyau ou du code utilisateur. Pour cette raison, lorsque le noyau est invoqué, il suffit de réutiliser la pile de threads et d'effectuer un nettoyage avant de retourner le contrôle aux activités interrompues dans le thread. Le même mécanisme fonctionne pour les gestionnaires d'interruption. Le même mécanisme est exploité par les gestionnaires de signaux.
À son tour, la pile de threads est divisée en deux parties isolées, dont l'une appelée pile utilisateur (car elle est utilisée lorsque le thread s'exécute en mode utilisateur), et la seconde est appelée pile du noyau (car elle est utilisée lorsque le thread s'exécute en mode noyau) . Une fois que le thread franchit la frontière entre le mode utilisateur et le mode noyau, le processeur le bascule automatiquement d'une pile à une autre. Les deux piles sont suivies différemment par le noyau et le CPU. Pour la pile du noyau, le CPU garde en permanence le pointeur à l'esprit en haut de la pile du noyau du thread. C'est facile, car cette adresse est constante pour le thread. Chaque fois que le thread entre dans le noyau, il trouve une pile de noyau vide et chaque fois qu'il revient en mode utilisateur, il nettoie la pile de noyau. Dans le même temps, le CPU ne garde pas à l'esprit le pointeur vers le haut de la pile utilisateur, lorsque le thread s'exécute en mode noyau. Au lieu de cela, lors de l'entrée dans le noyau, le CPU crée un cadre de pile spécial "interruption" en haut de la pile du noyau et stocke la valeur du pointeur de pile en mode utilisateur dans ce cadre. Lorsque le thread quitte le noyau, le CPU restaure la valeur de ESP à partir du cadre de pile "interruption" précédemment créé, immédiatement avant son nettoyage. (Sur les anciens x86, la paire d'instructions int/iret handle enter and exit du mode noyau)
Lors de l'entrée en mode noyau, immédiatement après que le CPU aura créé un cadre de pile "interrompu", le noyau pousse le contenu du reste des registres du CPU vers la pile du noyau. Notez que cela enregistre les valeurs uniquement pour ces registres, qui peuvent être utilisés par le code du noyau. Par exemple, le noyau n'enregistre pas le contenu de SSE s'enregistre juste parce qu'il ne les touchera jamais. De même, juste avant de demander au CPU de retourner le contrôle en mode utilisateur, le noyau renvoie le contenu précédemment sauvegardé dans registres.
Notez que dans des systèmes tels que Windows et Linux, il existe une notion de thread système (souvent appelé thread noyau, je sais que cela prête à confusion). Les threads système sont une sorte de threads spéciaux, car ils s'exécutent uniquement en mode noyau et n'ont donc aucune partie utilisateur de la pile. Le noyau les emploie pour des tâches d'entretien ménager auxiliaires.
Le changement de thread est effectué uniquement en mode noyau. Cela signifie que les threads sortants et entrants s'exécutent en mode noyau, les deux utilisent leurs propres piles de noyau, et les deux ont des piles de noyau ont des trames "d'interruption" avec des pointeurs vers le haut des piles utilisateur. Le point clé du commutateur de thread est un commutateur entre les piles de threads du noyau, aussi simple que:
pushad; // save context of outgoing thread on the top of the kernel stack of outgoing thread
; here kernel uses kernel stack of outgoing thread
mov [TCB_of_outgoing_thread], ESP;
mov ESP , [TCB_of_incoming_thread]
; here kernel uses kernel stack of incoming thread
popad; // save context of incoming thread from the top of the kernel stack of incoming thread
Notez qu'il n'y a qu'une seule fonction dans le noyau qui effectue un changement de thread. Pour cette raison, chaque fois que le noyau a changé de pile, il peut trouver un contexte de thread entrant en haut de la pile. Tout simplement parce que chaque fois avant le changement de pile, le noyau pousse le contexte du thread sortant vers sa pile.
Notez également qu'à chaque fois après le changement de pile et avant de revenir au mode utilisateur, le noyau recharge l'esprit du processeur par la nouvelle valeur du haut de la pile du noyau. Cela garantit que lorsque le nouveau thread actif tentera d'entrer dans le noyau à l'avenir, il sera commuté par le CPU vers sa propre pile de noyau.
Notez également que tous les registres ne sont pas enregistrés sur la pile pendant le changement de thread, certains registres comme FPU/MMX/SSE sont enregistrés dans une zone spécialement dédiée dans TCB du thread sortant. Le noyau utilise ici une stratégie différente pour deux raisons. Tout d'abord, tous les threads du système ne les utilisent pas. Il est inefficace de pousser et d'extraire leur contenu de la pile pour chaque thread. Et le second contient des instructions spéciales pour la sauvegarde et le chargement "rapides" de leur contenu. Et ces instructions n'utilisent pas la pile.
Notez également qu'en fait, la partie noyau de la pile de threads a une taille fixe et est allouée dans le cadre de TCB. (vrai pour Linux et je crois aussi pour Windows)