目录
一、前景回顾
二、位图bitmap及函数实现
三、内存池划分
四、运行
前面我们已经花了一个回合来完善了一下我们的系统,包括增加了makefile,ASSERT以及一些常见的字符串操作函数。关于makefile,还是我以前学习Linux系统编程的时候学了一点点,很久没用导致就几乎都忘了,还是花了一下午时间去补了一下。看来知识这个东西,还是得温故而知新。
随时还是要回过头来总结一下我们的工作,上面是目前为止的工作,其实我们可以看到,现在我们的主要工作就是不停地往init_all()里面去填充一系列初始化函数,本回合也不例外,今天我们开始进入内存管理系统。
长话短说,举个例子,当我们的程序在申请使用一块物理内存时,该物理内存肯定是不能被占用的。所以这就要求我们每使用一块物理内存,就需要做个标记,这个标记用来指示该物理内存是否已被占用。而我们又知道内存被划分为多个4KB大小的页,如果我们的系统能够标记每一页的使用情况,这样上面的问题就迎刃而解了。所以基于位图bitmap的思想,我们有了如下的位图与内存的关系:
如图所示,我们知道1个字节等于8位,我们用每一位0或者1的状态来表示一页内存是否被占用,0就是未被占用,1就被已被占用。所以我们用一页内存4KB,就可以表示4*1024*8*4KB=128MB内存。
在project/lib/kernel目录下,新建bitmap.c和bitmap.h文件,还需要完善一下stdint.h文件。
bitmap.h
bitmap.c
stdint.h
除去页表和操作系统1MB的内存,我们将剩余的物理内存均分为两部分,一部分用于操作系统自己使用,称作内核内存,另一部分用于用户进程使用,称作用户内存。所以,针对这两块内存,需要有两个位图来管理。
另外,由于我们现在处于保护模式下,且开启了分页机制,所以每个进程使用的都是虚拟地址,且名义上都有4GB的虚拟地址大小。进程在申请内存时,首先应该是申请一块虚拟内存,随后操作系统再在用户内存空间中分配空闲的物理块,最后在该用户进程自己的页表中将这两种地址建立好映射关系。
因此,每新建一个进程,我们需要为每一个进程提供一个管理虚拟地址的内存池,也就是需要一个位图来管理。
最后,再啰嗦一下,针对内核也不例外,因为内核也是用的虚拟地址,所以我们也需要一个位图来管理内核的虚拟地址。
说了这么多,还是联系实际内存分布来讲一下内存池具体是怎么个划分法。
在我们前面讲解分页机制那一回,操作系统底层1MB加上页表和页表项所占用的空间,我们已经使用了0x200000,即2MB的内存,忘记的同学请看这里第08回开启分页机制,所以我们的内存分配是从地址0x200000开始。如下图所示:
我们的系统只有32MB的内存,在bochsrc.disk文件中可以看到,也可以在这里设置为其他内存,所以最高可以寻址到0x1FFFFFF处。
可分配的内存从0x200000到0x1FFFFFF处,均分后内核内存的范围就从0x200000~0x10fffff处,用户内存就从0x1100000~到0x1FFFFFF处。按道理来说,32MB空间的位图仅需要1/4物理页便能表示完,但是考虑到拓展性,我们便在0x9a000到0x9e000中间预留了4页,即共计16KB的大小来存储位图。
我们知道内核内存位图和用户内存位图是用来表示内核内存和用户内存的,那么内核虚拟地址位图表示的内存范围是多少呢?事实上,在Linux中任意一个进程的高1GB的空间都是被映射到内核,也即是说我们的内核空间最多只有1GB,因此内核虚拟地址也只有1GB。内核所使用的虚拟地址从0xc0000000开始,除去已经占用的1MB内存,那么内核所能使用的虚拟地址便是从0xc0100000到0xFFFFFFFF。实际到不了0xFFFFFFFF,因为我们这个系统的内核空间有限,按我们现在的规划,内核空间被分配了15MB,所以虚拟地址最多只能到0xc0100000+15MB=0xc0FFFFFF。
最后便是代码实现,在目录project/kernel下建立memory.c和memory.h文件。
memory.c
memory.h
关于代码这块,如果读者认真去读的话,可能会对这两个函数有所困惑,当时我也是思考了挺久,这里我尝试以我的理解方式来讲解一下,希望能对读者有所帮助。
uint32_t *pte_ptr(uint32_t vaddr) { uint32_t *pte = (uint32_t *)(0xffc00000 + / ((vaddr & 0xffc00000) >> 10) + / PTE_IDX(vaddr) * 4); return pte; } uint32_t *pde_ptr(uint32_t vaddr) { uint32_t *pde = (uint32_t *)(0xfffff000 + PDE_IDX(vaddr) * 4); return pde; }
先看pde_ptr函数,这个函数的作用就是给定一个虚拟地址A,返回该地址所在的页表的位置。注意,这个返回的地址也是虚拟地址B,只是这个虚拟地址B在我们的页表机制中,映射到虚拟地址A所在页表的真实物理地址,有点绕,需要多读一下。
那么如何得到这个虚拟地址B呢?
首先来分析一个虚拟地址,例如0xFFFFF001。
我们知道它的地址高10位是用来在页目录表中寻址找到页表地址,中间10位是用来在页表中寻址找到物理页地址,最后12位是用来在物理页中做偏移的。
又因为我们在页目录表中的最后一项中将本该填写的页表地址填写为页目录表的地址,所以现在我们通过0xFFFFF000这样的地址就能访问到页目录表本身,此时对于CPU来讲,页目录表就是一个物理页。不清楚的同学可以将数据带进去寻址以便理解。那么对于虚拟地址0xFFFFF001来说,他所在的页表地址是高10位决定的,我们通过PDE_IDX()函数,便能得到这高10位数据,随后再将该10位数据乘以4加上0xFFFFF000,便能得到虚拟地址0xFFFFF001所对应的页表的虚拟地址。
再来看pte_ptr函数,这个函数的作用就是给定一个虚拟地址A,返回该地址所在的物理页的地址,同样的,这个返回的地址也是一个虚拟地址,这里称作虚拟地址B。我们知道,物理页的地址是存放在页表中的,所以我们需要先得到页表地址。
还是以虚拟地址A,0xFFFFF001为例。
首先我们构建一个虚拟地址C,0xFFC00000,这个地址带进去寻址很好理解,我们只看高10位,寻址完后依旧是跳转到页目录表地址处,注意,此时CPU认为它是一个页表,而不是页目录表。接下来我们将虚拟地址A的高10位(通过 (vaddr & 0xffc00000) >> 10的方式得到)用来在这个页表中寻址,得到一个地址。这个地址其实就是虚拟地址A所在页表的地址,最后我们将虚拟地址A的中间10位(通过 (vaddr & 0x003FF000) >> 10的方式得到)乘以4,用来在这个页表中(此时CPU认为这是一个物理页,所以需要手动乘4)寻址,便得到了虚拟地址A所对应的物理页的虚拟地址。
写到这里,我还是感觉没有说的很清楚,限于表达能力有限,希望读者能够一边画图一边理解吧。
前面说了这么多,是时候验证一下我们的代码正确性。修改init.c和main.c文件,最后,不要忘记在makefile中增加bitmap.o和memory.o。
init.c
main.c
可以看到运行效果与我们实际规划一致,这一回就到这里。预知后事如何,请看下回分解。
转 https://www.cnblogs.com/Lizhixing/p/15941769.html
原创文章,作者:kepupublish,如若转载,请注明出处:https://blog.ytso.com/271887.html