本章节所有代码托管在miniOS_32
章节任务介绍
任务简介
上一节,我们初步构建了管理内存需要的数据结构及其相关操作------位图
本节我们将在上一节的基础上正式实现操作系统的内存管理系统,直到malloc函数与free函数的完成
本章的主要任务有:
内存池的初始化
内存分配实现
最终实现的效果是
内核程序向虚拟内存申请page个虚拟内存页面
操作系统在内核虚拟内存中寻找到空闲的连续page个虚拟页面
操作系统在内核物理内存池中找到page个物理页面(可能不连续)
逐一构建在虚拟内存池中找到的虚拟页面与在物理内存池中找到的物理页面之间的映射关系(本质上是在构建页表项和页目录项,然后装填页表和页目录表)
内存池的规划
本节我们将规划实现三个内存池,分别是
内核虚拟内存池
内核物理内存池
用户物理内存池
事实上,应该还有一个用户虚拟内存池,但这部分内容我们在线程管理部分再进行补充
虚拟内存池规划在虚拟内存中,物理内存池规划在物理内存中,他们之间的关系通过页表进行关联
物理内存池的规划
目前为止,低1MB的物理内存是我们的内核相关代码和数据,从1MB往上2MB字节存放的是页目录表和内核页表内容,这两部分的数据我们是不能覆盖的,因此剩余的内存将供我们规划物理内存池
我们在剩余空间中各取一半,分别最为用户物理内存池和内核物理内存池,如下所示
虚拟内存池的规划
目前我们暂时只对内核虚拟内存池(内核堆区空间)进行规划,在3GB ~ 4GB中间的内核虚拟空间中取一块作为虚拟内存池,其中起始地址为0xc0100000
,也就是绕过低端的1MB内存
位图的规划存放
既然有物理内存池和虚拟内存池,位图作为管理内存空间的数据结构,当然也要有地方进行存放,这样我们才能从位图中知道哪些内存被使用了,哪些内存块是空闲的,从而进一步管理内存
三种内存池对应着三种位图,由于我们还有一个用户虚拟内存池还暂时没有开辟,因此在这里应该是四种位图
负责管理内核物理内存池的位图
负责管理用户物理内存池的位图
负责管理内核虚拟内存池的位图
负责管理用户虚拟内存池的位图
这里我们给出答案,我们位图放在低1MB字节物理空间中的内核文件中
如下是我们在低1MB内存空间中已经使用的内存
0x7c00
~0x7e00
是MBR程序
0x900
是loader的起始地址
0x70000
~0x9f000
是我们内核文件的存放位置。0x70000
是内核文件的起始地址,由于0x9fc000
地址以下是我们可用的空间,因此我们选取0x9f000
作为内核文件的最终位置(注意,这也意味着0x9f000
其实就是内核的栈顶指针)
由于我们要把位图放在内核文件中,因此我们要在0x70000
~ 0x9f000
的空间中选取一块位置,另外我们规定一张位图的大小也占用4KB,也就是一页
另外,将来我们要存放内核文件的PCB(占用4KB),所以0xc009e000
~ 0xc009f000
其实是拿来存放PCB的
于是由于 0xc009e000
已经是内核主线程的PCB,而一页大小为 0x1000
,故再减去4页,即 0xc009e000
-0x4000
=0xc009a000
。故我们的位图地址为0xc009a000
,如下所示
内存池的初始化
代码目录结构
bash
.
├── bin
├── boot
│ ├── include
│ │ └── boot.inc
│ ├── loader.S
│ └── mbr.S
├── kernel
│ ├── debug.c
│ ├── debug.h
│ ├── global.h
│ ├── init.c
│ ├── init.h
│ ├── interrupt.c
│ ├── interrupt.h
│ ├── kernel.S
│ ├── main.c
│ ├── memory.c
│ └── memory.h
├── lib
│ ├── kernel
│ │ ├── bitmap.c
│ │ ├── bitmap.h
│ │ ├── io.h
│ │ ├── print.h
│ │ └── print.S
│ ├── stdint.h
│ ├── string.c
│ └── string.h
├── Makefile
└── start.sh
定义管理内存池的数据结构
/kernel/memory.h
cpp
#ifndef __KERNEL_MEMORY_H
#define __KERNEL_MEMORY_H
#include "stdint.h"
#include "bitmap.h"
/*管理虚拟内存池的数据结果*/
struct virtual_addr
{
struct bitmap vaddr_map; // 管理虚拟内存池的位图
uint32_t vaddr_start; // 虚拟内存池的起始地址
};
/*管理物理内存池的数据结构*/
struct pool
{
struct bitmap pool_bitmap; // 管理物理内存池的位图
uint32_t phy_addr_start; // 物理内存池的起始地址
uint32_t pool_size; // 物理内存池的大小
};
void mem_init();
#endif
/kernel/memory.c
如下定义需要的变量
cpp
#define PG_SIZE 4096
/*虚拟内存池的位图在虚拟内存中的起始地址*/
#define MEM_BITMAP_BASE 0xc009a000
/*内核堆区的起始地址,堆区其实就是虚拟内存池*/
#define K_HEAP_START 0xc0100000
/*定义内核的虚拟内存池*/
struct virtual_addr kernel_vaddr;
/*定义内核的物理内存池和用户的物理内存池*/
struct pool kernel_pool, user_pool;
在初始化三种内存池之前,我们需要首先找到当前可用的物理内存
cpp
所有内核页表占据的物理内存
一张页表占据4KB,共256张页表(一张页目录表+0号页表项和768号页表项共同指向的一张页表+769~1022号页表项指向的254张页表)
*/
uint32_t page_table_size = PG_SIZE * 256;
// 目前已经使用的物理内存:0~1MB的内核空间+页表占据的空间
uint32_t used_mem = 0x100000 + page_table_size;
// 当前所有可用的物理内存
uint32_t free_mem = all_mem - used_mem;
// 当前所有可用的物理页数
uint16_t all_free_pages = free_mem / PG_SIZE;
// 设置所有可用的内核物理页(物理内存池)
uint16_t kernel_free_pages = all_free_pages / 2;
// 设置所有可用的用户物理页(用户内存池)
uint16_t user_free_pages = all_free_pages - kernel_free_pages;
如上所示
all_mem
表示物理内存总容量,该数值在loader.S
中获取过,存放在0xb00
位置处
used_mem
表示当前已经使用的物理内存容量,包括低1MB
的内核空间以及页目录表和255张页表占据的内存空间于是
free_mem
就表示剩余可用的物理内存空间
由以上计算,我们就可以计算出内核物理内存池的起始地址、用户物理内存池的起始地址以及管理内核物理内存池和用户物理内存池的位图长度,如下所示
cpp
/*
定义管理内核物理内存池的位图长度
位图中一个比特位管理一页物理内存,故用字节表示位图长度除以8即可
*/
uint32_t kbm_length = kernel_free_pages / 8;
// 定义管理用户物理内存池的位图长度
uint32_t ubm_length = user_free_pages / 8;
// 内核物理内存池的起始地址
uint32_t kp_start = used_mem;
// 用户物理内存池的起始地址
uint32_t up_start = kp_start + kernel_free_pages * PG_SIZE;
数据准备好之后,接下来初始化三种内存池
初始化内核物理内存池
cpp
/*以下初始化内核物理内存池*/
kernel_pool.pool_bitmap.btmp_bytes_len = kbm_length;
kernel_pool.pool_bitmap.bits = (void *)MEM_BITMAP_BASE;
kernel_pool.phy_addr_start = kp_start;
kernel_pool.pool_size = kernel_free_pages * PG_SIZE;
初始化用户物理内存池
cpp
/*以下初始化用户物理内存池*/
user_pool.pool_bitmap.btmp_bytes_len = ubm_length;
user_pool.pool_bitmap.bits = (void *)(MEM_BITMAP_BASE + kbm_length);
user_pool.phy_addr_start = up_start;
user_pool.pool_size = user_free_pages * PG_SIZE;
初始化内核虚拟内存池
cpp
/*初始化内核虚拟内存池*/
kernel_vaddr.vaddr_map.btmp_bytes_len = kbm_length;
kernel_vaddr.vaddr_map.bits = (void *)(MEM_BITMAP_BASE + kbm_length + ubm_length);
kernel_vaddr.vaddr_start = K_HEAP_START;
bitmap_init(&kernel_vaddr.vaddr_map);
以下是完整的/kernel/memory.c
cpp
#include "memory.h"
#include "print.h"
#define PG_SIZE 4096
/*虚拟内存池的位图在虚拟内存中的起始地址*/
#define MEM_BITMAP_BASE 0xc009a000
/*内核堆区的起始地址,堆区其实就是虚拟内存池*/
#define K_HEAP_START 0xc0100000
/*定义内核的虚拟内存池*/
struct virtual_addr kernel_vaddr;
/*定义内核的物理内存池和用户的物理内存池*/
struct pool kernel_pool, user_pool;
/*
初始化内存池
参数:物理内存的所有容量,为32MB
该容量的数值存储在物理内存的0xb00处
*/
static void mem_pool_init(uint32_t all_mem)
{
put_str("memory pool init start!\n");
/*
所有内核页表占据的物理内存
一张页表占据4KB,共256张页表(一张页目录表+0号页表项和768号页表项共同指向的一张页表+769~1022号页表项指向的254张页表)
*/
uint32_t page_table_size = PG_SIZE * 256;
// 目前已经使用的物理内存:0~1MB的内核空间+页表占据的空间
uint32_t used_mem = 0x100000 + page_table_size;
// 当前所有可用的物理内存
uint32_t free_mem = all_mem - used_mem;
// 当前所有可用的物理页数
uint16_t all_free_pages = free_mem / PG_SIZE;
// 设置所有可用的内核物理页(物理内存池)
uint16_t kernel_free_pages = all_free_pages / 2;
// 设置所有可用的用户物理页(用户内存池)
uint16_t user_free_pages = all_free_pages - kernel_free_pages;
/*
定义管理内核物理内存池的位图长度
位图中一个比特位管理一页物理内存,故用字节表示位图长度除以8即可
*/
uint32_t kbm_length = kernel_free_pages / 8;
// 定义管理用户物理内存池的位图长度
uint32_t ubm_length = user_free_pages / 8;
// 内核物理内存池的起始地址
uint32_t kp_start = used_mem;
// 用户物理内存池的起始地址
uint32_t up_start = kp_start + kernel_free_pages * PG_SIZE;
/******************** 初始化物理内存池 **********************/
/*以下初始化内核物理内存池*/
kernel_pool.pool_bitmap.btmp_bytes_len = kbm_length;
kernel_pool.pool_bitmap.bits = (void *)MEM_BITMAP_BASE;
kernel_pool.phy_addr_start = kp_start;
kernel_pool.pool_size = kernel_free_pages * PG_SIZE;
/*以下初始化用户物理内存池*/
user_pool.pool_bitmap.btmp_bytes_len = ubm_length;
user_pool.pool_bitmap.bits = (void *)(MEM_BITMAP_BASE + kbm_length);
user_pool.phy_addr_start = up_start;
user_pool.pool_size = user_free_pages * PG_SIZE;
/******************** 输出物理内存池信息 **********************/
put_str(" kernel_pool_bitmap_start:");
put_int((int)kernel_pool.pool_bitmap.bits);
put_str(" kernel_pool_phy_addr_start:");
put_int(kernel_pool.phy_addr_start);
put_str("\n");
put_str(" user_pool_bitmap_start:");
put_int((int)user_pool.pool_bitmap.bits);
put_str(" user_pool_phy_addr_start:");
put_int(user_pool.phy_addr_start);
put_str("\n");
/*将位图置为0,初始化位图*/
bitmap_init(&kernel_pool.pool_bitmap);
bitmap_init(&user_pool.pool_bitmap);
/******************** 初始化虚拟内存池 **********************/
/*初始化内核虚拟内存池*/
kernel_vaddr.vaddr_map.btmp_bytes_len = kbm_length;
kernel_vaddr.vaddr_map.bits = (void *)(MEM_BITMAP_BASE + kbm_length + ubm_length);
kernel_vaddr.vaddr_start = K_HEAP_START;
bitmap_init(&kernel_vaddr.vaddr_map);
put_str("memory pool init done!\n");
}
void mem_init()
{
put_str("mem_init start\n");
// 此处存放着物理内存容量,具体可见/boot/loader.S中total_mem_bytes的定义和计算
uint32_t mem_bytes_total = (*(uint32_t *)(0xb00));
// 初始化内存池
mem_pool_init(mem_bytes_total);
put_str("mem_init done\n");
}
然后我们将内存池的初始化代码添加到/kernel/init.c
中
cpp
#include "init.h"
#include "print.h"
#include "interrupt.h"
#include "memory.h"
/*初始化所有模块*/
void init_all()
{
put_str("init_all\n");
// 初始化中断
idt_init();
// 初始化内存管理系统
mem_init();
}
编译运行
cpp
mkdir -p bin
#编译mbr
nasm -o $(pwd)/bin/mbr -I $(pwd)/boot/include/ $(pwd)/boot/mbr.S
dd if=$(pwd)/bin/mbr of=~/bochs/hd60M.img bs=512 count=1 conv=notrunc
#编译loader
nasm -o $(pwd)/bin/loader -I $(pwd)/boot/include/ $(pwd)/boot/loader.S
dd if=$(pwd)/bin/loader of=~/bochs/hd60M.img bs=512 count=4 seek=2 conv=notrunc
#编译print函数
nasm -f elf32 -o $(pwd)/bin/print.o $(pwd)/lib/kernel/print.S
# 编译kernel
nasm -f elf32 -o $(pwd)/bin/kernel.o $(pwd)/kernel/kernel.S
#编译main文件
gcc-4.4 -o $(pwd)/bin/main.o -c -fno-builtin -m32 -I $(pwd)/lib/kernel/ -I $(pwd)/lib/ -I $(pwd)/kernel/ $(pwd)/kernel/main.c
#编译interrupt文件
gcc-4.4 -o $(pwd)/bin/interrupt.o -c -fno-builtin -m32 -I $(pwd)/lib/kernel/ -I $(pwd)/lib/ -I $(pwd)/kernel/ $(pwd)/kernel/interrupt.c
#编译init文件
gcc-4.4 -o $(pwd)/bin/init.o -c -fno-builtin -m32 -I $(pwd)/lib/kernel/ -I $(pwd)/lib/ -I $(pwd)/kernel/ $(pwd)/kernel/init.c
# 编译debug文件
gcc-4.4 -o $(pwd)/bin/debug.o -c -fno-builtin -m32 -I $(pwd)/lib/kernel/ -I $(pwd)/lib/ -I $(pwd)/kernel/ $(pwd)/kernel/debug.c
# 编译string文件
gcc-4.4 -o $(pwd)/bin/string.o -c -fno-builtin -m32 -I $(pwd)/lib/kernel/ -I $(pwd)/lib/ -I $(pwd)/kernel/ $(pwd)/lib/string.c
# 编译bitmap文件
gcc-4.4 -o $(pwd)/bin/bitmap.o -c -fno-builtin -m32 -I $(pwd)/lib/kernel/ -I $(pwd)/lib/ -I $(pwd)/kernel/ $(pwd)/lib/kernel/bitmap.c
# 编译memory文件
gcc-4.4 -o $(pwd)/bin/memory.o -c -fno-builtin -m32 -I $(pwd)/lib/kernel/ -I $(pwd)/lib/ -I $(pwd)/kernel/ $(pwd)/kernel/memory.c
#将main函数与print函数进行链接
ld -m elf_i386 -Ttext 0xc0001500 -e main -o $(pwd)/bin/kernel.bin $(pwd)/bin/main.o $(pwd)/bin/print.o $(pwd)/bin/init.o $(pwd)/bin/interrupt.o $(pwd)/bin/kernel.o $(pwd)/bin/memory.o $(pwd)/bin/bitmap.o $(pwd)/bin/string.o $(pwd)/bin/debug.o
#将内核文件写入磁盘,loader程序会将其加载到内存运行
dd if=$(pwd)/bin/kernel.bin of=~/bochs/hd60M.img bs=512 count=200 conv=notrunc seek=9
#rm -rf bin/*
运行结果如下所示
如下,输出内存初始化的相关信息
cpp
mem init startmemory pool init start!
kernel pool bitmap start:c009A000 kernel pool phy_addr _start:200000
user pool bitmap start:C009A1E0 user pool phy addr start:1100000
memory pool init done!
mem init done
内存管理第一步------分配页内存
目标
实现整页面的内存分配,我们最终会实现一个下边的函数,当传入参数3时,表示用户想获取3页内存,如果能够找到,就返回这3页内存的起始地址,否则返回NULL
cpp
void *addr = get_kernel_pages(3);
代码逻辑
在内核虚拟内存池中寻找到空闲的连续page个虚拟页面
在内核物理内存池中找到page个物理页面(可能不连续)
逐一构建找到的虚拟页面与物理页面之间的映射关系(本质上是在构建页表项和页目录项,然后装填页表和页目录表)
代码目录结构
bash
.
├── bin
├── boot
│ ├── include
│ │ └── boot.inc
│ ├── loader.S
│ └── mbr.S
├── kernel
│ ├── debug.c
│ ├── debug.h
│ ├── global.h
│ ├── init.c
│ ├── init.h
│ ├── interrupt.c
│ ├── interrupt.h
│ ├── kernel.S
│ ├── main.c
│ ├── memory.c
│ └── memory.h
├── lib
│ ├── kernel
│ │ ├── bitmap.c
│ │ ├── bitmap.h
│ │ ├── io.h
│ │ ├── print.h
│ │ └── print.S
│ ├── stdint.h
│ ├── string.c
│ └── string.h
├── Makefile
└── start.sh
数据结构准备
我们在C语言中申请内存时,由于我们是用户程序,操作系统直接会在用户内存池中分配内存,但这对应到内核中具体的操作时,必须
要"显式"指定在哪个内存池中申请,故我们在 memoryh 中新增了枚举结构 eum pool fags用来区分这两个内存池,此结构里面定义了两个成员
PF KERNEL值为1,它代表内核物理内存池。
PFUSER值为2,它代表用户物理内存池
在/kernel/memory.h
中添加相关信息
cpp
/*页表项属性宏*/
#define PG_P_1 1 // 页表项或页目录项存在属性位
#define PG_P_0 0 // 页表项或页目录项存在属性位
#define PG_RW_R 0 // R/W 属性位值, 读/执行
#define PG_RW_W 2 // R/W 属性位值, 读/写/执行
#define PG_US_S 0 // U/S 属性位值, 系统级
#define PG_US_U 4 // U/S 属性位值, 用户级
/*内存池类型*/
enum pool_flags
{
PF_KERNEL = 1,
PF_USER = 2
};
/*返回虚拟地址vaddr所代表的pde(页目录项)的虚拟地址*/
uint32_t *pde_ptr(uint32_t vaddr);
/*页表中添加虚拟地址_vaddr和物理地址_page_phyaddr的映射关系*/
uint32_t *pte_ptr(uint32_t vaddr);
/*分配pg_cnt个页空间,成功也返回起始虚拟地址,失败则返回NULL*/
void *malloc_page(enum pool_flags pf, uint32_t pg_cnt);
/*从内核物理内存池中申请pg_cnt页内存,成功则返回其虚拟地址,失败则返回NULL*/
void *get_kernel_pages(uint32_t pg_cnt);
void malloc_init(void);
虚拟内存池中寻找连续的空闲虚拟页
/kernel/memory.c
cpp
/*
在pf所指向的虚拟内存池中寻找pg_cnt空闲虚拟页,并分配之
*/
static void *vaddr_get(enum pool_flags pf, uint32_t pg_cnt)
{
int vaddr_start = 0, bit_idx_start = -1;
if (pf == PF_KERNEL)
{
/*内核虚拟内存池*/
// 寻找可用的连续页
bit_idx_start = bitmap_scan(&kernel_vaddr.vaddr_bitmap, pg_cnt);
if (bit_idx_start == -1)
return NULL;
// 如果找到就将这些连续空间设置为已分配
uint32_t cnt = 0;
while (cnt < pg_cnt)
bitmap_set(&kernel_vaddr.vaddr_bitmap, bit_idx_start + cnt++, 1);
vaddr_start = kernel_vaddr.vaddr_start + bit_idx_start * PG_SIZE;
}
else
{
/*用户虚拟内存池*/
}
return (void *)vaddr_start;
}
物理内存池中寻找空闲的物理页
/kernel/memory.c
cpp
/*
在m_pool(物理内存池数据结构)所指向的物理内存池中寻找一页空闲的物理页,并分配之
*/
static void *palloc(struct pool *m_pool)
{
int bit_idx = bitmap_scan(&m_pool->pool_bitmap, 1);
if (bit_idx == -1)
return NULL;
bitmap_set(&m_pool->pool_bitmap, bit_idx, 1);
uint32_t page_phyaddr = ((bit_idx * PG_SIZE) + m_pool->phy_addr_start);
return (void *)page_phyaddr;
};
注意,不同于虚拟内存,物理内存很多时候是不连续的,而且我们也不需要他们是连续的,只需要虚拟内存是连续的,然后在页表中构建他们之间的映射关系即可,因此在这里我们实现的是在物理内存池中寻找一页空闲的物理页
构建映射关系
为了构建虚拟页和物理页的映射关系,我们需要装填页目录项和页表项,所以首先需要逆向推出虚拟页
vaddr
所在的页目录项和页表项虚拟地址
,然后才能构建他们
以下是通过虚拟页的虚拟地址获取其对应的页表项的虚拟地址
cpp
// 取出虚拟地址的高10位
#define PDE_IDX(addr) ((addr & 0xffc00000) >> 22)
// 取出虚拟地址的中间10位
#define PTE_IDX(addr) ((addr & 0x003ff000) >> 12)
/*
返回虚拟地址vaddr所代表的pte(页表项)的虚拟地址
此举是为了取出虚拟地址vaddr所代表的页表项的内容
*/
uint32_t *pte_ptr(uint32_t vaddr)
{
/*
0xffc00000:表示页目录表的第1023个页目录项,该页目录项的存储的是页目录表的物理地址(见loader.S中页目录项的初始化部分内容)
vaddr & 0xffc00000:取出vaddr的高10位,也就是vaddr所代表的页目录项的索引
先访问到页目录表自己 + 再用页目录项pde(页目录内页表的索引)作为pte的索引访问到页表 + 再用pte的索引做为页内偏移
*/
uint32_t *pte = (uint32_t *)(0xffc00000 + ((vaddr & 0xffc00000) >> 10) + PTE_IDX(vaddr) * 4);
return pte;
}
理下思路,处理器处理 32 位地址的三个步骤如下。
(1)首先处理高10位的pde 索引,从而处理器得到页表物理地址。
(2)其次处理中间10位的pte索引,进而处理器得到普通物理页的物理地址。
(3)最后是把低12位作为普通物理页的页内偏移地址,此偏移地址加上物理页的物理地址,得到的地址之和便是最终的物理地址,处理器到此物理地址上进行读写操作。
也就是说,我们要创造的这个新的虚拟地址new vaddr,它经过处理器以上三个步骤的拆分处理,最终会落到 vaddr 自身所在的pte的物理地址上。
pte 位于页表中,因此要想访问 vaddr 所在的 pte,必须先访问到页目录表,再通过其中的页目录项 pde,找到相应的页表,在页表中才是页表项 pte。
因此,我们需要分别在地址的高 10 位、中间 10 位和低 12 位中填入合适的数,拼凑出满足此要求的新的32位地址newvaddr。
第一步,先访问到页目录表。
32 位虚拟地址中,高10位用于定位页目录项 ,由于最后一个页目录项保存的正是页目录表物理地址 ,我们可以让地址的高10位指向最后一个页目录项,即第1023个pde,这样刚好获取页目录表本身的物理地址。
1023 换算成十六进制是0x3,将其移到高10后,变成0xffc00000
。于是,0xffc00000
让处理器自动在最后一个 pde 中取出页目录表物理地址,此处页目录表物理地址为0x100000
,如果忘记的话,可以看看 boot/include/boot.inc
中的配置项 PAGE_DIR_TABLE_POS
,其值便为 0x100000
。
在我们眼里,最后一个 pde 中的物理地址是页目录表地址,因为这是咱们在创建页表时提前安排好的
但处理器把保存在pde中的地址都视为页表地址,即处理器会把刚刚获得的页目录表当成页表来处理
第二步,找到页表。
其次,处理器需要pte 索引。中间10位是页表项的索引,用来在页表中定位页表pte。
我们在上一步中已经得到了页目录表物理地址 (其实处理器把页目录表当成页表了),页表地址保存在页目录项中,因此我们要先想办法访问到 vaddr 所在的页目录项。
此时处理器已经把上一步获得的页目录表当成了页表,其需要的是 pte的索引,因此我们把 vaddr 的 pde 索引当作处理器视角中的 pte 索引就行了现在要做的是将参数 vaddr 的高10位(pde 索引)取出来,做新地址 new vaddr 的中间 10位(pte 索引)。
于是我们先用按位与操作(vaddr&0xffc00000
)获取高10位,再将其右移 10位,使其变成中间10位,就成了处理器眼中的pte索引。
这样在处理器处理新地址 new vaddr的pte 索引时,以为接下来获得的是 pte 中的普通物理页地址,但这只是处理器视角中的情景。而事实上由于上一步我们获得的是页目录表地址,并且本步中传给它的pte索引是 vaddr 中的pde索引,故此时处理器获得的是 vaddr 中高10位的 pde索引所对应的 pde里保存的页表的物理地址,并不是 pte 中保存的普通物理页的物理地址。
此时我们获得了vaddr 所在的页表物理地址。
第三步,在页表中找到 pte。
最后,处理器需要地址的低 12位。
上一步中处理器认为已经找到了最终的物理页地址,所以它此时需要的是32位地址中的低12位,用该12位作为上一步中获取到的物理页的偏移量,当然,这依然只是处理器的视角。
在我们眼里,上一步获得的是页表的物理地址,因此我们只要把 vaddr 的中间 10 位转换成处理器眼里的 12 位长度的页内偏移量就行了。
由于地址的低 12 位寻址范围正好是一页的 4KB 大小,故处理器直接拿低 12位去寻址,不会再为其自动乘以4,因此,咱们得手动将vaddr的pte部分乘4后再交给处理器。这里的做法是先用PTE_IDX(vaddr)
获取 vaddr
的 pte
索引,即中间 10 位,再将其乘 4,即 PTE_IDX(vaddr)*4
拼凑出了新虚拟地址new vaddr的低 12位。
故0xffc00000+((vaddr &0xffc00000)>>10)+PTE_IDX(vaddr)*4
的结果就是最终的新虚拟地址 new vaddr 的完整 32位数值,new vaddr 保存在指针变量 pte 中。由于此结果仅仅是个整型数值,需要将其通过强制类型转换,即(uint32_t *),转换成32位整型地址。此时指针变量 pte 指向 vaddr 所在的 pte。最后通过 return pte 将此指针返回。
同理,以下是返回虚拟地址vaddr所代表的pde(页目录项)的虚拟地址
cpp
/*
返回虚拟地址vaddr所代表的pde(页目录项)的虚拟地址
此举是为了取出虚拟地址vaddr所代表的页目录项的内容
*/
uint32_t *pde_ptr(uint32_t vaddr)
{
/* 0xfffff是用来访问到页表本身所在的地址 */
uint32_t *pde = (uint32_t *)((0xfffff000) + PDE_IDX(vaddr) * 4);
return pde;
}
由此,我们就可以构建申请的虚拟页和物理页之间映射关系了
cpp
/*
页表中添加虚拟地址_vaddr和物理地址_page_phyaddr的映射关系
*/
static void page_table_add(void *_vaddr, void *_page_phyaddr)
{
uint32_t vaddr = (uint32_t)_vaddr, page_phyaddr = (uint32_t)_page_phyaddr;
uint32_t *pde = pde_ptr(vaddr);
uint32_t *pte = pte_ptr(vaddr);
/* 判断页目录内目录项的P位,若为1,则表示其指向的页表已存在,直接装填对应的页表项*/
if (*pde & 0x00000001)
{
// 如果页目录项P位为1,说明页目录项指向的页表存在,但是页表项不存在,则直接创建页表项
if (!(*pte & 0x00000001))
{
// 填充页目录项
*pte = (page_phyaddr | PG_US_U | PG_RW_W | PG_P_1);
}
else
{
PANIC("pte repeat");
*pte = (page_phyaddr | PG_US_U | PG_RW_W | PG_P_1);
}
}
else
{
/*
页目录项指向的页表不存在,需要首先构建页表,然后再装填对应的页目录项和页表项
1.在物理内存池中寻找一页物理页分配给页表
2.将该页表初始化为0,防止这块内存的脏数据乱入
3.将该页表写入页目录项
4.构建页表项,填充进页表
*/
// 在内核物理内存池找一页物理页分配给页表
uint32_t pde_phyaddr = (uint32_t)palloc(&kernel_pool);
// 初始化页表清零
/* 访问到pde对应的物理地址,用pte取高20位便可.
* 因为pte是基于该pde对应的物理地址内再寻址,
* 把低12位置0便是该pde对应的物理页的起始*/
memset((void *)((int)pte & 0xfffff000), 0, PG_SIZE);
// 将分配的页表物理地址写入页目录项
*pde = (pde_phyaddr | PG_US_U | PG_RW_W | PG_P_1);
ASSERT(!(*pte & 0x00000001));
// 构建页表项,填充进页表
*pte = (page_phyaddr | PG_US_U | PG_RW_W | PG_P_1);
}
}
接下来,实现分配pg_cnt个页空间,成功则返回起始虚拟地址的函数
cpp
/*
分配pg_cnt个页空间,成功则返回起始虚拟地址,失败则返回NULL
*/
void *malloc_page(enum pool_flags pf, uint32_t pg_cnt)
{
/*********** malloc_page的原理是三个动作的合成: ***********
1通过vaddr_get在虚拟内存池中申请虚拟地址
2通过palloc在物理内存池中申请物理页
3通过page_table_add将以上得到的虚拟地址和物理地址在页表中完成映射
***************************************************************/
ASSERT(pg_cnt > 0 && pg_cnt < 3840);
// 获取连续的pg_cnt个虚拟页
void *vaddr_start = vaddr_get(pf, pg_cnt);
if (vaddr_start == NULL)
return NULL;
uint32_t vaddr = (uint32_t)vaddr_start, cnt = pg_cnt;
struct pool *mem_pool = pf & PF_KERNEL ? &kernel_pool : &user_pool;
/*
逐一申请物理页,然后再挨个映射进虚拟页
之所以物理页不能一次性申请,是因为物理页可能是离散的
但是虚拟页是连续的
*/
while (cnt--)
{
void *page_phyaddr = palloc(mem_pool);
if (page_phyaddr == NULL)
return NULL;
page_table_add((void *)vaddr, page_phyaddr);
vaddr += PG_SIZE;
}
return vaddr_start;
}
以下是/kernel/memory.c
文件的完整内容
cpp
#include "memory.h"
#include "print.h"
#include "debug.h"
#include "string.h"
#define PG_SIZE 4096
/*虚拟内存池的位图在虚拟内存中的起始地址*/
#define MEM_BITMAP_BASE 0xc009a000
/*内核堆区的起始地址,堆区其实就是虚拟内存池*/
#define K_HEAP_START 0xc0100000
/*定义内核的虚拟内存池*/
struct virtual_addr kernel_vaddr;
/*定义内核的物理内存池和用户的物理内存池*/
struct pool kernel_pool, user_pool;
/*
初始化内存池
参数:物理内存的所有容量,为32MB
该容量的数值存储在物理内存的0xb00处
*/
static void mem_pool_init(uint32_t all_mem)
{
put_str("memory pool init start!\n");
/*
所有内核页表占据的物理内存
一张页表占据4KB,共256张页表(一张页目录表+0号页目录项和768号页目录项共同指向的一张页表+769~1022号页表项指向的254张页表)
*/
uint32_t page_table_size = PG_SIZE * 256;
// 目前已经使用的物理内存:0~1MB的内核空间+页表占据的空间
uint32_t used_mem = 0x100000 + page_table_size;
// 当前所有可用的物理内存
uint32_t free_mem = all_mem - used_mem;
// 当前所有可用的物理页数
uint16_t all_free_pages = free_mem / PG_SIZE;
// 设置所有可用的内核物理页(物理内存池)
uint16_t kernel_free_pages = all_free_pages / 2;
// 设置所有可用的用户物理页(用户内存池)
uint16_t user_free_pages = all_free_pages - kernel_free_pages;
/*
定义管理内核物理内存池的位图长度
位图中一个比特位管理一页物理内存,故用字节表示位图长度除以8即可
*/
uint32_t kbm_length = kernel_free_pages / 8;
// 定义管理用户物理内存池的位图长度
uint32_t ubm_length = user_free_pages / 8;
// 内核物理内存池的起始地址
uint32_t kp_start = used_mem;
// 用户物理内存池的起始地址
uint32_t up_start = kp_start + kernel_free_pages * PG_SIZE;
/******************** 初始化物理内存池 **********************/
/*以下初始化内核物理内存池*/
kernel_pool.pool_bitmap.btmp_bytes_len = kbm_length;
kernel_pool.pool_bitmap.bits = (void *)MEM_BITMAP_BASE;
kernel_pool.phy_addr_start = kp_start;
kernel_pool.pool_size = kernel_free_pages * PG_SIZE;
/*以下初始化用户物理内存池*/
user_pool.pool_bitmap.btmp_bytes_len = ubm_length;
user_pool.pool_bitmap.bits = (void *)(MEM_BITMAP_BASE + kbm_length);
user_pool.phy_addr_start = up_start;
user_pool.pool_size = user_free_pages * PG_SIZE;
/******************** 输出物理内存池信息 **********************/
put_str(" kernel_pool_bitmap_start:");
put_int((int)kernel_pool.pool_bitmap.bits);
put_str(" kernel_pool_phy_addr_start:");
put_int(kernel_pool.phy_addr_start);
put_str("\n");
put_str(" user_pool_bitmap_start:");
put_int((int)user_pool.pool_bitmap.bits);
put_str(" user_pool_phy_addr_start:");
put_int(user_pool.phy_addr_start);
put_str("\n");
/*将位图置为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;
bitmap_init(&kernel_vaddr.vaddr_bitmap);
put_str("memory pool init done!\n");
}
/*
在pf所指向的虚拟内存池中寻找pg_cnt空闲虚拟页,并分配之
*/
static void *vaddr_get(enum pool_flags pf, uint32_t pg_cnt)
{
int vaddr_start = 0, bit_idx_start = -1;
if (pf == PF_KERNEL)
{
/*内核虚拟内存池*/
// 寻找可用的连续页
bit_idx_start = bitmap_scan(&kernel_vaddr.vaddr_bitmap, pg_cnt);
if (bit_idx_start == -1)
return NULL;
// 如果找到就将这些连续空间设置为已分配
uint32_t cnt = 0;
while (cnt < pg_cnt)
bitmap_set(&kernel_vaddr.vaddr_bitmap, bit_idx_start + cnt++, 1);
vaddr_start = kernel_vaddr.vaddr_start + bit_idx_start * PG_SIZE;
}
else
{
/*用户虚拟内存池*/
}
return (void *)vaddr_start;
}
/*
在m_pool(物理内存池数据结构)所指向的物理内存池中寻找一页空闲的物理页,并分配之
*/
static void *palloc(struct pool *m_pool)
{
int bit_idx = bitmap_scan(&m_pool->pool_bitmap, 1);
if (bit_idx == -1)
return NULL;
bitmap_set(&m_pool->pool_bitmap, bit_idx, 1);
uint32_t page_phyaddr = ((bit_idx * PG_SIZE) + m_pool->phy_addr_start);
return (void *)page_phyaddr;
};
// 取出虚拟地址的高10位
#define PDE_IDX(addr) ((addr & 0xffc00000) >> 22)
// 取出虚拟地址的中间10位
#define PTE_IDX(addr) ((addr & 0x003ff000) >> 12)
/*
返回虚拟地址vaddr所代表的pte(页表项)的虚拟地址
此举是为了取出虚拟地址vaddr所代表的页表项的内容
*/
uint32_t *pte_ptr(uint32_t vaddr)
{
/*
0xffc00000:表示页目录表的第1023个页目录项,该页目录项的存储的是页目录表的物理地址(见loader.S中页目录项的初始化部分内容)
vaddr & 0xffc00000:取出vaddr的高10位,也就是vaddr所代表的页目录项的索引
先访问到页目录表自己 + 再用页目录项pde(页目录内页表的索引)作为pte的索引访问到页表 + 再用pte的索引做为页内偏移
*/
uint32_t *pte = (uint32_t *)(0xffc00000 + ((vaddr & 0xffc00000) >> 10) + PTE_IDX(vaddr) * 4);
return pte;
}
/*
返回虚拟地址vaddr所代表的pde(页目录项)的虚拟地址
此举是为了取出虚拟地址vaddr所代表的页目录项的内容
*/
uint32_t *pde_ptr(uint32_t vaddr)
{
/* 0xfffff是用来访问到页表本身所在的地址 */
uint32_t *pde = (uint32_t *)((0xfffff000) + PDE_IDX(vaddr) * 4);
return pde;
}
/*
页表中添加虚拟地址_vaddr和物理地址_page_phyaddr的映射关系
*/
static void page_table_add(void *_vaddr, void *_page_phyaddr)
{
uint32_t vaddr = (uint32_t)_vaddr, page_phyaddr = (uint32_t)_page_phyaddr;
uint32_t *pde = pde_ptr(vaddr);
uint32_t *pte = pte_ptr(vaddr);
/* 判断页目录内目录项的P位,若为1,则表示其指向的页表已存在,直接装填对应的页表项*/
if (*pde & 0x00000001)
{
// 如果页目录项P位为1,说明页目录项指向的页表存在,但是页表项不存在,则直接创建页表项
if (!(*pte & 0x00000001))
{
// 填充页目录项
*pte = (page_phyaddr | PG_US_U | PG_RW_W | PG_P_1);
}
else
{
PANIC("pte repeat");
*pte = (page_phyaddr | PG_US_U | PG_RW_W | PG_P_1);
}
}
else
{
/*
页目录项指向的页表不存在,需要首先构建页表,然后再装填对应的页目录项和页表项
1.在物理内存池中寻找一页物理页分配给页表
2.将该页表初始化为0,防止这块内存的脏数据乱入
3.将该页表写入页目录项
4.构建页表项,填充进页表
*/
// 在内核物理内存池找一页物理页分配给页表
uint32_t pde_phyaddr = (uint32_t)palloc(&kernel_pool);
// 初始化页表清零
/* 访问到pde对应的物理地址,用pte取高20位便可.
* 因为pte是基于该pde对应的物理地址内再寻址,
* 把低12位置0便是该pde对应的物理页的起始*/
memset((void *)((int)pte & 0xfffff000), 0, PG_SIZE);
// 将分配的页表物理地址写入页目录项
*pde = (pde_phyaddr | PG_US_U | PG_RW_W | PG_P_1);
ASSERT(!(*pte & 0x00000001));
// 构建页表项,填充进页表
*pte = (page_phyaddr | PG_US_U | PG_RW_W | PG_P_1);
}
}
/*
分配pg_cnt个页空间,成功也返回起始虚拟地址,失败则返回NULL
*/
void *malloc_page(enum pool_flags pf, uint32_t pg_cnt)
{
/*********** malloc_page的原理是三个动作的合成: ***********
1通过vaddr_get在虚拟内存池中申请虚拟地址
2通过palloc在物理内存池中申请物理页
3通过page_table_add将以上得到的虚拟地址和物理地址在页表中完成映射
***************************************************************/
ASSERT(pg_cnt > 0 && pg_cnt < 3840);
// 获取连续的pg_cnt个虚拟页
void *vaddr_start = vaddr_get(pf, pg_cnt);
if (vaddr_start == NULL)
return NULL;
uint32_t vaddr = (uint32_t)vaddr_start, cnt = pg_cnt;
struct pool *mem_pool = pf & PF_KERNEL ? &kernel_pool : &user_pool;
/*
逐一申请物理页,然后再挨个映射进虚拟页
之所以物理页不能一次性申请,是因为物理页可能是离散的
但是虚拟页是连续的
*/
while (cnt--)
{
void *page_phyaddr = palloc(mem_pool);
if (page_phyaddr == NULL)
return NULL;
page_table_add((void *)vaddr, page_phyaddr);
vaddr += PG_SIZE;
}
return vaddr_start;
}
/*
从内核物理内存池中申请pg_cnt页内存,成功则返回其虚拟地址,失败则返回NULL
*/
void *get_kernel_pages(uint32_t pg_cnt)
{
void *vaddr = malloc_page(PF_KERNEL, pg_cnt);
if (vaddr)
memset(vaddr, 0, pg_cnt * PG_SIZE);
return vaddr;
}
/*
内存管理初始化入口
*/
void mem_init(void)
{
put_str("mem_init start\n");
// 此处存放着物理内存容量,具体可见/boot/loader.S中total_mem_bytes的定义和计算
uint32_t mem_bytes_total = (*(uint32_t *)(0xb00));
// 初始化内存池
mem_pool_init(mem_bytes_total);
put_str("mem_init done\n");
}
然后我们在/kernel/main.c文件中测试
cpp
#include "print.h"
#include "init.h"
#include "debug.h"
#include "memory.h"
int main(void)
{
put_str("I am kernel\n");
init_all(); // 初始化中断并打开时钟中断
void *addr = get_kernel_pages(3);
put_str("\n get_kernel_pages start vaddr is:");
put_int((uint32_t)addr);
put_str("\n");
while (1)
;
return 0;
}
编译运行
cpp
mkdir -p bin
#编译mbr
nasm -o $(pwd)/bin/mbr -I $(pwd)/boot/include/ $(pwd)/boot/mbr.S
dd if=$(pwd)/bin/mbr of=~/bochs/hd60M.img bs=512 count=1 conv=notrunc
#编译loader
nasm -o $(pwd)/bin/loader -I $(pwd)/boot/include/ $(pwd)/boot/loader.S
dd if=$(pwd)/bin/loader of=~/bochs/hd60M.img bs=512 count=4 seek=2 conv=notrunc
#编译print函数
nasm -f elf32 -o $(pwd)/bin/print.o $(pwd)/lib/kernel/print.S
# 编译kernel
nasm -f elf32 -o $(pwd)/bin/kernel.o $(pwd)/kernel/kernel.S
#编译main文件
gcc-4.4 -o $(pwd)/bin/main.o -c -fno-builtin -m32 -I $(pwd)/lib/kernel/ -I $(pwd)/lib/ -I $(pwd)/kernel/ $(pwd)/kernel/main.c
#编译interrupt文件
gcc-4.4 -o $(pwd)/bin/interrupt.o -c -fno-builtin -m32 -I $(pwd)/lib/kernel/ -I $(pwd)/lib/ -I $(pwd)/kernel/ $(pwd)/kernel/interrupt.c
#编译init文件
gcc-4.4 -o $(pwd)/bin/init.o -c -fno-builtin -m32 -I $(pwd)/lib/kernel/ -I $(pwd)/lib/ -I $(pwd)/kernel/ $(pwd)/kernel/init.c
# 编译debug文件
gcc-4.4 -o $(pwd)/bin/debug.o -c -fno-builtin -m32 -I $(pwd)/lib/kernel/ -I $(pwd)/lib/ -I $(pwd)/kernel/ $(pwd)/kernel/debug.c
# 编译string文件
gcc-4.4 -o $(pwd)/bin/string.o -c -fno-builtin -m32 -I $(pwd)/lib/kernel/ -I $(pwd)/lib/ -I $(pwd)/kernel/ $(pwd)/lib/string.c
# 编译bitmap文件
gcc-4.4 -o $(pwd)/bin/bitmap.o -c -fno-builtin -m32 -I $(pwd)/lib/kernel/ -I $(pwd)/lib/ -I $(pwd)/kernel/ $(pwd)/lib/kernel/bitmap.c
# 编译memory文件
gcc-4.4 -o $(pwd)/bin/memory.o -c -fno-builtin -m32 -I $(pwd)/lib/kernel/ -I $(pwd)/lib/ -I $(pwd)/kernel/ $(pwd)/kernel/memory.c
#将main函数与print函数进行链接
ld -m elf_i386 -Ttext 0xc0001500 -e main -o $(pwd)/bin/kernel.bin $(pwd)/bin/main.o $(pwd)/bin/print.o $(pwd)/bin/init.o $(pwd)/bin/interrupt.o $(pwd)/bin/kernel.o $(pwd)/bin/memory.o $(pwd)/bin/bitmap.o $(pwd)/bin/string.o $(pwd)/bin/debug.o
#将内核文件写入磁盘,loader程序会将其加载到内存运行
dd if=$(pwd)/bin/kernel.bin of=~/bochs/hd60M.img bs=512 count=200 conv=notrunc seek=9
#rm -rf bin/*
运行结果如下所示
如图所示,屏幕上如预期打印了我们想要的信息
内核物理内存池的起始物理地址,为
0x200000
在main.c中申请到的连续三个页面的其实虚拟地址为
0xc0100000
。