【操作系统】30天自制操作系统--(8)内存管理

        搞定了鼠标和键盘这两个外设的处理,终于走到了内存管理这一步。平时写上层应用的时候,就是malloc、free、memset这几件套,较少关注内存多少、内存能不能用等底层细节,但是要制作操作系统,对于内存的检查和管理就不可或缺了,所以一章的内容是比较重要的。

        这一章着重介绍了内存的检查和内存容量的管理。

一 内存检查

        考虑在操作系统上电初始化的时候做一次内存检查,内存检查的代码实现如下:

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

        内存检查memtest步骤如下:

【1】确认CPU是386还是486以上的

        如果是486以上,EFLAGS寄存器的第18位应该是所谓的AC标志位。这里,我们有意识地把1写入到这一位,然后再读出EFLAGS的值,继而检查AC标志位是否仍为1,最后,将AC标志位重置为0。

【2】禁止缓存

        为了避免CPU内部的高速缓存(cache memory)对内存管理的影响,这边在进行内存检查之前需要先禁止内部高速缓存,检查完毕之后再开启。

        实现的方法也很简单,就是对CR0控制寄存器的第29位NW和第30位CD置位,即可以禁止(具体可以参考我关于寄存器的帖子【操作系统】CPU寄存器详解)。

【3】内存检查memtest_sub

        内存检查的思路是这样的,给一段内存赋值0xaa55aa55,尝试反转之后对比是不是变成了0x55aa55aa,然后再反转对比是不是变回0xaa55aa55,如果两次反转结果都正确,那么则认为内存正常无误。

        作者一开始使用的C语言实现的,结果最终出来的结果不正确:

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

        通过查看编译出来的汇编语言,发现GCC在编译这一段C语言是,认为这一段操作反转来反转去还不是原值,所以在编译的过程中优化掉了。所以这一段内存检查的操作,还是需要使用底层的汇编直接实现:

_memtest_sub:	; unsigned int memtest_sub(unsigned int start, unsigned int end)
		PUSH	EDI
		PUSH	ESI
		PUSH	EBX
		MOV		ESI,0xaa55aa55
		MOV		EDI,0x55aa55aa
		MOV		EAX,[ESP+12+4]		; i = start
mts_loop:
		MOV		EBX,EAX
		ADD		EBX,0xffc		
		MOV		EDX,[EBX]
		MOV		[EBX],ESI
		XOR		DWORD [EBX],0xffffffff
		CMP		EDI,[EBX]
		JNE		mts_fin
		XOR		DWORD [EBX],0xffffffff
		CMP		ESI,[EBX]
		JNE		mts_fin
		MOV		[EBX],EDX
		ADD		EAX,0x1000
		CMP	    EAX,[ESP+12+8]
		JBE		mts_loop
		POP		EBX
		POP		ESI
		POP		EDI
		RET
mts_fin:
		MOV		[EBX],EDX
		POP		EBX
		POP		ESI
		POP		EDI
		RET

【4】允许缓存

        禁止缓存的逆操作。

        最终打印出来结果如下,检查了系统32M的内存:

二 内存管理

        内存管理作者这边提到了几种方法:

【1】管理表:比如将一段125M的内存,按4K每块的形式管理,则一共分为32K块。建立一个32K的数组,用数组元素的0/1来代表每块内存是否使用。(优点:简单易理解缺点:随着总内存的增大,管理表的大小也随之增大

【2】改进管理表(用位的形式来存储):对上面的管理表进行优化,因为表示内存使用情况只需要0/1,用比特位来代替字节的表示,可以将内存管理表缩小到原来的1/8。(优点:简单易理解,相比于管理表,占用空间有所缩减缺点:编程麻烦一些,而且相对于下面第三种方法,占用的空间还是较大

【3】列表标签的形式管理:结构体记录使用标签,把类似于“从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 = 0x0040 0000; /* 从0x00400000号地址开始,有124MB可用 */
memman.free[0].size = 0x07c0 0000;

        为了简化开发,我们基于第三种方法,采用了一种“暂时割舍小块内存”的优化方法,只有当memman有空余的时候,才会对割舍掉内存进行检查找回。

        申请内存memman_alloc如下(该函数很简单,不再赘述):

#define MEMMAN_FREES		4090	// 大约是 32 KB
#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->losts = 0;            /* 释放失败的内存大小总和 */
	man->lostsize = 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;
}

/**
 * @return start address of the mem ; 0 if can't alloc
 */
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) {
				man->frees--;
				// del 1 INFO
				for (; i < man->frees; i++) {
					// 移位处理,余量充足
					man->free[i] = man->free[i + 1];
				}
			}
			return a;
		}
	}
	return 0;
}

        释放内存memman_free如下:

/**
 * @brief 向 addr 处分配 size 大小的内存空间,考虑到前后内存的合并
 * 
 * @param man 
 * @param addr 
 * @param size 
 * @return 0 if successful ; -1 if fails
 */
int memman_free(struct MEMMAN *man, unsigned int addr, unsigned int size) {
	int i, j;
	/* 假装 free[] addr 是按照顺序排列的 */

	// 寻找地址
	// free[i - 1].addr < addr < free[i].addr 
	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) {
			/* 【1】与前面的内存合并 */
			man->free[i - 1].size += size;
			/* 【2】后面有可用内存 */
			if (i < man->frees) {
				if (addr + size == man->free[i].addr) {
					// 与后面的合并
					man->free[i - 1].size += man->free[i].size;
					man->frees--;
					for (; i < man->frees; i++) {
						man->free[i] = man->free[i + 1];
					}
				}
			}
			return 0;
		}
	}
	/* 【3】后面有可用内存 */
	if (i < man->frees) {
		if (addr + size == man->free[i].addr) {
			// 与后面的合并
			man->free[i].addr = addr;
			man->free[i].size += size;
			return 0;
		}
	}
	/* 【4】既不能与前面归纳到一起,也不能与后面归纳到一起 */
	if (man->frees < MEMMAN_FREES) {
		// free[i] 之后的向后挪
		for (j = man->frees; j > i; j--) {
			man->free[j] = man->free[j - 1];
		}
		man->free[i].addr = addr;
		man->free[i].size = size;
		man->frees++;
		if (man->maxfrees > man->frees) {
			// 更新最大值
			man->maxfrees = man->frees;
		}
		return 0;
	}
	man->losts++;
	man->lostsize += size;
	return -1;
}

        这个memman_free略显复杂,这边做一个详细的讲解,上面代码注释中的【1-4】分别代表了内存释放的几种情况:

【1】与前面内存合并的情况:

【2】前后相接的情况:

【3】与后面内存合并的情况:

【4】与前后均不相接的情况:

 

        除了上面四种情形之外的其他情形,诸如重复free同一块内存(double free)、free越界等情况,均视为free失败,报错退出

        综上,在主函数中的实际操作如下:

#define MEMMAN_ADDR			0x003c0000
void HariMain(void)
{
    //(中略)
    unsigned int memtotal;
    struct MEMMAN *memman = (struct MEMMAN *) MEMMAN_ADDR;
    //(中略)
    memtotal = memtest(0x00400000, 0xbfffffff);
    memman_init(memman);
    memman_free(memman, 0x00001000, 0x0009e000); /* 0x00001000 - 0x0009efff */
    memman_free(memman, 0x00400000, memtotal - 0x00400000);
    //(中略)
    sprintf(s, "memory %dMB free : %dKB",
    memtotal / (1024 * 1024), memman_total(memman) / 1024);
    putfonts8_asc(binfo->vram, binfo->scrnx, 0, 32, COL8_FFFFFF, s);
}

        运行结果如下:

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值