xv6 页面错误处理机制

好像没啥用的一些东西,之前想找个时间整理一下,结果弄明白之后懒得写了。

trap

第三章 陷入,中断和驱动程序 | xv6 中文文档 (gitbooks.io)

在执行 lw a3,0(zero) 产生 page fault 后,到 panic() 被调用这段时间发生了什么?

在 RISC-V 架构的 xv6 操作系统中,当执行类似 lw a3, 0(zero) 的指令导致页面错误(page fault)时,系统会通过以下几个步骤处理该异常,最终导致 panic() 的调用。下面详细说明从页面错误产生到调用 panic() 的过程。

当执行类似 lw a3, 0(zero) 的指令在 RISC-V 架构的 QEMU 模拟器上运行时,因虚拟地址 0x0 无效,会触发 页面错误(page fault)。QEMU 和 xv6 协同工作来检测和处理这一错误,从错误发生到调用 panic() 的全过程可以分为几个主要步骤。以下将深入阐述这段时间 QEMU 和 xv6 各自做了什么:

1. 指令执行:QEMU 模拟 CPU 行为

QEMU 作为 RISC-V CPU 的模拟器,负责模拟指令的执行。当执行指令 lw a3, 0(zero) 时:

  • lw 指令尝试从虚拟地址 0x0 加载数据到寄存器 a3 中。
  • 由于虚拟地址 0x0 是无效的(通常情况下,这一地址并未被映射到合法的物理内存),这会导致 页面错误

QEMU 负责检查虚拟地址并确保其有效性,具体通过其内存管理单元(MMU)模块来仿真地址转换和权限检查。

2. QEMU 的地址转换与页面错误检测

QEMU 模拟 RISC-V 处理器的分页机制。当指令试图访问内存时,QEMU 的 MMU 会根据当前的页表将虚拟地址转换为物理地址,并检查页面的有效性和权限。这个过程是 RISC-V 处理器硬件完成的部分,但在 QEMU 中由软件仿真。

  • 地址检查:QEMU 通过页表判断虚拟地址 0x0 是否已经映射到有效的物理地址。
  • 权限检查:即使地址被映射,QEMU 也会检查访问权限,比如是否有读取、写入、执行的权限。

在这个例子中,虚拟地址 0x0 既没有映射,也没有权限,因此 QEMU 检测到 页面错误

3. QEMU 触发异常并设置异常寄存器

一旦 QEMU 检测到页面错误,它会模拟 RISC-V 处理器的异常处理机制,触发一个 页面错误异常。QEMU 的异常处理流程包含以下步骤:

  • 设置寄存器

    • scause 寄存器:设置为页面错误的异常原因。对于加载(load)操作导致的页面错误,scause 的值为 0x0f(Load Page Fault)。
    • sepc 寄存器:保存当前正在执行的指令的地址,即 lw a3, 0(zero) 指令所在的地址。
    • stval 寄存器:保存导致页面错误的虚拟地址,在此情况下为 0x0

    QEMU 将这些寄存器设置为反映异常的具体信息。

  • 跳转到异常处理程序: 根据 RISC-V 架构,当异常发生时,处理器会跳转到操作系统定义的异常处理程序。这个跳转由 stvec 寄存器指定,该寄存器在 xv6 中设置为内核的异常处理入口地址 kernelvec

4. 进入 xv6 的异常处理程序

现在,QEMU 已经完成了对页面错误异常的仿真,控制权交给了 xv6 操作系统。此时,xv6 的内核接手处理异常。

进入 kernelvec: 当发生异常时,处理器会跳转到 kernelvec,这是一个汇编程序,负责从用户态切换到内核态,并保存当前的进程状态。这个过程包括:

  • 保存用户进程的寄存器状态到内核栈,以便在处理异常后可以恢复进程执行。
  • 将异常的关键信息(例如 scause, sepc, stval)传递给 C 语言编写的异常处理函数 trap()
.globl kernelvec
kernelvec:
  // 保存当前的寄存器状态
  // 跳转到 trap() 处理函数

调用 trap(): 通过 kernelvec 的汇编代码,系统最终会进入 C 语言的 trap() 函数,trap()xv6 中用于处理所有中断和异常的核心函数。

5. xv6trap() 处理页面错误

trap() 函数中,xv6 会根据 scause 的值判断异常的类型:

  • 检查 scause

    trap()

    函数通过读取

    scause

    寄存器的值,判断当前异常是何种类型。

    • 对于页面错误,scause 会包含一个表示页面错误的值,如 0x0f(Load Page Fault)。
if ((r_scause() & 0xff) == 0xf) {  // Load Page Fault
  // 页面错误处理逻辑
} else {
  panic("unexpected trap");
}

在此例中,trap() 确认是 Load Page Fault,由于虚拟地址 0x0 无效,xv6 无法处理这个页面错误。于是,它打印相关的寄存器信息,进入下一步的错误处理。

6. 打印调试信息

在决定系统无法处理页面错误后,trap() 会打印寄存器的信息以便调试。这些信息包括:

  • scause: 导致异常的原因。
  • sepc: 发生异常的指令地址。
  • stval: 导致异常的地址值(在此例中为 0x0)。
printf("scause 0x%016lx\n", r_scause());
printf("sepc=0x%016lx\n", tf->epc);
printf("stval=0x%016lx\n", r_stval());

7. 调用 panic() 停止系统

在打印完调试信息后,由于页面错误不可恢复,xv6 会调用 panic() 停止系统。panic() 的实现如下:

void panic(char *s) {
  cli();  // 关闭中断
  cprintf("panic: %s\n", s);  // 打印错误消息
  for(;;) ;  // 进入死循环,停止系统
}
  • 关闭中断cli() 函数关闭所有中断,防止系统在处理 panic 时受到干扰。
  • 打印错误信息:通过 cprintf() 打印 panic 消息,帮助开发人员调试问题。
  • 死循环:系统进入无限循环停止运行,防止进一步的损坏。

8. QEMU 进入死循环,结束仿真

xv6 进入 panic() 的死循环后,QEMU 作为仿真器也停止继续仿真 CPU 指令,系统陷入一个无响应的状态。此时,QEMU 会持续仿真死循环中的操作,但系统已经不再进行有效的任务。

总结:QEMU 和 xv6 的协同工作

  1. QEMU 检测页面错误:QEMU 通过其 MMU 仿真模块检测到无效的虚拟地址(0x0),并触发页面错误异常,设置寄存器 scause, sepc, 和 stval
  2. QEMU 跳转到异常处理程序:QEMU 仿真处理器将控制权交给 xv6 的异常处理入口 kernelvec,开始处理异常。
  3. xv6 的异常处理xv6 通过 trap() 检查 scause,确认是页面错误,并打印相关调试信息。
  4. 系统崩溃并调用 panic():由于无法处理页面错误,xv6 调用 panic() 停止系统运行,QEMU 进入死循环仿真。

QEMU 在这个过程中扮演了硬件模拟的角色,负责检测和触发异常;xv6 负责异常的具体处理和崩溃后的错误报告。这种协同使得开发者可以在仿真环境下调试复杂的操作系统问题。


为了深入理解 QEMU 和 xv6 在处理页面错误以及调用 panic() 的过程中各自执行了哪些操作,我将逐个部分大体介绍对应功能的代码实现位置。

1. 指令执行:QEMU 模拟 CPU 行为

在 QEMU 中,指令的执行由 CPU 仿真模块 负责,这里会调用与 RISC-V 架构相关的 CPU 仿真函数。

  • target/riscv/:QEMU 的 RISC-V 处理器仿真实现位于 target/riscv/ 目录。

  • riscv_translate.c:此文件中包含指令翻译和执行的逻辑,比如 lw 指令会被解析为相应的加载操作。

    • riscv/translate.c.inc:这个文件中包含 QEMU 对 RISC-V 指令的模拟执行逻辑。

QEMU 在执行 lw a3, 0(zero) 这条指令时,会进行内存访问,随后通过 MMU 执行虚拟地址到物理地址的转换。

2. QEMU 的地址转换与页面错误检测

QEMU 的 MMU(内存管理单元) 模块负责处理虚拟地址到物理地址的转换,并检查访问权限。

  • target/riscv/cpu_helper.c:QEMU 中的虚拟地址转换逻辑通常在 cpu_helper.c 中实现,这里包括访问虚拟内存的函数如 riscv_cpu_tlb_fill,它会进行地址检查。
  • accel/tcg/cputlb.c:QEMU 的 TLB(转换后备缓冲区)逻辑会用于加速地址转换,内存访问通过 TLB 缓存来快速进行映射。
  • 如果地址无效,QEMU 会触发页面错误,并调用异常处理函数。

3. QEMU 触发异常并设置异常寄存器

一旦检测到页面错误,QEMU 会通过设置 RISC-V 的异常寄存器并跳转到异常处理代码:

  • target/riscv/cpu.c:QEMU 中设置异常寄存器的代码位于 target/riscv/cpu.c。这里会设置 scause, sepc, stval 等寄存器值。例如,函数 riscv_cpu_do_interrupt() 会处理各种异常,并设置相应的寄存器状态。
  • riscv_cpu_do_interrupt():QEMU 调用这个函数来模拟 RISC-V CPU 中的异常处理机制,并设置寄存器值,准备进入操作系统的异常处理程序。

4. 进入 xv6 的异常处理程序

QEMU 完成对寄存器的设置后,控制权交给 xv6,通过跳转到 kernelvec 处理异常:

  • kernel/trampoline.Sxv6 中的异常处理入口是 kernelvec,这是汇编代码,它的实现位于 xv6kernel/trampoline.S 文件中。这里定义了从用户态到内核态的上下文切换代码。
  • kernelvec 保存了异常时的寄存器状态,并调用 C 函数 trap() 处理异常。

5. xv6trap() 处理页面错误

trap()xv6 的核心异常处理函数,负责分析异常并根据情况进行处理:

  • kernel/trap.ctrap() 函数的实现位于 xv6kernel/trap.c 文件中。在 trap() 中,根据 scause 寄存器的值判断异常类型(页面错误),并执行相应的处理逻辑。
void trap() {
  if ((r_scause() & 0xff) == 0xf) {
    // Handle page fault here
  } else {
    panic("unexpected trap");
  }
}

6. 打印调试信息

在无法处理页面错误时,trap() 函数会调用 printf() 打印相关的调试信息,包括 scause, sepc, 和 stval

  • kernel/printf.cprintf() 函数的实现位于 xv6kernel/printf.c 文件中,这里会负责输出调试信息。
  • printf 调用会输出寄存器的值,如 scause, sepc, stval

7. 调用 panic() 停止系统

xv6 确认无法处理页面错误后,最终会调用 panic() 停止系统运行:

  • kernel/printf.cpanic() 函数也位于 xv6kernel/printf.c 中。panic() 会关闭中断,打印错误信息,并进入死循环停止系统执行。
void panic(char *s) {
  cli(); // Disable interrupts
  cprintf("panic: %s\n", s);
  for(;;);
}

8. QEMU 进入死循环,结束仿真

xv6 进入 panic() 的死循环后,QEMU 会持续模拟这个死循环:

  • QEMU 不会中止模拟,它会继续模拟 CPU 执行死循环。这是通过 QEMU 中的 CPU 仿真主循环实现的,位于 accel/tcg/translate-all.c 中的 cpu_exec() 函数中,QEMU 会不断执行同一个指令,进入无限循环。

为了深入理解 QEMU 和 xv6 在处理页面错误以及调用 panic() 的过程中各自执行了哪些操作,我将逐个部分大体介绍对应功能的代码实现位置。

1. 指令执行:QEMU 模拟 CPU 行为

在 QEMU 中,指令的执行由 CPU 仿真模块 负责,这里会调用与 RISC-V 架构相关的 CPU 仿真函数。

  • target/riscv/:QEMU 的 RISC-V 处理器仿真实现位于 target/riscv/ 目录。
  • riscv_translate.c:此文件中包含指令翻译和执行的逻辑,比如 lw 指令会被解析为相应的加载操作。
  • riscv/translate.c.inc:这个文件中包含 QEMU 对 RISC-V 指令的模拟执行逻辑。

QEMU 在执行 lw a3, 0(zero) 这条指令时,会进行内存访问,随后通过 MMU 执行虚拟地址到物理地址的转换。

2. QEMU 的地址转换与页面错误检测

QEMU 的 MMU(内存管理单元) 模块负责处理虚拟地址到物理地址的转换,并检查访问权限。

  • target/riscv/cpu_helper.c:QEMU 中的虚拟地址转换逻辑通常在 cpu_helper.c 中实现,这里包括访问虚拟内存的函数如 riscv_cpu_tlb_fill,它会进行地址检查。
  • accel/tcg/cputlb.c:QEMU 的 TLB(转换后备缓冲区)逻辑会用于加速地址转换,内存访问通过 TLB 缓存来快速进行映射。
  • 如果地址无效,QEMU 会触发页面错误,并调用异常处理函数。

3. QEMU 触发异常并设置异常寄存器

一旦检测到页面错误,QEMU 会通过设置 RISC-V 的异常寄存器并跳转到异常处理代码:

  • target/riscv/cpu.c:QEMU 中设置异常寄存器的代码位于 target/riscv/cpu.c。这里会设置 scause, sepc, stval 等寄存器值。例如,函数 riscv_cpu_do_interrupt() 会处理各种异常,并设置相应的寄存器状态。
  • riscv_cpu_do_interrupt():QEMU 调用这个函数来模拟 RISC-V CPU 中的异常处理机制,并设置寄存器值,准备进入操作系统的异常处理程序。

4. 进入 xv6 的异常处理程序

QEMU 完成对寄存器的设置后,控制权交给 xv6,通过跳转到 kernelvec 处理异常:

  • kernel/trampoline.Sxv6 中的异常处理入口是 kernelvec,这是汇编代码,它的实现位于 xv6kernel/trampoline.S 文件中。这里定义了从用户态到内核态的上下文切换代码。
  • kernelvec 保存了异常时的寄存器状态,并调用 C 函数 trap() 处理异常。

5. xv6trap() 处理页面错误

trap()xv6 的核心异常处理函数,负责分析异常并根据情况进行处理:

  • kernel/trap.ctrap() 函数的实现位于 xv6kernel/trap.c 文件中。在 trap() 中,根据 scause 寄存器的值判断异常类型(页面错误),并执行相应的处理逻辑。
void trap() {
  if ((r_scause() & 0xff) == 0xf) {
    // Handle page fault here
  } else {
    panic("unexpected trap");
  }
}

6. 打印调试信息

在无法处理页面错误时,trap() 函数会调用 printf() 打印相关的调试信息,包括 scause, sepc, 和 stval

  • kernel/printf.cprintf() 函数的实现位于 xv6kernel/printf.c 文件中,这里会负责输出调试信息。
  • printf 调用会输出寄存器的值,如 scause, sepc, stval

7. 调用 panic() 停止系统

xv6 确认无法处理页面错误后,最终会调用 panic() 停止系统运行:

  • kernel/printf.cpanic() 函数也位于 xv6kernel/printf.c 中。panic() 会关闭中断,打印错误信息,并进入死循环停止系统执行。
void panic(char *s) {
  cli(); // Disable interrupts
  cprintf("panic: %s\n", s);
  for(;;);
}

8. QEMU 进入死循环,结束仿真

xv6 进入 panic() 的死循环后,QEMU 不会中止模拟,它会继续模拟 CPU 执行死循环。这是通过 QEMU 中的 CPU 仿真主循环实现的,位于 accel/tcg/translate-all.c 中的 cpu_exec() 函数中,QEMU 会不断执行同一个指令,进入无限循环。

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇