跟我一起写操作系统

    返回首页    发表留言
本文作者:李德强
          第一节 内存分页
 
 

        内存分页机制是实现虚拟内存的基础,它通过把现有的物理内存按一定的大小分成多个区域,这样的内存区域被称作“页面”。我们把每个页面大小设定为4kb,也就是4096字节。并通过页目录和页表来描述内存的分页使用情况。在开启分页之前要先设置好页目录和页表的内容,并为cr3寄存器设置页目录所在的物理内存地址。这样程序就会通过cr3寄存器找到页目录,进而找到页表,来管理和查看内存的使用情况。页目录和页表的格式如下:



 

  • P:页面是在内存中1,还是不在内存中0,如果不在内存中触发页面失效异常。

  • R/W:页面是只读0,还是可写1。

  • U/S:是普通用户0,还是超级用户1。

  • X:保留,设置为0。

  • D:保留,设置为0。

  • A:页面没有被存取0,还是已经被存取1。

  • Not Used:提供用户自定义。

  • PageTable's Physical Address:页表所在物理内存地址的高20位。

  • Page's Physical Address:页面所在物理内存地址的高20位。

        由于页目录和页表的地址域只有20位,所以对内存分页时每个页面要以4kb对齐。将页面的物理

        地址的高20位存入页表中,再将页表的物理地址的高20位存入页目录中,最后将页目录的物理地址存入cr3寄存器中。我们预计为每一个任务都分配4G的逻辑内存,于是page_no(页面数)= 4G / 4K = 1M也就是说一共需要1M个页面,这也正是页表中20个地址位所能够表示的数量( )。我们需要在内存中定义1024个页目录,每个页目录中存放了一组页表的首地址,每组有1024个页表,每个页表中存放了页面物理地址的高20位,如下图:



 

        在CPU开启分页之后,对于一个给定的逻辑地址,CPU在寻址时首先查看当前使用的GDT或LDT,做权限和长度检查,并将GDT或LDT中的基地址相加得到一个线性地址,再把这个线性地址除以页面大小(4096)等到页面号,再检查此页是否在内存中,如果此页不在内存中或权限不够,触发失效异常。在这个页面失效异常的处理程序中将再次检查这个失效页是尚未分配还是已经分配,如果是尚未分配,则为其分配一个新页面;如果是已经分配,说明此页面被换出到交换分区中,这时需要将此页面从交换分区中换回到内存当中。逻辑地址的高10位(22 ~ 31bit)表示的是页目录的索引号,中间10位(12 ~ 21bit)表示的是当前页表的索引号,低12位(0 ~ 11bit)表示的是页内偏移地址。

        例如:对于一个逻辑地址0xEF32F4A8,CPU对其地址解析的过程如下:

  1. 从cr3寄存器中读出页目录所在地址。

  2. 计算出逻辑地址0xEF32F4A8高10位的值为0x3BC。

  3. 取得页目录第0x3BC项的值,假设其值为0xEF5EAC007。

  4. 取得此页目录值的高20位得到页表地址0xEF5EAC000。

  5. 计算出逻辑地址0xEF32F4A8中间10位的值为0x32F。

  6. 取得0xEF5EAC000处页表组的第0x32F项的值,假设其值为0x1FEC5007。

  7. 取得此页表值的高20位等到页面地址0x1FEC5000。

  8. 计算出逻辑地址0xEF32F4A8的低12位的值为0x4A8即:页内偏移地址。

  9. 将页面地址0x1FEC5000加上内偏移地址0x4A8得到的就是实际物理地址0x1FEC54A8。

        需要说明的是,为了使逻辑地址被CPU处理起来更加的简单,我们采用的LDT段地址均为0 ~ 4G,也就是说逻辑地址与线性地址一致,也就省去了CPU从逻辑地址到线性地址的转换过程。当CPU遇到一个不存在的页或页表或者权限冲突,页面无效异常程序被执行。CR2存储了导致这个异常的逻辑地址,错误码被压入栈,格式如下:



 

        在开启分页之前首先要处理好默认的页目录和页表,并设置好cr3寄存器的值。也就是说在开启分页之前要先让cr3载入正确的页目录地址。

        定义页目录的起始地址:

#define PAGE_DIR                        (0x700000)
#define PAGE_TABLE                      (PAGE_DIR + 0x1000)
//页目录开始于 [0x700000, 0x701000) ,大小为1024个(每个4字节)共4096字节
u32 *page_dir = ((u32 *) PAGE_DIR);
//页表1开始于0x700000 + 0x1000 = 0x701000
u32 *page_table = ((u32 *) PAGE_TABLE);

        这里的page_dir和page_table与alloc.c中的MMAP一样是由系统内核静态分配的。我们设定小于16M的内存已经被使用了(实际上操作系统内核使用内存1M,其它为MMAP、PAGE_DIR、PAGE_TAB所使用),所以将page_dir的前4项设置为常驻内存,另外任务A和任务B在被安装到系统中时,它们都申请了一部分内存用于存放pcb的内容和它们所使用的代码段、数据段。这一区域的内存也要被设置为已在内存中,所以暂时将32M以下的内存也设置为已在内存中(这不是一个好主意,我们将在下一节中学习如何利用缺页异常来动态管理页面),所以page_dir的前8项被设置:

//用于内存地址计算
u32 address = 0;
//处理所有页目录
//页表开始于 [0x701000, 0xb01000) ,大小为4M
for (int i = 0; i < 1024; i++)
{
        if (i < 8)
        {
                for (int j = 0; j < 1024; j++)
                {
                        page_table[j] = address | 7;
                        address += MM_PAGE_SIZE;
                }
                page_dir[i] = ((u32) page_table | 7);
                page_table += 1024;
        }
        else
        {
                page_dir[i] = (6);
        }
}

        设置页目录的值到cr3寄存器:

__asm__ volatile("movl        %%eax, %%cr3" :: "a"(PAGE_DIR));

        开启内存分页,其实就是将cr0寄存器的31位置为1:

__asm__ volatile(
                "movl        %cr0, %eax;"
                "orl        $0x80000000, %eax;"
                "movl    %eax, %cr0;"
);

        再将任务B的代码中做一点修改,让其尝试访问32M以上的内存:

void run_B()
{
        //尝试读入32M以上的内存区域
        char *ps = (char *) 0x2000000;
        //这里是真正读内存,会触发缺页异常
        char ch = *ps;
        while (1)
        {
        }
}

        然后在安装任务A和任务B时,为它们tss中的cr3寄存器都置为PAGE_DIR(同样这也不是一个好注意,但这里我们也只是为了学习分页机制的原理,关于让每个任务都有自己能够独立使用的逻辑内存的问题我们会在下一节中学习):

pcb_A->tss.cr3 = PAGE_DIR;
pcb_B->tss.cr3 = PAGE_DIR;

        再在int.S中加入处理缺页异常的处理程序:

//页错误
_int_0x0e:
        cli
        //C函数中参数和变量所用的栈
        push        %ebp
        mov         %esp, %ebp
        //调用C函数来处理这个异常
        call        int_page_error
        leave
        sti
        iret

        在int_page_error中只是显示了缺页异常的提示信息,并没有对缺页做任何的处理:

void int_page_error()
{
        printf("int_page_error.\n");
        hlt();
}

        最后在Makefile中加入page.c的相关编译选项,编译并运行查看结果:


 

        可以看到,任务A正常运行并在屏幕的右下角显示了一个字符,但任务B要访问32M以上的内存区域,但这一内存页并不在内存中,于是出现了“int_page_error.”的缺页异常。
 

        源代码的下载地址为:

https           https://github.com/magicworldos/lidqos.git 
git             git@github.com:magicworldos/lidqos.git 
subverion       https://github.com/magicworldos/lidqos 
branch          v0.15

 

    返回首页    返回顶部
#1楼  雷锋vvv  于 2017年12月05日15:36:36 发表
 
页目录和页表的格式中P的值为0表示不在内存中吧,为1表示在内存中吧,不然和你程序相驳啊
#2楼  李德强  于 2017年12月09日20:26:04 发表
 
页表在初始化时,都被设定为page_table[j] = address | 7;也就是说这些页表的地址默认都不在内存中(尚未分配或被交换到外存),当程序尝试访问这个地址时,会触发异常,在异常处理程序中可以根据情况为其分配新的内存页或从外存中交换到内存。
#3楼  我是一楼那个yin  于 2017年12月17日12:12:59 发表
 
可是本章节任务B中if (i < 8)不是限定页表地址范围0x0~0x1999999内容为address | 7嘛,在process.c中,访问0x2000000地址才会触发缺页异常,事实证明确实触发了,说明address | 6 这段代码起作用才对吧
#4楼  我还是一楼那个yin  于 2017年12月17日12:27:38 发表
 
补充一点 我本人测试在process任务B访问了0x1ffffff(上条评论这里打错了),确实没有触发缺页异常呀
#5楼  李德强  于 2017年12月18日09:23:27 发表
 
非常感谢您的提醒!的确是我搞错了,应该是:“P:页面是在内存中1,还是不在内存中0,如果不在内存中触发页面失效异常。”

此节内容已经修正。
  看不清?点击刷新

 

  Copyright © 2015-2018 问渠网 辽ICP备15013245号