平台
处理器:Intel Celeron® Dual-CoreCPU
操作系统:Windows 7 专业版 x86
阅读书籍:《30天自制操作系统》—川合秀实[2015.04.05-04.06]
工具:../toolset/
1 内存使用情况
编写完鼠标移动程序后,内存分布如下。
Figure1. 内存使用情况
2 检测可用内存容量大小
x86保护模式下的内存地址空间为0x00000000 ~ 0xFFFFFFFF,其中包含ROM等存储器的地址空间,需要检测出RAM容量。在检测可用内存容量大小时,需要将CPU内的高速缓存(cache)功能关闭。才无cache功能的情况下,检测出的RAM空间才是准确的。
2.1 关闭高速缓存(cache)功能
首先检查CPU是否支持cache功能(EFLAG),如果支持cache,则关闭cache功能(CR0)。
/*检查内存地址空间中的RAM
*start,end分别是需要检查的内存的起始和结束地址
*/
unsigned int memtest(unsigned int start, unsigned int end)
{
char flg486 = 0;
unsigned int eflg, cr0, i;
//80386 CPU不支持cache,检查EFLAG的第18位
//如果不支持cache,EFLAG的bit[17]始终为0
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) {//支持cache
flg486 = 1;
}
eflg &= ~EFLAGS_AC_BIT;
io_store_eflags(eflg);
if (flg486 != 0) {
cr0 = load_cr0();
cr0 |= CR0_CACHE_DISABLE; //禁止cache功能:将cr0的bit[30:29]置1
store_cr0(cr0);
}
i = memtest_sub(start, end); //调用反转法检查RAM的函数
if (flg486 != 0) {
cr0 = load_cr0();
cr0 &= ~CR0_CACHE_DISABLE; //允许cache
store_cr0(cr0);
}
return i;
}
load_cr0()和store_cr0(cr0)函数是在naskfunc.nas中用汇编指令来操作CR0寄存器的汇编函数:
1. GLOBAL _load_cr0, _store_cr0
2. …
3. _load_cr0: ; int load_cr0(void);
4. MOV EAX,CR0
5. RET
6.
7. _store_cr0: ; void store_cr0(int cr0);
8. MOV EAX,[ESP+4]
9. MOV CR0,EAX
10. RET
2.2 反转法检查RAM
用“书”中“反转内存中的值”的方式检测内存是否为RAM(由于C编译器ccl.exe会优化掉反转代码,所以检测可用内存容量大小的程序直接在naskfunc.nas中采用汇编语言来写)。
1. _memtest_sub: ;unsigned int memtest_sub(unsigned intstart, unsigned int end)
2. ;start为检查内存段的起始地址,end为结束地址
3. PUSH EDI ;备份进入此函数前的寄存器的值
4. PUSH ESI
5. PUSH EBX
6. MOV ESI,0xaa55aa55
7. MOV EDI,0x55aa55aa
8. MOV EAX,[ESP+12+4] ;i = start;12为EDI, ESI,EBX 3个寄存器使用的栈空间,
9. ;4为CALL指令引起的(CS:)IP压栈空间
10. mts_loop:
11. MOV EBX,EAX
12. ADD EBX,0xffc ;以4KB为单位进行检查,检查4KB内存段的最后四个字节;
13. MOV EDX,[EBX] ;将[EBX]中的内容备份
14. MOV [EBX],ESI ;往[EBX]中写入0xaa55aa55
15. XOR DWORD[EBX],0xffffffff ;反转[EBX]内存中的每一位
16. CMP EDI,[EBX] ;检查[EBX]内存中的值是否反转成功,
17. JNE mts_fin ;不成功则跳转到mts_fin处
18.
19. XOR DWORD[EBX],0xffffffff ;再反转[EBX]4字节内存中的内存
20. CMP ESI,[EBX] ;检查是否反转成功,
21. JNE mts_fin ;反转不成功则跳转到mts_fin处
22. MOV [EBX],EDX ;将[EBX]的值恢复
23. ADD EAX,0x1000 ;检查下一个4KB内存块,
24. CMP EAX,[ESP+12+8] ;如果低于end则继续检查,否则返回EAX
25. JBE mts_loop
26. POP EBX
27. POP ESI
28. POP EDI
29. RET
30. mts_fin:
31. MOV [EBX],EDX ;恢复[EBX]的值
32. POP EBX
33. POP ESI
34. POP EDI
35. RET3 管理内存·简
3.1 选择数据结构
对于机器级及以上的程序来说,内存的管理都是基于逻辑的内存地址空间。首先需要一种数据结构来描述内存地址空间。
#define MEMMAN_FREES 4090 //空闲的不连续的内存块数目
struct FREEINFO { //内存地址和大小
unsigned int addr, size;
};
struct MEMMAN {
//空闲内存空间个数,空闲内存空间的最大个数,
//没能紧跟空闲内存释放的内存大小和个数
int frees, maxfrees, lostsize, losts;
struct FREEINFO free[MEMMAN_FREES];
};
3.2 内存分配
查看保存在数据结构内的空闲内存块,找到符合要求的内存块则返回此内存块的首地址。将分配出去的内存块从空闲内存块中除去。
/* 分配一块内存来使用
* man是描述当前内存空闲与否的数据结构
* size是当前要分配内存的大小
* 如果分配成功则返回所分配内存的首地址,失败则返回0
*/
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;
//如果free[i]这块内存刚好被分完,则在数据结构中将其溢出
if (man->free[i].size == 0) {
man->frees--;
for (; i < man->frees; i++) {
man->free[i] = man->free[i + 1];
}
}
return a;
}
}
return 0; //无空闲内存
}
3.3 内存释放
在标记内存可用的数据结构中,按照内存地址从小到大的顺序标记每一块可用的内存空间。释放不用的内存时也按照内存地址大小顺序标记在数据结构中。
/* 释放内存:将不用的内存标记在数据结构中,表明这些内存处于空闲状态
* man为描述内存空闲状态的数据结构
* addr,size分别为释放内存的起始地址和大小
* 释放[addr,addr + size]这段内存成功时返回0,否则返回-1
*/
int memman_free(struct MEMMAN *man, unsigned int addr, unsigned int size)
{
int i, j;
//数据结构描述空闲内存空间以内存地址从小到大的方式
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;
//在数据结构中清除free[i],它已经合并到free[i-1]描述的空闲内存中了
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) {
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;
}
3.4 其它函数
其它函数是为记录内存空闲的数据结构而服务的。
//给记录空闲内存的数据结构初始值
void memman_init(struct MEMMAN *man)
{
man->frees = 0;
man->maxfrees = 0;
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;
}
4 分析
4.1 组织文件
将_memtest_sub函数写进naskfunc.nas文件中,将其余函数写进memman.c文件中,按照“书”中那样组织主函数HariMain()的内容。依据各文件关系在各个文件中作声明,并修改Makefile。通过编译、链接后,在“!cons_nt.bat”中使用“makerun”命令运行程序,得到如下界面:
Figure2. 程序运行结果
4.2 引用结构体元素
#define MEMMAN_FREES 4090
#define MEMMAN_ADDR 0x003c0000
struct FREEINFO {
unsigned int addr, size;
};
struct MEMMAN {
int frees, maxfrees, lostsize, losts;
struct FREEINFO free[MEMMAN_FREES];
};
struct MEMMAN *memman = (struct MEMMAN *) 0x003c0000;
x86保护模式下,memman= 0x003c0000;memman->frees表示0x003c0000 + 0~0x003c0003这4字节内存;memman->maxfrees表示0x003c0000+ 4 ~ 0x003c0007这4字节内存;…;结构体被编译器编译后,每个元素表示相对结构体起始地址的偏移值。引用结构体元素时,按照“结构体起始地址+ 元素偏移值”访问到对应的内存。
Figure3. 编译器和结构体
4.3 内存管理函数的运行
内存检测函数检测0x00400000~ 0xbfffffff这段(0x00400000之前的都被使用过了,是RAM)内存空间(32M)都为RAM。
初始化structMEMMAN结构体的函数没有初始化结构体内的数组,在释放首地址为0x00001000,大小为0x0009e000这段内存空间时,这种情况属于释放内存不与数据结构记录的空闲空间相邻,故而.free[0].addr= 0x00001000,.free[0].size= 0x0009e000(647KB);再释放首地址为0x00400000,大小为32M– 0x00400000(28M)这段内存空间时,这段空闲内存将会被记录在.free.[1]中。所以在数据结构structMEMMAN中的空闲内存约29M左右。
4.4 以4KB为单位管理内存
内存的零散指在数据结构中记录的空闲内存空间的不连续。以字节为单位来管理内存会比以更大单位管理内存零散度要大,就会增加记录空闲空间变量的压力。为了内存空间零散度减小,可以增加管理内存的单位。x86操作系统大都以4KB为单位来管理内存,跟随。/*以4KB为单位的内存分配
*man为记录内存空闲的数据结构
*size所需分配内存大小
*分配成功返回分配内存的首地址,分配失败返回0
*/
unsigned int memman_alloc_4k(struct MEMMAN *man, unsigned int size)
{
unsigned int a;
size = (size + 0xfff) & 0xfffff000; //以4KB(0x1000)向上舍入
a = memman_alloc(man, size);
return a;
}
/*以4KB为单位进行释放内存,以空闲的状态记录在数据结构中
*man为记录内存空闲的数据结构,
*addr为即将释放内存的首地址,
*size为即将释放内存的大小
*内存释放成功返回0,否则返回-1
*/
int memman_free_4k(struct MEMMAN *man, unsigned int addr, unsigned int size)
{
int i;
size = (size + 0xfff) & 0xfffff000; //以4KB(0x1000)向上舍入
i = memman_free(man, addr, size);
return 0;
}
将这两个函数声明后,替换主函数HariMain()中的内存释放函数。就这样一步一步,内存分配和内存释放函数变得越来越靠谱。
HariMain()
/*bootpack.c*/
#include "bootpack.h"
#include <stdio.h>
void HariMain(void)
{
int data;
int mx, my;
char str[30];
char mcursor[256];
char mousebuf[128]; //接收鼠标数据缓冲区
unsigned int memtotal;
struct MOUSE_DEC mdec;
struct BOOTINFO *binfo = (struct BOOTINFO *) ADR_BOOTINFO;
struct MEMMAN *memman = (struct MEMMAN *) MEMMAN_ADDR;
init_gdtidt();
init_pic();
//CPU处理中断-----------
io_sti();
fifo8_init(&mousefifo, 32, mousebuf); //鼠标数据的缓冲区FIFO
io_out8(PIC0_IMR, 0xfb);//主PIC IRQ2中断开启
io_out8(PIC1_IMR, 0xef); //从PIC IRQ12中断开启
init_keyboard();
memtotal = memtest(0x00400000, 0xbfffffff);
memman_init(memman);
memman_free_4k(memman, 0x00001000, 0x00000300); //1kb
memman_free_4k(memman, 0x00400000, 0x00000400);//2kb
init_palette();
enable_mouse(&mdec);
init_screen(binfo->vram, binfo->scrnx, binfo->scrny);
init_mouse_cursor8(mcursor, COL8_000000);
sprintf(str, "memory %dMB free : %dKB",
memtotal / (1024 * 1024), memman_total(memman) / 1024);
putfonts8_asc(binfo->vram, binfo->scrnx, 0, 32, COL8_FFFFFF, str);
//不断查询缓冲区,如果有数据,将缓冲区内的数据显示完为止
for (;;) {
io_cli(); //查看缓冲区时,屏蔽中断
if (fifo8_status(&mousefifo) == 0) {
io_stihlt(); //如果缓冲区内无数据则开启中断并让CPU休眠,直到中断唤醒
} else {
data = fifo8_get(&mousefifo);
io_sti(); //从缓冲区读完数据立即开启中断
if (mouse_decode(&mdec, data) != 0) {
//获得解读到的鼠标坐标
sprintf(str, "[lcr %4d %4d]", mdec.x, mdec.y);
if ((mdec.btn & 0x01) != 0) {
str[1] = 'L';
}
if ((mdec.btn & 0x02) != 0) {
str[3] = 'R';
}
if ((mdec.btn & 0x04) != 0) {
str[2] = 'C';
}
boxfill8(binfo->vram, binfo->scrnx, COL8_000000, 0, 0, 32 + 15 * 8 - 1, 31);
putfonts8_asc(binfo->vram, binfo->scrnx, 0, 0, COL8_FFFFFF, str);
boxfill8(binfo->vram, binfo->scrnx, COL8_000000, mx, my, mx + 15, my + 15);//隐藏上次鼠标移动留下的痕迹
mx += mdec.x;
my += mdec.y;
if (mx < 0) {
mx = 0;
}
if (my < 0) {
my = 0;
}
if (mx > binfo->scrnx - 16) {
mx = binfo->scrnx - 16;
}
if (my > binfo->scrny - 16) {
my = binfo->scrny - 16;
}
sprintf(str, "(%3d, %3d)", mx, my);
putblock8_8(binfo->vram, binfo->scrnx, 16, 16, mx, my, mcursor, 16);
}
}
}
}
虽然实际要求的是释放1kb + 2kb = 3kb的内存,但实际会释放4kb + 4kb = 8kb的内存:
Figure4. 以4kb为单位管理内存
[x86OS] Note Over.
[2015.04.17]