内存管理-基础-初始化内存池
本节对应分支:
memory
概述
操作系统内存管理可以说是操作系统中最重要的部分 ,它是未来所有工作的基础。内存管理相当复杂,大约有如下内容:
但本节并不会讨论以上全部内容,而是根据我们自制操作系统的需要来进行。我们当前的任务是完成操作系统的内存划分(本节)以及虚拟内存的申请(下节),即虚拟空间到物理内存的映射,其他内容咋们后续按需补充。本节内容如下:
- 通过 BIOS 中断获取内存容量
既然要分配内存,就一定需要知道系统的内存容量有多大,这通过 BIOS 中断来获取。 - 通过位图来管理内存
管理内存时,肯定需要知道哪些内存已经被使用,哪些还没有使用,这些信息通过我们自己维护的位图来获取。 - 规划内存池
管理内存前,当然还需要对内存做出规划,比如,哪些内存给内核使用,哪些内存又给用户使用。 - 向页表填写映射关系
我们早就实现了分页机制,就差向其中填入映射关系啦!笔者期待已久,让我们开始吧。
获取内存容量
获取内存容量的例程已经由操作系统厂商写好并存入了 BIOS 中,因此我们只需要调用 BIOS 中断即可。现在问题是,进入保护模式后,BIOS 中断无法再被调用,这怎么办呢?不得已,我们只能回到 loader.s 中,即进入保护模式之前调用 BIOS 中断 。
为什么进入保护模式后不能再使用 BIOS 中断 ?
- BIOS 中断例程的地址存放在中断向量表(IVT)中,实模式下使用
int
指令调用中断时,会跳转到 IVT 描述符指向的例程地址;而保护模式下使用int
指令调用中断时,则是跳转到 IDT 描述符所指向的例程。因此,IVT 不再有效。- 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 字节为单位。
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 语句:
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
的地址值,它的位置如下:
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
;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 则表示该页为空闲状态,可以使用 。
位图结构体的声明如下:
struct bitmap {
uint32_t btmp_bytes_len;
/* 在遍历位图时,整体上以字节为单位,细节上是以位为单位,所以此处位图的指针必须是单字节 */
uint8_t* bits;
};
btmp_bytes_len
表示位图的长度,包含的总位数为 btmp_bytes_len*32 。该值由位图所管理的内存大小决定。bits
为位图的指针。位图也当然是存放在内存中的,所以我们用bits
指针来记录位图的起始地址 。
需要注意,虽然 bits
是 uint8_t*
型的指针,步长为 1 字节,但实际操作时我们会细化为按位处理,即通过掩码来判断相应位是否为 1 。
位图的操作函数有如下几个:
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 。
实现如下:
//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 行,重点理解这个按位检测,逻辑也比较简单。需要说明的是,《真相还原》中的原代码是:
#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 为单位 。
如何规划内存池
规划内存池,分为两个大的方向:
- 物理内存和虚拟内存的规划
- 用户内存和内核内存的规划
这两者必须结合在一起讨论。我们已经知道,虚拟内存这个概念,其本身是针对于进程而言的,每个进程都有 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 字节),其他剩余空间是预留的,以便于未来扩展此操作系统。
内存规划代码剖析
//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
结构体用于内核与用户物理内存池的管理。为什么pool
比virtual_addr
多一个 pool_size 成员呢?这是因为,物理内存是很有限的(本OS为32MB),虽然虚拟地址最大为 4GB,但相对而言却是无限的,因此 virtual_addr 无需记录容量。- 至于为什么还需要指定起始地址,阅读下面的代码后你就会彻底明白。
//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 内,注意,代码是不会运行在内核物理池中的!
好了,本节内容大致就是如此,各位一定还有未能想明白的问题,可在评论区留言。同时也请别着急,进入下一节内存管理-进阶-分配页内存后,也许你就会恍然大悟。