day9内存管理
整理源文件(harib06a)
残留问题:鼠标指针的叠加处理不太顺利,以后处理
先介绍cache(高速缓存)
每次访问内存,都将所访问的地址和内容存入高速缓存,
往内存里写入数据时也一样,首先更新高速缓存的信息,然后再写入内存。
如果是循环,就尽量在缓存内处理,循环处理完成,才发送内存写入命令
386的CPU没有缓存,486的缓存只有8-16KB,但两者的性能就差了6倍以上1。
内存容量检查(1)(harib06b)
内存检查时,要往内存随便写入一个值,然后马上读取,来检查读取的值和写入的值是否相等
如果内存连接正常,则写入的值能够记在内存里。如果没连接上,则读出的值肯定是乱七八糟的。
但是,如果CPU里加上了缓存,写入和读出的不是内存,而是缓存。结果,所有的内存都“正常”,检查处理不能完成
所以,只有在内存检查时才将缓存设为OFF。
具体来说,就是先查查CPU是不是在486以上,如果是,就将缓存设为OFF。
#define EFLAGS_AC_BIT 0x00040000
//0100 0000 0000 0000 0000
#define CR0_CACHE_DISABLE 0x60000000
//0110 0000 0000 0000 0000 0000 0000 0000
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以上,EFLAGS寄存器的第18位应该是所谓的AC标志位;如果CPU是386,那么就没有这个标志位,第18位一直是0。
• AC
:对齐检查标志(Alignment check (or access control) flag)是标志寄存器的第18位。当该标志位被设置且CR0
寄存器中的AM
位被设置时,将对用户态下对内存引用进行对齐检查,在存在未对齐的操作数时产生异常。
• 这里AC位测试只是用于判断CPU型号(是否486+)
//这个对EFLAGS的处理
eflg = io_load_eflags();//将EFLAGS赋值给eflg
eflg |= EFLAGS_AC_BIT; /* 0100 0000 0000 0000 0000 AC-bit = 1 */
//这一步将AC位有意识的赋值为1,如果是386,即使设定AC=1,AC的值还会自动回到0
io_store_eflags(eflg);//这一步就是将新的AC位为1的EFLAGS值写入EFLAGS
eflg = io_load_eflags();//重新将新的EFLAGS值赋值eflg
eflg &= ~EFLAGS_AC_BIT; /* AC-bit = 0 */
最后将AC位重新置为0
if (flg486 != 0) {
cr0 = load_cr0();
cr0 |= CR0_CACHE_DISABLE; /* 禁止缓存 */
store_cr0(cr0);
}
如果是486以上的cpu(也就是有缓存就禁止缓存)
缓存控制是通过CR0寄存器的CD(Cache Disable)位(第30位)实现:
#define CR0_CACHE_DISABLE 0x60000000 // 二进制 0110 0000 ...(实际同时设置CD和NW位)
0110 0000 0000 0000 0000 0000 0000 0000
- 当设置
CR0 |= CR0_CACHE_DISABLE
时(即CD位=1),是禁止缓存 - 当清除
CR0 &= ~CR0_CACHE_DISABLE
时(即CD位=0),是允许缓存
load_cr0和store_cr0
_load_cr0: ; int load_cr0(void);
MOV EAX,CR0
RET
_store_cr0: ; void store_cr0(int cr0);
MOV EAX,[ESP+4];[ESP+4]参数地址
MOV CR0,EAX
RET
这两个函数需要用汇编来写
MOV EAX,[ESP+4]解释:
在x86架构的函数调用约定中(假设这里使用C调用约定),当调用store_cr0
函数时:
- 参数
cr0
会被压入栈中 - 函数返回地址(由CALL指令自动压入)占用4字节
- 参数
cr0
位于返回地址之上
栈结构示意:
ESP+0 → 返回地址(4字节)
ESP+4 → 参数cr0(4字节) ← 这里用[ESP+4]获取参数
对应的C函数声明:
void store_cr0(int cr0); // 参数通过栈传递
因此MOV EAX, [ESP+4]
的完整解释流程:
- 当调用
store_cr0(cr0_value)
时,会先将参数cr0_value
压入栈 - 执行
CALL _store_cr0
时,CPU自动将返回地址压入栈(此时ESP指向返回地址) - 进入函数后,ESP指向返回地址,参数位于返回地址的上方(栈地址增长方向向下)
- 32位模式下栈操作以4字节为单位,因此第一个参数在
ESP+4
位置
补充示意图:
高地址
|-----------|
| cr0_value | ← 调用前PUSH的参数
|-----------|
| 返回地址 | ← 调用后ESP指向这里
|-----------|
低地址
memtest_sub函数
memtest_sub函数,是内存检查处理的实现部分
返回值本质:
- 返回的是最后一个成功验证的地址(而非容量)
- 实际可用容量需要通过计算得到:
可用容量 = 返回值 - start
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地址的范围内,能够使用的内存的末尾地址。
- 位反转测试原理 (
^= 0xffffffff
)
*p = pat0; // 写入测试模式 0xaa55aa55
*p ^= 0xffffffff; // 按位取反 → 0x55aa55aa (pat1)
- 双重验证逻辑:
// 第一次验证(取反后应为 pat1)
if (*p != pat1) { // 如果内存不可靠/不存在,值会不匹配
goto not_memory; // 跳转到恢复旧值的代码
// 第二次验证(再次取反应恢复 pat0)
*p ^= 0xffffffff; // 再次取反 → 应该恢复为 0xaa55aa55
if (*p != pat0) { // 检查是否恢复成功
goto not_memory;
}
- 控制流解析:
for (i = start; i <= end; i += 0x1000) { // 外层循环(按4KB步进)
...
break; // 退出当前内存块的测试(继续下一个4KB块)
...
goto not_memory; // 跳转到块内标签,执行恢复后退出当前块测试
}
完整解释:
^=
是按位异或操作符,0xffffffff
是32位全1掩码,效果等同于按位取反break
退出的是外层for (i = start;...)
循环- 双重反转验证可以检测:
- 内存单元能否正确保存数据
- 是否存在缓存干扰(需配合CR0缓存禁用)
- 是否发生位翻转现象
- 当某地址无法通过测试时,立即终止当前内存块的检测,返回故障地址
**加快检查速度:**内存容量检查(2)(harib06c)
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) {//4kb 4kb的检查
p = (unsigned int *) (i + 0xffc);
/*检查每个4kb的末尾的4个字节*/
old = *p; /* 先记住修改前的值 */
将可用最大内存容量显示到显示屏:
i = memtest(0x00400000, 0xbfffffff) / (1024 * 1024);//利用除法转换单位
sprintf(s, "memory %dMB", i);
putfonts8_asc(binfo->vram, binfo->scrnx, 0, 32, COL8_FFFFFF, s);
内存容量检查(2)(harib06c)
编译器脑中所想的(4)
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);
}
return i;
}
这里的地址变量p,虽然计算了地址,却一次也没有用到。这么说来,old、
pat0、pat1也都是用不到的变量。全部都舍弃掉吧。
unsigned int memtest_sub(unsigned int start, unsigned int end)
{
unsigned int i;
for (i = start; i <= end; i += 0x1000) { }
return i;
}
所以用汇编写这个就不存在编译器的编译所导致的一些问题
_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
_memtest_sub:
PUSH EDI ; 保存寄存器(调用约定要求保护这些寄存器的值)
PUSH ESI
PUSH EBX
MOV ESI,0xaa55aa55 ; 设置测试模式1
MOV EDI,0x55aa55aa ; 设置测试模式2
MOV EAX,[ESP+16] ; 获取第一个参数start(栈偏移计算:3个PUSH×4=12 + 返回地址4=16)
mts_loop:
MOV EBX,EAX ; 当前测试地址→EBX
ADD EBX,0xffc ; 定位到4KB内存块末尾(0x1000-4)
MOV EDX,[EBX] ; 保存原始内存内容到EDX
; 开始双重模式验证
MOV [EBX],ESI ; 写入测试模式1
XOR DWORD [EBX],0xffffffff ; 按位取反(0xaa55aa55 → 0x55aa55aa)
CMP EDI,[EBX] ; 验证是否等于模式2
JNE mts_fin ; 不匹配则跳转结束
XOR DWORD [EBX],0xffffffff ; 再次取反恢复原始测试模式
CMP ESI,[EBX] ; 验证是否恢复成功
JNE mts_fin ; 不匹配则跳转结束
MOV [EBX],EDX ; 恢复原始内存内容
ADD EAX,0x1000 ; 前进4KB(测试下一个内存块)
CMP EAX,[ESP+20] ; 比较第二个参数end(栈偏移20=16+4)
JBE mts_loop ; 循环直到超过end范围
POP EBX ; 恢复寄存器
POP ESI
POP EDI
RET
mts_fin:
MOV [EBX],EDX ; 恢复出错地址的原始内容
POP EBX
POP ESI
POP EDI
RET
- 在c语言使用这个汇编代码,首先需要在c语言中加入extern声明,比如unsigned int memtest_sub(unsigned int start, unsigned int end);。这样编译器就知道这个函数的存在。这个全在.h文件当中
- 接下来,汇编函数的命名和调用约定需要符合C的要求。在汇编代码中,函数名前面要加下划线,比如_memtest_sub,并且参数传递按照C的约定,比如通过栈传递参数。例如,start和end参数会按顺序压入栈中,然后通过call调用函数,返回值放在eax寄存器。
- 另外,需要确保汇编函数的编写符合C调用约定,比如参数的入栈顺序(从右到左)和清理栈的责任。在用户的情况下,memtest_sub接受两个参数,start和end,所以汇编代码应该正确地从栈中取出这两个值进行处理,并在返回前清理栈。
- 最后,链接阶段要确保汇编生成的目标文件与C代码的目标文件正确链接。
挑战内存管理(harib06d)
1. 内存管理核心目标
- 跟踪使用状态:通过位图或链表记录内存块的占用/空闲状态
- 分配机制:根据请求分配指定大小的内存区域
- 释放回收:回收不再使用的内存区域
- 内存保护:防止程序越界访问
典型分配步骤:
1.物理内存检测:通过memtest确定可用地址范围
2.初始化管理结构:
// 伪代码示例:使用位图管理
unsigned char mem_map[4096]; // 假设管理4GB内存(每bit表示4KB)
3.分配算法:
- 首次适应:从低地址开始找第一个足够大的块
- 最佳适应:找能满足要求的最小空闲块
- 伙伴系统:按2的幂次方大小分配(适合页式管理)
4.内存释放原理:
- 位图管理:将对应bit置0
- 链表管理:将释放的块加入空闲链表
- 合并空闲块:解决内存碎片问题
内存管理就是必须恰当管理好哪些内存可以使用(哪些内存空闲),哪些内存不可以使用(正在使用),还有就是
内存管理的基础,一是内存分配,一是内存释放。
位图的方式管理:
如果有128MB的内存,也就是说,有0x08000000个字节。假设以0x1000个字节(4KB)为单位进行管理了,0x08000000/0x1000 = 0x08000 = 32768,所以首先我们来创建32768字节的区域,可
以往其中写入0或者1来标记哪里是空着的,哪里是正在使用的。
char a[32768];
//这个用char数组,一个char类型的占1个字节,比较占内存
//如果内存是128MB,管理表只需要32768字节(32KB);
//如果内存最大是3GB,管理表是多大呢?0xc0000000 /0x1000 = 0xc0000 = 786432,
//也就是说光管理表就需要768KB。
//占0.02%,0.02%的比例是与容量没有关系的。
//如果用位(bit)构成管理表,管理表的大小可缩减到原来的1/8。
//如果是3GB内存,只需要96KB就可以管理整个内存了。这个比例只有0.003%。
for (i = 0; i < 1024; i++) {
a[i] = 1; /* 一直到4MB为止,标记为正在使用 */
}
for (i = 1024; i < 32768; i++) {
a[i] = 0; /* 剩下的全部标记为空 */
}
j = 0; // 起始查找位置
再来一次:
for (i = 0; i < 25; i++) { // 检查连续25个块
if (a[j + i] != 0) { // 发现已使用的块
j++; // 从下一个位置重新开始查找
if (j < 32768 - 25) // 防止数组越界的保护条件
goto 再来一次; // 继续尝试查找
"没有可用内存了"; // 整个内存空间不足
}
}
"从a[j]到a[j + 24]可用"; // 找到连续25个空闲块
//这段代码找出的j是25个连续的4kb块的起始块地址
如果找到了标记连续为0的地方,暂时将这些地方标记为“正在使用”,然后从j的值计算出对应的地址。
for (i = 0; i < 25; i++) {
a[j + i] = 1;
}
"从 j * 0x1000 开始的100KB空间得到分配";
如果要释放这部分内存空间,可以像下面这样做。
j = 0x00123000 / 0x1000;
for (i = 0; i < 25; i++) {
a[j + i] = 0;
}
列表管理:
是把类似于“从xxx号地址开始的yyy字节的空间是空着的”这种信息都列在表里。
1. 数据结构定义
struct FREEINFO { // 空闲内存块描述结构
unsigned int addr; // 起始地址(如0x00400000)
unsigned int size; // 块大小(如0x07c00000=124MB)
};
struct MEMMAN { // 内存管理器
int frees; // 当前空闲块数量(初始为1)
struct FREEINFO free[1000]; // 最大支持1000个空闲块
};
2. 初始化逻辑分析
struct MEMMAN memman;
memman.frees = 1; // 初始只有1个连续空闲块
memman.free[0].addr = 0x00400000; // 起始地址4MB(跳过低端内存)
memman.free[0].size = 0x07c00000; // 可用空间124MB(到128MB结束)
3. 内存分布示意图
0x00000000 ~ 0x003FFFFF : 保留区域(4MB)
0x00400000 ~ 0x07FFFFFF : 初始可用内存(124MB)
0x08000000 ~ ... : 后续可扩展区域
如果需要100KB的空间
for (i = 0; i < memman.frees; i++) {
if (memman.free[i].size >= 100 * 1024) {
"找到可用空间!";
"从地址memman.free[i].addr开始的100KB空间,可以使用哦!";
}
}
"没有可用空间";
如果找到了可用内存空间,就将这一段信息从“可用内存空间管理表”中删除。这相当于给这一段内存贴上了“正在使用”的标签
memman.free[i].addr += 100 * 1024; /* 可用地址向后推进了100KB */
memman.free[i].size -= 100 * 1024; /* 减去100KB */
如果size变成了0,那么这一段可用信息就不再需要了,将这条信息删除,frees减去1就可以了。
还可以合并空闲内存块:
free[0]:地址0x00400000号开始,0x00019000字节可用
free[1]:地址0x00419000号开始,0x07be7000字节可用
可以归纳为
free[0]:地址0x00400000号开始,0x07c00000字节可用
列表管理的优点:
- 占用内存少memman是8×1000 + 4 = 8004,还不到8KB。
- 那就是大块内存的分配和释放都非常迅速。比如我们考虑分配10MB内存的情形。如果按前一种方法,就要写入2560个“内存正在使用”的标记“1”,而释放内存时,要写入2560个“0”。这些都需要花费很长的时间。
- 只要加法运算和减法运算各执行一次就结束了。不管是10MB也好,100MB也好,还是40KB,任何情况都一样。释放内存的时候虽然没那么快,但是与写入2560个“0”相比,速度快得可以用“一瞬间”来形容。
列表管理的缺点:
当可用空间被搞得零零散散,怎么都归纳不到一块儿时,会将1000条可用空间管理信息全部用完
haribote OS采用的方法,即“割舍掉的东西,只要以后还能找回来,就暂时不去管它。”。
总的内存管理代码
#define MEMMAN_FREES 4090 /* 大约是32KB*/
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->lostsize = 0; /* 释放失败的内存的大小总和 */
man->losts = 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;
}
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; /* 没有可用空间 */
}
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;
return 0; /* 成功完成 */
}
/* 不能往后移动 */
man->losts++;
man->lostsize += size;
return -1; /* 失败 */
}
#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);
关于两次内存的释放
0x00001000 - 0x0009efff 区域的释放:
memman_free(memman, 0x00001000, 0x0009e000);
这是为了回收BIOS和实模式中断向量表之后的内存(约640KB以下区域)。在x86架构中:
- 0x00000000-0x00000fff:中断向量表等系统保留区域
- 0x00001000-0x0009efff:这段是DOS时代的常规内存区域,在保护模式操作系统中可以回收利用
- 0x00400000 之后的释放:
memman_free(memman, 0x00400000, memtotal - 0x00400000);
这是将内存检测得到的物理内存中高于4MB的部分(通过memtest()得到的memtotal)加入内存管理器。
这种做法的设计考量:
- 保留0x00000000-0x00000fff作为安全隔离区
- 0x0009f000-0x003fffff是操作系统内核、驱动程序等系统组件的保留区域
- 0x00400000是物理内存检测的起始地址(memtest参数设定)
- 通过分两次释放,既能回收低端内存又能管理高端内存
内存布局示意图:
0x00000000 - 0x00000fff : 系统保留
0x00001000 - 0x0009efff : 释放为可用内存 (约608KB)
0x0009f000 - 0x003fffff : 操作系统内核使用
0x00400000 - ... : 释放为可用内存
这样设计可以最大限度地利用物理内存资源,同时为系统组件保留必要的内存空间。
上述内存规划
- 内存管理器位置:
#define MEMMAN_ADDR 0x003c0000
内存管理结构MEMMAN
被放置在0x003c0000
地址,占用32KB空间(到0x003c7fff
)。这个区域位于内核保留区的末端,避免与以下区域冲突:
0x00000000-0x0009ffff
:传统内存区0x00100000-0x003bffff
:内核代码和数据结构区
- 第一块释放区域:
memman_free(memman, 0x00001000, 0x0009e000);
释放范围:0x00001000 - 0x0009efff
(共0x0009e000 - 0x00001000 + 1 = 0x9d000
字节 = 632KB)
这部分是传统内存区的可用部分:
0x00000000-0x00000fff
:保留给中断向量表等系统用途0x0009f000-0x0009ffff
:可能用于BIOS数据区或显存
- 第二块释放区域:
memman_free(memman, 0x00400000, memtotal - 0x00400000);
假设总内存为32MB(0x02000000):
- 释放范围:
0x00400000 - 0x01ffffff
(共28MB) - 保留
0x003c8000-0x003fffff
:为内核后续扩展预留
- 内存计算示例:
632KB + 28MB = 632 + 28*1024 = 632 + 28672 = 29304KB
这与QEMU显示的29304KB
一致
- 内存布局示意图:
地址范围 | 用途
------------------------------------------
0x00000000-0x00000fff | 中断向量表等系统保留
0x00001000-0x0009efff | 释放的632KB可用内存
0x0009f000-0x003bffff | 内核代码/数据区
0x003c0000-0x003c7fff | 内存管理结构(MEMMAN)
0x003c8000-0x003fffff | 内核保留扩展区
0x00400000-0x01ffffff | 释放的28MB可用内存
内存管理(续)(harib07a)
bootpack.c都已经有254行了。分出一部分到memory.c中去。memman_alloc和memman_free能够以1字节为单位进行内存管理,这种方式虽然不错,但是有一点不足——在反复进行内存分配和内存释放之后,内存中就会出现很多不连续的小段未使用空间,这样就会把man—>frees消耗殆尽
所以要编写一些总是以0x1000字节为单位进行内存分配和释放的函数,它们会把指定的内存大小按0x1000字节为单位向上舍入( roundup),
unsigned int memman_alloc_4k(struct MEMMAN *man, unsigned int size)
{
unsigned int a;
size = (size + 0xfff) & 0xfffff000;
a = memman_alloc(man, size);
return a;
}
int memman_free_4k(struct MEMMAN *man, unsigned int addr, unsigned int size)
{
int i;
size = (size + 0xfff) & 0xfffff000;
i = memman_free(man, addr, size);
return i;
}
关键是向上舍入,理解向上舍入,先从向下舍入讲起(round down)
向下舍入:
- 把变量i中的数字以0x1000为单位进行向下舍入的式子如下:
- i = i & 0xfffff000;
- 以0x10为单位向下舍入的式子是“i =i & 0xfffffff0;”。
向上舍入:
- **误区:i = (i & 0xfffff000) + 0x1000;**如果“i = 0x12345000;”时执行上述命令,结果就变成了“i=0x12346000;”。
- **具体做法:**先判断最后几位,如果本来就是零则什么也不做,如果不是零就进行下面的运算。if ((i & 0xfff) ! = 0) { i = (i & 0xfffff000) + 0x1000;}
- **简化:i = (i + 0xfff) & 0xfffff000;以十进制理解:**如果对400元进行向上舍入呢?先加上99元,得到499元,再进行向下舍入,结果是400元
COLUMN-6 十进制数的向下舍入:
不能用“与运算”来进行十进制数的向下舍入处理,“与运算”只能用于二进制数的向下舍入处理。而十六进制数因为是4位4位排在一起的二进制数,所以凑巧成功了。
十进制可以i = (i / 100)*100;改善一下就是i = i - (i%100);少了一个乘法,但是无论如何都有除法,而它恰恰是CPU最不好处理的命令之一,所以计算过程要花费较长的时间
由此可见,如果以1000字节或4000字节单位进行内存管理的话,每次分配内存时,都不得不进行繁琐的除法计算。但如果以1024字节或4096字节为单位进行内存管理的话(两者都是在二进制下易于取整的数字。附带说明:0x1000=4096),在向上舍入的计算中就可以使用“与运算”,这样也能够提高操作系统的运行速度,