第9天 内存管理
2020.4.8
1. 整理源文件(harib06a)
- 修改函数所在源文件
- 新增mouse.c和keyboard.c源文件。
- 因此,需要修改Makefile。
2. 内存容量检查(1)(harib06b)
-
进行内存管理之前,我们必须知道内存究竟有多大。具体方法有二:
- 在最初起启动的时候,BIOS肯定会检查内存容量,所以我们只要问一问BIOS,就知道内存容量有多大了。但这样有问题:
- 一方面,asmhead.nas会变长;
- 另一方面,BIOS版本不同,BIOS函数的调用方法也不同。
- 自己写。
- 这里,我们自己写代码去检查内存。
- 中心思想是:按顺序检查内存地址,直到内存检查出现异常,这时迭代的上限就是内存的容量大小。
- 在最初起启动的时候,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。
- 高速缓存cache:
-
有了上述思路,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
- 将AC位重置为0的时候,用到了
-
再就是函数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位汇编学得不是特别好。】
- 32位寄存器
-
删除bootpack.c下的memtest_sub函数。在harib06c中
make 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。
- 使用QEMU。会显示29304KB。
5. 依旧腰痛
- 2020.4.8 20:40
- 有点累了。
- 明天开始跑步锻炼身体。