虚拟内存管理和保护模式

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以下地址区域的映射方式不同,因此不同进程只看到彼此的内存和代码。这样便实现了操作系统的代码和数据由应用程序共享(所有任务共享同一个代码段和数据段),进程之间却能彼此隔离。

在创建内核映射表时,将对应的物理内存做了地址恒等映射

  • 16
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值