9.物理内存管理的实现

9.1分页式的内存管理

image-20240513202714321

线性地址是连续的,但是其实际指向的 物理地址就不见得是连续的了

image-20240513202919077

虚拟 内存实质上就是把物理内存中暂时用不到的内容暂时换出到外存里,空出内存放置现阶段 需要的数据。至于替换的策略当然有相应的算法了,比如最先换入原则,最少使用原则等 等方法可以使用

分级页表:以32位的地址来说,分为3段来寻址,分别是地址的低12位,中间10位和高10位。

  1. 高 10位表示当前地址项在页目录中的偏移,最终偏移处指向对应的页表,
  2. 中间10位是当前地 址在该页表中的偏移,我们按照这个偏移就能查出来最终指向的物理页了,
  3. 最低的12位表 示当前地址在该物理页中的偏移
image-20240513203948806

本章主要解决一下三个问题:

  1. 如何获取可用物理内存的大小和地址?
  2. 采用什么样的数据结构来描述物理内存?
  3. 申请和释放物理内存的算法如何实现?

问题一:如何获取可用物理内存的大小和地址?

在GRUB中已经获取物理内存的分布,并且将它们放置下面的成员里

include/multiboot.h

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
1 typedef
2 struct multiboot_t {
3
4 ... ...
5
6 /**
7 * 以下两项指出保存由 BIOS 提供的内存分布的缓冲区的地址和长度
8 * mmap_addr 是缓冲区的地址, mmap_length 是缓冲区的总大小
9 * 缓冲区由一个或者多个下面的 mmap_entry_t 组成
10 */
11 uint32_t mmap_length;
12 uint32_t mmap_addr;
13
14 ... ...
15
16 } __attribute__((packed)) multiboot_t;
17
18 /**
19 * size 是相关结构的大小,单位是字节,它可能大于最小值 20
20 * base_addr_low 是启动地址的低位,32base_addr_high 是高 32 位,启动地址总共有 64 位
21 * length_low 是内存区域大小的低位,32length_high 是内存区域大小的高 32 位,总共是 64 位
22 * type 是相应地址区间的类型,1 代表可用,所有其它的值代表保留区域 RAM
23 */
24 typedef
25 struct mmap_entry_t {
26 uint32_t size; // size 是不含 size 自身变量的大小
27 uint32_t base_addr_low;
28 uint32_t base_addr_high;
29 uint32_t length_low;
30 uint32_t length_high;
31 uint32_t type;
32 } __attribute__((packed)) mmap_entry_t;

GRUB将内存探测的结果按每个分段整理为mmap_entry结构体的数组mmap_addr是这 个结构体数组的首地址,mmap_length是整个数组的长度。

mm/pmm.c 打印所有物理内存段的操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include "multiboot.h"
#include "common.h"
#include "debug.h"
#include "pmm.h"

void show_memory_map(){
uint32_t mmap_addr = glb_mboot_ptr->mmap_addr;
uint32_t mmap_length = glb_mboot_ptr->mmap_length;

printk("Memory map:\n");
// mmap_addr是数组的起始地址 首地址 mmaplength 是长度
mmap_entry_t *mmap = (mmap_entry_t *)mmap_addr;
for (mmap = (mmap_entry_t *)mmap_addr; (uint32_t)mmap < mmap_addr + mmap_length; mmap++) {
printk("base_addr = 0x%X%08X, length = 0x%X%08X, type = 0x%X\n",
(uint32_t)mmap->base_addr_high , (uint32_t)mmap->base_addr_low ,
(uint32_t)mmap->length_high , (uint32_t)mmap->length_low ,
(uint32_t)mmap->type);
}
}

script/kernel.ld

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
PROVIDE( kern_start = . );  // 加上这两个变量
.text :
{
*(.text)
. = ALIGN(4096);
}
.data :
{
*(.data)
*(.rodata)
. = ALIGN(4096);
}
.bss :
{
*(.bss)
. = ALIGN(4096);
}
.stab :
{
*(.stab)
. = ALIGN(4096);
}
.stabstr :
{
*(.stabstr)
. = ALIGN(4096);
}
PROVIDE( kern_end = . ); // 加上这两个变量

需要知道内核本身加载到物理内存的信息,通过链接器脚本

添加头文件 include/pmm.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#ifndef INCLUDE_PMM_H
#define INCLUDE_PMM_H

#include "multiboot.h"

// 内核文件在内存中的起始和结束位置
// 在连接器脚本中定义了

extern uint8_t kern_start[];
extern uint8_t kern_end[];

// 输出bios提供的物理内存布局
void show_memory_map();

#endif // INCLUDE

修改入口代码 init/entry.c

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
#include "types.h"
#include "console.h"
#include "debug.h"
#include "gdt.h"
#include "idt.h"
#include "timer.h"
#include "pmm.h"

int kern_entry(){
init_debug();
init_gdt();
init_idt();

console_clear();
//console_write_color("Hello, OS kernel!\n", rc_black, rc_green);
//panic("test");



printk_color(rc_black, rc_green, "Hello OS!!!\n");

init_timer(200);

// `sti` 指令的作用是设置中断标志(Set Interrupt Flag)
// asm volatile("sti");

// 显示物理内存布局
printk("kernel in memory start: 0x%08X\n", kern_start);
printk("kernel in memory end: 0x%08X\n", kern_end);
printk("kernel in memory used: %d KB\n\n", (kern_end - kern_start + 1023) / 1024); // 换算成kb

show_memory_map();
return 0;
}

可用内存是两段 type1 表示ram可用内存,是1MB以下的0x0-0x9FC00和1M以上的0x100000-0x7EFE000两段。

本身的内核程序起始位置是 0x100000(1MB) 占用的内存大小为84KB

image-20240513213039330

问题二:采用什么样的数据结构来描述物理内存?

物理内存管理法— 伙伴算法:伙伴算法在申请和释放物理页框的时候会对物理页框进行合并操作,尽可能的 保证可用物理内存的连续性。

  • 内部碎片:内部碎片就是已经被分配出去却不能被利用的内存空间,比如我们为了管理 方便,按照4KB内存块进行管理
  • 外部碎片:内存频繁请求和释放大小不同的连续页框后,导致在已分配页框块周围分散了许多小 块空闲的页框,尽管这些空闲页框的总数可以满足接下来的请求,但却无法满足一个大块 的连续页框。

本项目涉及的内存管理方法:将物理页面的管理地址设定在1MB以上内核加载的结束位置之后,从这个起始位置到512MB的地址处将所有的物理内存按页划分, 将每页的地址放入栈里存储。这样在需要的时候就可以按页获取到物理内存了 — 通过栈实现

主要的步骤

  1. (kern_end - kern_start)内核加载完的结束位置到512MB的位置按照一个页4KB的大小划分页框
  2. 将页框依次压入栈中 — 每个页框的地址都会被记录下来
  3. 当需要分配使用物理内存的时候,弹出相应大小的页框地址
  4. 当系统释放内存的时候,将页框重新压入栈中

示例:

假设内核加载结束位置是2MB(0x200000),那么从2MB到512MB的范围内的所有内存按4KB页框划分。栈中存储的地址可能依次是:

1
0x00200000, 0x00201000, 0x00202000, ..., 0x1FFFE000

当需要分配一个页框时,从栈中弹出一个地址,如0x00200000,然后将这个页框分配给需要的任务。当任务完成并释放这个页框时,地址0x00200000重新压入栈中,等待下次分配。

include/pmm.h 头文件修改

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
#ifndef INCLUDE_PMM_H
#define INCLUDE_PMM_H

#include "multiboot.h"

// 线程栈的大小
#define STACK_SIZE 8192

// 支持的最大物理内存 512MB
#define PMM_MAX_SIZE 0x20000000

// 物理内存页框的大小 4KB
#define PMM_PAGE_SIZE 0x1000

// 最多支持的物理页面个数
#define PAGE_MAX_SIZE (PMM_MAX_SIZE / PMM_PAGE_SIZE)

// 页掩码按照 4096对齐
#define PHY_PAGE_MASK 0xFFFFF000

// 内核文件在内存中的起始和结束位置
// 在连接器脚本中定义了
extern uint8_t kern_start[];
extern uint8_t kern_end[];

// 动态分配物理内存页的总数
extern uint32_t phy_page_count;

// 输出bios提供的物理内存布局
void show_memory_map();

// 初始化物理内存管理
void init_pmm();

// 返回一个内存页的物理地址
uint32_t pmm_alloc_page();

// 释放申请的内存
void pmm_free_page(uint32_t p);

#endif // INCLUDE

mm/pmm.c 文件

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
#include "multiboot.h"
#include "common.h"
#include "debug.h"
#include "pmm.h"

// 物理内存页面管理的栈
static uint32_t pmm_stack[PAGE_MAX_SIZE+1];

// 物理内存管理的栈指针
static uint32_t pmm_stack_top;

// 物理内存页的数量
uint32_t phy_page_count;

void show_memory_map(){
uint32_t mmap_addr = glb_mboot_ptr->mmap_addr;
uint32_t mmap_length = glb_mboot_ptr->mmap_length;

printk("Memory map:\n");
// mmap_addr是数组的起始地址 首地址 mmaplength 是长度
mmap_entry_t *mmap = (mmap_entry_t *)mmap_addr;
for (mmap = (mmap_entry_t *)mmap_addr; (uint32_t)mmap < mmap_addr + mmap_length; mmap++) {
printk("base_addr = 0x%X%08X, length = 0x%X%08X, type = 0x%X\n",
(uint32_t)mmap->base_addr_high , (uint32_t)mmap->base_addr_low ,
(uint32_t)mmap->length_high , (uint32_t)mmap->length_low ,
(uint32_t)mmap->type);
}
}

void init_pmm()
{
mmap_entry_t *mmap_start_addr = (mmap_entry_t *)glb_mboot_ptr->mmap_addr;
mmap_entry_t *mmap_end_addr = (mmap_entry_t *)glb_mboot_ptr->mmap_addr + glb_mboot_ptr->mmap_length;

mmap_entry_t *map_entry;

for (map_entry = mmap_start_addr; map_entry < mmap_end_addr; map_entry++) {

// 如果是可用内存 ( 按照协议,1 表示可用内存,其它数字指保留区域 )
if (map_entry->type == 1 && map_entry->base_addr_low == 0x100000) {

// 把内核结束位置到结束位置的内存段,按页存储到页管理栈里
// 最多支持512MB的物理内存
uint32_t page_addr = map_entry->base_addr_low + (uint32_t)(kern_end - kern_start); // 内核代码的起始和结束地址
uint32_t length = map_entry->base_addr_low + map_entry->length_low;

while (page_addr < length && page_addr <= PMM_MAX_SIZE) {
pmm_free_page(page_addr);
page_addr += PMM_PAGE_SIZE;
phy_page_count++;
}
}
}
}

// 分配一个内存页的物理地址
uint32_t pmm_alloc_page()
{
assert(pmm_stack_top != 0, "out of memory"); // 断言当条件不成立时(即 pmm_stack_top == 0),
// 断言失败,程序会输出这个错误信息,并终止执行。

uint32_t page = pmm_stack[pmm_stack_top--];

return page;
}
// 释放申请的内存 --- 就是把内存还回来了了
void pmm_free_page(uint32_t p)
{
assert(pmm_stack_top != PAGE_MAX_SIZE, "out of pmm_stack stack");

pmm_stack[++pmm_stack_top] = p;
}

解析:

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
void init_pmm()
{
mmap_entry_t *mmap_start_addr = (mmap_entry_t *)glb_mboot_ptr->mmap_addr;
mmap_entry_t *mmap_end_addr = (mmap_entry_t *)glb_mboot_ptr->mmap_addr + glb_mboot_ptr->mmap_length;

mmap_entry_t *map_entry;

for (map_entry = mmap_start_addr; map_entry < mmap_end_addr; map_entry++) {

// 如果是可用内存 ( 按照协议,1 表示可用内存,其它数字指保留区域 )
if (map_entry->type == 1 && map_entry->base_addr_low == 0x100000) {

// 把内核结束位置到结束位置的内存段,按页存储到页管理栈里
// 最多支持512MB的物理内存
uint32_t page_addr = map_entry->base_addr_low + (uint32_t)(kern_end - kern_start); // 内核代码的起始和结束地址
uint32_t length = map_entry->base_addr_low + map_entry->length_low; // 计算需要分配内存块的长度

while (page_addr < length && page_addr <= PMM_MAX_SIZE) {
pmm_free_page(page_addr); // 放入栈中
page_addr += PMM_PAGE_SIZE; // 加上一页的大小
phy_page_count++;
}
}
}
}
  • 通过 glb_mboot_ptr 获取内存映射的起始和结束地址。
  • 遍历每个内存映射条目,检查是否是可用内存并且起始地址是 0x100000(1MB以上)。
  • 计算从内核结束位置开始的内存页地址,并按页存储到内存管理栈中。
  • 使用 pmm_free_page() 函数将每个页地址放入栈中。

while循环中:

  • map_entry->base_addr_low:这是内存映射条目的基地址,表示内存块的起始地址。

  • map_entry->length_low:这是内存映射条目的长度,表示内存块的大小。

  • page_addr:当前正在处理的内存页的地址。

  • length:内存块的结束地址,用于确定内存块的范围。

  • PMM_MAX_SIZE:系统支持的最大物理内存大小,在此假设为512MB。

  • pmm_free_page(page_addr):将当前页地址放入内存管理栈中。

  • page_addr += PMM_PAGE_SIZE:将页地址移动到下一个页框(假设页框大小为4KB)。

  • phy_page_count++:增加已处理的页框计数。

init/entry.c

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
#include "types.h"
#include "console.h"
#include "debug.h"
#include "gdt.h"
#include "idt.h"
#include "timer.h"
#include "pmm.h"

int kern_entry(){
init_debug();
init_gdt();
init_idt();

console_clear();
//console_write_color("Hello, OS kernel!\n", rc_black, rc_green);
//panic("test");



printk_color(rc_black, rc_green, "Hello OS!!!\n");

init_timer(200);

// `sti` 指令的作用是设置中断标志(Set Interrupt Flag)
// asm volatile("sti");

// 显示物理内存布局
printk("kernel in memory start: 0x%08X\n", kern_start);
printk("kernel in memory end: 0x%08X\n", kern_end);
printk("kernel in memory used: %d KB\n\n", (kern_end - kern_start + 1023) / 1024); // 换算成kb

show_memory_map();
// 初始化物理内存管理
init_pmm();

printk_color(rc_black, rc_red, "\nThe Count of Physical Memory Page is: %u\n\n", phy_page_count); // 栈中分配的物理也的大小
uint32_t allc_addr = NULL;
printk_color(rc_black, rc_light_brown , "Test Physical Memory Alloc :\n");

allc_addr = pmm_alloc_page(); // 分配一个页 物理内存
printk_color(rc_black, rc_light_brown , "Alloc Physical Addr: 0x%08X\n",allc_addr);
allc_addr = pmm_alloc_page();
printk_color(rc_black, rc_light_brown , "Alloc Physical Addr: 0x%08X\n",allc_addr);
allc_addr = pmm_alloc_page();
printk_color(rc_black, rc_light_brown , "Alloc Physical Addr: 0x%08X\n",allc_addr);
allc_addr = pmm_alloc_page();
printk_color(rc_black, rc_light_brown , "Alloc Physical Addr: 0x%08X\n",allc_addr);



return 0;
}
image-20240514210241195

因为是用栈 进行管理的,所以最先分配的物理内存地址是高地址

image-20240514210512354