硬件支持
如上图,对于x86硬件来说,其在寻址时(保护模式下),会通过CR3寄存器找到页目录表的地址,接着虚拟地址的高十位会作为页目录表的索引,从而找到对应的页表,接着虚拟地址的中间10个bit会被作为页表中的索引,从而找到对应的页的物理地址,最后虚拟地址的低12位被用来指示其在页中的偏移,由于偏移量占了12位,也就意味着每一个页的大小为4k个字节。
页目录表和页表都有1024条记录,这些记录的高20位对应的就是分配的物理地址,低12位为一些标志位,比如P标志位指示了当前该虚拟地址是否分配了物理地址,如果为0,则会产生错误(陷入中断),再比如U标志位指示了用户进程是否允许访问该页表,如果为0,则该页表项只能由内核访问。由于页目录表中和页表中每一项占32bit,所以每个页目录表和页表的大小也就为4096个字节。
进程地址空间
如上图,进程的地址空间从0地址一直到KERNBASE,再往上就是内核空间。上图中的各个宏的定义代码如下。
// Memory layout
#define EXTMEM 0x100000 // Start of extended memory
#define PHYSTOP 0xE000000 // Top physical memory
#define DEVSPACE 0xFE000000 // Other devices are at high addresses
// Key addresses for address space layout (see kmap in vm.c for layout)
#define KERNBASE 0x80000000 // First kernel virtual address
#define KERNLINK (KERNBASE+EXTMEM) // Address where kernel is linked
根据KERNBASE的值可以判断出每个进程所能使用的空间有2GB。
从上图中可以看出内核把自己映射到了0到PHYSTOP的物理地址,PHYSTOP代表物理地址的上限,但是由于内核部分的虚拟空间只有2GB,这也使得即使PHYSTOP大于2GB,也就是说物理内存大于2GB,内核也使用不了,因为物理空间超过了其虚拟空间的大小。
一些内存映射IO设备的物理地址从0xFE000000开始,所以虚拟地址直接映射过去就可以了。
另外内核部分的页表的U标志是清零的,这也代表内核空间是不允许用户进程访问的。
由于每个进程的地址空间都包含了内核和用户两部分,这也使得从用户空间切换到内核空间不需要进行页表的切换。
创建地址空间源码分析
在之前的源码分析系列文章中我们看到了给内核分配地址空间的是kvmalloc函数,其实也就是建立KERNBASE以上虚拟地址的映射,代码如下。
// Allocate one page table for the machine for the kernel address
// space for scheduler processes.
void
kvmalloc(void)
{
kpgdir = setupkvm();
switchkvm();
}
可以看到其主要是调用了setupkvm函数,代码如下。
// Set up kernel part of a page table.
pde_t*
setupkvm(void)
{
pde_t *pgdir;
struct kmap *k;
if((pgdir = (pde_t*)kalloc()) == 0)
return 0;
memset(pgdir, 0, PGSIZE);
if (P2V(PHYSTOP) > (void*)DEVSPACE)
panic("PHYSTOP too high");
for(k = kmap; k < &kmap[NELEM(kmap)]; k++)
if(mappages(pgdir, k->virt, k->phys_end - k->phys_start,
(uint)k->phys_start, k->perm) < 0) {
freevm(pgdir);
return 0;
}
return pgdir;
}
setupkvm函数主要是对内核部分的进程地址空间进行映射,代码定义如下。
static struct kmap {
void *virt;
uint phys_start;
uint phys_end;
int perm;
} kmap[] = {
{
(void*)KERNBASE, 0, EXTMEM, PTE_W}, // I/O space
{
(void*)KERNLINK, V2P(KERNLINK), V2P(data), 0}, // kern text+rodata
{
(void*)data, V2P