Skip to content

6.1810 2024 第9讲 & 第5讲:设备驱动、中断和系统调用入口

主题:设备驱动与中断

操作系统通过设备驱动程序来控制硬件设备(如存储、网络、显示器等)。这充满挑战,因为设备接口复杂、与CPU并行运行,并且通过中断来请求服务,这可能发生在任何不合时宜的时刻。

  • 内存映射I/O (Memory-Mapped I/O): 内核通过读写特定物理地址(设备寄存器)来控制硬件。
  • 等待与中断: 与其通过“忙等待”循环来等待设备就绪(这会浪费CPU),不如让设备在需要注意时(例如,数据到达)产生一个中断。CPU会暂停当前工作,跳转到内核的中断处理程序进行响应。
  • 中断处理流程: 设备 -> PLIC (平台级中断控制器) -> CPU -> 陷阱 -> devintr() -> uartintr()
  • 驱动程序的“上半部分”与“下半部分”:
    • 上半部分 (Top Half): 在进程的系统调用上下文中运行(如 read, write),负责启动设备操作并可能等待结果(通过 sleep)。
    • 下半部分 (Bottom Half): 在中断上下文中运行,处理设备硬件,读取/发送数据,并通过 wakeup 唤醒等待的进程。
    • 两者通过共享的缓冲区(如 cons.buf)和 sleep/wakeup 机制进行协调。

主题:用户态 -> 内核态转换

系统调用、异常和设备中断都使用相同的机制从用户态进入内核态。这个过程的设计对保证隔离性性能至关重要。

一次系统调用(如 write())的生命周期预览:

           用户空间                                内核空间
--------------------------------------------------------------------------------
           write() --(ecall)--> uservec (trampoline.S)
                                     |
                                     v
                                 usertrap() (trap.c)
                                     |
                                     v
                                  syscall() (syscall.c)
                                     |
                                     v
                                 sys_write() (sysfile.c)
                                     |
                                     v
(sret) <-- userret (trampoline.S) <-- usertrapret() (trap.c)

核心步骤详解:

  1. ecall 指令: 用户程序调用一个库函数(如 write),该函数最终执行 ecall 指令。ecall 会:

    • 将CPU模式从 用户模式 切换到 监控模式
    • 将当前程序计数器(PC)保存到 sepc 寄存器。
    • 跳转到 stvec 寄存器指定的地址,该地址由内核预设为 uservec
  2. uservec (in trampoline.S): 这是内核处理陷阱的第一段代码。

    • 保存用户寄存器: 它不能立即运行C代码,因为它需要一个内核栈并且不能破坏用户寄存器。它首先利用 sscratch 寄存器交换出一个通用寄存器(a0),用它来定位当前进程的陷阱帧 (trapframe)
    • 然后,它将所有31个用户通用寄存器保存在陷阱帧中。
  3. 切换到内核环境:

    • 从陷阱帧中加载内核栈指针到 sp
    • 从陷阱帧中加载内核页表的地址到 satp 寄存器,切换地址空间。
    • 从陷阱帧中加载 usertrap 函数的地址。
    • 跳转到 usertrap 函数。

    为何需要 Trampoline 页?uservec 的代码位于一个特殊的“跳板”页(Trampoline Page)。这个页面同时被映射在内核地址空间和每个用户地址空间的高地址处(但用户模式不可访问)。这使得在切换页表(satp)的瞬间,CPU仍然能找到下一条要执行的指令,因为新旧页表中该页的虚拟地址是相同的。

  4. usertrap() (in trap.c): 这是一个C函数,现在可以安全地运行。

    • 它检查 scause 寄存器来判断陷阱的类型(是系统调用、设备中断还是异常)。
    • 如果是系统调用,它会调用 syscall()
  5. syscall() (in syscall.c):

    • 从陷阱帧中恢复系统调用号(来自 a7 寄存器)和参数(来自 a0-a5)。
    • 根据系统调用号,在 syscalls 数组中查找并调用对应的内核函数(如 sys_write)。
  6. 返回用户空间:

    • 系统调用实现函数返回后,usertrap() 调用 usertrapret()
    • usertrapret() 设置返回用户空间所需的状态(如 sepc 指向用户下一条指令),然后调用 userret
    • userret (in trampoline.S):
      • 切换回用户的页表。
      • 从陷阱帧中恢复所有32个用户寄存器。
      • 执行 sret 指令。
  7. sret 指令: sret 会:

    • 将CPU模式切换回 用户模式
    • sepc 的值复制回 pc
    • 重新启用中断。

至此,CPU无缝地回到了用户程序中,仿佛只是进行了一次普通的函数调用。