Day09

今天主要做的就是内存方面的事情:检查内存容量,以及设计内存管理。

一些感想

检查内存容量

检查内存容量有两种方法:

  1. 从BIOS获得
    这样做确实挺简单,但是问题在于不同版本的BIOS,调用方法可能不同,而且会需要在asmhead.nas里多写一些东西。所以作者没有用这种方法
  2. 自己检查内存
    自己检查内存用的是如下方法:当我们向一个地址写入一个任意值然后立刻读出来,如果是正确值则说明该地址在内存中,否则不在内存中的地址会返回一个脏数据。所以,我们只需要看最大到什么时候仍是有效,就知道内存最大是多少了。
    但是,在检查之前需要先禁止cache,否则全部读写都会在cache中进行,达不到预期效果。

编译器优化

有时候,编译器会帮我们优化掉许多“不必要”的操作,但是有可能会使得程序不是我们预期的样子。所以需要有一定汇编知识,去看看汇编出来的结果是不是自己想要的。

应对编译器优化

为了防止自己想要的函数被优化掉,可以有如下一些方法:

  1. 设置编译命令
    让编译器不进行优化即可。但是作者在这里觉得,其他地方还是需要使用编译优化的,就没有这么用。
  2. 手写汇编
    把自己想要的函数用汇编语言写出来,供c语言调用——作者就是这么做的。
  3. 机器写汇编
    既然要写汇编,不如用c语言不加优化翻译成汇编,复制粘贴过来,应该是一个效果吧)
  4. 加修饰符
    本人这么测试了一下:
    volatile unsigned int i, *p, old, pat0 = 0xaa55aa55, pat1 = 0x55aa55aa;
    
    嗯。。前面加了一个volatile,就能得到想要的效果了。。

内存管理策略

  1. 分块管理
    我们可以把内存分成若干个大小相等的块,当有需要的时候,就把连续的几块内存分出去,并标记为正在使用。释放的时候则标记为未使用即可。
    这种做法好处是实现简单,坏处是占用内存较大。
  2. 列表管理
    维护一个列表,存储从某个地址号开始,有多少内存是空闲的。
    这样的好处是占用内存少、大块内存分配较快,但是管理程序比较复杂,而且可能会产生内存碎片。
    对于内存碎片又有一些应对方法,一种是把这些碎片用分块管理,另一种是先割舍掉一些空余的碎片,当memman有空余时,再对使用中的内存进行检查,将割舍掉的那部分内容再捡回来。

列表管理的算法

其它的流程比较简单,这里放一张自制的释放的流程图。
free
这么看,结合接下来的代码就明白一些了。

那么,进入正题。

整理源文件

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掉的两块内存可以查看前一天的内存分布图。
第一块是启动区要用到的(估计是),第二块就是空内存了。
嗯。。今天的内存管理就做好了。。