首页 > 其他分享 >【读书笔记-《30天自制操作系统》-8】Day9

【读书笔记-《30天自制操作系统》-8】Day9

时间:2024-08-24 18:53:49浏览次数:11  
标签:Day9 int 30 unsigned free 读书笔记 内存 size man

本篇的主题围绕着内存管理进行展开。首先编写了内存容量获取的程序,接下来详细讲解了内存管理的具体内容,以及两种实现内存管理的方式。在这里插入图片描述

1. 内存容量获取

前面已经实现了访问内存的扩展,能够使用的内存大大增加了。但是不同的应用程序在运行时,对内存的使用会有不同的要求,这就需要操作系统实现一个内存管理的功能,以确保各个应用程序都能够正常运行。

管理内存的第一步,当然是先要了解内存有多少,地址范围是哪里。内存相关的信息可以从BIOS获取,但不同的版本BIOS调用方式可能不同,因此这里作者决定自行开发程序去检查内存。

检查内存之前还需要先讲解一下CPU高速缓存(cache)的功能。
由于CPU的寄存器容量太小,而频繁从内存读写又会严重影响CPU的处理速度,考虑到这些,英特尔的CPU开发者们在CPU中增加了一些存储空间,被称为高速缓冲存储器(cache memory)。这一部分存储的读写速度能够跟得上CPU的处理速度,但是由于芯片造价太高,这部分的存储相比内存来说仍然很小,比如128KB。而这部分高速缓存是这样使用的:每次访问内存,都将所访问的内存地址和内容存入高速缓存。这样下次再读取内存时,先从高速缓存中查找,找到了之后就立即返回;同理,写入内存时,如果直接写入内存,在等待内存写入期间,CPU处于空闲状态。所以先写入高速缓存,缓存控制电路再配合内存的速度慢慢发送内存写入命令。
另外程序中遇到循环的地方也不少。比如for循环中,会多次改变循环变量i的值,即多次更改同一个内存地址的值。如果每一次更改都直接访问内存,很明显会拖慢CPU的处理速度。因此中间的过程都尽量在缓存中进行处理,到最后循环完成之后再将循环变量的值更新到内存中。
实际内存检查的过程,是需要向内存中写入一个任意值,再读取出来检查读取值是否与写入值相等。但如果开启了缓存功能,实际值会从缓存中读出,而不是实际访问内存,这样的检查结果都是“正常”,就无法达到实际检查内存的效果。
所以在具体执行内存检查前,需要将缓存功能关闭。内存检查相关代码如下:

#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) { /* 如果CPU是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;
}

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;
}

首先通过EFLAGS确认CPU是386还是486。如果是486以上的CPU,EFLAGS寄存器的第18位是AC标志位,而对于386的CPU,这一位始终是0。先将这一位写入1,再读出EFLAGS查看这一位是否仍然是1。用完之后再将AC这一位重新设置为0。
确认CPU为486以上,接下来就需要禁用缓存(高速缓存是从486开始引入的)。
禁用缓存需要操作cr0寄存器,用到的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]
		MOV		CR0,EAX
		RET

memtest_sub函数则是实际进行内存检查的处理。这里的操作是首先记录需要被检查的内存地址的内容,然后向该内存地址进行写入读取测试。之所以要进行反转,是因为有些机型不这样做会直接读出写入的数据。

每次增加4个字节,把所有内存地址都读写一遍,运行速度会很慢。其实BIOS之前已经检查过内存了,应该没什么问题,这里主要是获取容量。为了加快执行速度,修改为每次增加0x1000,并且p指针修改为p = i + 0xffc,即每4K检查最后的4个字节,这样就可以了。

接下来在主程序中调用,对0x00400000~0xbfffffff范围的内存进行检查。

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

根据上文的内存分布图,0x00400000之前的内存已经被使用了,应用程序不会运行到这里,因此不需要检查。因为是在QEMU虚拟机上进行运行,根据虚拟机的设置内存应该是32MB。运行:
在这里插入图片描述
实际结果和预期不符。为什么呢?

原因出在编译器上。
上面执行内存检查的memtest_sub程序,编译器在编译的过程中进行了优化,而这种优化导致没有实现我们预期的结果。

与我们的预期不同,编译器可能是按照以下思路进行优化的。

首先,写入pat0再反转,与pat1进行比较,编译器认为这一定是相等的,于是if(*p != pat1)得不到执行,编译过程中干脆删掉了。同理,下面再反转,与pat0进行比较,也被认为是一定相等的,于是下面的if语句也被删除掉了。程序于是变成了下面的样子:

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;
		*p ^= 0xffffffff;
		*p = old;
		}
		return i; 
}

这样看来,两次反转之后又回到原来的值,没什么意义,也删掉。

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 = old;
		}
		return i; 
}

既然 *p最终被赋值为old,那中间的*p = pat0也没有意义了,删掉。

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 = old;
		}
		return i; 
}

old = *p,紧接着又是*p = old?这有什么意义?删掉删掉。

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,但是完全没用到p的值,也删掉吧。于是最后编译出来的程序实现的是这样的效果:

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) {}
		return i; 
}

已经面目全非了,而且最终返回的就是end的值,并没有达到我们预期的目的。
当然编译器的优化还是有其用武之地的,还不能将优化关闭掉。只是这里我们需要严格按照设计一步一步进行执行,因此这里还是用汇编语言进行编写:

_memtest_sub:	; unsigned int memtest_sub(unsigned int start, unsigned int end)
		PUSH	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

再运行之后,获取到的内存容量就正常了:
在这里插入图片描述2. 内存管理

获取内存容量之后可以着手进行内存管理部分的开发了。简单来说,操作系统需要管理不同应用程序的内存使用需求,一方面是内存分配,一方面是内存释放。

首先假设共有128MB的内存,以4KB为单位进行管理,如何操作呢?下面从简单的情况开始逐一说明。

(1)128MB内存包含32768个4KB,这样我们创建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;
onemoretime:
for(i = 0;i < 25;i++)
{
	if(a[j + 1] != 0)
	{
		j++;
		if(j < 32768 -25)
		{
			goto onemoretime;
		}
		// no more memeory available
	}
}

找到了足够的地址,就将a中的地方标记为1表示正在使用。因为是以4KB(0x1000)字节为单位进行管理的,只需将j放大0x1000倍就可以计算出地址了。
同理,要释放这部分内存,用起始地址除以0x1000可以计算出j,再将对应的数组成员标记为0即可。
这样的管理方式比较方便,但占用的空间还是大了一点。其实只用来存储0或1,用比特位代替字节存储就可以,这样占用空间可用进一步缩小。

(2)还有一种列表管理的方法,是把类似于“从xxx号地址开始的yyy字节的空间是空着的”保存在列表里。

struct FREEINFO{ /* 用于保存可用状况*/
	unsigned int addr, size;
};

struct MEMMAN{	/* 内存管理*/
	int frees;
	struct FREEINFO free[1000];
};

	struct MEMMAN memman;
	memman.frees = 1; /* 可用状况list中只有1条*/
	memman.free[0].addr = 0x00400000;	/* 从0x00400000号地址开始,有124MB可用*/
	memman.free[0].size = 0x07c00000;

简单理解,就是一个列表中保存了一条一条的空闲内存信息,包括起始地址和内存大小。
用这种方法管理内存的优点在于占用的内存少,分配速度快。不管一条信息保存的内存有多大,占用的空间都是固定的8字节。这里设置1000的数组也会有富余,因为可用空间不会这么的分散,实际占用的空间可能会更小。而将一段内存分配出去只需要将起始地址后移,将可用长度减少,执行两次运算就可以了。

当然这样的管理方法也有缺陷。采用这样的分配方法,时间长了可用的内存段会变得很零碎。如果此时需要分配较大的内存时,虽然有很多零散的小块内存,却无法分配给应用程序使用。

作者这里采用的就是第二种方法,并在此基础上采取了一些改进措施。对于零碎而无法分配使用的内存,暂时舍弃,在后续有机会时再对零碎的内存整合起来。程序如下:

#define MEMMAN_FREES		4090	/* 大约32KB */
#define MEMMAN_ADDR			0x003c0000

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].size变成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; /* 失败 */
}

其实释放内存的本质还是在列表中给一段内存找位置,能合并则合并,实在不能合并就在列表中新增一项。代码看起来有些复杂,多读几遍还是能明白意思的。

下一篇会处理之前鼠标遗留的一点小问题,优化鼠标显示。敬请期待。

标签:Day9,int,30,unsigned,free,读书笔记,内存,size,man
From: https://blog.csdn.net/Ocean1994/article/details/141458575

相关文章

  • 你每天的销售数据日报需要多少时间处理?如果有办法能30秒出报表,你怎么选?
    每天的销量数据的获取对于品牌连锁企业来说,是商品管理部门、营运部门同事或者企业领导每天早上的必做事情之一。因此,对于如何准确,快速,低代价的获取销量信息,是一门艺术。据观察了解,目前来说各企业获取每日销量数据的方式参差不齐,大致来说主要分为两类:①、每日销量由各门店手工......
  • 《数据资产管理核心技术与应用》读书笔记-第五章:数据服务(一)
    《数据资产管理核心技术与应用》是清华大学出版社出版的一本图书,全书共分10章,第1章主要让读者认识数据资产,了解数据资产相关的基础概念,以及数据资产的发展情况。第2~8章主要介绍大数据时代数据资产管理所涉及的核心技术,内容包括元数据的采集与存储、数据血缘、数据质量、数据监控与......
  • Springboot计算机毕业设计旅游景点综合服务系统30079
    本系统(程序+源码+数据库+调试部署+开发环境)带论文文档1万字以上,文末可获取,系统界面在最后面。系统程序文件列表用户,景点管理,景点信息,订票信息,纪念商品,商品购买,景点分类,商品分类开题报告内容一、选题背景及意义随着旅游业的蓬勃发展,游客对于旅游体验的需求日益多元......
  • MBR30200FCT-ASEMI智能AI专用MBR30200FCT
    编辑:llMBR30200FCT-ASEMI智能AI专用MBR30200FCT型号:MBR30200FCT品牌:ASEMI封装:ITO-220AB批号:最新恢复时间:35ns最大平均正向电流(IF):30A最大循环峰值反向电压(VRRM):200V最大正向电压(VF):0.70V~0.90V工作温度:-65°C~175°C芯片个数:2芯片尺寸:mil正向浪涌电流(IFMS):275AMBR30200F......
  • JESD79-5C_v1.30-2024 JEDEC DDR5 SOLID STATE TECHNOLOGY ASSOCIATION 最新内存技术
    JESD79-5C_v1.30-2024JEDECDDR5SOLIDSTATETECHNOLOGYASSOCIATION最新DDR5内存技术规范​JEDEC技术协会公布新DDR5内存规范、更稳定、安全,支持PRAC新技术下载: https://download.csdn.net/download/tgs2033/89661013样本下载:链接:https://pan.baidu.com/s/13-Ioep......
  • 安装MySQL报错ERROR 2003 (HY000): Can‘t connect to MySQL server on ‘localhost:3
    今天要在本地装个mysql,发现mysql-5.7.19-winx64版总是报错由于找不到MSVCP120.dll,无法继续执行代码。重新安装程序可能会解决此问题。,去微软官网找到了最新的VisualC++RedistributableforVisualStudio,下载后发现还是不停地报错。怀疑是系统不兼容,没办法只好安装mysql-8.0.37......
  • 【读书笔记-《30天自制操作系统》-6】Day7
    本篇向着移动鼠标的目标继续前进。先对中断处理进行一些补充说明,然后建立完善缓冲区来实现键盘数据接收。最后是在此基础上的初始化鼠标控制电路与鼠标的数据接收。1.中断处理程序补充说明前面的处理中,接收到键盘中断后只是显示一行信息,现在把按键的信息也一并显示出来......
  • 2024暑假集训测试30
    前言比赛链接。T1普及了一下异或哈希,T2、T3赛时应该算乱搞题,还搞挂了,T4高级平衡树题,不太可做。原题全部出自:2022牛客OI赛前集训营-提高组(第四场)。T1博弈部分分\(30pts\):\(O(n^2)\)暴力。正解:不难推出必胜策略就是\((x,y)\)路径上每个边权出现的次数不全为......
  • 《Prometheus监控实战》读书笔记
    监控简介Google服务层次结构图,监控是底座一些监控反模式:事后监控机械式监控不(够)准确的监控静态监控:不是说超过某个绝对阈值系统就一定出现问题,更有意义的监控是对比(环比)动态监控。数据库性能分析供应商VividCortex的首席执行官BaronSchwartz对此评论道[插图]:它们比一个停......
  • 全球创新药商业化服务平台市场展望:2030年预计达到113660百万美元
    随着全球医药行业的快速发展和创新药物研发的不断涌现,创新药商业化服务平台行业作为支持新药上市和商业化的关键服务领域,其市场前景受到业界广泛关注。据恒州恒思(YHresearch)团队研究的数据显示,2023年全球创新药商业化服务平台市场规模已达到33210百万美元,并预计在未来六年内,该......