XV6

XV6 通过页表机制实现了对内存空间的管理。通常你写程序的地址成为逻辑地址,经过分段机制变为线性地址,经过分页机制变为真正的物理地址。 我们下面不讨论分段机制,因此你只需要分清楚以下几个名词:程序空间(虚拟空间)、物理空间、逻辑地址(虚拟地址)、物理地址。

为了通俗易懂,我决定反过来讲,从结果推到起因。

1 内存分割

32 位的操作系统,将一个虚拟地址分割为如下形式:

directory table offset
10 10 12

也就是 4 KB 作为一页。 我们将物理内存切成多块,每块 4 KB,这里称之为物理页帧。

XV6 通过 freelist 来管理所有的空闲页。一开始 freelist 只是一个空指针,初始化的时候通过调用 kfree 函数将所有的物理页帧串成一个链表。

为了直观,在 kinit2 函数下,插入两条语句。运行得到 80400000 ~ 8e000000 。也就是说这个区间的内存都是空闲的。注意 :这里的地址是逻辑地址,在程序中的地址都是逻辑地址,所有数据的访问都要经过页表转换。也就是说,逻辑空间的 80400000~8e000000 都是空闲内存,对应物理空间的 400000~e000000 。XV6 巧妙的用一个叫 KERNBASE 的偏移值搞定了。

cprintf("第二次分配起始地址:%p\n", vstart);
cprintf("第二次分配结束地址:%p\n", vend);

1.1 内核占用内存

操作系统内核也是一个程序,因此也需要分配内存空间给系统内核,XV6 还没执行主函数的时候就将内核代码和数据放到了 KERNBASE + 0x100000 开始的逻辑地址上,对应于物理空间的 0X100000 ,就是简单的偏移。

XV6 将程序空间 0x80000000 后面的空间全部作为内核空间。一开始主函数调用了 kinit1() 分配了 [end, 4MB) 的物理内存,对应于程序空间的 [KERNBASE+end, KERNBASE+4MB) ,这里的 end 是加载 kernel 后的第一个地址。也就是说,从 [0X100000, 0xe000000) 的物理空间中,除了加载 kernel 的 ELF 文件所占用的内存,其他所有内存都被 freelist 给收纳了。

1.2 kinit1 的由来

主函数一开始并没有分配足够多的内存,而是分配了 4 MB 的内存。因为这里遇到了自举问题(分配器初始化空闲链表需要用到页表,页表需要空闲链表),因此 XV6 通过在 entry 中使用一个特别的页分配器来解决这个问题。该分配器会在内核数据部分的后面分配内存。该分配器不支持释放内存,并受限于 entrypgdir 中规定的 4 MB 分配大小。此时 entrypgdir 中的物理内存(0~4MB)映射到了程序空间的两个地方。

2 为内核创建页表映射

主函数调用了 kvmalloc() 函数生成了新的页表,此时内核建立的页表更加精巧地映射了内存空间。kmap 数组将物理空间分成了四部分(将数组切成四份)。

内核页表目录是一个全局变量 kpgdir ,首先从空闲链表中找一页出来作为页表目录。主要有四个步骤

  1. cr0 寄存器置为 1。cr0 是一个特殊的寄存器,是 X86 的分页机制的开关。

  2. 创建并填写页表目录。由函数 walkpgdir 实现。

    首先给你一个虚拟地址,利用前 10 位作为索引查找,如果 P 位为 0,说明没有分配相应的页表,这时候就需要给这个虚拟地址分配相应的内存创建页表了。

  3. 创建并填写页表。由函数 mappages() 实现。

    利用虚拟地址的 [10~19] 这十位作为页表索引找到相应的页表项,如果没有分配,那么分配物理页帧并填写权限和 P 位。

  4. 设置 CR3 页表寄存器,将其指向页表目录。

3 分配页帧和回收页帧

可以使用的物理页帧是 [end, PHYSTOP) 这个区间,。一般用于用户进程或者操作系统的记录需求。

这些物理页帧用链表 freelist 记录,由 kalloc() 统一分配,由 kfree() 回收。分配和回收都是在链表头部操作。

而且很重要的一点是,链表的节点地址就存储在相应空闲物理块的首地址,不需要额外的空间存储。

4 参考资料