一个操作系统的设计与实现——第10章 任务(一):共享内核

一直以来,我们的操作系统在启动后,运行的都是Kernel.c中的main函数。只运行这一个函数是不够的,操作系统应当有能力加载并运行其他程序。

从本章开始,将使用四章的篇幅讨论操作系统如何加载并运行任务。这里的任务(Task)与进程(Process)是同义词,在操作系统领域中,任务这个词更为常用,请读者知悉。

10.1 内核地址空间与任务地址空间

不同任务之间的内存应当是互相隔离的,这种隔离体现在页目录表上。只要操作系统为每个任务构造不同的页目录表,就能将每个任务的虚拟地址映射到不同的物理地址上。然而,操作系统是一个被所有任务共享的资源,这包括操作系统提供的GDT、IDT、各种函数等。怎么做到既隔离任务,又共享操作系统呢?

可以将4G的虚拟地址空间分为两部分,一部分专用于操作系统,剩下的用于任务。这样,只要保证内核的PDE在每个任务中都是一样的,就能实现操作系统的共享了。于是,在创建每个任务时,需要将内核的PDE复制到任务的页目录表中。

考察页分配器的实现:如果发现PDE不存在,就会分配一页,然后将页的物理地址与属性填入PDE。这一行为会导致页目录表被修改,而一旦页目录表发生修改,先前复制出去的内核PDE,就会和将来复制出去的内核PDE不同,这对于那些已经存在的任务来说是个隐患,一旦这些任务使用了新出现的PDE,就会因找不到PDE而引发错误。

想要避免这个问题,就需要在打开分页模式之前填好内核的所有PDE,并初始化内核的所有页表。这样一来,无论将来怎么分配内存,内核的PDE都不会再发生改变。

在我们的操作系统中,4G虚拟地址空间的前3G供任务使用,最后1G供内核使用,因此,内核可用的虚拟地址从0xc0000000开始。1个页表能够映射4M内存,所以,1G内存就需要256个页表。又因为页目录表的最后一项并没有指向页表,所以,只需要255个页表。1个页目录表和255个页表占用的内存刚好是1M,于是,可将其放置在0x100000~0x200000这段内存中。

10.2 重新组织内存

本章没有新增的模块,而是需要修改一些现有实现。

请看本章代码10/Mbr.s

第28行,将循环次数修改为0x100000 / 4。现在整个1M内存都用于页目录表和页表。

第33~42行,安装内核页目录表的第768~1022个PDE,这些PDE指向的页表的物理地址从0x101000开始向后递增,页属性为0x3。这样一来,由于第0个PDE和第768个PDE指向的都是第0个页表,所以从0x0开始的1M虚拟地址与从0xc0000000开始的1M虚拟地址是等价的。

在MBR中,以下三个地址需要抬升到内核地址空间:

  1. ESP
  2. GDT
  3. EIP

第64行,将ESP修改为低端1M内存可用部分的最高处:0xc00a0000,此地址以下的一页是内核栈。这样设计的目的将在后续章节中讨论。

第66~67行,将GDT的起始地址抬升到内核地址空间,然后重新加载GDT。

对EIP的修改将在稍后讨论。

第100行,将ELF缓冲区的地址抬升到内核地址空间。

第107~110行,将ELF中的几个地址抬升到内核地址空间。

第120行,将ELF的起始地址抬升到内核地址空间。

第137行,将ELF中的入口地址抬升到内核地址空间。

EIP可由jmp指令修改,而137行的这个jmp指令读取的是ELF中的入口地址,其值由链接器决定。所以,只要修改链接命令,就能在jmp指令执行时修改EIP了。

接下来,请看本章代码10/Makefile

第6行,将-Ttext-segment 0x0修改为-Ttext-segment 0xc0000000,从而将ELF中的所有地址都抬升到内核地址空间。

接下来,请看本章代码10/Print.hpp

需要将此文件中的所有0xb8...修改为0xc00b8...,使用代码编辑器的"替换"功能即可完成。

接下来,请看本章代码10/Memory.hpp

第12行,将物理地址位图的地址修改为0xc009e000,其位于内核栈的前一页。

第13行,将虚拟地址位图的地址修改为0xc009d000,其位于物理地址位图的前一页。

内核虚拟地址从0xc0100000开始可用。所以,第69行和第104行,将页分配器和页回收器的起始虚拟地址修改为0xc0100000

至此,针对内核地址空间的所有修改就都完成了。

10.3 测试

本章代码10/Kernel.c8/Kernel.c一样,用于测试修改后的内存管理系统与显卡驱动是否工作正常。

posted @ 2023-11-12 09:49  樱雨楼  阅读(29)  评论(0编辑  收藏  举报