验证缺页中断机制

问题背景

在x86系列的cpu上开启分页机制后,指令涉及到”内存读写”操作时,用到的”内存地址”都是”虚拟地址”。”mmu硬件”会按照”分段”、”分页”的计算规则,将”虚拟地址”转换成”物理地址”。最终指令可以对”物理地址”读写数据。

上面这段关于内存的”理论知识”,我觉得如果可以实践看一看,就能多一些”感觉”。

本文记录我对下面两个结论的调试和验证过程,也是我对极客时间的操作系统实战 45 讲课程中”内存管理”章节的部分学习和实践,基于第26课代码

“mmu硬件”在转换”虚拟地址”时有可能发生”缺页异常”,包括:

  • 访问”未向内核申请的虚拟地址”会导致”缺页异常”
  • 当第一次访问申请的虚拟地址时,也会产生”缺页异常”;第二次访问同样的虚拟地址时,不会产生”缺页异常”

验证过程

  • 访问”未向内核申请的虚拟地址”会导致”缺页异常”

    按照理论来说:访问未申请的虚拟地址时,”mmu硬件”在转换”虚拟地址”时会发出”缺页异常”(可能因为找不到对应的”页表项”)

    在验证这个理论前,先介绍一下cosmos(课程自制的内核系统名称)相关的代码,以方便读者大概理解文中出现的函数作用

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    void test_vadr()
    {
    adr_t vadr = vma_new_vadrs(&initmmadrsdsc, NULL, 0x1000, 0, 0); // 0x1000是想要申请的空间大小,vadr是申请的内存地址
    if(NULL == vadr)
    {
    kprint("分配虚拟地址空间失败\n");
    }
    ...
    hal_memset((void*)vadr, 0, 0x1000); // 初始化刚申请的内存为0
    ...
    }

    上面test_vadr是cosmos中用来验证”虚拟内存”相关功能的测试函数。

    在cosmos中,想要使用虚拟空间,需要先调用vma_new_vadrs申请一个虚拟地址。

    cosmos相关代码介绍完毕,下面用gdb调试课程代码,来实际验证一下”理论”:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    (gdb) break test_vadr   // 在 test_vadr 函数下断点(选择在test_vadr函数下断点,是因为此时 虚拟内存管理、物理内存管理、缺页异常处理 等功能都已经实现)
    Breakpoint 1 at 0xffff80000202444a: file ../kernel/krlvadrsmem.c, line 289.
    (gdb) continue
    Continuing.

    Breakpoint 1, test_vadr () at ../kernel/krlvadrsmem.c:289
    289 {
    (gdb) break krluserspace_accessfailed // 在 krluserspace_accessfailed 函数下断点
    Breakpoint 2 at 0xffff800002026588: file ../kernel/krlvadrsmem.c, line 1145.
    (gdb) call hal_memset(0x400,0,0x1) // 调用hal_memset函数,向 0x400地址 写一个字节内容

    Breakpoint 2, krluserspace_accessfailed (fairvadrs=0) at ../kernel/krlvadrsmem.c:1145 //
    ...
    (gdb) where // 可以看到 当前代码在../kernel/krlvadrsmem.c文件1145行的krluserspace_accessfailed
    #0 krluserspace_accessfailed (fairvadrs=0) at ../kernel/krlvadrsmem.c:1145
    #1 0xffff8000020135d2 in hal_fault_allocator (faultnumb=14, krnlsframp=0xffff80000008fe18)
    at ../hal/x86/halintupt.c:171
    #2 0xffff80000200d830 in exc_page_fault ()
    ...
    (gdb) continue
    Continuing.
    ^C // 因为在这里卡死了,所以我按了ctrl+c
    Program received signal SIGINT, Interrupt.
    hal_sysdie (errmsg=0xffff80000203a237 "缺页处理失败\n") at ../hal/x86/halcpuctrl.c:185
    185 ;

    从上面的调试信息可以看出来:cpu在执行hal_memset函数访问0x400虚拟地址时,跳到了krluserspace_accessfailed函数,最后卡死在halcpuctrl.c第185行

    image

    这里的0x400地址不是通过vma_new_vadrs申请的地址。

    krluserspace_accessfailed函数只有cpu遇到中断并且中断号为14(也就是”缺页异常”)的时候才会被调用

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    // 异常处理
    void hal_fault_allocator(uint_t faultnumb, void *krnlsframp) //eax,edx
    {
    ...
    if (faultnumb == 14) // 缺页异常 中断号是14
    {
    ...
    if (krluserspace_accessfailed(fairvadrs) != 0)
    {
    system_error("缺页处理失败\n");
    }
    ...
    }
    ...
    }

    所以可以推测出来:访问0x400时,mmu硬件发出了”缺页中断”,接着cpu处理”缺页中断”就会到krluserspace_accessfailed函数,因为krluserspace_accessfailed函数处理”页表映射”失败,所以会调用system_error函数,最终执行for循环卡死。

    这样可以看出来理论没有问题。

  • 访问”已申请的虚拟地址”

    按照理论:在第一次访问”已申请的虚拟地址”,因为”虚拟内存”和”物理内存”的映射关系还没写到”页表”中,所以”mmu硬件”在转换”虚拟地址”时会发出”缺页异常”,然后”缺页异常”处理程序会写这个映射关系到”页表”中;第二次访问同一个”已申请的虚拟地址”,因为”虚拟内存”和”物理内存”的映射关系已经写到”页表”,”mmu硬件”就可以正常转换,不会发出”缺页异常”。

    下面同样用gdb调试来验证一下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    (gdb) continue
    Continuing.

    Breakpoint 1, test_vadr () at ../kernel/krlvadrsmem.c:289
    289 {
    (gdb) break krluserspace_accessfailed
    Breakpoint 2 at 0xffff800002026588: file ../kernel/krlvadrsmem.c, line 1145.
    (gdb) x /bx 0x40000
    0x40000: Cannot access memory at address 0x40000
    (gdb) call vma_new_vadrs(&initmmadrsdsc,0x40000,0x1000, 0, 0) // 向内核申请虚拟空间,地址是0x40000,大小是0x1000
    $1 = 0
    (gdb) call hal_memset(0x40000,0,0x1) // 第一次访问0x40000时进入到"中断处理函数"

    Breakpoint 2, krluserspace_accessfailed (fairvadrs=0) at ../kernel/krlvadrsmem.c:1145
    ...
    (gdb) finish
    Run till exit from #0 krluserspace_accessfailed (fairvadrs=2097282) at ../kernel/krlvadrsmem.c:1145
    ...
    (gdb) finish
    Run till exit from #0 0xffff8000020135d2 in hal_fault_allocator (faultnumb=14,
    krnlsframp=0xffff80000008fe18) at ../hal/x86/halintupt.c:171
    0xffff80000200d830 in exc_page_fault ()
    (gdb) finish
    Run till exit from #0 0xffff80000200d830 in exc_page_fault ()
    (gdb) where
    #0 test_vadr () at ../kernel/krlvadrsmem.c:289
    ...
    (gdb) call hal_memset(0x40000,0,0x1) // 第二次访问0x40000时没有产生"中断"
    (gdb) x /bx 0x40000
    0x40000: 0x00

    从上面可以看到:调用vma_new_vadrs申请0x40000地址后,第一次调用hal_memset去访问0x40000时会进入到”缺页异常”处理程序(krluserspace_accessfailed函数)中,第二次再访问0x40000时就不会进入到”缺页异常”处理程序。

总结

通过调试,很容易对cosmos中”虚拟内存”访问时的业务流程做一点研究。

同样我们可以通过”调试”去观察”分页”机制,来验证课程中的知识点。

PS:后面会有一篇文章讲解怎么调试cosmos代码