首页 > 系统相关 >30天开发操作系统 第 9 天 -- 内存管理

30天开发操作系统 第 9 天 -- 内存管理

时间:2024-12-29 10:25:49浏览次数:8  
标签:-- 30 unsigned free int 内存 size man

今天叙述很多,让大家理解一些内存及编译器的运行机制。内容不是很难,让我们开始吧!

整理源文件

`现在我们还残留一个问题,就是鼠标指针的叠加处理不太顺利。不过如果一味进行鼠标处理 的话,大家可能很容易腻烦,所以我们今天干点儿别的。鼠标指针的叠加处理问题迟早会解决的, 大家不用担心,暂时先忘掉这个事情吧。 那么,今天做什么呢?我们今天就做内存管理吧。好不容易变成了32位模式,终于可以使用电脑的全部内存了,大家肯定也想用一用试试吧。 刚想改造bootpack.c,却发现为了解决鼠标处理问题而大加修改程序导致程序变大了很多, 足足有182行。嗯,程序太长了,怎么看都不舒服,所以我们决定将程序整理一下。
函数名 移动前 移动后
wait KBC sendready – bootpack.c – keyboard.c
init keyboard – bootpack.c – keyboard.c
enable mouse – bootpack.c – mouse.c
mouse decode – bootpack.c – mouse.c
inthandler21 – init.c – keyboard.c
inthandler2c – init.c – mouse.c

要做的事情很简单,仅仅是把函数写到不同的地方而已。此时,如果不知道哪个函数写在什么地方,可就麻烦了,所以在bootpack.h里还要加上函数声明 在Makefile的"OBJSBOOTPACK= 那里,要将keyboard.obj和mouse.obj也补进去 我们顺便确认一下运行情况。 还能像以前那样运行。这样bootpack.c 不错不错 make run 就减到了86行。真清爽!

一、内存容量检查1.0

现在我们要进行内存管理了。首先必须要做的事情,是搞清楚内存究竟有多大,范围是到哪 里。如果连这一点都搞不清楚的话,内存管理就无从谈起。 在最初启动时,BIOS肯定要检查内存容量,所以只要我们问一问BIOS,就能知道内存容量 有多大。但问题是,如果那样做的话,一方面asmhead.nas会变长,另一方面,BIOS版本不同,BIOS 函数的调用方法也不相同,麻烦事太多了。所以,笔者想与其如此,不如自己去检查内存。
下面介绍一下做法。 首先, 暂时让486以后的CPU的高速缓存(cache)功能无效。回忆一下最初讲的CPU与内存 的关系吧。我们说过,内存与CPU的距离地与CPU内部元件要远得多, 因此在寄存器内部MOV, 要比从寄存器MOV到内存快得多。但另一方面,有一个问题,CPU的记忆力太差了,即使知道 内存的速度不行,还不得不频繁使用内存。 考虑到这个问题,英特尔的大叔们在CPU里也加进了一点存储器,它被称为高速缓冲存储器 (cache memory)。cache这个词原是指储存粮食弹药等物资的仓库。但是能够跟得上CPU速度的高 速存储器价格特别高,一个芯片就有一个CPU那 么贵。如果128MB内存全部都用这种高价存储器, 容量只有这个数值的千分之一,也就是128KB左右。高级CPU, 预算上肯定受不了。高速缓存, 也许能有1MB高速缓存,但即便这样,也不过就是128MB的百分之一。 为了有效使用如此稀有的高速缓存,英特尔的大叔们决定,每次访问内存, 都要将所访问的 地址和内容存入到高速缓存里。也就是存放成这# 羊: 18号地址的值是54。如果下次再要用18号地址 的内容,CPU就不再读内存了,而是使用高速缓存的信息, 马上就能回答出18号地址的内容是54。 往内存里写入数据时也一样,首先更新高速缓存的信息,然后再写入内存。如果先写人内存 的话,在等待写入完成的期间,CPU处于空闲状态,这样就会影响速度。所以,先更新缓存,缓 存控制电路配合内存的速度,然后再慢慢发送内存写入命令。 观察机器语言的流程会发现,9成以上的时间耗费在循环上。所谓循环,是指程序在同一个 地方来回打转。所以,那个地方的内存要遍又一遍读进来。从第2圈循环开始,那个地方的内存信息已经保存到缓存里了,就不需要执行费时的读取内存操作了,机器语言的执行速度因而得 以大幅提高。 另外,就算是变量,也会有像“for(i=0;i<100;i++)0”这样,颜繁地被引用,被赋值的 情况,最初是0,紧接着是1,下一个就是2。也就是说,要往内存的同一个地址,一次又一次 写入不同的值。缀存控制电路观察会这一特性, 在写人值不断变化的时候,试图不写入缓慢的内 存,而是尽量在缓存内处理。循环处理完成, 最终的值变成100以后,才发送内存写入命令。这 样,就省略了99次内存写入命令,CPU儿乎不用等就能连续执行机器语言。 386的CPU没有缓存,486的缓存只有8-16KB,但两者的性能就差了6倍以上 。286进化到386 时,性能可没提高这么多。386进化到486时,除了缓存之外还有别的改善,不能光靠缓存来解释 这么大的性能差异,但这个性能差异,居然比16位改良到32位所带来的性能差异还要大。

内存检查时,要往内存里随便写入一个值,然后马上读取,来检查读取的值与写入的值是否 相等。如果内存连接正常,则写入的值能够记在内存里。如果没连接上,则读出的值肯定是乱七八糟的。方法很简单。但是,如果CPU里加上了 缓存会怎么样呢?写入和读出的不是内存,而是 缓存。结果,所有的内存都“正常”,检查处理不能完成。 只有在内存检查时才将缓存设为OFF。其体来说,就是先查查CPU是不是在486以上, 所以, 如果是,就将缓存设为OFF。按照这一思路,我们创建了以下函数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;
}

最初对EFLAGS进行的处理,是检查CPU是486以上还是386。如果是486以上,EFLAGS寄存 器的第18位应该是所谓的AC标志位;如果CPU是386,那么就没有这个标志位,第18位一直是0。 这里,我们有意识地把1写入到这一位,然后再读 出EFLAGS的值,维而检查AC标志位是否仍为1。 最后,将AC标志位重置为0。 将AC标志位重置为0时,用到了AND运算,那里出现了一个运算符“~”,它是取反运算符, 就是将所有的位都反转的意思。所以,~EFLAGS_AC_BIT与0xffbff一样。 为了禁止缓存,需要对CR0寄存器的某一标志位进行操作。对哪里操作,怎么操作,大家一 看程序就能明白。这时,需要用到函数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
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;
}

这个程序所做的是:调查从start地址到end地址的范围内,能够使用的内存的末尾地址。要做 的事情很简单。首先如果.不是指针,就不能指定地址去读政内存,所以先执行“p-i”。紧接着 使用这个p,将原值保存下来(变量old)。接着试写0xaa55aa55,在内存里反转该值,检查结果是 否正确"。如果正确,就再次反转它,检查一下 是否能回复到初始值。最后,使用old变量,将内 …如果在某个环节没能恢复成預想的值,那么就在那个环节终止调查,并报 存的值恢复回去。 告终止时的地址。 关于反转,我们用XOR运算来实现,其运算符是“A" “p=0xfff;"是“*p=p0xfmm,” Om 的省略形式。 的值每次增加4是因为每次要检查4个字节。之所以把变量命名为patO、patI是因为这些变量 表示测试时所用的儿种形式。 执行了一下这个程序,发现运行速度特别慢,于是就对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=(unsigmed int*)(i +0xffc);
	old =*p;/*先记住修改前的值 */
}

改变的内容只是for语句中i的增值部分以及p的赋值部分。每次只增加4,就要检查全部内存, 速度太慢了,所以改成了每次增加0x1000, 相当于4KB,这样一来速度就提高了1000倍。p的赋 值计算式也变了,这是因为,如果不进行任何改变仍写作“pi,”的话,程序就会只检查4KB最 开头的4个字节。所以要改为“pi+0xffc;",让它只检查末尾的4个字节。 毕竞在系统启动时内存已经被仔细检查过了,所以像这次这样,目的只是确认容量的话,做到如此程度就足够了。甚至可以说每次检查1MB都没什么问题。
添加的程序如下: 那好,下面我们来改造HariMain。

i =memtest(0x00400000,0xbfffffff)/(1024*1024);
sprintf(s,"memory gdMB",i);
putfonts8_asc(binfo->vram,binfo->scrnx,0,32,COL8_FFFFFF,s);

暂时先使用以上程序对0x00400000~0xbfnmm范围的内存进行检查。这个程序最大可以识别 3GB范围的内存。0x00400000号以前的内存已经 被使用了(参考8.5节的内存分布图),没有内存, 程序根本运行不到这里,所以我们没做内存检查 。如果以byte或KB为单位来显示结果不容易看明 白,所以我们以MB为单位 也不知道能不能正常运行。如果在QEMU上运 行,根据模拟器的设定,内存应该为32MB。 运行“ make run。
在这里插入图片描述

哎?怎么搞的?内存容量怎么不是32MB,而是3072MB?这不就是3GB吗?为什么会失败呢?我们已经把缓冲OFF了啊。

二、内存容量检查2.0

这种做法本身没有问题,所以看到上述结果很纳闷。这种内存检查方法在很多机型上都能运行。我们坚信程序没有问题,可运行结果却不是很对劲。经过多方调查,终于搞清楚了原因。如果我们不用“make run” ,而是用“make -r bootpack.nas" 来运行的话,就可以确认bootpack.c被编译成了 什么样的机器语言。用文本编辑器看一看生成的 bootpack.nas会发现,最下边有memtest_sub的编译结果。我们将编译结果列在下面。(为了读起来方便,添加了注释。)

1.引入库

代码如下(示例):

memtest sub:
	PUSH EBP;C编译器的固定语句
	MOV EBP,ESP
	MOV EDX,DWORD [12+EBP] ;EDX = end;
	MOV EAX, DWORD [8+EBP] ;EAX=start;/*EAX是i*/
	CMP EAX,EDX ;if(EAX>EDX)goto L30;
	JA L30
L36:
L34:
	ADD EAX,4096 ;EAX += 0x1000;
	CMP EAX,EDX ;if(EAX<=EDX)goto L36;
L30:
	POP EBP ;接收前文中PUSH的EBP
	RET ;return

有些细节大家可能不太明白, 但是可以跟memtest_sub比较一下。可以发现,以上的编译结果有点不正常。
大家会发现,编译后没有XOR等指令,而且,好像编译后只剩下了for语句。怪不得显示结果是3GB呢。但是,为什么会这样呢?

开始以为这是C编详器的bug, 但仔细查一查, 发现并非如此。反倒是编译器太过优秀了。 编译器在编译时,应该是按下面思路考虑问题的。 首先将内存的内容保存到old里,然后写入pat0的值,再反转,最后跟patl进行比较。这 不是肯定相等的吗?i语句不成立,得不到执行,所以把它删掉。怎么?下面还要反转吗? 这家伙好像就喜欢反转。这次是不是要比较#p和pat0呀?这不是也肯定相等吗?这些处理不 是多余么?为了提高速度, 将这部分也删掉吧。这样一来,程序就变成了:

unsigned int memtest sub(unsigmed int start,unsigned int end)
{
	unsigned int i,*p,old,pat0=0xaa55aa55,pat1 =0x55aa55aa;
	for(i=start;i<=end;i+=0x1000){
		p=(unsigmed int*)(i + 0xffc);
		old =*p;/*先记住修改前的值*/
		*p = patO;/*试写*/
		*p ^= 0xffffffff;/* 反转 */
		*p ^= Oxffffffff;/*再次反转 */
		*p =old;/* 恢复为修改前的值 */
	}
	return i;
}

反转了两次会变回之前的状态,所以这些处理也可以不要嘛。因此程序就变成了这样:

unsigned int memtest sub(unsigmed int start,unsigned int end)
{
	unsigned int i,*p,old,pat0=0xaa55aa55,pat1 =0x55aa55aa;
	for(i=start;i<=end;i+=0x1000){
		p=(unsigmed int*)(i + 0xffc);
		old =*p;/*先记住修改前的值*/
		*p = patO;/*试写*/
		*p=old;/* 恢复为修改前的值 */
	}
	return i;
}

还有,“p=pat0;”本来就没有意义嘛。反正要将old的值赋给p。

unsigned int memtest sub(unsigmed int start,unsigned int end)
{
	unsigned int i,*p,old,pat0=0xaa55aa55,pat1 =0x55aa55aa;
	for(i=start;i<=end;i+=0x1000){
		p=(unsigmed int*)(i + 0xffc);
		old =*p;/*先记住修改前的值*/
		old =*p;/* 恢复为修改前的值 */
	}
	return i;
}

这程序是什么嘛?结果,*p里面不是没写进任何内容吗?这有什么意义?

unsigned int memtest sub(unsigmed int start,unsigned int end)
{
	unsigned int i,*p,old,pat0=0xaa55aa55,pat1 =0x55aa55aa;
	for(i=start;i<=end;i+=0x1000){
		p=(unsigmed int*)(i + 0xffc);
	}
	return i;
}

这里的地址变量p,虽然计算了地址,却一次也没有用到。这么说来,old、pat0、pat1也都是用不到的变量。全部都舍弃掉吧。

unsigned int memtest sub(unsigmed int start,unsigned int end)
{
	unsigned int i,*p,old,pat0=0xaa55aa55,pat1 =0x55aa55aa;
	for(i=start;i<=end;i+=0x1000){
	}
	return i;
}

根据以上编译器的思路,我们可以看出,它进行了最优化处理。但其实这个工作本来是不需 要的。用于应用程序的C编译器,根本想不到会对没有内存的地方进行读写。 如果更改编译选项,是可以停止最优化处理的。可是在其他地方,我们还是需要如此考虑周 密的最优化处理的,所以不想更改编译选项。那怎样来解决这个问题呢?想来想去,还是觉得很麻烦,于是决定memtest_sub也用汇编来写算了。 这次C编译器只是好心干了坏事,但意外的是, 它居然会考虑得如此周到、镇密来进行最优化处理…这个编译器真是聪明啊!顺便说一句,这种事偶尔还会有的,所以能够看见中途结果很有用。而且懂汇编语言很重要。

_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

我们好久没写过这么长的汇编程序了。程序里加上了足够的注释,应该很好懂。虽然XOR 指令(异或)是第一次出现, 不过不用特别解释大家也应该能明白。 运行一下看看那好, 我们删除bootpack.c中的memtest_sub的数, 结果会怎么样呢?
在这里插入图片描述

挑战内存管理

刚才笔者一个劲儿地说内存管理长,内存管理短的,那到底什么是内存管理呢?为什么要进 行内存管理呢? 比如说,假设内存大小是128MB,应用程序A暂时需要100KB,画面控制需要1.2MB. 像这样,操作系统在工作中,有时需要分配一定大小的内存,用完以后又不再需要,这种事会频 繁发生。为了应付这些需求,必须恰当管理好哪些内存可以使用(哪些内存空闲),哪些内存不 可以使用(正在使用),这就是内存管理。如果不进行管理,系统会变得一塌糊涂,要么不知道 哪里可用,要么多个应用程序使用同一地址的内存。 内存管理的基础,一是内存分配,一是内存释放。“现在要启动应用程序B了,需要84KB内 存,哪儿空着呢?”如果问内存管理程序这么一个问题,内存管理程序就会给出一个能够自由使 “内存使用完了,现在把内存归还给内存管理用的84KB的内存地址,这就是内存分配。另一方面, “内存归还给内存管理程序”,这一过程就是内存的释放过程。
先假设有128MB的内存吧。也就是说,有0x08000000个字节。另外我们假设以0x1000个字节 PDO (4KB)为单位进行管理。大家会如何管理呢?答案有很多,我们从简单的方法开始介绍。 0x08000000/0x1000=0x08000=32768,所以首先我们来创建32768字节的区域,可以往其中写入0或者1来标记哪里是空着的,哪里是正在使用的。

char a[32768];
for(i=0;i<1024;i++){
	a[i]=1;/*一直到4MB为止,标记为正在使用 */
}
for(i=1024;i<32768;i++){
	a[i]=0;/*剩下的全部标记为空 */
}
------------------------------------------------------------------

比如需要100KB的空间,那么只要从a中找出连续25个标记为0的地方就可以了。
	j=0:
再来一次:
for(i=0;i<25;i++){
	if(a[j + i]!= 0){
	++j;
	if(j<32768-25)goto 再来一次;
		"没有可用内存了";
	}
}
"从a[j]到a[j+ 24]为止,标记连续为0";
---------------------------------------------------------------------------

如果找到了标记连续为0的地方,暂时将这些地方标记为“正在使用”,然后从j的值计算出
对应的地址。这次是以0x1000字节为管理单位的,所以将j放大0x1000倍就行了。
for(i=0;i<25;i++){
a[j + i] = 1;
}
"从j*0x1000 开始的100KB空间得到分配";
----------------------------------------------------------------------

如果要释放这部分内存空间,可以像下面这样做。比如,如果遇到这种情况:“刚才取得的
从0x00123000开始的100KB,已经不用了,现在归还。谢谢你呀。”那该怎么办呢?用地址值除
以0x1000,计算出i就可以了。
j=0x00123000/0x1000;
for(i=0;i<25;i++){
	a[j + i]= 0;
}

上面这个方法虽然很好懂,但是有一点问题。如果内存是128MB,管理表只需要32768字节 (32KB);如果内存最大是3GB,管理表是多大呢? 0xc0000000/0x1000=0xc0000=786432,也就是说光管理表就需要768KB。当然,虽说768KB不小,但从3GB看来, 只不过是0.02%。 事实上,0.02%的比例是与容量没有关系的。用32KB管理128MB时,比例也是0.02%。如果 容量是个问题,这个管理表可以不用char来构成 ,而是使用位(bit)来构成。归根到底,储存的 只有0和1,用不了一个字节,一位就够了。这样做, 程序会变得复杂些,但是管理表的大小可缩 减到原来的1/8。如果是3GB内存,只需要96KB 就可以管理整个内存了。这个比例只有0.003%。 我们后面还会讲到,这虽然不是最好的方法,但Windows的软盘管理方法,与这个方法很接近(1.44MB的容量,以512字节为单位分块管理)。

除了这个管理方法之外,还有一种列表管理的方法,是把类似于“从xxx号地址开始的yyy字节的空间是空着的”这种信息都列在表里。

struCt FREEINEO{
/*可用状况*/
	unsigned int addr,size;
}
struct MEMMAN{
	/*内存管理 */
	int frees;
	struCt FREEINFO free[1000];
};
struct MEMMAN merman;
memmman.frees=1;/*可用状况list中只有1件*/
meman.free[0l.addr= 0x00400000;/*从0x00400000号地址开始,有124MB可用*/
memman.free[0].size =0x07c00000;

大体就是这个样子。之所以有1000个free,是考虑到即使可用内存部分不连续,我们也能写入到这1000个free里。memman是咱们起的名字,代表memorymanager。
比如,如果需要100KB的空间,只要查看memman中free的状况,从中找到100MB以上的可用空间就行了。

memman.free[i],addr += 100*1024;/*可用地址向后推进了100KB*/
memman.free[i].size -= 100*1024;/*减去100KB */

如果找到了可用内存空间,就将这一段信息从“可用内存空间管理表”中删除。这相当于给这一段内存贴上了“正在使用”的标签。

如果size变成了0,那么这一段可用信息就不再需要了,将这条信息删除,frees减去1就可 以了。 释放内存时,增加一条可用信息,frees加1。而且,还要调查一下这段新释放出来的内存, 与相邻的可用空间能不能连到一起。如果能连到一起,就把它们归纳为一条。 能够归纳为一条的例子: 0x00019000字节可用 free[0]:地址0x00400000号开始, free[1]:地址0x00419000号开始,0x07be7000字节可用可以归纳为 free[0]:地址0x00400000号开始,0x07c00000字节可用 如果不将它们归纳为一条,以后系统要求“请给我提供0x07bf0000字节的内存”时,本来有 这么多的可用空间,但以先前的查找程序却会找不到。
上述新方法的优点, 首先就是占用内存少。memman是8x1000+4=8004, 还不到8KB。与 上一种方法的32KB相比,差得可不少。而且,这里的1000是个很充裕的数字。可用空间不可能 如此零碎分散(当然,这与内存的使用方法有关)。所以,这个数字或许能降到100。这样的话, 只要804字节就能管理128MB的内存了。 如果用这种新方法,就算是管理3GB的内存,也只需要8KB左右就够了。当然, 可用内存可 能更零碎些,为了安全起见,也可以设定10000条可用区域管理信息。即使这样也只有80KB。 这样新方法,还有其他优点,那就是大块内存的分配和释放都非常迅速。比如我们考虑分配 “内存正在使用”的标记“1”, 10MB内存的情形。如果按前一种方法,就要写入2560个 而释放 内存时,要写入2560个“0”。这些都需要花费很长的时间。 另一方面,这种新方法在分配内存时,只要加法运算和减法运算各执行一次就结束了。不管 是10MB也好,100MB也好,还是40KB,任何情况都一样。释放内存的时候虽然没那么快,但是与写入2560个“0”相比,速度快得可以用“一瞬间”来形容。

事情总是有两面性的,占用内存少,分配和释放内存速度快,现在看起来全是优点,但是实际上也有缺点,首先是管理程序变复杂了。特别是将可用信息归纳到一起的处理,变得相当复杂。 还有一个缺点是,当可用空间被搞得零零散散,怎么都归纳不到一块儿时,会将1000条可用空间管理信息全部用完。虽然可以认为这几乎不会发生,但也不能保证绝对不能发生。这种情况 下,要么像一个更大的MEMMAN,要么就只能割舍掉小块内存。被割含掉的这部分内存,虽然 实际上空着,但是却被误认为正在使用,而再也不能使用。 为了解决这一问题,实际上操作系统想尽了各种办法。有一种办法是,暂时先割舍掉,当 memman有空余时,再对使用中的内存进行检查,将割舍掉的那部分内容再捡回来。还有一种方法是, 如果可用内存太零碎了,就自动切换到之前那种管理方法。 那么,我们的操作系统会采用什么办法呢?经过斟酌,采用了这样一种做法, 即“割舍掉的东西,只要以后还能找回来,就暂时不去管它”。如果我们陷在这个 问题上不能自拔,花上好几天时间,大家就会厌烦的。笔者还是希望大家能开开心心地开发系统。而且万一出了问题,到时候我们再回过头来重新修正内存管理程序也可以。

#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; /* 没有空间的话 */
}

一开始的struct MEMMAN, 只有1000组的话, 可能不够。所以,我们创建了4000组,留出 不少余量。这样一来,管理空间大约是32KB。其中还有变量maxfrees.lostsize、losts等,这些变 量与管理本身没有关系, 不用在意它们。如果特别想了解的话, 可以看看函数memman init的注 样, 里面有介绍。 函数memman init对memman进行了初始化,设定为空。主要工作,是将fees设为0,而其他 是initialize(初始化)的缩写。 的都是附属性设定。这里的init, 函数memman_total用来计算可用内存的合计大小并返回。笔者觉得有这个功能应该很方便, 所以就创建了这么一个函数。原理很简单,不用解释大家也会明白。total这个英文单词,是“合 计”的意思。 最后的memman_alloc函数,功能是分配指定大小的内存。除了free[门].size变为0时的处理以外 在前面已经说过了。alloc是英文allocate(分配)的缩写。在编程中,需要分配内存空间 的部分, 时,常常会使用allocate这个词。 memman_alloc函数中free[i].size等于0的处理,与FIFO缓冲区的处理方法很相似,要进行移位处理。大家注意以下写法:
man->free [1] .addr= man->free [i+1].addr;
man->free[1].size=man->free [i+1].size;
我们在这里将其归纳为了:
man->free[i] = man->free [i+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) {
			/* �O�̂����̈�ɂ܂Ƃ߂��� */
			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; /* 失败 */
}

程序太长了,用文字来描述不易于理解,所以在程序里加了注释。如果理解了以前讲解的原理,现在只要细细读读程序,大家肯定能看懂。 另外,我们前面已经说过了哦,如果可用信息表满了,就按照舍去之后带来损失最小的原则进行割舍。但是在这个程序里,我们并没有对损失程度进行比较,而是含去了刚刚进来的可用信息, 这只是为了图个方便。

有一道题目叫做合并区间,意思差不多。大家不理解的可以去力扣做一下这道题,看看题解

以数组 intervals 表示若干个区间的集合,其中单个区间为 intervals[i] = [starti, endi] 。请你合并所有重叠的区间,并返回 一个不重叠的区间数组,该数组需恰好覆盖输入中的所有区间 。

示例 1:
输入:intervals = [[1,3],[2,6],[8,10],[15,18]]
输出:[[1,6],[8,10],[15,18]]
解释:区间 [1,3] 和 [2,6] 重叠, 将它们合并为 [1,6].

示例 2:
输入:intervals = [[1,4],[4,5]]
输出:[[1,5]]
解释:区间 [1,4] 和 [4,5] 可被视为重叠区间。

总结

memman需要32KB,我们暂时决定使用自0x003c0000开始的32KB(0x00300000号地址以后, 今后的程序即使有所增加,预计也不会到达0x003c0000,所以我们使用这一数值),然后计算内存总量memtotal,将现在不用的内存以0x1000个字节为单位注册到mcmman里。最后,显示出合 计可用内存容量。在QEMU上执行时,有时会注册成632KB和28MB。632+28672-29304,所以屏 幕上会显示出29304KB。 那好,运行一下“make run”看看。哦,运行正常。我们明天继续吧。
在这里插入图片描述

标签:--,30,unsigned,free,int,内存,size,man
From: https://blog.csdn.net/suy123/article/details/144790186

相关文章

  • 寄存器1
    EIPEIP永远指向下一个将要执行的指令,指向的地方在od中,是灰色底色,黑色字体。ESP永远指向栈顶的位置,指向的地方在od中,是灰色底色,黑色字体。EAX,EBX,ECX,EDX存储方式如下,都是16进制存储数。3.1EAX=12345678,AX=5678,AH=56,AL=78.3.2EBX=23452389,BX=2389,B......
  • 什么是硬链接和软链接?
    在Linux中,硬链接(HardLink)和软链接(SoftLink,也称为符号链接SymbolicLink)是两种用于引用文件或目录的机制。以下是关于这两种链接的详细解释:1.硬链接(HardLink)定义:硬链接是通过文件系统中的索引节点(inode)来进行连接的。多个文件名可以指向同一个索引节点,这就是硬链接。特......
  • 2024第一届Solar杯应急响应挑战赛WP
    日志流量日志流量-1直接放到D盾分析解码flag{A7b4_X9zK_2v8N_wL5q4}日志流量-2哥斯拉流量工具解一下flag{sA4hP_89dFh_x09tY_lL4SI4}日志流量-3tcp流6复制data流解码改pdfflag{dD7g_jk90_jnVm_aPkcs}内存取证内存取证-1vol.py-f123.raw--profile=Win7SP......
  • 如何使用浏览器开发者工具直接获取 .crx 文件?
    ggggggggggggggggggggggggggggggpt要使用浏览器开发者工具直接获取.crx文件,可以按照以下步骤操作:步骤1:打开Chrome网上应用店打开Chrome浏览器,访问Chrome网上应用店。搜索并选择你想要安装的插件。步骤2:获取插件的ID每个插件在Chrome网上应用店中都有一个......
  • 实验7
    1.实验任务4程序task4.c源码1#include<stdio.h>2#defineN103#defineM100456intmain(){7charx[N][M];8inti,j,cnt=0,count=0;9FILE*fp;1011fp=fopen("data4.txt","r");12if(!fp){13......
  • JavaScript引擎在优化标识符查询方面做了什么?
    JavaScript引擎在优化标识符查询方面采取了多种策略和技术,以提高代码执行效率和性能。以下是一些主要的优化方法:作用域链和变量对象的优化:JavaScript引擎通过创建作用域链来管理变量的访问。每个函数都有一个[[Scope]]属性,指向函数的作用域链。当函数执行时,会创建一个执行上下文......
  • 如何在一台不能联网的电脑上的chrome浏览器上安装插件?
    gggggggggggggggggggggggpt要在无法联网的计算机上安装Chrome插件,你可以按照以下步骤操作:步骤1:下载插件的CRX文件在联网的计算机上打开Chrome浏览器并访问Chrome网上应用店。搜索并找到你需要安装的插件。使用一个Chrome扩展插件下载工具(如CRXDownloader,或使......
  • .Net Core 8 NLog连接PostgreSQL数据库
    最近在做的项目需要把日志记录到本地和数据库,我使用的是NLog,主要参考博文链接:.NET项目中NLog的配置与使用-追逐时光者-博客园,下面是NLog连接PostgreSQL数据库的步骤,网上关于NLog连接PostgreSQL数据库的实例比较少,大多数都是mysql的。1、创建Nlog.config配置文件,将下面配置文......
  • 如何带领团队进化?
    带领团队进化,尤其是在前端开发领域,是一个持续不断的过程,旨在提升团队的整体能力、适应性和创新力。以下是一些关键步骤和策略,可以帮助您实现这一目标:设定明确、可衡量的目标:与团队成员共同制定具体、可衡量的季度或月度目标,确保每个人都清楚自己的职责和期望成果。将目标细......
  • GitHub 汉化插件,GitHub 中文化界面安装全教程
    概述GitHub作为全球最大的代码托管平台,拥有庞大的用户群体。对于中文用户来说,如果能将GitHub界面汉化,将大大提高使用体验和工作效率。本文将详细介绍如何通过安装汉化插件,实现GitHub界面的中文化。感谢maboloshi作者的无私奉献.GitHub汉化插件,GitHub中文化界面安装......