类似于Unix的OS设计-虚拟地址空间(6)

在上一篇文章中,我们研究了在IA-32保护模式下工作的基础。 今天是时候学习如何使用虚拟地址空间了。

目录


构建系统(make,gcc,gas)。 初始引导(多次引导)。 启动(qemu)。 C库(strcpy,memcpy,strext)。

C库(sprintf,strcpy,strcmp,strtok,va_list ...)。 以内核模式和用户应用程序模式构建库。

内核系统日志。 显存 输出到终端(kprintf,kpanic,kassert)。
动态内存,堆(kmalloc,kfree)。

内存和中断处理的组织(GDT,IDT,PIC,syscall)。 例外情况
虚拟内存(页面目录和页面表)。

过程。 策划人 多任务处理。 系统调用(kill,exit,ps)。

内核(initrd),elf及其内部文件系统。 系统调用(执行)。

字符设备驱动程序。 系统调用(ioctl,fopen,fread,fwrite)。 C库(fopen,fclose,fprintf,fscanf)。

Shell作为内核的完整程序。

用户保护模式(ring3)。 任务状态段(tss)。

虚拟记忆体


需要虚拟内存,以便每个进程都可以与另一个进程隔离,即 无法阻止他。 如果没有虚拟内存,则每次必须将elf文件加载到内存中的不同地址。 但您知道,可执行文件可能包含指向特定地址(绝对地址)的链接。 因此,在编译elf时,已经知道它将在内存中的哪个地址加载(请参阅链接程序脚本)。 因此,我们不能在没有虚拟内存的情况下加载两个elf文件。 但是即使打开了虚拟内存,也可以在任何地址加载动态库(例如.so)。 由于它们具有重新定位部分,因此可以在任何地址下载它们。 在本节中,将注册使用绝对寻址的所有位置,并且内核在加载此类elf文件时必须用笔固定这些地址,即 向他们添加实际地址和所需下载地址之间的差异。

我们将考虑处理4 KB页面。 在这种情况下,我们可以寻址多达4 MB的RAM。 对我们来说足够了。 地址映射如下所示:

0-1 mb :请勿触摸。
1-2 mb :代码和内核数据。
2-3 mb :一堆内核。
3-4 MB :上传的elf文件的自定义页面。

启用页面分页时的线性地址(从平面模型获取)不等于物理地址。 而是用地址除以偏移量(低位),页表中条目的索引和页目录的索引(高位)。 每个进程将具有其自己的页面目录,以及相应的页面表。 这就是您组织虚拟地址空间的条件。

页面目录条目如下所示:

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); 

页面表元素如下所示:

 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); 

对于内核,我们将页面目录和页面表描述为静态变量。 的确,要求它们在页面边框上对齐。

 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)); 

我们将采用简单的方法,并使整个物理地址空间可供内核访问。 内核页面的特权级别应该是管理员,以便没有人进入。 必须在内核模式下运行的轻量级进程将与内核共享相同的地址空间。 通过此过程,我们将有一个待处理的执行队列来处理待处理的中断。 但是在有关字符设备驱动程序的课程中,更多内容。 在考虑此主题之前,我们尚未实现多任务处理。

创建内核页面目录和相应的页面表。 内核初始化后,它将处于活动状态。

 /* * 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 */ } } 

当我们上传elf文件时,我们将需要为用户进程创建一个页面目录。 您可以使用以下功能执行此操作:

 /* * 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; } 

默认情况下,进程页面表将包含内核页面和用于以后的进程页面的空条目,即 清除当前标志且物理页地址为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; } 

我们需要学习如何向流程页表添加新的物理页,因为默认情况下将没有任何物理页。 在将elf文件加载到内存中时,在加载程序标头中描述的段时,我们将需要此功能。 该功能将帮助我们:

 /* * 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; } 

分页模式由处理器标志寄存器中的某个位打开和关闭。

 /* * 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 

在学习了如何创建进程的地址空间之后,我们需要以某种方式管理物理页面,哪些页面忙,哪些页面空闲。 有一种位图机制,每页一位。 我们不会描述最大3 MB的页面,因为它们属于内核并且总是很忙。 我们开始选择3到4兆字节的用户页面。

 static u32 bitmap[MM_BITMAP_SIZE]; 


根据以下功能分配和释放物理页面。 实际上,我们只是在页面的物理地址中找到映射中所需的位,反之亦然。 不便之处在于我们的存储单元大小有限,因此您必须使用两个坐标:字节数和位数。

 /* * 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; } 

这足以在内核中引入对虚拟内存的完全支持。

参考文献


视频教程中的详细信息和说明。

git存储库中的源代码(您需要lesson6分支)。

参考文献


1.詹姆斯·莫洛伊(James Molloy)。 滚动自己的玩具UNIX克隆操作系统。
2.牙齿。 DOS,Windows,Unix的汇编器
3.卡拉什尼科夫。 汇编程序很简单!
4. Tanenbaum。 操作系统。 实施与开发。
5.罗伯特·洛夫(Robert Love)。 Linux内核 开发过程的描述。

Source: https://habr.com/ru/post/zh-CN467759/


All Articles