此前设置了一个简单的页表,使得内核可以在其链接地址 0xf0100000
运行,尽管它实际上是在 ROM BIOS 上方的物理内存 0x00100000
处加载的。这个页表只映射了 4MB 的内存。接下来会扩展这个映射,使得从虚拟地址 0xf0000000
开始的前 256MB 物理内存和虚拟地址空间的其他一些区域都被映射。先讲解内核地址空间的布局细节,随后增加映射。
内核地址空间
下面是虚拟内存对应的图示,其中定义了内核和用户模式软件都需要的内存管理相关的定义。
/*
* 虚拟内存映射: 权限
* 内核/用户
*
* 4 Gig --------> +------------------------------+ 这是4GB的虚拟内存空间的顶部。在32位系统中,虚拟内存地址范围是0到4GB。
* | | RW/--
* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
* : . :
* : . :
* : . :
* |~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~| RW/--
* | | RW/--
* | 重新映射的物理内存 | RW/--
* | | RW/--
* KERNBASE, ----> +------------------------------+ 0xf0000000 --+ 内核空间的开始,它在虚拟地址空间的高端。0xf0000000 意味着内核空间是1GB。
* KSTACKTOP | CPU0的内核栈 | RW/-- KSTKSIZE | KSTACKTOP是内核栈的顶部,每个CPU都有自己的内核栈。
* | - - - - - - - - - - - - - - -| |
* | 无效内存 (*) | --/-- KSTKGAP | 一个保护页,用于防止内核栈溢出。
* +------------------------------+ |
* | CPU1的内核栈 | RW/-- KSTKSIZE |
* | - - - - - - - - - - - - - - -| PTSIZE
* | 无效内存 (*) | --/-- KSTKGAP |
* +------------------------------+ |
* : . : |
* : . : |
* MMIOLIM ------> +------------------------------+ 0xefc00000 --+ 内存映射I/O的上限。
* | 内存映射的I/O | RW/-- PTSIZE
* ULIM, MMIOBASE --> +------------------------------+ 0xef800000 用户空间的上限和内存映射I/O的基址。
* | 当前页表 (用户 R-) | R-/R- PTSIZE
* UVPT ----> +------------------------------+ 0xef400000 用户虚拟页表的地址。
* | RO PAGES | R-/R- PTSIZE
* UPAGES ----> +------------------------------+ 0xef000000 页结构的只读副本的地址。
* | RO ENVS | R-/R- PTSIZE
* UTOP,UENVS ------> +------------------------------+ 0xeec00000 用户可访问的虚拟内存的顶部和环境结构的只读副本的地址。
* UXSTACKTOP -/ | 用户异常栈 | RW/RW PGSIZE 用户异常栈的顶部。
* +------------------------------+ 0xeebff000
* | 空内存 (*) | --/-- PGSIZE
* USTACKTOP ---> +------------------------------+ 0xeebfe000 普通用户栈的顶部。
* | 正常用户栈 | RW/RW PGSIZE
* +------------------------------+ 0xeebfd000
* | |
* | |
* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
* . .
* . .
* . .
* |~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~|
* | 程序数据和堆 |
* UTEXT --------> +------------------------------+ 0x00800000 用户程序开始的地方。
* PFTEMP -------> | 空内存 (*) | PTSIZE
* | |
* UTEMP --------> +------------------------------+ 0x00400000 --+ 临时页映射的地址。
* | 空内存 (*) | |
* | - - - - - - - - - - - - - - -| |
* | 用户STAB数据 (可选) | PTSIZE
* USTABDATA ----> +------------------------------+ 0x00200000 | 用户级STAB数据的地址。
* | 空内存 (*) | |
* 0 ------------> +------------------------------+ --+ 虚拟内存空间的底部,地址为0。
*
*/
这个虚拟内存映射图示提供了操作系统如何管理和使用虚拟内存的视图,包括内核空间、用户空间、内存映射 I/O 等等。
内核空间的映射
接下来建立三个映射,首先要在虚拟地址 UPAGES 处映射 'pages' 数组。
boot_map_region(kern_pgdir, UPAGES, pages_size, PADDR(pages), PTE_U | PTE_P);
这段代码的目的是在虚拟地址 UPAGES 处映射'pages'数组。'pages'数组是一个包含所有物理页面信息的数组,每个元素是一个struct PageInfo
结构体,表示一个物理页面的状态。
boot_map_region
函数实现细节上一篇文章已经讲过,用于在虚拟地址和物理地址之间建立映射。这里,它将虚拟地址 UPAGES 映射到'pages'数组的物理地址(通过PADDR(pages)
获取)。映射的大小是pages_size
,权限设置为PTE_U | PTE_P
,表示用户和内核都可以读取,但不能写入。这样,操作系统就可以通过访问虚拟地址 UPAGES 来访问和管理所有的物理页面信息了。
随后为内核栈分配物理内存。内核栈从虚拟地址 KSTACKTOP 向下增长。整个范围[KSTACKTOP-PTSIZE, KSTACKTOP)
都是内核栈,但将其分为两部分:
boot_map_region(kern_pgdir, KSTACKTOP - KSTKSIZE, KSTKSIZE, PADDR(bootstack), PTE_W);
[KSTACKTOP-KSTKSIZE, KSTACKTOP)
:由物理内存支持。[KSTACKTOP-PTSIZE, KSTACKTOP-KSTKSIZE)
:不由物理内存支持;因此,如果内核溢出其栈,它将出错而不是覆盖内存。这被称为"保护页"。
即分别对应下图中的两部分,其中 Invalid Memory 用作保护。
KERNBASE, ----> +------------------------------+ 0xf0000000 --+
KSTACKTOP | CPU0's Kernel Stack | RW/-- KSTKSIZE |
| - - - - - - - - - - - - - - -| |
| Invalid Memory (*) | --/-- KSTKGAP |
+------------------------------+ |
权限设置为:内核可读写,用户无权限。
上述代码将虚拟地址[KSTACKTOP-KSTKSIZE, KSTACKTOP)
映射到'bootstack'所指向的物理内存。这样,内核就可以使用这段内存作为其栈空间。
这种设计是为了防止内核栈溢出导致的内存覆盖问题。当内核栈溢出时,由于保护页没有物理内存支持,所以会触发一个错误,而不是覆盖其他内存区域。这样可以提高系统的稳定性和安全性。
boot_map_region(kern_pgdir, KERNBASE, 0xffffffff - KERNBASE, 0, PTE_W);
在这个例子中,虚拟地址范围[KERNBASE, 2^32)
被映射到物理地址范围[0, 2^32 - KERNBASE)
。虽然我们可能没有2^32 - KERNBASE
字节的物理内存,但我们仍然设置了映射。这是因为,即使物理内存不足,操作系统也需要能够访问整个虚拟地址空间。
这种设计可以简化内存管理,并提高系统的效率和安全性。因为内核可以直接通过虚拟地址访问任何物理内存,而无需进行复杂的地址转换。同时,由于用户进程不能直接访问物理内存,因此可以防止用户进程误操作或恶意操作导致的系统崩溃或数据泄露。