1、内存管理存在问题
由于整个系统中内存资源有限,而应用程序的数据相当是无限的,操作系统需要管理这些有限的内容,合理地分配给各个程序使用。同时,在应用程序使用内存时还会存在多种问题。
86在硬件对实现这些功能提供了部分支持,操作系统利用硬件的机制完成上述功能。其主要有两方面的硬件支持:分段机制和分页机制
由于分段机制较为复杂,本课程中并未利用该功能解决上述问题,而只使用分页机制
1)分页处理
在分页机制中,CPU将内存看作一块固定大小的内存页,这个页大小是由硬件支持的4KB大小的页
分页机制:用于将程序所需的内存划分为固定的块,并将这些页动态映射到物理内存上的不同位置。
每个应用程序在运行起来后都有一个地址转换的页表,这个页表可以将进程访问的地址进行转换、转换到某个右侧的物理页,而进程自己看到的则是左侧的内存分布,即看到的是自己分布在连续内存空间的虚拟地址空间。
2、位图数据结构与初始化
位图是一种非常简单的用于标识某些状态的数据结构,它是很多个位组成的。由于每个位只有0、1两种值,所以可以用于标识一个内存页是否已经被使用。
使用了一小块内存用于存储位图空间,然后将该结构中的每个位于用于表示对应的4KB内存页是否已经被分配出去使用。
1)位图的分配
主要涉及了位图中位的清0、置位、读取以及空闲位的分配
bitmap_alloc_nbits实现算法简单、易于理解,但缺点也很明显,效率太低。每次都需要从头开始查找,感兴趣的同学可以自行实现更为快速方法。
2)位图的缺陷
在扩展性方面,如果允许一个内存页被重复分配出去多次,用于实现内存共享功能,采用位图就无法表示,还需要给内存加上一个计数,用于表示被分配出去多少次。
3、创建地址分配结构
课程中定义的addr_alloc_t结构,里面包含了位图,同时也包含了它所管理的物理内存区域的起始地址,大小,以及内存页的大小。
typedef struct _addr_alloc_t {
mutex_t mutex; // 地址分配互斥信号量
bitmap_t bitmap; // 辅助分配用的位图
uint32_t page_size; // 页大小
uint32_t start; // 起始地址
uint32_t size; // 地址大小
}addr_alloc_t;
后续有关内存的分配和释放,都由该结构以及相关处理函数来完成。
4、规划内存空间的分配
由于操作系统内核比较小,因此其重要的数据结构及代码放在1MB以下就可以了,1MB以上的内存给进程使用,按页分配
5、分页机制介绍
启动分页机制后,应用程序就看不到机器的物理内存了,看到的就是虚拟的,不存在的内存空间。
这块虚拟内存也是划分成了一个个4KB的内存页。操作系统会通过·页表将这些虚拟内存空间中连续的内存页,从物理内存空间中找空闲的物理页,简历一个映射关系。
就可以将多个不连续的物理页,变成了进程使用的连续内存页。这样进程就感觉自己在使用连续的内存页,而实际最终在物理内存的表现上却是实现不连续的内存页。
页表由CR3来管理
1)开启分页机制
由于一级页表太费内存,比如完整的4GB空间,需要4MB的内存空间来存储转换关系
因此采用二级页表。
需要的内存空闲大小为多大呢?4KB+4096*1024*4=4MB+4KB,对比一级页表显然要大的多。但是在绝大多数的情况下,并不需要的表示4GB整个地址范围的转换,而是可能只保存一部分的转换。从这个角度来看二级页表节省大量的内存。
特殊处理:本课程也采用恒等映射。在虚拟地址空间中的0~4MB,这块内容和原来的没开内存中的一模一样
6、内存层面
1)创建内核页表
设计目标是根据内核的lds(之前设置的)脚本中的设备,将其各处不同的区域进行映射到不同的权限下。例如代码和只读设置只读,其它设置成可读写。且所有这些内存区域都不能允许用户访问。所有这些功能都可使用页表完成
整个映射功能,由一下函数完成,注意分页是4KB,考虑对齐的问题
void create_kernel_table (void) {
extern uint8_t s_text[], e_text[], s_data[], e_data[];
extern uint8_t kernel_base[];
/*
_text[] 和 e_text[]:内核代码段(文本段)的起始和结束地址。
s_data[] 和 e_data[]:内核数据段的起始和结束地址。
kernel_base[]:内核的基地址,通常用于内核的栈区域或其他初始内存布局。
*/
static memory_map_t kernel_map[] = {
{kernel_base, s_text, 0, PTE_W}, // 内核栈区
{s_text, e_text, s_text, 0}, // 内核代码区
{s_data, (void *)(MEM_EBDA_START - 1), s_data, PTE_W}, // 内核数据区
};
/*
{kernel_base, s_text, 0, PTE_W}:映射内核栈区,起始虚拟地址是 kernel_base,结束地址是 s_text,物理地址起始于 0,属性为 PTE_W(假设表示可写权限)。
{s_text, e_text, s_text, 0}:映射内核代码区,起始和结束虚拟地址为 s_text 和 e_text,物理地址从 s_text 开始,属性为 0(假设表示只读)。
{s_data, (void *)(MEM_EBDA_START - 1), s_data, PTE_W}:映射内核数据区,起始虚拟地址为 s_data,结束地址为 MEM_EBDA_START - 1(高于数据段的区域),物理地址从 s_data 开始,属性为 PTE_W。
*/
// 清空页目录表
kernel_memset(kernel_page_dir, 0, sizeof(kernel_page_dir));
// 清空后,然后依次根据映射关系创建映射表
for (int i = 0; i < sizeof(kernel_map) / sizeof(memory_map_t); i++) {
memory_map_t * map = kernel_map + i;
// 可能有多个页,建立多个页的配置
// 简化起见,不考虑4M的情况
int vstart = down2((uint32_t)map->vstart, MEM_PAGE_SIZE);
int vend = up2((uint32_t)map->vend, MEM_PAGE_SIZE);
int page_count = (vend - vstart) / MEM_PAGE_SIZE;
memory_create_map(kernel_page_dir, vstart, (uint32_t)map->pstart, page_count, map->perm);
}
}
解释每行代码的含义
2)为进程创建页表
每个进程都有自己的页表,用于描述进程自己的虚拟地址空间到实际物理内存之间的转换关系。
这个页表的地址结构保存在TSS结构的CR3寄存器。每个进程的TSS结构中均有一个CR3字段,以允许每个进程拥有自己的页表。
将进程自己的0x80000000以下的地址全部设置成与kernel_page_dir(内核的虚拟地址)的一样。如此一样,系统中所有的进程由于都进行了这样的设置,因此所有进程在自己的虚拟地址空间中0x80000000的内容,看到的是模一样的。这部分的区域正好是由操作系统管理,因此所有进程都能看到操作系统的代码和数据。
但是由于不同的进程对0x80000000以下地址区域的映射方式不同,因此不同进程只看到彼此的内存和代码。这样便实现了操作系统的代码和数据由应用程序共享(所有任务共享同一个代码段和数据段),进程之间却能彼此隔离。
在创建内核映射表时,将对应的物理内存做了地址恒等映射