《30天自制操作系统》笔记(07)——内存管理

《30天自制操作系统》笔记(07)——内存管理

进度回顾

上一篇中处理掉了绝大部分与CPU配置相关的东西。本篇介绍内存管理的思路和算法。

现在想想,从软件工程师的角度看,CPU也只是一个软件而已:它的功能就是加载指令、执行指令和响应中断,而响应中断也是在加载指令、执行指令。就像火车沿着一条环形铁轨前进;当中断发生时,就好像铁轨岔口处变轨了,火车就顺着另一条轨迹走了;走完之后又绕回来重新开始。决定CPU是否变轨的,就是CPU里的特定寄存器。

这是题外话,就此为止。

什么是内存管理

假设内存大小是128MB,应用程序A暂时需要100KB,画面控制需要1.2MB……。

像这样,操作系统有时要分配一定大小的内存,用完后又要收回。因此,必须管理好哪些内存空闲可用,哪些正在被占用。这就是内存管理。

内存管理的两个任务,一是内存分配,一是内存释放。

如何获取内存容量


检查内存容量的方法

要管理内存,首先得知道操作系统所在的这个计算机内存有多大。检查内存容量的方法很简单,就是从要检查的起始位置到最后位置依次写入一个数值(例如0xaa55aa55),然后按位反转,检查是否正确地完成了反转(变成0x55aa55aa);然后再次反转,检查是否正确地完成了反转。如果反转结果都是正确的,说明这个地址的内存是存在的;否则就说明内存的最大地址就到此为止了。其代码如下。


 1 unsigned int memtest_sub(unsigned int start, unsigned int end)
2 {
3 unsigned int i, *p, old, pat0 = 0xaa55aa55, pat1 = 0x55aa55aa;
4 for (i = start; i <= end; i += 0x1000) {
5 p = (unsigned int *) (i + 0xffc);
6 old = *p; /* 先记住修改前的值 */
7 *p = pat0; /* 试写 */
8 *p ^= 0xffffffff; /* 反转 */
9 if (*p != pat1) { /* 检查反转结果 */
10 not_memory:
11 *p = old;
12 break;
13 }
14 *p ^= 0xffffffff; /* 再次反转 */
15 if (*p != pat0) { /* 检查是否恢复 */
16 goto not_memory;
17 }
18 *p = old; /* 恢复为修改前的值 */
19 }
20 return i;
21 }

但直接用C语言来写这个函数的话,C编译器会把它优化成这个样子。


1 unsigned int memtest_sub(unsigned int start, unsigned int end)
2 {
3 unsigned int i, *p, old, pat0 = 0xaa55aa55, pat1 = 0x55aa55aa;
4 for (i = start; i <= end; i += 0x1000) {
5 //全部被优化掉 了
6 }
7 return i;
8 }

C编译器不会理睬"内存到头了"这种事情,它只在应用程序的层面看问题。所以它认为for循环里的if判定都是必然为真(或为假)的,认为没有被其它代码使用的变量都是没用的。所以它就干脆把这些"没用的"代码删掉了。

为了解决这个问题,还是用汇编语言来写这个memtest_sub函数好了。代码如下。

 1 _memtest_sub:    ; unsigned int memtest_sub(unsigned int start, unsigned int end)
2 PUSH EDI ; (由于还要使用EBX, ESI, EDI)
3 PUSH ESI
4 PUSH EBX
5 MOV ESI,0xaa55aa55 ; pat0 = 0xaa55aa55;
6 MOV EDI,0x55aa55aa ; pat1 = 0x55aa55aa;
7 MOV EAX,[ESP+12+4] ; i = start;
8 mts_loop:
9 MOV EBX,EAX
10 ADD EBX,0xffc ; p = i + 0xffc;
11 MOV EDX,[EBX] ; old = *p;
12 MOV [EBX],ESI ; *p = pat0;
13 XOR DWORD [EBX],0xffffffff ; *p ^= 0xffffffff;
14 CMP EDI,[EBX] ; if (*p != pat1) goto fin;
15 JNE mts_fin
16 XOR DWORD [EBX],0xffffffff ; *p ^= 0xffffffff;
17 CMP ESI,[EBX] ; if (*p != pat0) goto fin;
18 JNE mts_fin
19 MOV [EBX],EDX ; *p = old;
20 ADD EAX,0x1000 ; i += 0x1000;
21 CMP EAX,[ESP+12+8] ; if (i <= end) goto mts_loop;
22 JBE mts_loop
23 POP EBX
24 POP ESI
25 POP EDI
26 RET
27 mts_fin:
28 MOV [EBX],EDX ; *p = old;
29 POP EBX
30 POP ESI
31 POP EDI
32 RET

汇编版本的memtest_sub

知道了内存容量,就可以进行管理了。

关闭CPU高速缓存

486以上的CPU是有高速缓存(cache)的。CPU每次访问内存,都要将所访问的地址和内容存入cache,也就是存放成这样"18号地址的值是54"。如果下次要用18号地址的内容,CPU就不需要再读内存了(速度慢),而是直接从cache中取得18号地址的内容(速度快)。

如果开启着CPU高速缓存(cache),上述的检测代码就不会正常工作。因为写入一个内存地址,然后立即读出,这样的操作符合cache到的情况,CPU不会从内存读,而是直接读cache到的东西。结果,所有的内存都"正常",检测代码就起不到作用了。


 1 #define EFLAGS_AC_BIT        0x00040000
2 #define CR0_CACHE_DISABLE 0x60000000
3
4 unsigned int memtest(unsigned int start, unsigned int end)
5 {
6 char flg486 = 0;
7 unsigned int eflg, cr0, i;
8
9 /* 确认CPU是386还是486以上的 */
10 eflg = io_load_eflags();
11 eflg |= EFLAGS_AC_BIT; /* AC-bit = 1 */
12 io_store_eflags(eflg);
13 eflg = io_load_eflags();
14 if ((eflg & EFLAGS_AC_BIT) != 0) { /* 如果是386,即使设定AC=1,AC的值还会自动回到0 */
15 flg486 = 1;
16 }
17 eflg &= ~EFLAGS_AC_BIT; /* AC-bit = 0 */
18 io_store_eflags(eflg);
19
20 if (flg486 != 0) {
21 cr0 = load_cr0();
22 cr0 |= CR0_CACHE_DISABLE; /* 禁止缓存 */
23 store_cr0(cr0);
24 }
25
26 i = memtest_sub(start, end);
27
28 if (flg486 != 0) {
29 cr0 = load_cr0();
30 cr0 &= ~CR0_CACHE_DISABLE; /* 允许缓存 */
31 store_cr0(cr0);
32 }
33
34 return i;
35 }

内存管理算法

假设内存有128MB(0x08000000字节),以4KB(0x1000字节)为单位进行管理。

最简单的方法

128MB/4KB=32768。所以我们创建32768字节的区域,写入0表示对应的内存是空闲的,写入1表示对应的内存是正在使用的。这个方法的名字我没有查到。


1 char a[32768];
2 for (i = 0; i < 1024; i++) {
3 a[i] = 1; //一直到4MB为止,标记为正在使用(BIOS、OS等)
4 }
5 for (i = 1024; i< 32768; i++) {
6 a[i] = 0; //剩下的全部标记为空闲
7 }

比如需要100KB的内存,那么只要从a中找出连续25个标记为0的地方就可以了。


 1 //从a[j]到a[j + 24]为止,标记连续为0";
2 j = 0;
3 再来一次:
4 for (i = 0; i < 25; i++) {
5 if (a[j + i] != 0) {
6 j++;
7 if (j < 32768 - 25) goto 再来一次;
8 "没有可用内存了";
9 }
10 }
11 //从j * 0x1000开始的100KB空间得到分配
12 for (i = 0; i < 25; i++) {
13 a[j + i] = 1;
14 }

需要收回这100KB的时候,用地址值/0x1000,计算出j就可以了。

1 j = 0x00123000 / 0x1000;
2 for (i = 0; i < 25; i++) {
3 a[j + i] = 0;
4 }

当然,我们可以用1bit来代替1个char,这样所需的管理空间就可以省下7/8,使用方法则是一样的。

列表管理

把类似于"从xxx号地址开始的yyy字节的空间是空闲的"这种信息都存储到列表里。


 1 struct FREEINFO {    /* 可用状况 */
2 unsigned int addr, size;
3 };
4
5 struct MEMMAN { /* 内存管理 */
6 int frees, maxfrees, lostsize, losts;
7 struct FREEINFO free[MEMMAN_FREES];
8 };
9 struct MEMMAN memman;
10 memman.frees = 1;//可用状况list中只有1件
11 memman.free[0].addr = 0x00400000;//从0x00400000号地址开始
12 memman.free[0].size = 0x07c00000;//有124MB可用

比如,如果需要100KB的内存,只要查看memman中free的状况,从中找到100MB以上的可用空间就行了。


1 for (i = 0; i < memman.frees; i++) {
2 if (memman.free[i].size >= 100 * 1024) {
3 //找到可用空间
4 memman.free[i].addr += 100 * 1024;
5 memman.free[i].size -= 100 * 1024;
6 }
7 }

释放内存时,增加1条可用信息,frees加1。而且还要看看新释放出的内存与相邻的内存能不能连到一起,如果可以,就要归为1条。

与上文的最简单的方法相比,这种列表管理的方法,占用内存少,且内存的申请和释放更迅速。

缺点是释放内存的代码比较复杂。另外,如果内存变成零零碎碎的,那么需要的MEMMAN里的数组就会超过1000,这是个问题。如果真发生这种情况,只能将一部分零碎的空闲内存都视作被占用的,然后合并。


  1 void memman_init(struct MEMMAN *man)
2 {
3 man->frees = 0; /* 可用信息数目 */
4 man->maxfrees = 0; /* 用于观察可用状况:frees的最大值 */
5 man->lostsize = 0; /* 释放失败的内存的大小总和 */
6 man->losts = 0; /* 释放失败次数 */
7 return;
8 }
9
10 unsigned int memman_total(struct MEMMAN *man)
11 /* 报告空余内存大小的合计 */
12 {
13 unsigned int i, t = 0;
14 for (i = 0; i < man->frees; i++) {
15 t += man->free[i].size;
16 }
17 return t;
18 }
19
20 unsigned int memman_alloc(struct MEMMAN *man, unsigned int size)
21 /* 分配 */
22 {
23 unsigned int i, a;
24 for (i = 0; i < man->frees; i++) {
25 if (man->free[i].size >= size) {
26 /* 找到了足够大的内存 */
27 a = man->free[i].addr;
28 man->free[i].addr += size;
29 man->free[i].size -= size;
30 if (man->free[i].size == 0) {
31 /* 如果是free[i]变成了0,就减掉一条可用信息 */
32 man->frees--;
33 for (; i < man->frees; i++) {
34 man->free[i] = man->free[i + 1]; /* 代入结构体 */
35 }
36 }
37 return a;
38 }
39 }
40 return 0; /* 没有可用空间 */
41 }
42
43 int memman_free(struct MEMMAN *man, unsigned int addr, unsigned int size)
44 /* 释放 */
45 {
46 int i, j;
47 /* 为便于归纳内存,将free[]按照addr的顺序排列 */
48 /* 所以,先决定应该放在哪里 */
49 for (i = 0; i < man->frees; i++) {
50 if (man->free[i].addr > addr) {
51 break;
52 }
53 }
54 /* free[i - 1].addr < addr < free[i].addr */
55 if (i > 0) {
56 /* 前面有可用内存 */
57 if (man->free[i - 1].addr + man->free[i - 1].size == addr) {
58 /* 可用与前面的可用内存归纳到一起 */
59 man->free[i - 1].size += size;
60 if (i < man->frees) {
61 /* 后面也有 */
62 if (addr + size == man->free[i].addr) {
63 /* 也可以与后面的可用内存归纳到一起 */
64 man->free[i - 1].size += man->free[i].size;
65 /* man->free[i]删除 */
66 /* free[i]变成0后归纳到前面去 */
67 man->frees--;
68 for (; i < man->frees; i++) {
69 man->free[i] = man->free[i + 1]; /* 结构体赋值 */
70 }
71 }
72 }
73 return 0; /* 成功完成 */
74 }
75 }
76 /* 不能与前面的可用空间归纳到一起 */
77 if (i < man->frees) {
78 /* 后面还有 */
79 if (addr + size == man->free[i].addr) {
80 /* 可用与后面的内容归纳到一起 */
81 man->free[i].addr = addr;
82 man->free[i].size += size;
83 return 0; /* 成功完成 */
84 }
85 }
86 /* 既不能与前面归纳到一起,也不能与后面归纳到一起 */
87 if (man->frees < MEMMAN_FREES) {
88 /* free[i]之后的,向后移动 */
89 for (j = man->frees; j > i; j--) {
90 man->free[j] = man->free[j - 1];
91 }
92 man->frees++;
93 if (man->maxfrees < man->frees) {
94 man->maxfrees = man->frees; /* 更新最大值 */
95 }
96 man->free[i].addr = addr;
97 man->free[i].size = size;
98 return 0; /* 成功完成 */
99 }
100 /* 不能往后移动 */
101 man->losts++;
102 man->lostsize += size;
103 return -1; /* 失败 */
104 }

总结


内存管理要结合GDT的设定进行。按段(Segment)设计的GDT,内存就得按段申请和回收。按页设计的GDT,额我不知道,以后再说。

内存管理需要的预备知识还包括"获取内存容量"、"禁止/启用高速缓存"、"数据结构-链表"。

内存管理的算法还有很多,本篇只给出了两种最基本最简单的,够做个简易的OS就行了,现在不是深究算法的时候。

时间: 2024-10-13 22:51:40

《30天自制操作系统》笔记(07)——内存管理的相关文章

30天自制操作系统笔记(第四天)

这一节讲的最出彩的地方是c语言的地址. 而要理清c语言地址,又必须追根溯源,看看汇编里内存地址的使用. MOV AL,0X15 MOV [1024],AL MOV BYTE[1024],0X15 这两种指令效果相同,都是在这个内存地址里存入一个数据,而学过汇编的我们知道,直接往内存某地址存入数据时,要说明填入的数据大小,或者说数据类型,不然机器不知道怎么填入该数据,到底是按照8位填入,还是十六位填入.因此,这个byte必不可少,而前面的指令,由于AL已经明确了是八位,因此不用说明. 好了,接下来

《30天自制操作系统》笔记(06)——CPU的32位模式

<30天自制操作系统>笔记(06)--CPU的32位模式 进度回顾 上一篇中实现了启用鼠标.键盘的功能.屏幕上会显示出用户按键.点击鼠标的情况.这是通过设置硬件的中断函数实现的,可以说硬件本身的设计就具有事件驱动的性质,所以软件层面上才有基于事件的消息机制. 但上一篇没有说明中断的来龙去脉,本篇就从头到尾描述一下CPU与此相关的设置问题. Segment 32位的CPU使用32条地址线,能区分232=4G个内存地址.每个内存地址都有1Byte的内容. 分段,就是将4GB的内存分成很多块(blo

内存管理(30天自制操作系统--读书笔记)

今天继续读书笔记,“挑战内存管理”(30天自制操作系统). 为什么对这块内容敢兴趣呢,因为曾经遇到这么一个问题.在STM32程序中想使用队列,可不是上篇讲的FIFO,而是使用了较大的内存空间,又想做队列的顺序存取管理. 在这个队列里用到了malloc,动态申请内存,一开始是直接申请不到内存,后来在启动脚本里更改了设置堆的地址值,可以申请成功,但发现申请几次后,也申请不到内存. 果然MCU级别的程序,内存这块处理起来就没有windows程序那么随心所欲了.讲了这么多,开始正题吧. 1.相关数据结构

多定时器处理1(30天自制操作系统--读书笔记)

自认为写过很多MCU程序,但总是回头想想,我所了解的MCU编程思想大体有两种,其中具体的想法我得再找时间写下来. 总想总结出一个可扩展的,易移植的写法,但能力还没到这个层次.但<30天自制操作系统>这本书确实给我了一个思路,就像我已经写过的两篇读书笔记. 将两个独立的内容--FIFO和内存动态管理做到高度模块化,尤其是其中数据结构模型的设计更是我学习的好例子. 今天要学习的设计内容是多定时器处理.原书对这部分的处理讲的很详细,由浅入深,看得我由衷佩服作者,也可能是因为我水平低,稍稍看出点门道来

《30天自制操作系统》笔记(08)——叠加窗口刷新

<30天自制操作系统>笔记(08)--叠加窗口刷新 进度回顾 上一篇中介绍了内存管理的思路和算法,我们已经可以动态申请和释放内存了.这不就是堆(Heap)么.在此基础上,本篇要做一段程序,一并解决窗口和鼠标的叠加处理问题. 问题 在之前的<<30天自制操作系统>笔记(05)--启用鼠标键盘>篇,已经能够移动鼠标了.但是遗留了如下图所示的一个小问题. 我们希望的情形是这样的: 实际上,当前版本的OS还没有窗口图层的东西.本篇要做一段程序,一并解决窗口和鼠标的叠加处理问题.

《30天自制操作系统》笔记(04)——显示器256色

<30天自制操作系统>笔记(04)--显示器256色 进度回顾 从最开始的(01)篇到上一篇为止,已经解决了开发环境问题和OS项目的顶层设计问题. 本篇做一个小练习:设置显卡显示256色. 原理 设置显卡模式 调用BIOS中断命令INT 0x10,设置显卡模式为VGA图形模式,320*200*8位彩色模式,调色板模式.代码如下. 1 MOV AL,0x13 ; VGA图形模式,320*200*8位彩色模式 2 MOV AH,0x00 3 INT 0x10 设置调色板 256色的调色板是这样一个

《30天自制操作系统》笔记(09)——绘制窗口

<30天自制操作系统>笔记(09)--绘制窗口 进度回顾 上一篇中介绍了图层式窗口管理的思路和算法.在此基础上,本篇就解决绘制窗口及其简单的优化问题. 这里稍微吐槽一下<30天自制操作系统>原作者.全书我刚刚看了三分之一,写得确实不错,但是我能感受到原作者是习惯用汇编语言和汇编思维来写程序的.虽然书里尽量使用了C语言,但给我一种用C写汇编的感觉.也可能是原作者故意简化了OS开发过程,方便初学者理解吧.幸好这在我预料之中,一开始我就打算先看完这本书然后自己再从零设计OS的. 看似立体

《30天自制操作系统》读书笔记(5) GDT&amp;IDT

梳理项目结构 项目做到现在, 前头的好多东西都忘了, 还是通过Makefile重新理解一下整个项目是如何编译的: 现在我们拥有这么9个文件: ipl10.nas    InitialProgramLoader, 占用了软盘的第一个扇区并符合启动盘的规范, 默认被载入地址是0x7c00 到 0x7e00, 负责将10个柱面读入到0x8200到0x34fff (10个柱面共10*2*18 = 360 个扇区但是第一个没有被读入); asmhead.nas     包含一些暂时未知的设定; naskf

单字节的FIFO缓存(30天自制操作系统--读书笔记)

从今天起,写一些读书笔记.最近几个月都在看<30天自制操作系统这本书>,书虽说看的是电子书,但可以花钱买的正版书,既然花费了金钱,就总得有些收获. 任何人都不能总是固步自封,想要进步就得学习别人的知识,对于程序员而言,最简单的方法即是学习别人的代码. 今天的标题是“单字节的FIFO缓存”,其实就是做一个FIFO,看名字就知道了.也就4个函数和1个相关结构体,这样的小代码在嵌入式系统中很常用,也会很好用. 1.相关数据结构体 struct FIFO8 { unsigned char *buf;