经过了从实模式到保护模式的跃迁之后,现在开始讨论操作系统内核一个很重要的组成部分——内存管理模块,由上节的探讨可知,内核现在的寻址过程为:虚拟地址->线性地址->物理地址,即经过ld链接的内核代码的虚拟地址要经过段机制得到线性地址,这里我们设置成了平坦模式,相当于间接跨过了段机制,访存的线性地址即等于虚拟地址,然后经过页表机制把线性地址经过MMU处理得到访存的物理地址。
接下来所要谈到的内存管理模块就包括物理内存管理、虚拟内存管理、堆动态内存管理三个方面去探讨。
在对内核完成内存管理之后,相信你再看下面这张图时会有不一样的收获,你会发现所有的东西几乎一目了然。
先来看第一个问题,为什么需要内存管理。首先需要明白我们的内核代码经过bootloader的引导之后是加载到计算机内存里面去的,也就是说我们的内核代码已经占据了一部分内存空间,这部分内存空间是为内核保留的,不能被其它程序任意修改。那么剩下的那一部分内存空间就需要操作系统去进行管理,假设在没有负责全局物理内存管理的操作系统上去调用malloc的话,一般就会炸!!!还有一个比较重要的原因就是在开启分页机制之后需要由操作系统去进行虚拟地址的映射,否则会page fault。
物理内存管理
显然就是对剩下的物理内存空间进行管理,把剩下的内存空间按照页大小进行组织,使用的是页管理栈,将内存按照页大小进行分割,然后将地址存储到页管理栈中,为后面的堆动态内存管理做好准备。
至于内核代码结束的位置,可以在ld链接脚本最后添加PROVIDE( kern_end = . );
,相当于定义了一个变量,然后在c文件里声明一下extern uint8_t kern_end[];
就可以使用了。
虚拟内存管理
主要实现线性地址的物理映射关系,构造内核的页目录和页表,在访问虚存的时候能够找到对应的物理内存地址就可以了。
堆动态内存管理
上面提到对剩下的内存进行了物理内存管理,管理的目的就是为了更好的去使用。堆动态内存管理主要就是实现malloc动态内存分配和free内存释放操作,管理结构使用的是双向链表,内存的分配主要涉及到的就是链表的操作。需要注意的一点是堆内存的起始地址定义的是0xE0000000,这里使用的是虚拟地址,即堆内存的动态分配地址是从0xE0000000到之后,分配的地址是连续增长的,但是对应的物理地址既可能不是连续的也可能不是增长的。这里的堆地址是在栈内存之后,堆和栈正好相向生长,栈向上生长,即向小地址生长,而堆向下生长,即向大地址方向,其间剩余部分是自由空间。
代码分析
物理内存管理
内核本身加载到物理内存的空间除外,这块内存必须是物理内存管理所保留的,对剩下的全局物理内存空间进行管理,按照页大小进行分割,并将内存块的起始地址存储在内存页面管理栈中。
//物理内存页面管理的栈
static uint32_t mm_stack[PAGE_MAX_SIZE];
//页面管理栈帧
static uint32_t mm_top = 0;
//物理内存页面数
static uint32_t mm_page_count = 0;
//初始化内存物理分页
void init_physical_mm()
{
//内存缓冲区地址和长度
uint32_t mm_addr_start = glb_mboot_ptr->mmap_addr;
//mmap_entry_t 的字节长度
uint32_t mm_addr_end = glb_mboot_ptr->mmap_addr + glb_mboot_ptr->mmap_length;
mmap_entry_t *mm_entry = (mmap_entry_t *)mm_addr_start;
for (; (uint32_t)mm_entry < mm_addr_end; mm_entry++)
{
// 如果是可用内存 ( 按照协议,1 表示可用内存,其它数字指保留区域 )
if ( mm_entry->type == 1 && mm_entry->base_addr_low == 0x100000)
{
// 把内核结束位置到物理内存结束位置的内存段,按页存储到页管理栈里
// 最多支持512MB的物理内存
uint32_t phy_addr = 0x100000 + (kern_end - kern_start);
uint32_t phy_end = 0x100000 + mm_entry->length_low;
//将物理内存按照页存储到页管理栈里
while(phy_addr < phy_end && phy_addr <= MM_MAX_SIZE)
{
//如果剩余内存不足一页则退出
if (phy_end - phy_addr < MM_PAGE_SIZE)
{
break;
}
mm_free_page(phy_addr);
phy_addr += MM_PAGE_SIZE;
mm_page_count++;
}
}
}
}
//分配内存物理页
uint32_t mm_alloc_page()
{
if (mm_top <= 0)
{
printk("alloc error : out of memory !");
return -1;
}
uint32_t page_addr = mm_stack[--mm_top];
return page_addr;
}
//释放内存物理页
void mm_free_page(uint32_t p_addr)
{
if (mm_top >= PAGE_MAX_SIZE)
{
printk("free error : out of stack !");
return;
}
mm_stack[mm_top++] = p_addr;
}
虚拟内存管理
主要实现虚拟地址va到物理地址pa的映射关系以及给你一个虚拟地址要返回给我实际的物理内存地址,便于物理内存页的释放,因为物理内存页的分配和释放都是对应的真实物理地址。
这里需要注意几点:
1.内核代码是按照虚拟地址进行编址的,而MMU虚拟内存映射需要物理地址,因此在把页目录的基地址加载到cr3寄存器时需要减去内核偏移0xC00000000。
2.要有个概念页表的存储页需要内存空间,为页表申请物理内存页空间p_table_new = (p_table_t *)mm_alloc_page();,这里内核页目录数组的大小是#define P_DIR_INDEX_SIZE 1024个,是全局变量,页目录所有空间都存储在静态区,而页表是二维数组,每个页表中包含1024个页表项,而且并不是所有的页表都存储在全局静态区,即有一部分页表的地址映射是动态申请的,占用的是内核剩余部分的内存空间,可以说是和堆区去抢内存空间。
3.需要注意页目录的结构是:页表基地址|flags。在get_mapping得到地址映射的时候要&PAGE_MASK去清除flags项,PAGE_MASK是页掩码即一个页的大小,这里#define PAGE_MASK 4096。
4.可以用下面的代码进行地址映射测试,如果输出map_test:0
则测试成功,如果page falut
则测试失败。
uint32_t addr = mm_alloc_page();
map(p_kern_dir,0xE0000000,addr,PAGE_PRESENT | PAGE_WRITE);
uint32_t *data = (uint32_t *)0xE0000000;
printk("map_test:%x\n",*data);
关于虚拟内存管理的相关函数如下:
// 使用 flags 指出的页权限,把物理地址 pa 映射到虚拟地址 va
void map(p_dir_t *p_dir_ptr,uint32_t va,uint32_t pa,uint32_t flags)
{
uint32_t p_dir_index = GET_DIR_INDEX(va);
uint32_t p_table_index = GET_TABLE_INDEX(va);
//这里一定要记住&PAGE_MASK!!! 因为后面有页内存flag!!!
p_table_t *p_table_new = (p_table_t *)(p_dir_ptr[p_dir_index] & PAGE_MASK);
//取对应的页目录项,看是否已经映射过
//对于已经映射过的不用申请页表
if(p_table_new != 0)
{
//转换到内核线性地址
p_table_new = (p_table_t *)((uint32_t)p_table_new + KERN_OFFSET);
}
else
{
//为页表申请空间
p_table_new = (p_table_t *)mm_alloc_page();
//挂载到页目录上
p_dir_ptr[p_dir_index] = (uint32_t)p_table_new | PAGE_PRESENT | PAGE_WRITE;
//将申请的页表转换到内核线性地址并清0
p_table_new = (p_table_t *)((uint32_t)p_table_new + KERN_OFFSET);
memset(p_table_new, 0, P_TABLE_INDEX_SIZE * 4);
}
//设置页表项
p_table_new[p_table_index] = (pa & PAGE_MASK) | flags;
// 通知 CPU 更新页表缓存
asm volatile ("invlpg (%0)" : : "a" (va));
}
// 取消虚拟地址 va 的物理映射
void unmap(p_dir_t *p_dir_ptr, uint32_t va)
{
uint32_t p_dir_index = GET_DIR_INDEX(va);
uint32_t p_table_index = GET_TABLE_INDEX(va);
p_table_t *p_table_new = (p_table_t *)(p_dir_ptr[p_dir_index] & PAGE_MASK);
//没有映射
if (p_table_new == 0)
{
return;
}
//转换到内核线性地址
p_table_new = (p_table_t *)((uint32_t)p_table_new + KERN_OFFSET);
//取消映射
p_table_new[p_table_index] = 0;
// 通知 CPU 更新页表缓存
asm volatile ("invlpg (%0)" : : "a" (va));
}
//得到虚拟地址的物理映射地址
void get_mapping(p_dir_t *p_dir_ptr,uint32_t va,uint32_t *pa)
{
uint32_t p_dir_index = GET_DIR_INDEX(va);
uint32_t p_table_index = GET_TABLE_INDEX(va);
p_table_t *p_table = (p_table_t *)(p_dir_ptr[p_dir_index] & PAGE_MASK);
if (p_table == 0)
{
return;
}
//转换到内核线性地址
p_table = (p_table_t *)((uint32_t)p_table + KERN_OFFSET);
//返回对应的物理地址
*pa = p_table[p_table_index];
}
堆动态内存管理
对剩余的内存空间进行动态分配,主要实现对内存的malloc和free操作。堆内存管理头是用双向链表的方式组织的,即每个管理头的空间都需要动态申请,内存申请和释放的时候要通过该结构去管理,那么问题来了,管理内存动态申请空间的管理头的空间也需要动态申请,陷入死循环,这里把申请的空间先放置管理头,然后减去管理头的空间返回给申请者,管理头的大小为12byte,即sizeof(heap_header_t)。
主要涉及的就是链表的相关操作,可以在纸上画一画基本上就清楚了。
//堆内存管理头,全局变量,记录堆内存管理头的起始地址
heap_header_t *glb_heap_header;
//堆内存最大位置,尾部
uint32_t glb_heap_max;
//申请堆内存空间
void *mallock(uint32_t len)
{
heap_header_t *cur_heap_header = glb_heap_header;
//指向堆内存最后一个管理结构
heap_header_t *rear_heap_header = glb_heap_header;
//申请长度需要加上堆内存管理头
len += sizeof(heap_header_t);
while(cur_heap_header)
{
if (cur_heap_header->allocated == 0 && cur_heap_header->length >= len)
{
goto todo_split;
}
rear_heap_header = cur_heap_header;
cur_heap_header = cur_heap_header->next;
}
//现有内存堆不能满足内存申请需求,需要从全局物理内存管理中申请内存页
//这里传递的一定是最后一个内存管理结构,这样申请的内存页才能链接上
cur_heap_header = alloc_chunk(rear_heap_header,len);
if (cur_heap_header == 0)
{
//物理内存不足
return 0;
}
//第一次申请内存页则初始化内存管理结构头指针
if (glb_heap_header == 0)
{
glb_heap_header = cur_heap_header;
}
todo_split:
//按照申请内存长度切割内存块
split_chunk(cur_heap_header,len);
cur_heap_header->allocated = 1;
//返回的指针在管理结构之后
return (void *)((uint32_t)cur_heap_header + sizeof(heap_header_t));
}
//向全局物理内存申请内存页
static heap_header_t *alloc_chunk(heap_header_t *rear_heap_header,uint32_t len)
{
//添加堆内存管理头用
uint32_t heap_max_old = glb_heap_max;
uint32_t chunk_base = (uint32_t)rear_heap_header;
if (rear_heap_header == 0)
{
chunk_base = HEAP_BASE;
}
else if (rear_heap_header != 0 && rear_heap_header->allocated == 1)
{
//存在管理节点,但是最后一块已经被分配
chunk_base += rear_heap_header->length;
}
//申请内存页
while(chunk_base + len > glb_heap_max)
{
uint32_t page_addr = mm_alloc_page();
if (page_addr == -1)
{
//物理内存不足
return 0;
}
map(p_kern_dir, glb_heap_max, page_addr, PAGE_PRESENT | PAGE_WRITE);
glb_heap_max += MM_PAGE_SIZE;
}
//最后一块内存仍有可用空间
if (rear_heap_header != 0 && rear_heap_header->allocated == 0)
{
rear_heap_header->length = glb_heap_max - (uint32_t)rear_heap_header;
return rear_heap_header;
}
else
{
heap_header_t *heap_header_new = (heap_header_t *)heap_max_old;
//内存块链接 如果不是0则肯定存在前驱
if (rear_heap_header != 0)
{
//申请内存页存在前驱
rear_heap_header->next = heap_header_new;
heap_header_new->prev = rear_heap_header;
}
else
{
heap_header_new->prev = 0;
}
heap_header_new->next = 0;
//为新申请的内存块分配长度
heap_header_new->allocated = 0;
heap_header_new->length = glb_heap_max - heap_max_old;
return heap_header_new;
}
}
//按照申请长度切割内存块
static bool_t split_chunk(heap_header_t *cur_heap_header,uint32_t len)
{
//切割之前保证剩下的内存块能够容纳一个内存管理块的大小
if (cur_heap_header->length - len > sizeof(heap_header_t))
{
//切割无非就是添加内存管理块然后链接之
//注意这里一定是(uint32_t)cur_heap_header!!!
heap_header_t *heap_header_new = (heap_header_t *)((uint32_t)cur_heap_header + len);
//内存块链接
if (cur_heap_header->next)
{
//被切割内存块存在后继
cur_heap_header->next->prev = heap_header_new;
heap_header_new->next = cur_heap_header->next;
}
else
{
heap_header_new->next = 0;
}
heap_header_new->prev = cur_heap_header;
cur_heap_header->next = heap_header_new;
//为新切割的内存块分配长度
heap_header_new->allocated = 0;
heap_header_new->length = cur_heap_header->length - len;
//修改当前内存块长度
cur_heap_header->length = len;
return true_t;
}
return false_t;
}