30天自制操作系统(day9)

第9天:内存管理

1、内容1:整理源文件

因为之前为了能让鼠标动起来,不断的修改bootpack.c文件,使得文件程序代码变得很长,因此将程序进行整理修改。
具体修改如下:
30天自制操作系统(day9)
需要注意的是,在Makefile文件中的OBJS_BOOTPACK=里,要将keyboard.obj和mouse.obj
也要补进去。
运行之后一切正常。

2、内容2:内存容量检查(1)

要进行内存管理的第一步便是要确定内存有多大,范围是从哪里到哪里。通过自己制作程序去检查内存,而不是通过BIOS来询问。
为什么不用BIOS呢?
因为虽然在最初启动时,BIOS会检查内存容量,我们只需要询问BIOS就可以知道内存容量有多大。但这样的做法,会使asmhead.nas变长,另一方面,因为BIOS的版本不同,BIOS函数的调用方法也不相同,会导致很多的麻烦。
自己检查内存的方法:
首先先让486以后的CPU的高速缓存功能无效,在读出、向内存里写入数据时,首先更新高速缓存的信息,然后再写入内存。如果先写入内存,那么在等待写入完成的期间,CPU处于空闲状态,这样会影响速度,因此,先更新缓存,缓存控制配合内存的速度,然后再慢慢发送内存写入命令。
在内存检查时,要往内存里随便写入一个值,然后马上读取,来检查读取的值与写入的值是否相等。如果内存连接正常,那么写入的值能够记在内存里。如果没连接上,那么读出的值便会是混乱的。如果在CPU里加上了缓存,那么写入和读出的便不是内存,而是缓存,最后便会导致所有的内存检查都是正常的,那么检查处理便不能完成。因此,只有在内存检查时将缓存设为OFF,这样才能进行正常的内存检查。
如何判断缓存是否要关闭,就需要先检查CPU是不是在486以上,如果是的话,那么就将缓存关闭。
创建memtest函数来实现(在b文件夹中的bootpack文件):
30天自制操作系统(day9)
首先判断CPU是否是486以上还是386的,如果是486以上的话,那么EFLAGS寄存器的第18位就是所谓的AC标志位,如果CPU是386的,那么就没有这个标志位,第18位会是一直保持为0。现将1写入到第18位,然后再读出EFLAGS的值,继而检查AC标志位是否仍未1,最后,将AC标志位重置为0。
将AC标志位重置为0时,将eflag与上EFLAGS_AC_BIT即可。是取反运算符,将所有的位都反转,~EFLAGS_AC_BIT与0xfffbffff一样。
使用汇编语言编写函数load_cr0和store_cr0,对CR0寄存器的某一标志位进行操作来禁止缓存。
Load_cr0和store_cr0函数如下(在b文件夹中的naskfunc文件):
30天自制操作系统(day9)
Memtest_sub函数:内存检查处理的实现部分(在b文件夹中的bootpack文件)
30天自制操作系统(day9)
调查从起始地址到结束地址的范围,即能够使用的内存的末尾地址。如果p不是指针的话,就不能通过制定地址去读取内存,因此先执行p=i,然后用这个p,先将原来的值保存下来。接着试写0xaa55aa55,在内存里反转这个值,检查反转后的结果是否正确,如果是正确的,那么就再反转它,检查是都等于一开始设置的初始值。然后再用old变量,将内存的值恢复回去。如果在这个过程中,没有恢复到原来的值,那么就在某个地方停止,并报告停止时的地址。
反转的实现通过xor运算符来实现,*p=0xffffffff是*p=*p0xffffffff的省略形式。I的值每次增加4是因为每次要检查4个字节。
运行程序之后发现运行速度很慢,因此改变i的赋值,i在之前是每次增加4,而要检查全部内存的话,步长太小了,因此改成了每次增加0x1000,相当于4KB,这样速度变提升了。除此以外,将p=i修改为p=i+0xffc,让其只检查末尾的4个字节。
30天自制操作系统(day9)每次增加4字节
修改HariMain程序:
30天自制操作系统(day9)
暂时先使用程序对0x00400000~0xbfffffff范围的内存进行检查,这个程序最大可以识别3GB范围的内存。0x0040000号以前的内存已经被使用过了,没有内存,程序也不会运行到这里,因此就没有做内存检查。在这里我们使用MB为单位,因为byte或KB为单位来显示结果不容易看明白。
实验结果:
30天自制操作系统(day9)
内存容量并不是我们所预期的32MB,而是3072MB,也就是我们之前的检查程序出现了问题。

3、内容3:内存容量检查(2)*6b文件夹

在之前的内存检查中检查到内存容量为3072MB,而不是32MB,这是因为C编译器的bug,它自动将我们的程序进行了修改以达到它所谓的最优程序。
首先用make –r bootpack.nas来运行,先确认bootpack.c被编译成了什么样的机器语言。
30天自制操作系统(day9)
30天自制操作系统(day9)
将上述与之前的memtest_sub函数比较,可以发现编译结果与我们原来程序的显示是不一样的。
C编译器的过程:
首先将内存的内容保存到old里,然后写入part0的值,再反转,最后跟pat1进行比较。在编译器的思路中,这是一定相等的,因而就将其删掉,接下来是比较*p和pat0是否相等,在它觉得,这也是一定相等的,因而为了提升速度,将这一部分也删去,因此,之前的c程序就变为了:
30天自制操作系统(day9)
再接着,反转再反转,最终还是回到初始的状态,因而这些处理也可以删去,因此程序就变成了下述情况:
30天自制操作系统(day9)
30天自制操作系统(day9)
再接着,p=pat0,本来就没有意义,要将old的值赋给p,因此程序变为了:
30天自制操作系统(day9)
在这里,*p并没有写入什么内容,并且,地址变量p虽然计算了地址,但其实一次也没有用掉,编译器便将这些全部舍弃。
最终编译器优化后的代码如下:
30天自制操作系统(day9)
并不是一开始所设计的程序的意思了,因此需要进行修改。如果更改编译选项,是可以停止最优化处理的,但是之后还有其他的部分需要用到编译器的最优化处理,因此,决定将memtest_sub也用汇编来描写。
用汇编写的程序如下:6c文件夹
30天自制操作系统(day9)
编写完后,将bootpack.c中的memtest_sub函数删去,再运行。
运行结果:
30天自制操作系统(day9)

4、内容4:挑战内存管理

什么是内存管理?为什么要进行内存管理?
操作系统在工作中,有时需要分配一定大小的内存,在用完之后又不再需要,为了应付这些需求,需要管理好哪些内存可以使用(即哪些内存空闲),哪些内存不可以使用(即正在使用)。如果不进行管理,系统便会不知道哪里可用,或者多个应用程序使用同一地址的内存。
内存管理的基础,一是内存分配,一是内存释放。如果程序需要多少内存,内存管理程序便会给出一个能*使用的内存地址,另一方面,内存使用完了,需要把内存归还给内存管理程序。
假设有128MB的内存,即0x08000000个字节,假设以0x1000个字节为单位管理。
方案1:
0x80000000/0x1000=0x8000=32768,首先创建32768字节的区域,可以向其中写入0或者1来标记哪里是空的,哪里是正在使用的。
如果需要100KB的空间,那么只要从a中找出连续25个标记位0的地方即可。
30天自制操作系统(day9)
如果找到了标记连续为0的地方,暂时将这些地方标记为“正在使用”,然后从j的值计算出对应的地址。以0x1000字节为管理单位,所以将j放大0x1000倍即可。
30天自制操作系统(day9)
30天自制操作系统(day9)
如果要释放这部分内存空间,用地址值除以0x1000,计算出j即可。
30天自制操作系统(day9)
再次需要内存的时候,这个地方又可以再次使用。
方案2:
列表管理,类似于将“从xxx号地址开始的yyy字节的空间是空着的”这种信息都存在列表里。
30天自制操作系统(day9)
考虑到即使可用内存部分不连续,设置1000个free。如果需要100KB的空间,只要查看memman中free的状态,从中找到100MB以上的可用空间即可。
30天自制操作系统(day9)
如果找到了可用内存空间,就将这一段信息从可用内存空间管理表中删除。
30天自制操作系统(day9)
如果size变成了0,即这一段可用信息不再需要了,就将这条信息删除,frees减去1即可。释放内存时,增加一条可用信息,frees加1。如果这段新释放出来的内存与相邻的可用空间能连到一起,就将他们归纳为一条。
比如:
30天自制操作系统(day9)
归纳为:
30天自制操作系统(day9)
如果不将可归纳的归纳,当之后系统要求提供0x07bf00000字节的内存时,即使有这么多可用空间,但查找程序却会找不到。
内存管理表的优点:
占用内存少,管理3GB的内存只需要8KB左右即可。大块内存的分配和释放都非常迅速。在分配内存时,只要加法运算和减法运算各执行一次就结束了。
缺点:
管理程序变复杂了,尤其是将可用信息归纳到一起的处理,会变得十分复杂。当可用空间被搞得零零散散时,会将1000条可用空间管理信息全部用完。
为了解决这些问题,可以先将一部分内存割舍掉,当memmam有空余时,再对使用中的内存进行检查,将割舍掉的那部分内容再捡回来。
创建的程序:
30天自制操作系统(day9)
30天自制操作系统(day9)
在struct MEMMAN,创建了4000组,留出余量。函数memmam_init对memman进行初始化,设定为空,主要是将frees设为0。函数memmam_total用来计算可用内存的合计大小并返回。最后的memmam_alloc函数,分配指定大小的内存。
释放内存的函数:
30天自制操作系统(day9)
Memmam需要32KB,因此使用0x003c0000开始的32KB,然后计算内存总量memtotal,将现在不用的内存以0x1000个字节为单位注册到memman里,最后显示合计可用内存容量。在QEMU上执行时,有时会注册成632KB和28MB,632+28672=29304,因此屏幕上会显示出29304KB。
实验结果:
30天自制操作系统(day9)

二、遇到的问题及解决方法

1、问题1:cache具体的工作原理是什么?
30天自制操作系统(day9)
解决方法:当CPU处理数据时,它会先到Cache中去寻找,如果数据因之前的操作已经读取而被暂存其中,就不需要再从随机存取存储器(Main memory)中读取数据——由于CPU的运行速度一般比主内存的读取速度快,主存储器周期(访问主存储器所需要的时间)为数个时钟周期。因此若要访问主内存的话,就必须等待数个CPU周期从而造成浪费。
提供“缓存”的目的是为了让数据访问的速度适应CPU的处理速度,其基于的原理是内存中“程序执行与数据访问的局域性行为”,即一定程序执行时间和空间内,被访问的代码集中于一部分。为了充分发挥缓存的作用,不仅依靠“暂存刚刚访问过的数据”,还要使用硬件实现的指令预测与数据预取技术——尽可能把将要使用的数据预先从内存中取到缓存里。

2、问题2:AC标志位是什么?
解决方法:AC (Alignment check)——位18,对齐检查。置位该标志和控制寄存器CR0 的AM 标志则启用对内存引用的对齐检查,清除这两个标志则禁用对齐检查。当引用一个没有对齐的操作数时,将会产生一个对齐检查异常,比如在奇地址引用一个字地址或在不是4 的倍数的地址引用一个双字地址。对齐检查异常只在用户态(3 级特权)下产生。默认特权为0 的内存引用,比如段描述符表的装载,并不产生这个异常,尽管同样的操作在用户态会产生异常。对齐检查异常用于检查数据的对齐,当处理器之间交换数据时这很有用,交换数据需要所有的数据对齐。对齐检查异常也可供解释程序使用。让某些指针不对齐就好比做上特殊标记,这样就无需对每个指针都进行检查,只在用到的时候,对这些特殊指针进行处理就可以了。