第9天 内存管理

第9天 内存管理

2020.4.8

1. 整理源文件(harib06a)

  • 修改函数所在源文件
    • 新增mouse.c和keyboard.c源文件。
    • 因此,需要修改Makefile。

2. 内存容量检查(1)(harib06b)

  • 进行内存管理之前,我们必须知道内存究竟有多大。具体方法有二:

    • 在最初起启动的时候,BIOS肯定会检查内存容量,所以我们只要问一问BIOS,就知道内存容量有多大了。但这样有问题:
      • 一方面,asmhead.nas会变长;
      • 另一方面,BIOS版本不同,BIOS函数的调用方法也不同。
    • 自己写。
      • 这里,我们自己写代码去检查内存。
      • 中心思想是:按顺序检查内存地址,直到内存检查出现异常,这时迭代的上限就是内存的容量大小。
  • 首先,暂时让486以后的CPU的高速缓存(cache)功能无效。

    • 高速缓存cache:
      • 内存与CPU的距离与CPU内部元件之间的距离远得多。因此在CPU的寄存器内部MOV比在CPU和内存之间MOV要快得多。
      • 但是CPU的记忆能力很差,即使知道访问内存的速度不快,还是得不得不频繁地使用内存。
      • 因此,英特尔的大叔们在CPU里面加了一点儿存储器,它被称为高速缓冲存储器(cache memory)。这种高速缓冲存储器的价格昂贵,因此它的容量只能比较小(一般是内存的千分之一)。
    • 使用高速缓存的方法:
      • 类似于快表机制
      • 时间局限性:某些指令被执行后,不久后可能会在此执行,某些数据被访问后,不久可能会再次访问。【循环
      • 空间局限性:一旦程序访问了某个存储单元,不久后其相邻的存储单元也可能被访问。【数组
      • 简单来讲,CPU访问内存获取数据时,先访问高速缓存,如果有所需数据,那么成功击中;如果没有,再访问内存,获取数据后再将数据写入高速缓存。 CPU访问内存写数据时,先向高速缓存中写入,然后再写入内存。
      • 386的CPU没有高速缓存,486的高速缓存只有8-16KB,但二者的性能相差了6倍以上,这主要归功于高速缓存。
    • 为什么要屏蔽缓存?
      • 内存检查时,要往内存中随便写入一个值,然后马上读取,如果写入的值和读取的值相同,那么内存连接正常,否则内存连接失败。
      • 如果CPU有缓存,那么优先访问缓存,这样就不是内存检查了,而是缓存检查了(笑)。
    • 因此,只有在内存检查的时候,我们才将缓存设为OFF。
      • 具体做法是:先检查CPU是不是486及以上,如果是就将缓存设置为OFF。
  • 有了上述思路,bootpack.c中的memtest函数应运而生:

    #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;
    }
    
    • 将AC位重置为0的时候,用到了~'~':按位取反。
    • 为了禁止缓存,需要对CR0寄存器的某一标志位进行操作【将29位和30位置为1】。
    • 函数load_cr0和store_cr0不能用C语言写,因此在naskfunc.nas中用汇编语言来写:
      _load_cr0:		; int load_cr0(void);
              MOV		EAX,CR0
              RET
      
      _store_cr0:		; void store_cr0(int cr0);
              MOV		EAX,[ESP+4]
              MOV		CR0,EAX
              RET
      
      代码比较简单,此处不再详述。
  • 再就是函数memtest_sub:

    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) {
            p = (unsigned int *) i;
            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;
    }
    
    • 这个函数的作用是:调查从start地址到end地址的范围内,能够使用的内存的末尾地址。
    • 首先,定义指针p(int类型,一次读4字节)。
    • 其次,将指针p指向的内存地址的内容保存下来,保存到变量old中。
    • 再次,试写0xaa55aa55(这个值随便定义),在内存里反转该值,检查反转结果是否正确。
      • 有些机种即便不进行这种检查也没问题。但是有些机种,如果不做这种检查就会直接读出写入的数据。
    • 如果正确,就再次反转,检查一下是否和原先的值一致。
    • 最后,用old变量将p指向的内存区域写回原来的值。
    • 在反转的时候,使用按位异或^
    • i+=4代表一次检查4字节。
  • 加快函数memtest_sub的执行速度:

    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);
            ……
    }
    
    • i += 0x1000 : 每次检查0x1000字节,也就是4KB。这样,速度提升1000倍。
    • p = (unsigned int *) (i + 0xffc) : 每次只检查4KB的末尾4个字节。如果末尾4字节读写正确,我们便认为这4KB也是正确的。
    • 只是确认内存容量的话,每次检查1MB也是可以的。
  • 在bootpack.cd的HariMain中添加代码:

    i = memtest(0x00400000, 0xbfffffff) / (1024 * 1024);
    sprintf(s, "memory %dMB", i);
    putfonts8_asc(binfo->vram, binfo->scrnx, 0, 32, COL8_FFFFFF, s);
    
    • 上述代码对0x00400000 ~ 0xbfffffff范围的内存进行检查。
    • 显然,最大识别0xbfffffff+1字节(end=0xbfffffff,‭3221225472‬字节,3GB)的内存。0x00000000 ~ 0x00400000的内存已经被使用了,这一部分不需要再进行内存检查了。
    • / (1024 * 1024)是以MB为单位。
  • make run:

    • 根据QEMU的设定,内存应该是32MB。但是显示3072MB,这明显是3GB啊!!
    • 出现问题了。

3. 内存容量检查(2)(harib06c)

  • 使用make -r bootpack.nas,确认bootpack.c被编译成了什么样的机器语言。

  • 打开harib06b中的bootpack.nas:

    _memtest_sub:
    PUSH	EBP
        MOV	EBP,ESP
        MOV	EDX,DWORD [12+EBP]
        MOV	EAX,DWORD [8+EBP]
        CMP	EAX,EDX
        JA	L30
    L36:
    L34:
        ADD	EAX,4096
        CMP	EAX,EDX
        JBE	L36
    L30:
        POP	EBP
        RET
    
  • 对比harib06b中的memtest_sub:

    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;
    }
    
    • 我们发现,经过C语言编译器以后,bootpack.c中原本的异或消失了,也就是说bootpack.nas中没有XOR指令!
  • 这并不是C语言编译器有bug,相反,是它太过优秀了。


    • 显然,C语言编译器做了最优化处理。在这里,这个最优化处理使我们不需要的。
    • 可以更改编译选项,让这个最优化处理停止。但是在其他地方我们是需要最优化处理的。
    • 因此,解决这个问题的最优方式是:用汇编写memtest_sub函数。
    • C语言编译器好心做了坏事,不过从这件事情上来看,C语言编译器非常聪明了。在寻找问题的过程中,我们发现:能够看到中间结果(bootpack.nas)是非常有用的。同时,懂汇编语言很重要!
  • naskfunc.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;
    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
    
    
    • 32位寄存器
      32位寄存器介绍
      reference: https://blog.csdn.net/xuehuafeiwu123/article/details/76019828
    • C语言函数调用栈

      reference: https://blog.csdn.net/summonlight/article/details/81123785
      【显然,32位汇编学得不是特别好。】
  • 删除bootpack.c下的memtest_sub函数。在harib06cmake run

  • 使用VMware运行,设定虚拟机的内存是256M:

    开启虚拟机:

  • 内存容量显示正常。下面开始内存管理。

4. 挑战内存管理(haib06d)

  • 内存管理的定义:内存管理是指软件运行时对计算机内存资源的分配和使用的技术。其最主要的目的是如何高效,快速的分配,并且在适当的时候释放和回收内存资源。

  • 内存管理的基础:

    • 内存分配
    • 内存释放
  • 假设有128MB内存。也就是说有0x08000000个字节。我们以0x1000个字节 (4KB)为单位 进行内存管理。

  • 第一种内存管理方式

    • 0x08000000 / 0x1000 = 32768。所以,创建32768字节的区域,可以往其中写入0或1来标记哪里是空的,哪里是正在使用的。

      • 根据内存分布图,0x00000000到0x00400000的内存(前4MB)已经使用了。因此,设置为1。其余未被使用,设置为0。
    • 比如,需要100KB的空间,只要从a中找出连续25个标记为0的地方就行了。

    • 释放某部分内存空间。比如:释放从0x00123000开始的100KB。

  • 第一种方式的优缺点:

    • 优点:简单。
    • 缺点:管理表会太大。如果内存是128MB,管理表就需要32768字节(32KB)。如果内存是3GB,管理表就需要0xc0000000/0x1000 = 0xc0000 = 786432字节(768KB)。
  • 第二种方法(基于第一种方法):

  • 列表管理方法(第三种方法)

    • 存储的信息是类似于:把从xxx号地址开始的yyy字节的内存是空着的。
    • 数据结构如下:
    • 分配100KB的内存:查看memman中free的情况,从中找到100MB以上的空间即可。


      如果size变成了0,那么这一段的可用信息就不需要了,将frees-1就可以了。
    • 释放内存:增加一条可用信息,frees+1。同时,还要调查一下这段释放的内存,与相邻的可用空间相邻与否。
      • 分为四种情况:
  • 第三种方法的优点:

    • 管理表占用内存小。管理128MB内存时,memman占用8*1000 + 4字节,还不到8KB。管理3GB内存时,设结构体数组大小是10000,也才不到80KB。
    • 大块内存分配和释放的过程相当迅速。因为不需要像第一种和第二种那样,挨个遍历。
    • 分配内存时,加法和加法运算各执行一次就OK;释放内存需要考虑的情况较多,但是相比于第一种和第二种,速度还是快得多。
  • 第三种方法的缺点:

    • 内存管理的函数变得复杂了。
    • 超过1000条上限就麻烦了。也就是说当可用空间零零散散,怎么归纳都归纳不到一起时,这样就会把1000条空间管理信息全部用完。这种情形几乎不会发生。但是还是有可能的。 要么做一个更大的memman;要么割舍掉小块内存。
  • 我们采取解决上述问题的办法是:
    对于回收时,一旦超出结构体数组的上限,那么我们先忽略之,将其记录下来。只要以后能找回来,我们暂时就先不去管它。

  • bootpack.c中内存管理的数据结构:

    #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];
    };
    
    • 我们创建了4000组,还留出了余量。这样大概是4090*8 + 4 *4字节,大约是32KB。也就是说管理空间是32KB。
    • frees代表可用信息数目
    • maxfrees用于观察可用情况:frees的最大值
    • lostsize用于释放失败内存的大小的总和
    • losts用于记录释放内存失败次数。
  • bootpack.c中memman_init函数:

    void memman_init(struct MEMMAN *man)
    {
        man->frees = 0;			/* 可用信息数目 */
        man->maxfrees = 0;		/* 用于观察可用情况:frees的最大值 */
        man->lostsize = 0;		/* 用于释放失败内存的大小的总和 */
        man->losts = 0;			/* 用于记录释放内存失败次数 */
        return;
    }
    
    • 此函数对memman进行初始化。
  • bootpack.c中memman_total函数:

    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;
    }
    
  • bootpack.c中memman_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) {
                    /* 如果free[i]变成了0,就减掉一条可用信息 */
                    man->frees--;
                    for (; i < man->frees; i++) {
                        man->free[i] = man->free[i + 1]; /* 集体前移 */
                    }
                }
                return a;
            }
        }
        return 0; /* 没有找到可用空间 */
    }
    
    • alloc是allocate的简写,allocate(分配)。
    • man->free[i] = man->free[i + 1];这种形式是结构体赋值。相当于代码:
      man->free[i].addr = man->free[i+1].addr;
      man->free[i].size = man->free[i+1].size;
      
  • bootpack.c中memman_free函数:

    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) {
                /* 可以跟前面的内存区域连在一起(1) */
                man->free[i - 1].size += size;
                if (i < man->frees) {
                    /* 后面也有可用内存 */
                    if (addr + size == man->free[i].addr) {
                        /* 也可以跟后面的内存区域连在一起(3) */
                        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) {
                /* 与后面的可用空间相连(2) */
                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->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; /* 失败 */
    }
    
  • 修改bootpack.c中的HariMain函数:

    #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);
        ……
    }
    
    • memman需要32KB的内存空间。我们暂时决定使用0x003c0000开始的32KB,0x00300000地址以后,即使今后的程序增加,预计也不会到达0x003c0000。
    • memman_free(memman, 0x00001000, 0x0009e000)将内存地址0x00001000到0x0009efff的632KB注册(释放)到memman中。【0x00000000~0x000fffff这1MB,从内存分布上看,当程序执行到这里时,0x00001000到0x0009efff的632KB的确可用。】
    • memman_free(memman, 0x00400000, memtotal - 0x00400000);将内存地址0x00400000到最大地址注册到memman中。
      • 这里,使用QEMU,默认会被注册成28MB。
      • 如果使用VMware(设置内存256MB),那么就是252MB。
    • 因此:
      • 使用QEMU。会显示29304KB。
      • 使用VMware,会显示258680KB。

5. 依旧腰痛

  • 2020.4.8 20:40
  • 有点累了。
  • 明天开始跑步锻炼身体。
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值