XV6 通过页表机制实现了对内存空间的管理。通常你写程序的地址成为逻辑地址,经过分段机制变为线性地址,经过分页机制变为真正的物理地址。 我们下面不讨论分段机制,因此你只需要分清楚以下几个名词:程序空间(虚拟空间)、物理空间、逻辑地址(虚拟地址)、物理地址。
为了通俗易懂,我决定反过来讲,从结果推到起因。
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);
操作系统内核也是一个程序,因此也需要分配内存空间给系统内核,XV6 还没执行主函数的时候就将内核代码和数据放到了 KERNBASE + 0x100000
开始的逻辑地址上,对应于物理空间的 0X100000
,就是简单的偏移。
XV6 将程序空间 0x80000000
后面的空间全部作为内核空间。一开始主函数调用了 kinit1()
分配了 [end, 4MB)
的物理内存,对应于程序空间的 [KERNBASE+end, KERNBASE+4MB)
,这里的 end
是加载 kernel 后的第一个地址。也就是说,从 [0X100000, 0xe000000)
的物理空间中,除了加载 kernel 的 ELF 文件所占用的内存,其他所有内存都被 freelist
给收纳了。
主函数一开始并没有分配足够多的内存,而是分配了 4 MB 的内存。因为这里遇到了自举问题(分配器初始化空闲链表需要用到页表,页表需要空闲链表),因此 XV6 通过在 entry
中使用一个特别的页分配器来解决这个问题。该分配器会在内核数据部分的后面分配内存。该分配器不支持释放内存,并受限于 entrypgdir
中规定的 4 MB
分配大小。此时 entrypgdir
中的物理内存(0~4MB)映射到了程序空间的两个地方。
主函数调用了 kvmalloc()
函数生成了新的页表,此时内核建立的页表更加精巧地映射了内存空间。kmap
数组将物理空间分成了四部分(将数组切成四份)。
内核页表目录是一个全局变量 kpgdir
,首先从空闲链表中找一页出来作为页表目录。主要有四个步骤
将 cr0
寄存器置为 1。cr0
是一个特殊的寄存器,是 X86 的分页机制的开关。
创建并填写页表目录。由函数 walkpgdir
实现。
首先给你一个虚拟地址,利用前 10 位作为索引查找,如果 P 位为 0,说明没有分配相应的页表,这时候就需要给这个虚拟地址分配相应的内存创建页表了。
创建并填写页表。由函数 mappages()
实现。
利用虚拟地址的 [10~19]
这十位作为页表索引找到相应的页表项,如果没有分配,那么分配物理页帧并填写权限和 P 位。
设置 CR3 页表寄存器,将其指向页表目录。
可以使用的物理页帧是 [end, PHYSTOP)
这个区间,。一般用于用户进程或者操作系统的记录需求。
这些物理页帧用链表 freelist
记录,由 kalloc()
统一分配,由 kfree()
回收。分配和回收都是在链表头部操作。
而且很重要的一点是,链表的节点地址就存储在相应空闲物理块的首地址,不需要额外的空间存储。