Comment un système d'exploitation gère-t-il généralement la mémoire du noyau et la gestion des pages?

StackOverflow https://stackoverflow.com/questions/271115

Question

Je travaille sur la conception du noyau et j'ai quelques questions concernant la pagination.

L’idée de base que j’ai jusqu’à présent est la suivante: chaque programme obtient sa propre mémoire (ou du moins qu’il pense) 4G, moins une section réservée quelque part que je réserve aux fonctions du noyau que le programme peut appeler. Le système d’exploitation doit donc trouver un moyen de charger en mémoire les pages que le programme doit utiliser pendant son fonctionnement.

Maintenant, en supposant que nous ayons une quantité infinie de mémoire et de temps processeur, je pourrais charger / allouer toutes les pages du programme écrites ou lues au fur et à mesure en utilisant des erreurs de page pour les pages inexistantes (ou remplacées). afin que le système d’exploitation puisse rapidement les allouer ou les échanger. Dans le monde réel, j’ai cependant besoin d’optimiser ce processus pour ne pas avoir un programme consommant en permanence toute la mémoire qu’il a jamais touchée.

Donc, je suppose que ma question est la suivante: comment un système d’exploitation agit-il en général? Ma pensée initiale est de créer une fonction que le programme appelle pour définir / libérer des pages, qu’elle peut ensuite gérer seule, mais un programme le fait-il généralement, ou le compilateur suppose-t-il qu’il a le temps libre? En outre, comment le compilateur gère-t-il les situations dans lesquelles il doit allouer un segment de mémoire assez volumineux? Dois-je fournir une fonction qui essaie de lui donner X pages dans l'ordre?

Ce n'est évidemment pas une question spécifique au langage, mais je suis un partisan du C standard et je suis doué avec le C ++. J'aimerais donc que tous les exemples de code soient soit dans celui-là, soit dans l'assembly. (L’assemblage ne devrait pas être nécessaire, j’ai bien l’intention de le faire fonctionner avec autant de code C que possible et d’optimiser la dernière étape.)

Une autre chose à laquelle il devrait être plus facile de répondre également: comment gère-t-on généralement les fonctions du noyau qu'un programme doit appeler? Est-ce juste de disposer d'une zone de mémoire définie (je pensais vers la fin de l'espace virtuel) qui contient la plupart des fonctions de base / de la mémoire spécifique à un processus que le programme peut appeler? Ma pensée à partir de là serait de faire en sorte que les fonctions du noyau fassent quelque chose de très chic et échangent les pages (pour que les programmes ne puissent pas voir les fonctions sensibles du noyau dans leur propre espace) lorsque les programmes doivent faire quelque chose de majeur, mais je ne suis pas vraiment en mettant l'accent sur la sécurité à ce stade.

Je suppose donc que je suis plus préoccupé par les idées de conception générale que par les détails. J'aimerais rendre le noyau complètement compatible avec GCC (en quelque sorte) et je dois m'assurer qu'il fournit tout ce dont un programme normal aurait besoin.

Merci pour tout conseil.

Était-ce utile?

La solution

Un bon point de départ pour toutes ces questions est de regarder comment Unix le fait. Comme le dit une célèbre citation: "Ceux qui ne comprennent pas UNIX sont condamnés à le réinventer, mal."

Premièrement, à propos de l’appel des fonctions du noyau. Il ne suffit pas d'avoir simplement les fonctions à un endroit où un programme peut appeler, car le programme fonctionne probablement en mode "utilisateur". (anneau 3 sur IA-32) et le noyau doit fonctionner en " mode noyau " (généralement le 0 sur l’IA-32) pour effectuer ses opérations privilégiées. Vous devez en quelque sorte faire la transition entre les deux modes, ce qui est très spécifique à l’architecture.

Sur l’IA-32, la méthode traditionnelle consiste à utiliser une porte dans l’IDT avec une interruption logicielle (Linux utilise int 0x80). Les processeurs les plus récents disposent d'autres méthodes (plus rapides) pour le faire, et celles qui sont disponibles dépendent du type de processeur (AMD ou Intel) et du modèle de processeur spécifique. Pour s'adapter à cette variation, les noyaux Linux récents utilisent une page de code mappée par le noyau en haut de l'espace d'adressage pour chaque processus. Ainsi, sous Linux récent, pour faire un appel système, vous appelez une fonction sur cette page, qui fera alors le nécessaire pour passer en mode noyau (le noyau a plusieurs copies de cette page et choisit celle qui sera utilisée au démarrage en fonction des fonctionnalités de votre processeur).

Maintenant, la gestion de la mémoire. C’est un sujet énorme ; vous pourriez écrire un gros livre à ce sujet et ne pas expliquer le sujet.

N'oubliez pas qu'il existe au moins deux vues de la mémoire: la vue physique (l'ordre réel des pages, visible par le sous-système de la mémoire matérielle et souvent par les périphériques externes) et la vue logique (l'ordre des pages vues par les programmes exécutés sur la CPU). Il est assez facile de confondre les deux. Vous allouerez des pages physiques et les affecterez à des adresses logiques dans le programme ou dans l'espace d'adressage du noyau. Une seule page physique peut avoir plusieurs adresses logiques et peut être mappée vers différentes adresses logiques dans différents processus.

La mémoire du noyau (réservée au noyau) est généralement mappée en haut de l'espace d'adressage de chaque processus. Cependant, il est configuré pour ne pouvoir être accédé qu'en mode noyau. Il n’est pas nécessaire de recourir à des astuces sophistiquées pour masquer cette partie de la mémoire; le matériel effectue tout le travail de blocage de l'accès (sur IA-32, cela se fait via des indicateurs de page ou des limites de segment).

Les programmes allouent de la mémoire sur le reste de l'espace d'adressage de plusieurs manières:

  • Une partie de la mémoire est allouée par le programme de chargement du noyau. Ceci inclut le code de programme (ou "texte"), les données initialisées du programme ("données"), les données non initialisées du programme ("bss", rempli à zéro), la pile et plusieurs cotes et fins. Quelle quantité allouer, où, quel devrait être le contenu initial, quels indicateurs de protection utiliser et plusieurs autres choses sont lus à partir des en-têtes du fichier exécutable à charger.
  • Traditionnellement sous Unix, il existe une zone de mémoire qui peut être agrandie et réduite (sa limite supérieure peut être modifiée via l'appel système brk () ). Ceci est traditionnellement utilisé par le tas (l'allocateur de mémoire de la bibliothèque C, dont malloc () est l'une des interfaces, est responsable du tas).
  • Vous pouvez souvent demander au noyau de mapper un fichier sur une zone d'espace d'adressage. Les lectures et les écritures dans cette zone sont (via la magie de pagination) dirigées vers le fichier de sauvegarde. Cela s'appelle généralement mmap () . Avec un mmap anonyme, vous pouvez allouer de nouvelles zones de l'espace d'adressage qui ne sont pas sauvegardées par un fichier, mais agissent de la même manière. Le chargeur de programme du noyau utilisera souvent mmap pour allouer des parties du code du programme (par exemple, le code du programme peut être sauvegardé par l'exécutable lui-même).

Les zones d'accès à l'espace adresse qui ne sont attribuées d'aucune manière (ou sont réservées au noyau) sont considérées comme des erreurs et sous Unix, un signal sera envoyé au programme.

Le compilateur alloue la mémoire de manière statique (en le spécifiant dans les en-têtes de fichiers exécutables; le chargeur de programmes du noyau allouera la mémoire lors du chargement du programme) ou de manière dynamique (en appelant une fonction dans la bibliothèque standard du langage, qui appelle généralement un fonction dans la bibliothèque standard du langage C, qui appelle ensuite le noyau pour allouer de la mémoire et la subdivise si nécessaire).

La meilleure façon d’apprendre les bases de tout cela est de lire l’un des nombreux ouvrages sur les systèmes d’exploitation, en particulier ceux qui utilisent une variante Unix comme exemple. Cela ira beaucoup plus en détail que je ne pourrais sur une réponse sur StackOverflow.

Autres conseils

La réponse à cette question dépend fortement de l’architecture. Je suppose que vous parlez de x86. Avec x86, un noyau fournit généralement un ensemble de appels système , qui constituent des points d'entrée prédéterminés dans le noyau. Le code utilisateur ne peut entrer dans le noyau qu'à ces points spécifiques. Le noyau contrôle donc soigneusement son interaction avec le code utilisateur.

Sous x86, il existe deux manières d'implémenter les appels système: avec des interruptions et avec les instructions sysenter / sysexit. Avec les interruptions, le noyau configure une table de descripteurs d’interruption (IDT), qui définit les points d’entrée possibles dans le noyau. Le code utilisateur peut ensuite utiliser l'instruction int pour générer une interruption logicielle à appeler dans le noyau. Les interruptions peuvent également être générées par le matériel (appelées interruptions brutales); ces interruptions doivent généralement être distinctes des interruptions légères, mais elles ne doivent pas nécessairement l'être.

Les instructions sysenter et sysexit constituent un moyen plus rapide d'effectuer des appels système, car la gestion des interruptions est lente. Je ne les connais pas très bien, je ne peux donc pas vous dire si elles conviennent mieux à votre situation.

Quel que soit votre mode d’emploi, vous devrez définir l’interface d’appel système. Vous voudrez probablement passer les arguments d'appel système dans les registres et non sur la pile, car la génération d'une interruption vous fera basculer les piles sur la pile du noyau. Cela signifie que vous devrez presque certainement écrire des stubs en langage assembleur à la fois en mode utilisateur pour passer l'appel système et à nouveau en fin de noyau pour rassembler les arguments de l'appel système et enregistrer les registres.

Une fois que tout est en place, vous pouvez commencer à penser à la gestion des défauts de page. Les défauts de page ne sont en réalité qu'un autre type d'interruption - lorsque le code utilisateur tente d'accéder à une adresse virtuelle pour laquelle il n'y a pas d'entrée de table de page, il génère l'interruption 14 et vous obtenez également l'adresse défectueuse en tant que code d'erreur. Le noyau peut prendre ces informations, puis décider de lire la page manquante à partir du disque, d’ajouter le mappage de table de pages et de revenir au code utilisateur.

Je vous recommande vivement de consulter certains des éléments figurant dans les systèmes d'exploitation MIT classe. Consultez la section des références, elle contient une foule de bonnes choses.

scroll top