实际上 XV6 只允许扩展和收缩进程空间(虚拟空间),其进程空间是连续的。学习 XV6 进程空间的内存管理方法:
sysproc.c
中的 sys_sbrk() 系统调用入手,sys_sbrk()
将进一步调用 growproc(n) 进行内存空间的调整。proc.c
中的 growproc(n)
根据 n 的正负不同,分别利用 allocuvm()
进行扩展或 deallocuvm()
进行收缩。vm.c
中的 allocvum() 和 deallocuvm() 则是需要大家认真分析的。学习这两个函数时,要明确区分:物理页帧、页表、虚存地址三者之间的关系。其中 kalloc() 将分配一个物理页帧,kfree() 将释放一个页帧;mappages() 用于将建立虚存地址和物理页帧之间的页表影射。对 XV6 的进程空间的扩展和收缩有了解之后,可以思考如何实现 Linux 方式的 alloc()
和 free()
,因为它们可能造成内存空间中的孔洞,而 XV6 当前并不支持这样的内存布局。
XV6 的进程空间只有一个连续区间,只允许扩展和收缩两个操作,因此只需要一个 sz 成员就可以记录。如果要实现类似 Linux 操作系统的内存分配 alloc()
和释放 free()
操作,那么就可能在进程空间上造成空洞,这种不连续的空间需要其他额外信息来描述。
我们定义一个连续内存空间的描述符 vma 结构体,进程控制块添加 vm[10],可以记录 9 个连续内存,在生成进程的函数 allocproc 中初始化。
修改 proc.c
中的 procdump(),增加内存影像的输出,因此只需要按 Ctrl + p
就可以将每个进程的各 vma
起始地址和长度显示出来。
添加 myfree() 和 myalloc() 系统调用,在分配空间时需要:
vma
进行描述。在释放空间时需要:
vma
。我们这里分配和释放内存空间,都以页(4 KB)的整数倍为大小,以减少编成细节、减轻大家的编程工作量。在我的实现中,mygrowproc() 完成虚拟空间的检测和 vma
的分配,而 myreduceproc() 则完成虚拟空间的回收。
myallocuvm() 完成虚拟空间到物理页帧的映射,而 mydeallocuvm() 完成虚拟空间到物理页帧的解绑。
如果允许用户分配任意尺寸的空间,例如分配了 16 个字节和 32 字节的两个空间,那么你使用两个页帧来映射它们,还是用一个页帧来映射它们?如果用一个页来影射着 48 个字节,那么释放时又如何处理,你能提供一个比较完整的解决方案使得页帧的空间利用率较高?
参考答案:可以在内核态实现,也可以将这些细节封装成用户态库,用户直接调用函数即可。推荐使用用户态库,尽量保证内核态的简洁和高效。
为了测试新的内核代码,必须添加相应的系统调用 myalloc()
和 myfree()
,这里直接给出相应的辅助代码:
syscall.c
中的系统调用跳转表 syscalls[],并添加外部函数声明 sys_myalloc() 和 sys_myfree()
。编写应用程序 myalloc.c 并添加到 XV6,该程序连续分配 5 个空间,然后释放其中的 2、4。
开启系统后,运行 myalloc
,然后该程序会睡眠一段时间,再按 Ctrl + p
可以看到具体的空间。如下:
pid: 3, state: sleep , name: myalloc
start: 12288, length: 8192
start: 32768, length: 4096
start: 65536, length: 36864
若试图在空洞里面写数据,会出现如下结果:
pid 3 myalloc: trap 14 err 6 on cpu 1 eip 0x82 addr 0x5000--kill proc