搞定了鼠标和键盘这两个外设的处理,终于走到了内存管理这一步。平时写上层应用的时候,就是malloc、free、memset这几件套,较少关注内存多少、内存能不能用等底层细节,但是要制作操作系统,对于内存的检查和管理就不可或缺了,所以一章的内容是比较重要的。
这一章着重介绍了内存的检查和内存容量的管理。
一 内存检查
考虑在操作系统上电初始化的时候做一次内存检查,内存检查的代码实现如下:
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;
}
内存检查memtest步骤如下:
【1】确认CPU是386还是486以上的
如果是486以上,EFLAGS寄存器的第18位应该是所谓的AC标志位。这里,我们有意识地把1写入到这一位,然后再读出EFLAGS的值,继而检查AC标志位是否仍为1,最后,将AC标志位重置为0。
【2】禁止缓存
为了避免CPU内部的高速缓存(cache memory)对内存管理的影响,这边在进行内存检查之前需要先禁止内部高速缓存,检查完毕之后再开启。
实现的方法也很简单,就是对CR0控制寄存器的第29位NW和第30位CD置位,即可以禁止(具体可以参考我关于寄存器的帖子【操作系统】CPU寄存器详解)。
【3】内存检查memtest_sub
内存检查的思路是这样的,给一段内存赋值0xaa55aa55,尝试反转之后对比是不是变成了0x55aa55aa,然后再反转对比是不是变回0xaa55aa55,如果两次反转结果都正确,那么则认为内存正常无误。
作者一开始使用的C语言实现的,结果最终出来的结果不正确:
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) {
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;
}
通过查看编译出来的汇编语言,发现GCC在编译这一段C语言是,认为这一段操作反转来反转去还不是原值,所以在编译的过程中优化掉了。所以这一段内存检查的操作,还是需要使用底层的汇编直接实现:
_memtest_sub: ; unsigned int memtest_sub(unsigned int start, unsigned int end)
PUSH EDI
PUSH ESI
PUSH EBX
MOV ESI,0xaa55aa55
MOV EDI,0x55aa55aa
MOV EAX,[ESP+12+4] ; i = start
mts_loop:
MOV EBX,EAX
ADD EBX,0xffc
MOV EDX,[EBX]
MOV [EBX],ESI
XOR DWORD [EBX],0xffffffff
CMP EDI,[EBX]
JNE mts_fin
XOR DWORD [EBX],0xffffffff
CMP ESI,[EBX]
JNE mts_fin
MOV [EBX],EDX
ADD EAX,0x1000
CMP EAX,[ESP+12+8]
JBE mts_loop
POP EBX
POP ESI
POP EDI
RET
mts_fin:
MOV [EBX],EDX
POP EBX
POP ESI
POP EDI
RET
【4】允许缓存
禁止缓存的逆操作。
最终打印出来结果如下,检查了系统32M的内存:
二 内存管理
内存管理作者这边提到了几种方法:
【1】管理表:比如将一段125M的内存,按4K每块的形式管理,则一共分为32K块。建立一个32K的数组,用数组元素的0/1来代表每块内存是否使用。(优点:简单易理解;缺点:随着总内存的增大,管理表的大小也随之增大)
【2】改进管理表(用位的形式来存储):对上面的管理表进行优化,因为表示内存使用情况只需要0/1,用比特位来代替字节的表示,可以将内存管理表缩小到原来的1/8。(优点:简单易理解,相比于管理表,占用空间有所缩减;缺点:编程麻烦一些,而且相对于下面第三种方法,占用的空间还是较大)
【3】列表标签的形式管理:结构体记录使用标签,把类似于“从xxx号地址开始的yyy字节的空间是空着的”这种信息存在列表里,并根据实际使用情况进行更新。(优点:占用内存少,存取速度快,前两种都需要循环查表的操作,这个方法不用;缺点:管理程序变的复杂了,而且如果内存使用比较零散时,记录也会相应地增加)
struct FREEINFO { /* 可用状况 */
unsigned int addr, size;
};
struct MEMMAN { /* 内存管理 */
int frees;
struct FREEINFO free[1000];
};
struct MEMMAN memman;
memman.frees = 1; /* 可用状况list中只有1件 */
memman.free[0].addr = 0x0040 0000; /* 从0x00400000号地址开始,有124MB可用 */
memman.free[0].size = 0x07c0 0000;
为了简化开发,我们基于第三种方法,采用了一种“暂时割舍小块内存”的优化方法,只有当memman有空余的时候,才会对割舍掉内存进行检查找回。
申请内存memman_alloc如下(该函数很简单,不再赘述):
#define MEMMAN_FREES 4090 // 大约是 32 KB
#define MEMMAN_ADDR 0x003c0000
struct FREEINFO { /* 可用信息 */
unsigned int addr, size;
};
struct MEMMAN { /* 内存管理 */
int frees, maxfrees, lostsize, losts;
struct FREEINFO free[MEMMAN_FREES];
};
void memman_init(struct MEMMAN *man){
man->frees = 0; /* 可用信息数目 */
man->maxfrees = 0; /* 可用于观察可用状况:frees的最大值 */
man->losts = 0; /* 释放失败的内存大小总和 */
man->lostsize = 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;
}
/**
* @return start address of the mem ; 0 if can't alloc
*/
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) {
man->frees--;
// del 1 INFO
for (; i < man->frees; i++) {
// 移位处理,余量充足
man->free[i] = man->free[i + 1];
}
}
return a;
}
}
return 0;
}
释放内存memman_free如下:
/**
* @brief 向 addr 处分配 size 大小的内存空间,考虑到前后内存的合并
*
* @param man
* @param addr
* @param size
* @return 0 if successful ; -1 if fails
*/
int memman_free(struct MEMMAN *man, unsigned int addr, unsigned int size) {
int i, j;
/* 假装 free[] addr 是按照顺序排列的 */
// 寻找地址
// free[i - 1].addr < addr < free[i].addr
for (i = 0; i < man->frees; i++) {
if (man->free[i].addr > addr) {
break;
}
}
/* 前面有可用内存 */
if (i > 0) {
if (man->free[i - 1].addr + man->free[i - 1].size == addr) {
/* 【1】与前面的内存合并 */
man->free[i - 1].size += size;
/* 【2】后面有可用内存 */
if (i < man->frees) {
if (addr + size == man->free[i].addr) {
// 与后面的合并
man->free[i - 1].size += man->free[i].size;
man->frees--;
for (; i < man->frees; i++) {
man->free[i] = man->free[i + 1];
}
}
}
return 0;
}
}
/* 【3】后面有可用内存 */
if (i < man->frees) {
if (addr + size == man->free[i].addr) {
// 与后面的合并
man->free[i].addr = addr;
man->free[i].size += size;
return 0;
}
}
/* 【4】既不能与前面归纳到一起,也不能与后面归纳到一起 */
if (man->frees < MEMMAN_FREES) {
// free[i] 之后的向后挪
for (j = man->frees; j > i; j--) {
man->free[j] = man->free[j - 1];
}
man->free[i].addr = addr;
man->free[i].size = size;
man->frees++;
if (man->maxfrees > man->frees) {
// 更新最大值
man->maxfrees = man->frees;
}
return 0;
}
man->losts++;
man->lostsize += size;
return -1;
}
这个memman_free略显复杂,这边做一个详细的讲解,上面代码注释中的【1-4】分别代表了内存释放的几种情况:
【1】与前面内存合并的情况:
【2】前后相接的情况:
【3】与后面内存合并的情况:
【4】与前后均不相接的情况:
除了上面四种情形之外的其他情形,诸如重复free同一块内存(double free)、free越界等情况,均视为free失败,报错退出。
综上,在主函数中的实际操作如下:
#define MEMMAN_ADDR 0x003c0000
void HariMain(void)
{
//(中略)
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);
}
运行结果如下: