Conception de système d'exploitation de type Unix - Espace d'adressage virtuel (6)

Dans l'article précédent, nous avons examiné les bases du travail en mode protégé IA-32. Aujourd'hui, il est temps d'apprendre à travailler avec l'espace d'adressage virtuel.

Table des matières


Construisez le système (make, gcc, gas). Démarrage initial (multiboot). Lancez (qemu). Bibliothèque C (strcpy, memcpy, strext).

Bibliothèque C (sprintf, strcpy, strcmp, strtok, va_list ...). Construction de la bibliothèque en mode noyau et en mode application utilisateur.

Le journal système du noyau. Mémoire vidéo Sortie vers le terminal (kprintf, kpanic, kassert).
Mémoire dynamique, tas (kmalloc, kfree).

Organisation de la mémoire et gestion des interruptions (GDT, IDT, PIC, syscall). Exceptions
Mémoire virtuelle (répertoire de pages et table de pages).

Processus. Planificateur Multitâche. Appels système (kill, exit, ps).

Le système de fichiers du noyau (initrd), elf et ses composants internes. Appels système (exec).

Pilotes de périphériques de caractères. Appels système (ioctl, fopen, fread, fwrite). Bibliothèque C (fopen, fclose, fprintf, fscanf).

Shell comme programme complet pour le noyau.

Mode de protection utilisateur (ring3). Segment d'état de la tâche (tss).

Mémoire virtuelle


La mémoire virtuelle est nécessaire pour que chaque processus puisse être isolé d'un autre, c'est-à-dire ne pouvait pas l'arrêter. S'il n'y avait pas de mémoire virtuelle, nous devions charger des fichiers elf à différentes adresses en mémoire à chaque fois. Mais comme vous le savez, les fichiers exécutables peuvent contenir des liens vers des adresses spécifiques (absolues). Par conséquent, lors de la compilation d'elf, il est déjà connu à quelle adresse en mémoire il sera chargé (voir script de l'éditeur de liens). Par conséquent, nous ne pouvons pas charger deux fichiers elf sans mémoire virtuelle. Mais même avec la mémoire virtuelle activée, il existe des bibliothèques dynamiques (telles que .so) qui peuvent se charger à n'importe quelle adresse. Ils peuvent être téléchargés à n'importe quelle adresse car ils disposent d'une section relocalisation. Dans cette section, tous les endroits où l'adressage absolu est utilisé sont enregistrés, et le noyau, lors du chargement d'un tel fichier elf, doit corriger ces adresses avec des stylos, c'est-à-dire ajoutez-leur la différence entre l'adresse de téléchargement réelle et souhaitée.

Nous envisagerons de travailler avec des pages de 4 kilo-octets. Dans cette situation, nous pouvons traiter jusqu'à 4 mégaoctets de RAM. C’est suffisant pour nous. La carte d'adresses ressemblera à ceci:

0-1 mb : ne touchez pas.
1-2 Mo : données de code et de noyau.
2-3 mb : un tas de grains.
3-4 Mo : des pages personnalisées de fichiers elf téléchargées.

L'adresse linéaire (obtenue à partir du modèle plat) lorsque la pagination de page est activée n'est pas égale à l'adresse physique. Au lieu de cela, l'adresse est divisée par le décalage (bits faibles), l'index des entrées dans la table de pages et l'index du répertoire de pages (bits élevés). Chaque processus aura son propre répertoire de pages et, par conséquent, des tables de pages. C'est ce qui vous permet d'organiser un espace d'adressage virtuel.

L'entrée du répertoire des pages ressemble à ceci:

struct page_directory_entry_t { u8 present : 1; u8 read_write : 1; u8 user_supervisor : 1; u8 write_through : 1; u8 cache_disabled : 1; u8 accessed : 1; u8 zero : 1; u8 page_size : 1; u8 ignored : 1; u8 available : 3; u32 page_table_addr : 20; } attribute(packed); 

L'élément de table de page ressemble à ceci:

 struct page_table_entry_t { u8 present : 1; u8 read_write : 1; u8 user_supervisor : 1; u8 write_through : 1; u8 cache_disabled : 1; u8 accessed : 1; u8 dirty : 1; u8 zero : 1; u8 global : 1; u8 available : 3; u32 page_phys_addr : 20; } attribute(packed); 

Pour le noyau, nous décrirons le répertoire de pages et la table de pages comme des variables statiques. Certes, il est nécessaire qu'ils soient alignés sur la bordure de la page.

 static struct page_directory_entry_t kpage_directory attribute(aligned(4096)); static struct page_table_entry_t kpage_table[MMU_PAGE_TABLE_ENTRIES_COUNT] attribute(aligned(4096)); 

Nous allons suivre la voie simple et rendre l'ensemble de l'espace d'adressage physique accessible au noyau. Le niveau de privilège des pages du noyau doit être superviseur afin que personne n'y monte. Les processus légers qui doivent s'exécuter en mode noyau partageront le même espace d'adressage avec le noyau. Avec ce processus, nous aurons une file d'attente d'exécution en attente pour gérer les interruptions en attente. Mais plus à ce sujet dans la leçon sur les pilotes de périphériques de caractères. Nous n'avons pas encore réalisé le multitâche avant d'envisager ce sujet.

Créez un répertoire de pages du noyau et une table de pages correspondante. Lorsque le noyau est initialisé, il sera actif.

 /* * Api - init kernel page directory * Here assumed each entry addresses 4Kb */ extern void mmu_init() { memset(&kpage_directory, 0, sizeof(struct page_directory_entry_t)); /* set kernel page directory */ kpage_directory.zero = 1; kpage_directory.accessed = 0; kpage_directory.available = 0; kpage_directory.cache_disabled = 0; kpage_directory.ignored = 0; kpage_directory.page_size = 0; /* 4KB */ kpage_directory.present = 1; /* kernel pages always in memory */ kpage_directory.read_write = 1; /* read & write */ kpage_directory.user_supervisor = 1; /* kernel mode pages */ kpage_directory.write_through = 1; kpage_directory.page_table_addr = (size_t)kpage_table >> 12; /* set kernel table */ for (int i = 0; i < MMU_PAGE_TABLE_ENTRIES_COUNT; ++i) { kpage_table[i].zero = 0; kpage_table[i].accessed = 0; kpage_table[i].available = 0; kpage_table[i].cache_disabled = 0; kpage_table[i].dirty = 0; kpage_table[i].global = 1; kpage_table[i].present = 1; /* kernel pages always in memory */ kpage_table[i].read_write = 1; /* read & write */ kpage_table[i].user_supervisor = 1; /* kernel mode pages */ kpage_table[i].write_through = 1; kpage_table[i].page_phys_addr = (i * 4096) >> 12; /* assume 4Kb pages */ } } 

Lorsque nous téléchargerons les fichiers elf, nous devrons créer un répertoire de pages pour le processus utilisateur. Vous pouvez le faire avec la fonction suivante:

 /* * Api - Create user page directory */ extern struct page_directory_entry_t* mmu_create_user_page_directory(struct page_table_entry_t* page_table) { struct page_directory_entry_t* upage_dir; upage_dir = malloc_a(sizeof(struct page_directory_entry_t), 4096); upage_dir->zero = 1; upage_dir->accessed = 0; upage_dir->available = 0; upage_dir->cache_disabled = 0; upage_dir->ignored = 0; upage_dir->page_size = 0; /* 4KB */ upage_dir->present = 1; upage_dir->read_write = 1; /* read & write */ upage_dir->user_supervisor = 0; /* user mode pages */ upage_dir->write_through = 1; upage_dir->page_table_addr = (size_t)page_table >> 12; /* assume 4Kb pages */ return upage_dir; } 

Par défaut, la table des pages de processus contiendra des pages du noyau et des entrées vides pour les pages de processus futures, c'est-à-dire enregistrements avec le drapeau actuel effacé et l'adresse de page physique à 0.

 /* * Api - Create user page table */ extern struct page_table_entry_t* mmu_create_user_page_table() { struct page_table_entry_t* upage_table; upage_table = malloc_a(sizeof(struct page_table_entry_t) * MMU_PAGE_TABLE_ENTRIES_COUNT, 4096); /* share kernel pages */ memcpy(upage_table, kpage_table, sizeof(struct page_table_entry_t) * MMU_KERNEL_PAGES_COUNT); /* fill user pages */ for (int i = MMU_KERNEL_PAGES_COUNT; i < MMU_PAGE_TABLE_ENTRIES_COUNT; ++i) { struct page_table_entry_t* current; current = upage_table + i; current->zero = 0; current->accessed = 0; current->available = 0; current->cache_disabled = 0; current->dirty = 0; current->global = 1; current->present = 0; /* not present as so as there is no user pages yet */ current->read_write = 1; /* read & write */ current->user_supervisor = 0; /* user mode page */ current->write_through = 1; current->page_phys_addr = 0; /* page is not present */ } return upage_table; } 

Nous devons apprendre à ajouter de nouvelles pages physiques à la table des pages de processus, car il n'y en aura pas par défaut. Nous en aurons besoin lors du chargement des fichiers elf en mémoire, lorsque nous chargerons les segments décrits dans les en-têtes de programme. La fonction nous aidera avec ceci:

 /* * Api - Occupy user page */ extern bool mmu_occupy_user_page(struct page_table_entry_t* upage_table, void* phys_addr) { for (int i = MMU_KERNEL_PAGES_COUNT; i < MMU_PAGE_TABLE_ENTRIES_COUNT; ++i) { struct page_table_entry_t* current; current = upage_table + i; if (current->present) { /* page is buzy */ continue; } current->zero = 0; current->accessed = 0; current->available = 0; current->cache_disabled = 0; current->dirty = 0; current->global = 1; current->present = 1; current->read_write = 1; /* read & write */ current->user_supervisor = 0; /* user mode page */ current->write_through = 1; current->page_phys_addr = (size_t)phys_addr >> 12; /* assume 4Kb pages */ return true; } return false; } 

Le mode de pagination est activé et désactivé par un bit dans le registre d'indicateur de processeur.

 /* * Enable paging * void asm_enable_paging(void *page_directory) */ asm_enable_paging: mov 4(%esp),%eax # page_directory mov %eax,%cr3 mov %cr0,%eax or $0x80000001,%eax # set PE & PG bits mov %eax,%cr0 ret /* * Disable paging * void asm_disable_paging() */ asm_disable_paging: mov %eax,%cr3 mov %cr0,%eax xor $0x80000000,%eax # unset PG bit mov %eax,%cr0 ret 

Après avoir appris à créer l'espace d'adressage des processus, nous devons en quelque sorte gérer les pages physiques, celle qui est occupée et celle qui est libre. Il existe un mécanisme bitmap pour cela, un bit par page. Nous ne décrirons pas les pages jusqu'à 3 mégaoctets, car elles appartiennent au noyau et sont toujours occupées. Nous commençons à sélectionner les pages utilisateur de 3ème à 4ème mégaoctets.

 static u32 bitmap[MM_BITMAP_SIZE]; 


Les pages physiques sont allouées et désallouées selon les fonctions suivantes. En fait, nous trouvons simplement le bit souhaité dans la carte à l'adresse physique de la page et vice versa. L'inconvénient est que nous avons une taille de cellule de mémoire limitée, vous devez donc utiliser deux coordonnées: le nombre d'octets et le nombre de bits.

 /* * Api - allocate pages */ extern void* mm_phys_alloc_pages(u_int count) { /* find free pages */ for (int i = 0; i < MM_DYNAMIC_PAGES_COUNT; ++i) { bool is_found = true; for (int j = 0; j < count; ++j) { is_found = is_found && !mm_get_bit(i + j); } if (is_found) { /* occupy */ for (int j = 0; j < count; ++j) { assert(!mm_get_bit(i + j)); mm_set_bit(i + j); } return (void *)mm_get_addr(i); } } return null; } /* * Api - free page */ extern bool mm_phys_free_pages(void* ptr, u_int count) { size_t address = (size_t)ptr; assert(address >= MM_AREA_START); assert(address % MM_PAGE_SIZE == 0); /* find page */ for (int i = 0; i < MM_DYNAMIC_PAGES_COUNT; ++i) { size_t addr = mm_get_addr(i); if (addr == address) { /* free pages */ for (int j = 0; j < count; ++j) { assert(mm_get_bit(i + j)); mm_clear_bit(i + j); } return true; } } return false; } 

C'est assez pour introduire une prise en charge complète de la mémoire virtuelle dans votre noyau.

Les références


Détails et explications dans le didacticiel vidéo .

Le code source dans le référentiel git (vous avez besoin de la branche de leçon6).

Les références


1. James Molloy. Faites rouler votre propre système d'exploitation jouet UNIX-clone.
2. Dents. Assembleur pour DOS, Windows, Unix
3. Kalachnikov. L'assembleur est facile!
4. Tanenbaum. Systèmes d'exploitation. Mise en œuvre et développement.
5. Robert Love. Noyau Linux Description du processus de développement.

Source: https://habr.com/ru/post/fr467759/


All Articles