本章内容包含两个方面:内存容量检查和内存管理。
1.内存容量检查
要进行内存管理,首先要知道内存的容量大小。最初启动时,BIOS会检查内存容量,所以BIOS知道内存容量大小,但由于BIOS版本不同,要写一段通用的代码会很费事,所以不如直接自己去检查内存。
在进行内存容量检查之前先说一下高速缓存cache。486以后的CPU具有高速缓存,它的读写速度可以接近CPU速度。每次访问内存,都要将所访问的地址和内容存入到高速缓存里。也就是存放成这样:18号地址的值是54,如果下次要用18号地址的内容,CPU就不读内存,而是用cache的信息。往内存写入数据也一样,首先更新cache信息,然后再写入内存。
内存检查时,要往内存里写入一个值,然后马上读取,来检查读取的值与写入的值是否相等。如果内存连接正常,写入的值能够记在内存里,如果没连接上,则读取的值是乱七八糟的数。
如果CPU使用了cache,则写入和读出的都是cache,所有内存检查都“正常”,检查处理不能完成。
综上,内存检查的步骤为:
1.首先关闭高速缓存:这里我们要先判断自己使用的CPU是否带有缓存(可以去掉判断,现在的CPU都有cache),然后关闭。
2.通过向内存写入数据,操作,看内存是否正确,然后在操作,看数据是否正确。最后回复原值。
注意:这里要注意C编译器的优化功能,因此作者把检测这一段使用了汇编来编写。
代码如下:
unsigned int memtest (unsigned int start, unsigned int end)
{
char flg486 = 0;
unsigned int eflg, cr0, i;
/* 确认CPU是386还是486以上的 */
eflg = io_load_eflags();
eflg |= EFLAGS_AC_BIT; /* AC-bit=1 */
io_store_eflags(eflg);
eflg = io_load_eflags();
if((eflg & EFLAGS_AC_BIT)!=0){ /* 如果是386,即使设定AC=1,AC的值还会自动回到0 */
flg486 = 1;
}
eflg &= ~EFLAGS_AC_BIT; /* AC-bit = 0 */
io_store_eflags(eflg);
if(flg486!=0) {
cr0 = load_cr0();
cr0 |= CR0_CACHE_DISABLE; /* 禁止缓存 */
store_cr0(cr0);
}
i = memtest_sub(start, end);
if(flg486!=0) {
cr0 = load_cr0();
cr0 &= ~CR0_CACHE_DISABLE; /* 允许缓存 */
store_cr0(cr0);
}
return i;
}
/* 注意:
这里要注意C编译器的优化功能,因此作者把检测这一段使用了汇编语言写在了nasfun.nas里。*/
unsigned int memtest_sub(unsigned int start, unsigned int end)
{
unsigned int i, *p, old, pat0=0xaa55aa55, pat1=0x55aa55aa;
for (i=start; i<=end; i+= 0x1000){ /* 每次增加0x1000 相当于4KB */
p = (unsigned int *)(i+0xffc); //只检查后四个字节
old = *p; // 先记住修改前的值
*p = pat0; // 试写
*p ^= 0xffffffff; // 反转
if (*p != pat1){ // 检查反转结果
not_memory:
*p = old;
break;
}
*p ^= 0xffffffff; // 再次反转
if (*p != pat0){ // 检查值是否恢复
goto not_memory;
}
*p = old; // 恢复为修改前的值
}
return i;
}
nasfun.nas中的memtest_sub函数:
_memtest_sub: ;unsigned int memtest_sub(unsigned int start, unsigned int end)
PUSH EDI ; 由于还要使用EBX,ESI,EDI
PUSH ESI
PUSH EBX
MOV ESI,0xaa55aa55 ; pat0 = 0xaa55aa55;
MOV EDI,0x55aa55aa ; pat1 = 0x55aa55aa;
MOV EAX,[ESP+12+4] ; i = start; 加12是因为前面有3次PUSH,堆栈指针下移12
mts_loop:
MOV EBX,EAX
ADD EBX,0xffc ; p = i+0xffc;
MOV EDX,[EBX] ; old = *p;
MOV [EBX],ESI ; *P = pat0;
XOR DWORD [EBX],0xffffffff ; *p ^= 0xffffffff;
CMP EDI,[EBX] ; if(*p!=pat1) goto fin;
JNE mts_fin
XOR DWORD [EBX],0xffffffff ; *p ^= 0xffffffff;
CMP ESI,[EBX] ; if(*p!=pat0) goto fin;
JNE mts_fin
MOV [EBX],EDX ; *p=old;
ADD EAX,0x1000 ; i+= 0x1000;
CMP EAX,[ESP+12+8] ; if(i<=end) goto mts_loop;
JBE mts_loop
POP EBX
POP ESI
POP EDI
RET
mts_fin:
MOV [EBX],EDX ; *p = old;
POP EBX
POP ESI
POP EDI
RET
bootpack.c节选:
i = memtest(0x00400000, 0xbfffffff) / (1024 * 1024);
sprintf(s, "memory %dMB", i);
putfonts8_asc(binfo->vram, binfo->scrnx, 0, 32, COL8_FFFFFF, s);
暂时先使用以上程序对0x00400000 ~ 0xbfffffff范围的内存进行检查,最大可识别3GB范围,0x00400000号以前的内存已经被使用了(参考8.5节内存分布图),没有内存程序根本不会运行到这里,所以没做内存检查。
如果在QEMU上运行,根据模拟器的设定,内存应为32MB。
2.内存管理
内存管理的基础:一是内存分配,而是内存释放。
书中介绍了两种方法:
法一:
以4KB(举例)为单位,然后建立数组。若已用,值为1;若未用,值为0。(这种方法很简单,但是有很大的压缩空间。因为记录一个单元的状态的只有0,1两种,也就是说1位就够了 ,内存记录所占空间变为原来的1/8,但程序变得复杂。)
优点:不用做拼接,碎片问题容易解决,内存使用的比较充分。
缺点:内存记录所使用的存储比较大,并且当分配或释放大块内存时,需要大量的写1或写0。
法二:
采用列表管理的方法,把类似“从xx号地址开始的yy字节是空的”这种信息都列在表里。
优点:省内存,而且在分配和释放大块内存时都很迅速。
缺点:管理程序麻烦,可用空间可能被搞得零零散散,怎么都归纳不到一块儿时,会将1000条可用空间管理信息全部用完。
作者最终在法二的基础上,加了一点处理,即“割舍掉的东西,只要以后还能找回来,就暂时不去管它”。
bootpack.c 代码如下:
void memman_init(struct MEMMAN *man)
{
man->frees = 0; /* 可用信息数目 */
man->maxfrees = 0; /* 用于观察可用状况:frees的最大值 */
man->lostsize = 0; /* 释放失败的内存大小总和 */
man->losts = 0; /* 释放失败次数 */
return;
}
unsigned int memman_total(struct MEMMAN *man)
/* 报告空余内存大小的合计 */
{
unsigned int i, t = 0;
for (i=0;i<man->frees;i++){
t += man->free[i].size;
}
return t;
}
unsigned int memman_alloc(struct MEMMAN *man, unsigned int size)
/* 分配 */
{
unsigned int i, a;
for (i=0; i < man->frees;i++){
if(man->free[i].size >= size){ /* 找到了足够大的内存 */
a = man->free[i].addr;
man->free[i].addr += size;
man->free[i].size -= size;
if(man->free[i].size==0){ /* 如果free[i]变成了0,就减掉可用信息 */
man->frees--;
for(; i<man->frees; i++){
man->free[i]=man->free[i+1]; /* 代入结构体 */
}
}
return a;
}
}
return 0; /* 没有可用空间 */
}
int memman_free(struct MEMMAN *man, unsigned int addr, unsigned int size)
/* 释放 */
{
int i, j;
/* 为了便于归纳总结,将free[]按照addr的顺序排列 */
/* 所以, 先决定应该放在哪里 */
for (i=0;i<man->frees;i++){
if (man->free[i].addr > addr){
break;
}
}
/* free[i - 1].addr < addr < free[i].addr */
if(i>0){
/* 前面有可用内存 */
if (man->free[i-1].addr +man->free[i-1].size == addr){
/* 可以与前面的可用内存归纳到一起 */
man -> free[i-1].size += size;
if(i < man->frees) {
/* 后面也有 */
if(addr + size == man->free[i].addr){
/* 也可以与后面的可用内存归纳到一起 */
man->free[i-1].size += man->free[i].size;
/* man->free[i]删除 */
/* free[i]变成0后归纳到前面去 */
man->frees--;
for(; i<man->frees; i++){
man->free[i] = man->free[i+1]; /*结构体赋值*/
}
}
}
return 0; /* */
}
}
/* 不能与前面的可用空间归纳到一起 */
if (i< man->frees){
/* 后面还有 */
if (addr + size == man->free[i].addr){
/* 可以与后面的内容归纳到一起 */
man->free[i].addr = addr;
man->free[i].size += size;
return 0; /* 成功完成 */
}
}
/* 既不能与前面归纳到一起,也不能与后面归纳到一起 */
if(man->frees < MEMMAN_FREES){
/* free[i]之后的,向后移动,腾出一点可用空间 */
for(j=man->frees; j>i; j--){
man->free[j] = man->free[j-1];
}
man->frees++;
if (man->maxfrees < man->frees){
man->maxfrees = man->frees; /* 更新最大值 */
}
man->free[i].addr = addr;
man->free[i].size = size;
return 0; /* 成功完成 */
}
/* 不能往后移动 */
man->losts++;
man->lostsize +=size;
return -1; /* 失败 */
}
HariMain中代码:
...
unsigned int memtotal;
struct MEMMAN *memman = (struct MEMMAN *)MEMMAN_ADDR;
...
memtotal = memtest(0x00400000, 0xbfffffff);
memman_init(memman);
memman_free(memman, 0x00001000, 0x0009e000); /* 0x00001000 - 0x0009efff */
memman_free(memman, 0x00400000, memtotal-0x00400000);
...
sprintf(s, "memory %dMB free : %dKB",
memtotal/(1024*1024), memman_total(memman)/1024);
putfonts8_asc(binfo->vram, binfo->scrnx, 0, 32, COL8_FFFFFF, s);
这些天由于忙于其他事而停下了对操作系统的学习,今天重新拾起,再接再厉,加油!