xv6 中的内存管理与虚拟内存
概述
注意:本文仍在撰写中。
内核的内存管理
关于内核的内存分配与回收的代码非常简单,只有三个函数:
// kalloc.c
void kinit(void);
void* kalloc(void);
void kfree(void *);
在系统初始化时,内核将所有空闲的物理内存页放入了一个空闲页链表中,即通过上面的 kinit
函数。
kalloc
函数即从空闲页链表中取出 head
,并更新新的 head
为 head->next
。
而 kfree
则是将要回收的 page 的物理内存地址作为新的 head
来更新链表。
注意,多核情况下需要加自旋锁来保证并发访问情况下的执行结果的正确性。
相关代码:
struct {
struct spinlock lock;
struct run *freelist;
} kmem;
内核的初始化工作
在 xv6 内核启动时,ID 为 0 的 CPU 会首先初始化物理页分配器,
其会将从内核的数据段的末尾到可用的最大物理地址 PHYSTOP
范围内的所有页加入到空闲页链表 kmem->freelist
中。
即下图中的 free memeory 部分:
之后其开始初始化内核的虚拟地址空间所需的页表。具体来说,
- 首先分配一个物理页给根页表项,
- 之后为所有可用的物理内存以及 I/O 设备的地址设置页表项,
- 对于每一个页表项,其值的物理内存地址部分设置为与对应的虚拟内存地址相同的值,即
direct-mapped
(The kernel maps each physical RAM address to the corresponding kernel virtual address)。 - 对于权限部分,内核的文本段权限设置为可读可执行不可写,以防止有意无意的改动内核代码。
- 对于每一个页表项,其值的物理内存地址部分设置为与对应的虚拟内存地址相同的值,即
- 遍历过程中,我们构建的是一个三级页表(Sv39: Page-Based 39-bit Virtual-Memory System),
内核的页表初始化完成后,就可以启用虚拟内存了。在 RISC-V 架构下即向 satp
寄存器写入虚拟内存地址空间的页表的根页表项的物理地址。
以上三部分在内核的 main
函数中的代码:
kinit(); // physical page allocator
kvminit(); // create kernel page table
kvminithart(); // turn on paging
注意,以上工作只需要 CPU 0 进行即可。
其他 CPU 在 CPU 0 完成初始化后,同样需要为自己的 satp
寄存器写入内核的虚拟内存地址空间的页表的根页表项的物理地址。
在设置好页表并启用虚拟内存后,页表硬件将自动为 CPU 将指令中的虚拟内存地址转换为对应的物理内存地址。
页表硬件通过 satp
寄存器来确定使用哪一个页表,因此在内核切换进程时,其同时也需要为页表设置好对应的 stap
寄存器。
由于我们为 kernel 初始化页表时,有意将其虚拟内存地址构造成对应的物理内存地址,因此启用虚拟内存后之前所创建的指针的值依然可以使用。
并且后续,在需要物理内存地址的情况下(例如给进程设置页表时),我们可以直接用内核的虚拟内存地址代替,因为两者值是一样的,这种情况称之为。
注意,kernel 的部分虚拟内存地址并不是 direct-mapped
的,例如上图中 MAXVA
到 PHYSTOP
部分。
内核栈的初始化
实际上在初始化内核页表后,xv6 还为所有的进程设置了内核栈。
在 xv6 中,系统支持的进程个数是有上限的,由 NPROC
宏所定义,值为 64。
这里其直接遍历进程列表,为每个进程创建内核栈。在第一个图中的左侧上部可以看到,亦即:
其中的 Guard Page 的 valid
位设置为 0,这样如果内核栈溢出就会导致 kernel panic,以尽快使 kernel 死掉以便方便定位问题。
注意,我们并不是真的分了一个物理内存页做 Guard Page,只是在内核的页表中设置了对应页表项而已。
相关代码:
// Allocate a page for each process's kernel stack.
// Map it high in memory, followed by an invalid
// guard page.
void
proc_mapstacks(pagetable_t kpgtbl)
{
struct proc *p;
for(p = proc; p < &proc[NPROC]; p++) {
char *pa = kalloc();
if(pa == 0)
panic("kalloc");
uint64 va = KSTACK((int) (p - proc));
kvmmap(kpgtbl, va, (uint64)pa, PGSIZE, PTE_R | PTE_W);
}
}
进程的内存地址空间
进程的用户地址空间:
在创建第一个用户进程 initcode
时,我们首先为其
ELF 文件的加载
xv6 中的可执行文件采取 ELF
格式。
Links: xv6-memory