《30天自制操作系统》笔记(07)——内存管理
2014-06-04 17:14
337 查看
[b]《30天自制操作系统》笔记(07)——内存管理 [/b]
现在想想,从软件工程师的角度看,CPU也只是一个软件而已:它的功能就是加载指令、执行指令和响应中断,而响应中断也是在加载指令、执行指令。就像火车沿着一条环形铁轨前进;当中断发生时,就好像铁轨岔口处变轨了,火车就顺着另一条轨迹走了;走完之后又绕回来重新开始。决定CPU是否变轨的,就是CPU里的特定寄存器。
汇编版本的memtest_sub
知道了内存容量,就可以进行管理了。
如果开启着CPU高速缓存(cache),上述的检测代码就不会正常工作。因为写入一个内存地址,然后立即读出,这样的操作符合cache到的情况,CPU不会从内存读,而是直接读cache到的东西。结果,所有的内存都"正常",检测代码就起不到作用了。
比如需要100KB的内存,那么只要从a中找出连续25个标记为0的地方就可以了。
需要收回这100KB的时候,用地址值/0x1000,计算出j就可以了。
当然,我们可以用1bit来代替1个char,这样所需的管理空间就可以省下7/8,使用方法则是一样的。
比如,如果需要100KB的内存,只要查看memman中free的状况,从中找到100MB以上的可用空间就行了。
释放内存时,增加1条可用信息,frees加1。而且还要看看新释放出的内存与相邻的内存能不能连到一起,如果可以,就要归为1条。
与上文的最简单的方法相比,这种列表管理的方法,占用内存少,且内存的申请和释放更迅速。
缺点是释放内存的代码比较复杂。另外,如果内存变成零零碎碎的,那么需要的MEMMAN里的数组就会超过1000,这是个问题。如果真发生这种情况,只能将一部分零碎的空闲内存都视作被占用的,然后合并。
内存管理需要的预备知识还包括"获取内存容量"、"禁止/启用高速缓存"、"数据结构-链表"。
内存管理的算法还有很多,本篇只给出了两种最基本最简单的,够做个简易的OS就行了,现在不是深究算法的时候。
请查看下一篇《《30天自制操作系统》笔记(08)——叠加窗口刷新》
进度回顾
上一篇中处理掉了绝大部分与CPU配置相关的东西。本篇介绍内存管理的思路和算法。现在想想,从软件工程师的角度看,CPU也只是一个软件而已:它的功能就是加载指令、执行指令和响应中断,而响应中断也是在加载指令、执行指令。就像火车沿着一条环形铁轨前进;当中断发生时,就好像铁轨岔口处变轨了,火车就顺着另一条轨迹走了;走完之后又绕回来重新开始。决定CPU是否变轨的,就是CPU里的特定寄存器。
_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
知道了内存容量,就可以进行管理了。
关闭CPU高速缓存
486以上的CPU是有高速缓存(cache)的。CPU每次访问内存,都要将所访问的地址和内容存入cache,也就是存放成这样"18号地址的值是54"。如果下次要用18号地址的内容,CPU就不需要再读内存了(速度慢),而是直接从cache中取得18号地址的内容(速度快)。如果开启着CPU高速缓存(cache),上述的检测代码就不会正常工作。因为写入一个内存地址,然后立即读出,这样的操作符合cache到的情况,CPU不会从内存读,而是直接读cache到的东西。结果,所有的内存都"正常",检测代码就起不到作用了。
#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; }
内存管理算法
假设内存有128MB(0x08000000字节),以4KB(0x1000字节)为单位进行管理。最简单的方法
128MB/4KB=32768。所以我们创建32768字节的区域,写入0表示对应的内存是空闲的,写入1表示对应的内存是正在使用的。这个方法的名字我没有查到。char a[32768]; for (i = 0; i < 1024; i++) { a[i] = 1; //一直到4MB为止,标记为正在使用(BIOS、OS等) } for (i = 1024; i< 32768; i++) { a[i] = 0; //剩下的全部标记为空闲 }
比如需要100KB的内存,那么只要从a中找出连续25个标记为0的地方就可以了。
//从a[j]到a[j + 24]为止,标记连续为0"; j = 0; 再来一次: for (i = 0; i < 25; i++) { if (a[j + i] != 0) { j++; if (j < 32768 - 25) goto 再来一次; "没有可用内存了"; } } //从j * 0x1000开始的100KB空间得到分配 for (i = 0; i < 25; i++) { a[j + i] = 1; }
需要收回这100KB的时候,用地址值/0x1000,计算出j就可以了。
j = 0x00123000 / 0x1000; for (i = 0; i < 25; i++) { a[j + i] = 0; }
当然,我们可以用1bit来代替1个char,这样所需的管理空间就可以省下7/8,使用方法则是一样的。
列表管理
把类似于"从xxx号地址开始的yyy字节的空间是空闲的"这种信息都存储到列表里。struct FREEINFO { /* 可用状况 */ unsigned int addr, size; }; struct MEMMAN { /* 内存管理 */ int frees, maxfrees, lostsize, losts; struct FREEINFO free[MEMMAN_FREES]; }; struct MEMMAN memman; memman.frees = 1;//可用状况list中只有1件 memman.free[0].addr = 0x00400000;//从0x00400000号地址开始 memman.free[0].size = 0x07c00000;//有124MB可用
比如,如果需要100KB的内存,只要查看memman中free的状况,从中找到100MB以上的可用空间就行了。
for (i = 0; i < memman.frees; i++) { if (memman.free[i].size >= 100 * 1024) { //找到可用空间 memman.free[i].addr += 100 * 1024; memman.free[i].size -= 100 * 1024; } }
释放内存时,增加1条可用信息,frees加1。而且还要看看新释放出的内存与相邻的内存能不能连到一起,如果可以,就要归为1条。
与上文的最简单的方法相比,这种列表管理的方法,占用内存少,且内存的申请和释放更迅速。
缺点是释放内存的代码比较复杂。另外,如果内存变成零零碎碎的,那么需要的MEMMAN里的数组就会超过1000,这是个问题。如果真发生这种情况,只能将一部分零碎的空闲内存都视作被占用的,然后合并。
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; /* 失败 */ }
总结
内存管理要结合GDT的设定进行。按段(Segment)设计的GDT,内存就得按段申请和回收。按页设计的GDT,额我不知道,以后再说。内存管理需要的预备知识还包括"获取内存容量"、"禁止/启用高速缓存"、"数据结构-链表"。
内存管理的算法还有很多,本篇只给出了两种最基本最简单的,够做个简易的OS就行了,现在不是深究算法的时候。
请查看下一篇《《30天自制操作系统》笔记(08)——叠加窗口刷新》
相关文章推荐
- 30天自制操作系统开发笔记——内存管理
- 《30天自制操作系统》07_day_学习笔记
- 30天自制操作系统开发笔记——响应鼠标和键盘(上)
- 30天自制操作系统第八天学习笔记(u盘软盘双启动版本)
- 30天自制操作系统笔记(十一十二)——源码
- 30天自制操作系统笔记 第0天
- 30天自制操作系统笔记 第1天
- 30天自制操作系统笔记(九十)
- 30天自制操作系统笔记(十三十四)
- 30天自制操作系统笔记(九十)——源码
- 30天自制操作系统第四天学习笔记
- 30天自制操作系统笔记(一二)
- 30天自制操作系统笔记(十三十四)——源码
- 30天自制操作系统开发笔记——IDT中断描述符表
- 从你的u盘启动:30天自制操作系统第四天u盘启动学习笔记
- 30天自制操作系统笔记(四)
- 30天自制操作系统笔记(五六)
- 《30天自制操作系统》笔记(01)——hello bitzhuwei’s OS!
- 《30天自制操作系统》笔记一二
- 30天自制操作系统笔记(十一十二)