showwindow 窗口不弹出_计算机自制操作系统(二0):Windows窗口界面---内存分配管理...

d47f78e62e586e749425ca52aa6f3ca1.png

我们的操作系统在生产出了桌面并成功驱动键盘鼠标之后,自然的目标就是制作Windows窗口了。在上一章中,我们的鼠标使用起来其实还有点问题。请看下图:

c3e369f9827efc69fe4dd0a260faf44c.png

那就是移动鼠标的时候,会破坏掉任务栏的图像 。这是什么原因呢?因为我们没有利用图层叠加显示技术。

一、解决方案

所谓图层叠加显示技术就是把整个屏幕要显示的元素全部规划到“窗口”的概念,每一个窗口可以看做一层(鼠标也是),制作窗口的时候需要把每层的图像数据都单独保留在内存区,到显示的时候,才把每层窗口的数据全部写到显卡内存。这样就能保证每个窗口都在独立显示,不会因为窗口交叉而产生遮住的问题。

579b3fd7a6b1fd47ae2a1bdf48055d5c.png

那么现在我们要把每层窗口的显示数据放进内存区存储起来,最常用的方法肯定是用数组。但是这个数组会特别的大,因为就算是普通的256色,一屏幕显示所需要的数据空间就是1024*768=768KB。这个就是接近1MB的数据量,相比来说,比我们现在的内核程序都大太多了。而且,一旦图层数量增多,这个内存空间占用量就非常的吓人,比如有10个窗口,就需要10MB。

在C语言里,普通变量是存储在栈内空间的,所以这么大的数组,我们用普通变量申请临时数组肯定是不行的(首先编译都不能通过)。那么,我们只有考虑用全局变量或静态变量的方式(用Static关键字定义)来申请数组存放数据,但是这些类型变量在编译之后都是会放置在.data段或.bss段内的,这样链接出来的程序就会非常的大。一定会出现这样的结果:我们的操作系统镜像文件大小有几十MB,但其实我们的内核程序本身其实只占几KB,这样的结果肯定是不能接受的。而且这么大的操作系统镜像文件,目前我们怎么能用1.44MB的软盘来装呢?

也许你会说,按前面章节的C语言机制分析来看,我们可以把这些数组全部设置成不初始化的全局变量,它编译之后会放进.bss段内,最终生成的可执行就不会很大了啊。这是一种思路,但是由于我们的链接器不是生成Linux下的标准ELF格式(或Windows下的EXE格式)采用的“压缩”方式,因此它还是会把.bss段内的数据直接放到.data段后面而链接进最终的二进制文件中去,这个文件会非常的大。另外,之前也分析过了,程序真正运行的时候,它任何类型的变量都是需要占用内存空间的,无论我们中途怎么压缩,最终还是必须在内存里给它分配这段空间。

最后,采用普通数组变量存放图层数据最大的问题是:当用户增加窗口之后,我们需要不断的定义新的数组变量来存放新的数据,这样内存的可用空间会单方向一直减少。本来当用户关闭窗口之后,它的数组变量数据其实已经没有了意义,但目前这种情况下下,它仍然是存在的,因为我们并没有很好的办法来回收这部分无效的内存区。

二、内存分配管理

为了解决这个突出的问题,我们就必须采用新的方法:内存分配管理。也即为每一个窗口图层临时申请内存空间,当这个窗口关闭的时候,再回收这部分内存空间。而且,这种方法根本不需要把大量的数据做成变量,让C程序在编译和链接的时候,产生巨大的负担。因此,内存分配管理,是图形分层技术不可回避的问题。

有C语言编程经验的人都知道有一个叫动态内存分配 malloc的函数,但是这个函数的使用是建立在有操作系统支撑基础上的。我们现在要分配内存,就只能自己解决想办法解决,也即:需要建立一套内存分配管理机制。

(一)内存检测

内存管理的第一个问题是操作系统需要知道计算机的总共内存大小以及剩余可用内存。

1.内存总空间

检查内存总空间的方法是:遍历所有的内存空间地址(32位最多到4GB),往内存里写数据然后再读出,如果写入和读出相等,则表面是正常的内存。遍历跨度一般可按4KB(0X1000)为单位。程序沿用30天这本书的方法,这里需要特别注意的是:由于C语言编译器对这种写入“写入内存数据不等于读出内存数据”的做法不认可,它对源程序做出优化编译之后,会导致我们的逻辑无法实现,因此无法通过C程序进行验证,还必须要用汇编语言,然后在C语言中调用它实现。C程序端的函数为:unsigned int memtest(unsigned int start, unsigned int end)。这段程序通过调用汇编程序的标号函数:_memtest_sub,该程序通过返回EAX的值来取得总的内存大小,注意返回的这个内存总大小值表示的是内存的最高绝对物理地址。而不是表start和end地址之间的长度!

_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

这个汇编程序并不复杂,以我的水平绝对能写得出来。具体到我们的操作系统,因为程序全部是放在1MB以内的,所以我们只需要把start设置成0x00100000(1MB)开始即可,end可以设置成0xFFFFFFFF(4GB)。

2.内存可用空间

这个问题只有在下一小节才能正确回答。

(二)内存分配和释放

内存管理最核心的部分是内存的分配与释放,而内存管理本身又是整个操作系统的核心任务。这里我们抄用30天这本书的方法。

1.内存管理

要建立内存管理机制,必须要定义一个相关的数据结构来对内存的使用情况进行监控,这个数据结构在整个内存管理中会一直存在。

首先是内存片

//内存片
//起始地址和大小
typedef struct _FREEINFO{
	unsigned int *addr, size;
}FREEINFO;

其次核心的就是内存管理器

//内存管理器
typedef struct _MEMMAN{
	int frees, maxfrees, lostsize, losts;
	struct FREEINFO free[MEMMAN_FREES];
}MEMMAN

这两种数据结构搭配使用就起到了内存管理的作用:FREEINFO 结构用来表示可用内存的起始地址和大小,MEMMAN 表示内存管理器,其中的frees 表示当前可用内存对应的FREEINO结构体有多少个,maxfrees 表示我们的内存管理器最多可以容纳多少个可用内存片,一个可用内存片就是一个FREEINFO结构体。当有内存释放时,有些释放后的内存块无法重现加入内存管理器,这些内存块就得丢掉,那么lostsize 就用来记录,内存管理器总共放弃了多少内存碎片,losts记录的是碎片的数量。

很明显MEMMAN结构体套用了FREEINFO结构体,而且是FREEINFO结构体的数组,这个数组可以理解为像一片片雪花一样的内存片......

接下来,我们就可用以下的方法来初始化内存管理器以及计算内存可用空间的大小。方法:

//初始化内存管理器
void memman_init(MEMMAN *man);
 
//内存总共可用量
unsigned int memman_total(MEMMAN *man);

内存总共可用量很好理解,因为可用内存既然已经被一片片的记在了MEMMAN里面,而每一片都有大小的,那么全部加在一起不就是表面可用大小了吗?

可以看到,这个内存管理数据结构引发出来的函数和变量还挺多,感觉管理起来有一定的混乱。我这个时候突然想到了如果用C++来编程会不会效果更好,因为这些变量数据和函数完全可以封装在一个类里面。

2.内存释放

为什么要先说内存释放,再说内存分配?因为刚开始初始化内存管理器MEMMAN的时候,里面的数据是空的,一片都没有。所以,我们第一步就是要先把计算机的可用内存全部装进去,可以一次性的把全部可用空间当成一片装进去,也可以砍成N片装进去....第一次完整的释放完全部的内存之后,后面分配和释放,就可以根据我们的程序来运作了。

内存释放的逻辑相对复杂,这块也直接抄书了,等有精力再自己来写,以我现在的C语言水平,肯定是写不出来的。后面有精力再来练习,绝对可以提高自己的C语言编程水平。

int memman_free(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;
		}
	}
	
	//可以往前合并或者同时前后合并
	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->frees--;
				}
			}
			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;
		}
	}
	
	//不能和前后合并 
	//如果此时内存片数量没有超过最大值 则新增一个空闲块
	//将释放的内存块放在i位置  i位置的内存块依次后移
	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;
}

具体解释直接看网上的解读:

当有内存释放的时候,我们需要把释放内存的起始地址和大小作为参数传入。假定我们要释放的内存片起始地址是 0x200, 大小是0x100, 如果内存管理对象中存在着一片可用内存,起始地址是0x100, 长度为0x100, 那么当前释放的内存片就可以跟原有可用内存合并,合并后的内存块就是起始地址为0x100, 长度为0x200的一片内存块。

如果内存管理对象不但含有起始地址为0x100, 长度为0x100的内存片,而且还含有起始地址为0x300, 长度为0x100的内存片,这样的话,三块内存片就可以合并成一块内存,也就是起始地址为0x100, 长度为0x300的一个大块内存片。

这就是代码if( i > 0) {….} 这个模块的逻辑。

如果不存在上面的情况,比如当前内存管理模块存在的内存块是其实地址为0x100, 长度为0x50, 另一块内存块起始地址是0x350, 长度为0x100:

 FREEINFO{ addr : 0x100; size : 0x50;};
 FREEINFO{addr: 0x350; size: 0x100};

这样的话,我们就构建一个对应于要释放内存的FREEINFO对象,然后把这个对象插入到上面两个对象之间:

FREEINFO{ addr : 0x100; size : 0x50;};
FREEINFO{addr: 0x200; size: 0x100};
FREEINFO{addr: 0x350; size: 0x100};

这就是对应于if (i < man->frees){…} 这个分支的主要逻辑。

如果当前所有可用内存的起始地址都大于要释放的内存块,则将释放的内存块插到最前面,例如当前可用内存块为:

FREEINOF {addr: 0x350; size: 0x100;} 
FREEINFO {addr: 0x460; size: 0x100;}

那么释放起始地址为0x200的内存块后,情况如下:

FREEINFO{addr: 0x200; size: 0x100;} 
FREEINOF {addr: 0x350; size: 0x100;} 
FREEINFO {addr: 0x460; size: 0x100;}

或者如果当前释放的内存块起始地址大于所有可用内存块,例如要释放的内存块起始地址是0x450, 其他可用的内存块起始地址分别是0x100, 0x300, 那么释放的内存块则添加到末尾:

FREEINFO{addr: 0x100; size: 0x100;} 
FREEINOF {addr: 0x350; size: 0x50;} 
FREEINFO {addr: 0x450; size: 0x100;}

这就是 if (man->frees < MEMMAN_FREES) {…} 的实现逻辑。

如果以上情况都不满足的话,那么当前回收的内存块则直接丢弃。

3.内存分配

内存分配相对来说就非常简单了,我都能自己写出来:

//简单内存分配算法
//从可用内存中找到第一块大于所需内存大小的内存片地址返回
unsigned int* memman_alloc(MEMMAN *man, unsigned int size){
	unsigned int *a;
	for(int i=0; i<man->frees; i++){
		if(man->free[i].size>=size){
			a = man->free[i].addr;
			man->free[i].size -= size;
			man->free[i].addr = (unsigned int *)(((char *)(man->free[i].addr))+size);
			if(man->free[i].size==0){
				for(int j=i+1; j<man->frees; j++){
					man->free[j-1] = man->free[j];
				}
				man->frees--;
			}
			return a;
		}
	}
	return (unsigned int*)0;
}

三、内存展示

我们把内存管理器放置在内存2MB处,那么目前我们的操作系统内存分配情况再次更新如下:

8ee2cd3e939b2159a269d3565b4a9048.png
Jiangos操作系统内存分配图

把以上内存管理器的逻辑写入程序中,并启动操作系统,可以看到,我们的内存几乎都还没怎么用。那么从下一章开始,就来分配内存开始窗口界面制作。

	/*---内存管理---*/
	unsigned int memtotal,memrest;
	struct MEMMAN *memman = (struct MEMMAN *) MEMMAN_ADDR;
        /*内存管理器放在0x00200000,共32KB*/ 
	memtotal = memtest(0x00100000, 0xffffffff); /*检测总内存大小*/ 
	memman_init(memman);/*初始化内存管理器*/ 
	memman_free(memman, 0x00300000, memtotal - 0x00300000);
 	/*内存从3MB处的空间一次性释放可用*/ 
        memrest=memman_total(memman);  /*检测可用内存大小*/ 

	sprintf(s, "TotalMem:%dMB   Free:%dKB",
	        memtotal / (1024 * 1024), memrest/1024); 
 	putfonts8_asc(binfo->vram, binfo->scrnx, 100, 136, COL8_FFFFFF,s);
	line(binfo->vram,binfo->scrnx,156,c); 

4f3d5181a075842c37bffa14e53f7ae6.png
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值