Day09
今天主要做的就是内存方面的事情:检查内存容量,以及设计内存管理。
一些感想
检查内存容量
检查内存容量有两种方法:
- 从BIOS获得
这样做确实挺简单,但是问题在于不同版本的BIOS,调用方法可能不同,而且会需要在asmhead.nas里多写一些东西。所以作者没有用这种方法 - 自己检查内存
自己检查内存用的是如下方法:当我们向一个地址写入一个任意值然后立刻读出来,如果是正确值则说明该地址在内存中,否则不在内存中的地址会返回一个脏数据。所以,我们只需要看最大到什么时候仍是有效,就知道内存最大是多少了。
但是,在检查之前需要先禁止cache,否则全部读写都会在cache中进行,达不到预期效果。
编译器优化
有时候,编译器会帮我们优化掉许多“不必要”的操作,但是有可能会使得程序不是我们预期的样子。所以需要有一定汇编知识,去看看汇编出来的结果是不是自己想要的。
应对编译器优化
为了防止自己想要的函数被优化掉,可以有如下一些方法:
- 设置编译命令
让编译器不进行优化即可。但是作者在这里觉得,其他地方还是需要使用编译优化的,就没有这么用。 - 手写汇编
把自己想要的函数用汇编语言写出来,供c语言调用——作者就是这么做的。 - 机器写汇编
既然要写汇编,不如用c语言不加优化翻译成汇编,复制粘贴过来,应该是一个效果吧) - 加修饰符
本人这么测试了一下:
嗯。。前面加了一个volatile,就能得到想要的效果了。。volatile unsigned int i, *p, old, pat0 = 0xaa55aa55, pat1 = 0x55aa55aa;
内存管理策略
- 分块管理
我们可以把内存分成若干个大小相等的块,当有需要的时候,就把连续的几块内存分出去,并标记为正在使用。释放的时候则标记为未使用即可。
这种做法好处是实现简单,坏处是占用内存较大。 - 列表管理
维护一个列表,存储从某个地址号开始,有多少内存是空闲的。
这样的好处是占用内存少、大块内存分配较快,但是管理程序比较复杂,而且可能会产生内存碎片。
对于内存碎片又有一些应对方法,一种是把这些碎片用分块管理,另一种是先割舍掉一些空余的碎片,当memman有空余时,再对使用中的内存进行检查,将割舍掉的那部分内容再捡回来。
列表管理的算法
其它的流程比较简单,这里放一张自制的释放的流程图。
这么看,结合接下来的代码就明白一些了。
那么,进入正题。
整理源文件
harib06a
昨天的各种鼠标、键盘中断,往bootpack.c里写了好多函数,现在把它们分出去。
具体操作略。
内存容量检查(1)
harib06b
再进行内存管理之前,要先检查最大可用容量。前面已经提到,需要禁用cache,进行检查。
#define EFLAGS_AC_BIT 0x00040000
#define CR0_CACHE_DISABLE 0x60000000
#define EFLAGS_AC_BIT 0x00040000
#define CR0_CACHE_DISABLE 0x60000000
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;
}
486机器开始添加了cache,因此需要先检查是否存在cache,并禁用/恢复。通过对CR0(之前也提到过这个寄存器)的某位进行操作,就可以禁用cache了。
然后是检查内存这个函数,这么做的目的开始已经提到过了,注意有一点技巧是i每次的步幅可以稍微大一些。
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 += 4)
for(i = start; i <= end; i += 0x1000)
{
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;
}
之后在主函数里添加相应代码
i = memtest(0x00400000, 0xbfffffff) / (1024 * 1024);
sprintf(s, "memory %dMB", i);
putfonts8_asc(binfo->vram, binfo->scrnx, 0, 32, COL8_FFFFFF, s);
我们预期的结果是32M,但是因为编译器优化,函数被完全优化掉了)
内存容量检查(2)
harib06c
为什么会被优化呢?
unsigned int i, *p, old, pat0 = 0xaa55aa55, pat1 = 0x55aa55aa;
// for(i = start; i <= end; i += 4)
for(i = start; i <= end; i += 0x1000)
{
p = (unsigned int *)(i + 0xffc);
old = *p; //先记住修改之前的值
*p = pat0; // 试写
*p ^= 0xffffffff; // 反转
if(*p != pat1) // 检查反转结果
{
not_memory:
*p = old;
break;
}
先写入pat0(const),然后XOR一个const,那就直接计算出结果吧。。发现肯定会和pat1(const)相等。所以if就被删掉了。
p = (unsigned int *)(i + 0xffc);
old = *p; //先记住修改之前的值
*p = pat0; // 试写
*p ^= 0xffffffff; // 反转
*p ^= 0xffffffff; // 再次反转
*p = old; // 恢复为修改之前的值
两次翻转相当于啥都没干,删掉。
p = (unsigned int *)(i + 0xffc);
old = *p; //先记住修改之前的值
*p = pat0; // 试写
*p = old; // 恢复为修改之前的值
连续写入,删掉
p = (unsigned int *)(i + 0xffc);
old = *p; //先记住修改之前的值
*p = old; // 恢复为修改之前的值
来回赋值,删掉。
p = (unsigned int *)(i + 0xffc);
p用不上,删掉
然后就成了一个空循环了。。难怪检测不到。
为了避免编译器优化,作者就把检查函数改成汇编语言来写了。(但还是感觉加一个volatile省时又省力。。)
naskfunc.nas
_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;
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
现在就能实现正确的结果了。
挑战内存管理
harib06d
作者采用了列表管理的方法,具体来看一下allloc和free都是怎么实现的。
结构体定义:
#define MEMMAN_FREES 4090 // 大约是32KB
#define MEMMAN_ADDR 0x003c0000
struct FREEINFO
{ // 可用信息
unsigned int addr, size;
};
struct MEMMAN
{ // 内存管理
int frees, maxfrees, lostsize, losts;
struct FREEINFO free[MEMMAN_FREES];
};
初始化、统计可用和alloc
void memman_init(struct MEMMAN *man)
{
man->frees = 0; // 可用信息条目
man->maxfrees = 0; // 用于观察可用状况:free的最大值
man->lostsize = 0; // 释放失败的内存的大小总和
man->losts = 0; // 释放失败次数
}
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; // 没有可用空间
}
这三个函数比较好理解。
然后是free的函数,看着流程图应该会清楚一些。
unsigned 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;
}
++man->losts;
man->lostsize += size;
return -1; //失败
}
实现好了之后,在主函数里添加对应调用
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, 0x0040000, 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);
注:作者free掉的两块内存可以查看前一天的内存分布图。
第一块是启动区要用到的(估计是),第二块就是空内存了。
嗯。。今天的内存管理就做好了。。