本节对应分支:memory

概述

操作系统内存管理可以说是操作系统中最重要的部分 ,它是未来所有工作的基础。内存管理相当复杂,大约有如下内容:
操作系统—内存管理

但本节并不会讨论以上全部内容,而是根据我们自制操作系统的需要来进行。我们当前的任务是完成操作系统的内存划分(本节)以及虚拟内存的申请(下节),即虚拟空间到物理内存的映射,其他内容咋们后续按需补充。本节内容如下:

  1. 通过 BIOS 中断获取内存容量
    既然要分配内存,就一定需要知道系统的内存容量有多大,这通过 BIOS 中断来获取。
  2. 通过位图来管理内存
    管理内存时,肯定需要知道哪些内存已经被使用,哪些还没有使用,这些信息通过我们自己维护的位图来获取。
  3. 规划内存池
    管理内存前,当然还需要对内存做出规划,比如,哪些内存给内核使用,哪些内存又给用户使用。
  4. 向页表填写映射关系
    我们早就实现了分页机制,就差向其中填入映射关系啦!笔者期待已久,让我们开始吧。

获取内存容量

获取内存容量的例程已经由操作系统厂商写好并存入了 BIOS 中,因此我们只需要调用 BIOS 中断即可。现在问题是,进入保护模式后,BIOS 中断无法再被调用,这怎么办呢?不得已,我们只能回到 loader.s 中,即进入保护模式之前调用 BIOS 中断

为什么进入保护模式后不能再使用 BIOS 中断

  1. BIOS 中断例程的地址存放在中断向量表(IVT)中,实模式下使用 int 指令调用中断时,会跳转到 IVT 描述符指向的例程地址;而保护模式下使用 int 指令调用中断时,则是跳转到 IDT 描述符所指向的例程。因此,IVT 不再有效。
  2. BIOS 中断例程是在实模式,即 16 位模式下运行的代码,这些代码并不能直接运行在 32 位保护模式下。

Linux 采用了三种方式来检测内存容量,如果一种方式失败,就调用下一种,全部失败则挂起。这三种方式都是通过调用 0x15 号 BIOS 中断来进行的,它们的功能号及其特点为:

  • EAX = 0xE820 :遍历主机所有内存。
  • AX = 0xE801 :最大支持 4GB 。
  • AH = 0x88 :最大支持 64 MB 内存。

功能号需要装在 EAX 或 AX 中。

由于咋们的操作系统最大不会超过 100 KB,因此我们只使用第三种方式,即 0x88 功能号。因为该部分只需要调用中断,没有其他需要强调或理解的地方,所以此处笔者就不详细记录这三个功能号的中断参数和用途了,详情还请读者朋友移步《操作系统真相还原》第 177 页。下面只贴出我们要用到的 0x88 功能号:

需要注意两点:

  • 0x88 功能号返回的内存不包括低端 1 MB,因此我们算总内存时还需要加上 1MB 。
  • 返回后 AX 中的值以 1KB 为单位,所以还需要换算成以 1 字节为单位。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
total_mem_bytes dd 0
;----------------- int 15h ah = 0x88 获取内存大小,只能获取64M之内 ----------
.e801_failed_so_try88:
;int 15后,ax存入的是以kb为单位的内存容量
mov ah, 0x88
int 0x15
jc .error_hlt ;CF为0则跳转
and eax,0x0000FFFF;加上1024,即低端的1MB(1024KB)

;16位乘法,被乘数是ax,积为32位.积的高16位在dx中,积的低16位在ax中
mov cx, 0x400 ;0x400等于1024,将ax中的内存容量换为以byte为单位
mul cx
shl edx, 16 ;把dx移到高16位
or edx, eax ;把积的低16位组合到edx,为32位的积
add edx,0x100000 ;0x88子功能只会返回1MB以上的内存,故实际内存大小要加上1MB

.mem_get_ok:
mov [total_mem_bytes], edx ;将内存换为byte单位后存入total_mem_bytes处。
jmp prepare ;跳转,准备进入保护模式

.error_hlt: ;出错则挂起
hlt

代码逻辑很简单,注释也足够清晰,不再赘述。需要强调的是,标号 total_mem_bytes 用来存放所得结果,此结果待会会在 memory.c 中使用,因此,我们还得手动算出该标号所代表的地址,以方便在 C 文件中通过指针引用该值。有读者可能会疑惑了,为什么还得手动算地址呢?难道不能像我们之前那样,使用 global 关键字导出 total_mem_bytes ,然后在 C 文件中声明 extern total_mem_bytes 来直接引用这个变量吗?是的,不能。原因在于,我们链接时并没有将 loader.o 包含进来,看下面的 makefile 语句:

1
2
3
4
KERNEL=build/guide.o  build/print.o  build/main.o build/interrupt.o build/idt.o build/port_io.o \
build/timer.o build/intrmgr.o build/debug.o build/string.o build/memory.o build/bitmap.o \
$(BUILD)/kernel.bin: $(KERNEL)
ld -m elf_i386 $^ -o $@ -Ttext 0x00001500

其中并没有包含 loader.s 。我猜到你要说什么了:那就包含 loader.s 呗…昂,kernel.bin 可是咋们的内核呀,现在又将 loader 包含进去,可谓不伦不类啦。所以,我们需要手动算出 total_mem_bytes 的地址值,它的位置如下:

1
2
3
4
5
6
7
8
9
SECTION loader vstart=BASE_ADDR              ;定义用户程序头部段
program_length dd program_end ;程序总长度[0x00]
;用户程序入口点
code_entry dw start-BASE_ADDR ;偏移地址[0x04]
dd section.loader.start ;段地址[0x06]
realloc_tbl_len dw 0 ;段重定位表项个数为0
;=========================================================
;检测出的总内存大小,位于0x90c处.前面一共0xc,即12字节
total_mem_bytes dd 0
1
2
3
;BASE_ADDR=0X900
;dd=4bytes, dw=2bytes
BASE_ADDR+4+2+4+2=0x90c

因此,total_mem_bytes 的地址为 0x90c 。如此,咋们就轻松获取了内存容量的大小,为 32MB,即 0x2000000

位图

位图并不止用于内存管理,它是一种映射,用一个位来表示一个对象的状态 。在之前我们也接触过位图,比如 8259A 中的各个寄存器就是关于 IRQ 的位图。现在,我们要使用位图来管理内存,即用一个位来表示某片内存的状态(是否已经使用)。问题是,一个位应该映射成多大的一片内存呢?通过前面内存分页的学习,我们知道内存是按页来分隔的,一页的大小是 4KB,出于这个原因,我们将一个位映射为 4KB 内存,即一个位管理一页内存。如果某位为 1,则表示对应的页已经被使用;为 0 则表示该页为空闲状态,可以使用

位图结构体的声明如下:

1
2
3
4
5
struct bitmap {
uint32_t btmp_bytes_len;
/* 在遍历位图时,整体上以字节为单位,细节上是以位为单位,所以此处位图的指针必须是单字节 */
uint8_t* bits;
};
  • btmp_bytes_len 表示位图的长度,包含的总位数为 btmp_bytes_len*32 。该值由位图所管理的内存大小决定。
  • bits 为位图的指针。位图也当然是存放在内存中的,所以我们用 bits 指针来记录位图的起始地址

需要注意,虽然 bitsuint8_t* 型的指针,步长为 1 字节,但实际操作时我们会细化为按位处理,即通过掩码来判断相应位是否为 1 。
位图的操作函数有如下几个:

1
2
3
4
void bitmap_init(struct bitmap* btmp);
bool bitmap_scan_test(struct bitmap* btmp, uint32_t bit_idx);
void bitmap_set(struct bitmap* btmp, uint32_t bit_idx, int8_t value);
int bitmap_apply(struct bitmap* btmp, uint32_t cnt);
  • bitmap_init :用来初始化位图,根据传入的 btmp 参数来决定将哪片内存视为位图,并将其初始化为 0
  • bitmap_scan_test :用来检测位图中第 bit_idx 位是否为 1,该函数只在 bitmap_apply 中调用。
  • bitmap_set :用来将位图中的第 bit_idx 位赋值为 0/1 。
  • bitmap_apply :在位图中申请连续 cnt 个位,若成功则返回起始位的索引(下标),失败则返回 -1 。

实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
//bitmap.c
#define BITMAP_MASK 7
void bitmap_init(struct bitmap* btmp)
{
memset(btmp->bits, 0, btmp->btmp_bytes_len);
}

/* 判断bit_idx位是否为1,若为1则返回true,否则返回false */
bool bitmap_scan_test(struct bitmap* btmp, uint32_t bit_idx)
{
uint32_t byte_idx = bit_idx / 8; // 向下取整,取得该位所在字节
uint32_t bit_odd = bit_idx % 8; // 取余,取得该位在此字节中的位置
return (btmp->bits[byte_idx] & (BITMAP_MASK >> bit_odd));
}

/* 在位图中申请连续cnt个位,成功则返回其起始位下标,失败返回-1 */
int bitmap_apply(struct bitmap* btmp, uint32_t cnt)
{
uint32_t idx_byte = 0; // 用于记录空闲位所在的字节
/* 先逐字节比较,蛮力法 */
while (( 0xff == btmp->bits[idx_byte]) && (idx_byte < btmp->btmp_bytes_len))
{
/* 1表示该位已分配,所以若为0xff,则表示该字节内已无空闲位,向下一字节继续找 */
idx_byte++;
}

assert(idx_byte < btmp->btmp_bytes_len);
if (idx_byte == btmp->btmp_bytes_len)
{ // 若该内存池找不到可用空间
return -1;
}

/* 若在位图数组范围内的某字节内找到了空闲位,
* 在该字节内逐位比对,返回空闲位的索引。*/
int idx_bit = 0;
/* 和btmp->bits[idx_byte]这个字节逐位对比 */
while ((uint8_t)(BITMAP_MASK >> idx_bit) & btmp->bits[idx_byte])
idx_bit++;

int bit_idx_start = idx_byte * 8 + idx_bit; // 空闲位在位图内的下标
if (cnt == 1)
return bit_idx_start;

uint32_t bit_left = (btmp->btmp_bytes_len * 8 - bit_idx_start); // 记录还有多少位可以判断
uint32_t next_bit = bit_idx_start + 1;
uint32_t count = 1; // 用于记录找到的空闲位的个数

bit_idx_start = -1; // 先将其置为-1,若找不到连续的位就直接返回
while ((bit_left--) > 0)
{
if (!(bitmap_scan_test(btmp, next_bit))) // 若next_bit为0
count++;
else
count = 0;
if (count == cnt) //若找到连续的cnt个空位
{
bit_idx_start = next_bit - cnt + 1;
break;
}
next_bit++;
}
return bit_idx_start;
}

/* 将位图btmp的bit_idx位设置为value */
void bitmap_set(struct bitmap* btmp, uint32_t bit_idx, int8_t value)
{
assert(value == 1);
assert(value == 0);
uint32_t byte_idx = bit_idx / 8; // 向下取整,取得该位所在字节
uint32_t bit_odd = bit_idx % 8; // 取余,取得该位在此字节中的位置

/* 将1任意移动后再取反,或者先取反再移位,可用来对位置0操作。*/
if (value) // 如果value为1
btmp->bits[byte_idx] |= (BITMAP_MASK >> bit_odd);
else // 若为0
btmp->bits[byte_idx] &= ~(BITMAP_MASK >> bit_odd);
}

注释较清晰,下面挑重点解释:

  • 第 13 行,重点理解这个按位检测,逻辑也比较简单。需要说明的是,《真相还原》中的原代码是:

    1
    2
    3
    4
    #define BITMAP_MASK 1
    //.......
    return (btmp->bits[byte_idx] & (BITMAP_MASK << bit_odd));
    //.......

    这和我们的代码效果是相同的,笔者之所以作如此改动,是因为改动之后的逻辑更符合我们直觉,即,位是按顺序排列的:

    而原代码的逻辑是:

  • 第 41 行,为什么要把 cnt==1 的情况单独拿出来呢?因为后面我们申请物理内存时,物理内存池中的页可以不连续,所以传参时 cnt=1;申请虚拟内存时,必须连续,所以 cnt 不必为 1 。由于会大量用到 cnt=1 的情况,所以单独拿出来,避免再做后续处理以提高效率。

    没懂上述解释的同学不用慌,下节内存管理进阶我们还会说到这点。

  • 第 54 行,每当连续的位被断开时,cnt 就需要清零,因为函数要求的是找出连续的 cnt 个位。

位图的介绍就是这些,下面我们用位图来规划内存。

值得一提的是,位图只是内存管理的一种方法,其他常用的方式还有链表,参见空闲内存管理,位图,空闲链表-CSDN

规划内存池

什么是内存池?

说得高深一点,内存池是内存集合的抽象;说白了,内存池就是上面所说的用来管理内存的位图。所以,内存池的职责就是管理内存,需要内存时,从内存池(位图)中申请;回收内存时,则归还内存池。

可以将位图理解成内存池的物理形式。既然将内存池等同于位图,就说明内存池的存取粒度和位图一样,都是以 4KB 为单位

如何规划内存池
规划内存池,分为两个大的方向:

  1. 物理内存和虚拟内存的规划
  2. 用户内存和内核内存的规划

这两者必须结合在一起讨论。我们已经知道,虚拟内存这个概念,其本身是针对于进程而言的,每个进程都有 4GB 的虚拟内存,其中一部分虚拟地址会映射到物理内存。那么,我们就不得不考虑如下两点:

  • 这么多进程都有各自的虚拟空间,它们都会争用物理内存,所以操作系统必须知道哪些物理内存被用了,哪些还未被使用 ,因此,我们需要建立物理内存池,以管理物理内存的使用情况。

  • 虽然每个进程都有自己的虚拟 4GB 空间,但在进程内部,虚拟内存也不能重复使用,即,虚拟地址在进程内是唯一的 。同样,为了管理虚拟地址的使用情况,我们需要建立虚拟内存池。

    注意,虚拟内存在进程是唯一的,但多个进程之间,可以使用相同的虚拟地址,各自的虚拟地址对外是不可见的,相互独立的!

除此之外,我们还需要将物理内存分为内核内存和用户内存,这是基于以下两点原因:

  • 操作系统会占用较多内存,毕竟它是其他用户进程的载体,不仅要引导用户程序的运行,还要负责任务调度,内存开辟等诸多重要任务。
  • 为了内核的正常运行,不能用户申请多少内存就给多少,否则有可能因为物理内存不足而导致内核自己都不能正常运行

因此,咋们专门分出内核的专属内存,其他物理内存则划给用户。

综上所述,我们最后划出三个内存池:1)内核物理内存池;2)内核虚拟内存池;3)用户物理内存池
有了前两者,当内核申请内存时,便会先从内核虚拟地址池中申请发配虚拟地址,接着从内核物理地址池中申请分配物理地址,最后在内核自己的页表中建立虚拟地址到物理地址的映射关系 。这个过程我们很快就会用代码展现出来。接着,你一定会问,为什么只有用户物理内存池,而没有用户虚拟内存池呢?是这样的,用户物理内存池是供所有用户进程使用的,用户共享这一片物理内存池;而 用户虚拟内存池则是每个用户进程私有的,当创建用户进程时,也会在其内部开辟虚拟池给自己使用 。用户虚拟内存池将在我们实现用户进程时介绍。

规划细节
话不多说,先放出本操作系统的具体内存安排:

下面对以上内存规划进行阐述:

  • 低端 1 MB 完全给内核使用,内核的代码只运行在这 1MB 内,且低端 1MB 物理内存和虚拟内存是一一映射的。这点在开启分页-代码详解中讲解过。

  • 为什么将这三个内存池位图放在低端 1MB 内呢?因为 低端 1MB 是不会放入内存池中的 ,这 1MB 空间相当于是上帝视角,不受内存管理约束,原因很简单——它自己就是管理者。内存池用于管理其他内存,而不用关心自己所在的内存,否则就是自己管理自己啦,这么说来,将自己所在内存的对应位置 1,岂不是相当于自杀了?哈哈哈哈开个玩笑,原因大概就是如此。

  • PCB (Process Control Block, 进程控制块),用来管理进程,每个进程的信息(pid、进程状态等)都保存在各自的 PCB 内。关于 PCB 的详细内容会在后面讲线程时提到,现在读者只需记住两点:

    • PCB 需要用一个自然页存放 ,即 PCB 的起始地址必须为 0xXXXXX000,结尾必须是 0xXXXXXfff 。

      这就是为什么 0x9f000~0x9fbff 未使用的原因——为了凑一个自然页。这么说大家可能还不清楚什么叫做未使用,放一张图各位就知道了:

      原本 0x7E00~0x9FBFF 都是空闲区域,咋们大可以将 PCB 放在 0x9E000~0x9FBFF 处,但无奈 PCB 只能占用一个自然页,所以 0x9F000~0x9FBFF 只能被废弃。

    • PCB 的最高处 0xXXXXXfff 以下用于进程/线程在 ring0 下使用的栈 。为什么将栈放置于 PCB 内以及为什么只用于 ring0,这会在后面实现线程时详细阐述。现在读者仍只需知晓,我们的内核代码本身就是一个线程(主线程,或者说单线程进程),所以它也有自己的 PCB ,没错,就是上上图的那个 PCB。因此,在进入内核前(guide.s),我们会将 esp 指向 PCB 的顶端(栈底),即 mov esp,0x9f000

      先向读者透露一下,PCB 中的栈与线程切换息息相关,可以说,线程切换就是通过栈操作来进行的。

  • 注意,在开启分页中,我们将页目录表和页表放在了 1MB 地址之上,刚好占用了 1MB~2MB 地址。这里的目录表和页表是供内核进程使用的,已经被占用所以这部分内存也不能划入用户/内核物理内存池

  • 除开低 2MB 的内存外,剩下的 30MB 物理内存平均(各自15MB)分给用户/内核物理内存池 。注意,内核物理内存池位图管理 15MB 物理内存(kernel_pool),所以内核虚拟内存池位图也管理 15MB 虚拟内存;用户物理内存池位图虽然管理 15MB 物理内存(user_pool),用户虚拟内存池位图(位于用户进程中)却管理 4GB 虚拟内存。

  • 注意,虽然图示中 0x9a000~0x9e000 用来存放这三个位图,但实际上并没有完全放满。因为我们的操作系统目前就 32MB,压根用不了这么多,这个位图空间我们只用了一小部分(三个位图一共才占 0x5a0 字节),其他剩余空间是预留的,以便于未来扩展此操作系统。

内存规划代码剖析

1
2
3
4
5
6
7
8
9
10
//memory.h
struct virtual_addr {
struct bitmap vaddr_bitmap; // 内核虚拟内存池用到的位图结构
uint32_t vaddr_start; // 内核虚拟起始地址
};
struct pool {
struct bitmap pool_bitmap; // 内核/用户物理内存池用到的位图结构
uint32_t phy_addr_start; // 内存池所管理物理内存的起始地址
uint32_t pool_size; // 内存池字节容量
};
  • virtual_addr 结构体目前仅用于内核虚拟内存池,后期还会用该结构管理用户虚拟内存池。
  • pool 结构体用于内核与用户物理内存池的管理。为什么 poolvirtual_addr 多一个 pool_size 成员呢?这是因为,物理内存是很有限的(本OS为32MB),虽然虚拟地址最大为 4GB,但相对而言却是无限的,因此 virtual_addr 无需记录容量。
  • 至于为什么还需要指定起始地址,阅读下面的代码后你就会彻底明白。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
//memory.c
#define PG_SIZE 4096
/*************** 位图地址 ********************
* 因为0xc009f000是内核主线程栈顶,0xc009e000是内核主线程的pcb.
* 一个页框大小的位图可表示128M内存, 位图位置安排在地址0xc009a000,
* 这样本系统最大支持4个页框的位图,即512M */
#define MEM_BITMAP_BASE 0xc009a000

/* 0xc0000000是内核从虚拟地址3G起. 0x100000意指跨过低端1M内存,使虚拟地址在逻辑上连续 */
#define K_HEAP_START 0xc0100000

#define MEM_SIZE_ADDR 0x90c

struct pool kernel_pool, user_pool; // 生成内核内存池和用户内存池
struct virtual_addr kernel_vaddr; // 此结构是用来给内核分配虚拟地址
/* 初始化内存池 */
static void mem_pool_init(uint32_t all_mem) {
put_str("\nmem_pool_init start...\n",DEFUALT);
uint32_t page_table_size = PG_SIZE * 256; // 为什么乘256,详见下文解析
uint32_t used_mem = page_table_size + 0x100000; // 低端1M内存+页表/目录表都不算入内存池
uint32_t free_mem = all_mem - used_mem;
uint16_t all_free_pages = free_mem / PG_SIZE; // 1页为4k,不管总内存是不是4k的倍数,
// 对于以页为单位的内存分配策略,不足1页的内存不用考虑了。
uint16_t kernel_free_pages = all_free_pages / 2; // 平均分给内核物理内存池和用户物理内存池
uint16_t user_free_pages = all_free_pages - kernel_free_pages;

/* 为简化位图操作,余数不处理,坏处是这样做会丢内存。
好处是不用做内存的越界检查,因为位图表示的内存少于实际物理内存*/
uint32_t kbm_length = kernel_free_pages / 8; // Kernel BitMap的长度,位图中的一位表示一页,以字节为单位
uint32_t ubm_length = user_free_pages / 8; // User BitMap的长度.

uint32_t kp_start = used_mem; // Kernel Pool start,内核内存池的起始地址
uint32_t up_start = kp_start + kernel_free_pages * PG_SIZE; // User Pool start,用户内存池的起始地址

kernel_pool.phy_addr_start = kp_start;
user_pool.phy_addr_start = up_start;

kernel_pool.pool_size = PG_SIZE * kernel_free_pages;
user_pool.pool_size = PG_SIZE * user_free_pages ;

kernel_pool.pool_bitmap.btmp_bytes_len = kbm_length;
user_pool.pool_bitmap.btmp_bytes_len = ubm_length;

/********* 内核内存池和用户内存池位图 ***********
* 位图是全局的数据,长度不固定。
* 全局或静态的数组需要在编译时知道其长度,
* 而我们需要根据总内存大小算出需要多少字节。
* 所以改为指定一块内存来生成位图.
* ************************************************/
// 内核使用的最高地址是0xc009f000,这是主线程的栈地址.(内核的大小预计为70K左右)
// 32M内存占用的位图是2k.内核内存池的位图先定在MEM_BITMAP_BASE(0xc009a000)处.
kernel_pool.pool_bitmap.bits = (void*)MEM_BITMAP_BASE;

/* 用户内存池的位图紧跟在内核内存池位图之后 */
user_pool.pool_bitmap.bits = (void*)(MEM_BITMAP_BASE + kbm_length);
/******************** 输出内存池信息 **********************/
put_str("\nkernel_pool_bitmap_start: ",DEFUALT);
put_uint((uint32_t)kernel_pool.pool_bitmap.bits,DEFUALT,HEX);
put_str("\nkernel_pool_bitmap_end: ",DEFUALT);
put_uint((uint32_t)kernel_pool.pool_bitmap.bits + kernel_pool.pool_bitmap.btmp_bytes_len,DEFUALT,HEX);
put_str("\nkernel_pool_phy_addr_start: ",DEFUALT);
put_uint(kernel_pool.phy_addr_start,DEFUALT,HEX);
put_str("\nkernel_pool_phy_addr_end: ",DEFUALT);
put_uint(kernel_pool.phy_addr_start + kernel_pool.pool_size,DEFUALT,HEX);
put_str("\nuser_pool_bitmap_start: ",DEFUALT);

put_uint((uint32_t)user_pool.pool_bitmap.bits,DEFUALT,HEX);
put_str("\nuser_pool_bitmap_end: ",DEFUALT);
put_uint((uint32_t)user_pool.pool_bitmap.bits + user_pool.pool_bitmap.btmp_bytes_len,DEFUALT,HEX);
put_str("\nuser_pool_phy_addr_start: ",DEFUALT);
put_uint(user_pool.phy_addr_start,DEFUALT,HEX);
put_str("\nuser_pool_phy_addr_end: ",DEFUALT);
put_uint(user_pool.phy_addr_start + user_pool.pool_size,DEFUALT,HEX);

/* 将位图置0*/
bitmap_init(&kernel_pool.pool_bitmap);
bitmap_init(&user_pool.pool_bitmap);

/* 下面初始化内核虚拟地址的位图,按实际物理内存大小生成数组。*/
// 用于维护内核堆的虚拟地址,所以要和内核内存池大小一致
kernel_vaddr.vaddr_bitmap.btmp_bytes_len = kbm_length;

/* 位图的数组指向一块未使用的内存,目前定位在内核内存池和用户内存池之外*/
kernel_vaddr.vaddr_bitmap.bits = (void*)(MEM_BITMAP_BASE + kbm_length + ubm_length);
kernel_vaddr.vaddr_start = K_HEAP_START;
put_str("\nkernel_vaddr.vaddr_bitmap.start: ",DEFUALT);
put_uint((uint32_t)kernel_vaddr.vaddr_bitmap.bits,DEFUALT,HEX);
put_str("\nkernel_vaddr.vaddr_bitmap.end: ",DEFUALT);
put_uint((uint32_t)kernel_vaddr.vaddr_bitmap.bits + kernel_vaddr.vaddr_bitmap.btmp_bytes_len,DEFUALT,HEX);

bitmap_init(&kernel_vaddr.vaddr_bitmap);
put_str("\nmem_pool_init done\n",DEFUALT);
}

/* 内存管理部分初始化入口 */
void mem_init() {
put_str("mem_init start...\n",DEFUALT);
uint32_t mem_bytes_total = *((uint32_t*)MEM_SIZE_ADDR);
put_str("memory size:",DEFUALT);
put_uint(mem_bytes_total,DEFUALT,HEX);
mem_pool_init(mem_bytes_total);
put_str("\nmem_init done\n",DEFUALT);
}/* 初始化内存池 */

上面的代码配合注释以及之前的讲解,是完全能够看懂的,便不再挨个解释了,只对部分内容进行说明:

  • 第 12 行,MEM_SIZE_ADDR ,即 0x90c ,这就是前文我们存放内存容量的地址;第 98 行,通过对该地址解引用,取得内存大小,随后传参给 mem_pool_init() ,开始初始化内存池。

  • 第 19 行,为什么页目录和页表所占内存大小是 PG_SIZE*256 ?(1)页目录表,占 1 页;(2)第0、768号页目录项都指向第 0 号页表,此页表占 1 页;(3)第 769~1022 号页目录项一共指向 254 个页表,占 254 页。因此,所占内存大小为 4096*(1+1+254)=PG_SIZE*256

    有人肯定纳闷了,我们之前不是仅为 769~1022 号页目录项安装了页表的地址吗?并没有创建页表页呀?那为什么还要算入这 254 页的内存大小呢?就是因为我们提前指向了这些页表的地址,每个地址相差 4096 字节,所以才必须为这些页表预留空间哒!笔者起初对这点很疑惑,想了好一会才反应过来,不知道读者会不会有这样的问题。另外,我们在开启分页-代码详解中也提到过,提前为 769~1022 号页目录项安装页表的地址是为了实现内核的完全共享,忘记的朋友不妨回头看看。

  • 第 35 行,物理内存池所管理的物理内存被规定为从 2MB 开始 ,那么为什么要跳过这 2MB 呢?这在前文已经详细说明,0~1MB 是内核空间,1~2MB 是内核进程的页目录和页表,因此这部分物理内存不能再使用,所以不可划入内存池。

  • 第 86 行,为什么将内核虚拟起始地址设为 K_HEAP_START ,即 0xc0100000 ?这是因为在开启分页-代码详解中,我们已经将虚拟空间高 1GB 处的起始 1MB 直接映射到物理内存的低 1MB 了,所以 0x0~0xc0100000 实际上是运行的内核代码(内核镜像)。因此,内核虚拟池中对应的起始虚拟地址必须跳过这 1MB,即从 0xc0100000 开始分配 。另外,从 K_HEAP_START 应该也能看出,内核物理池是用来存放内核开辟的堆 ,读者应该对堆很熟悉了吧,这就是程序在运行时动态开辟的内存。内核代码都存放在 0x9a000 内,注意,代码是不会运行在内核物理池中的!

好了,本节内容大致就是如此,各位一定还有未能想明白的问题,可在评论区留言。同时也请别着急,进入下一节内存管理-进阶-分配页内存后,也许你就会恍然大悟。