一直以来,我们的操作系统在启动后,运行的都是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中,以下三个地址需要抬升到内核地址空间:
- ESP
- GDT
- 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.c
与8/Kernel.c
一样,用于测试修改后的内存管理系统与显卡驱动是否工作正常。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。