📖
R‘Notes
  • 关于本仓库/网站
  • Note
    • Golang的知识碎片
      • 关于Golang的一些碎片知识
    • LeetCode
      • LCR 121. 寻找目标值 - 二维数组
      • LCR 125. 图书整理 II
      • LCR 127. 跳跃训练
      • LCR 143. 子结构判断
      • LCR 159. 库存管理 III
      • LCR 161. 连续天数的最高销售额
      • LCR 170. 交易逆序对的总数
      • LCR 174. 寻找二叉搜索树中的目标节点
      • LeetCode--1. 两数之和
      • LeetCode--10. 正则表达式匹配
      • LeetCode--1004. 最大连续1的个数 III
      • LeetCode--101. 对称二叉树
      • LeetCode--102. 二叉树的层序遍历
      • LeetCode--1027. 最长等差数列
      • LeetCode--103. 二叉树的锯齿形层序遍历
      • LeetCode--1035. 不相交的线
      • LeetCode--104. 二叉树的最大深度
      • LeetCode--1044. 最长重复子串
      • LeetCode--1049. 最后一块石头的重量 II
      • LeetCode--105. 从前序与中序遍历序列构造二叉树
      • LeetCode--106. 从中序与后序遍历序列构造二叉树
      • LeetCode--110. 平衡二叉树
      • LeetCode--111. 二叉树的最小深度
      • LeetCode--112. 路径总和
      • LeetCode--113. 路径总和 II
      • LeetCode--1137. 第 N 个泰波那契数
      • LeetCode--114. 二叉树展开为链表
      • LeetCode--1143. 最长公共子序列
      • LeetCode--115. 不同的子序列
      • LeetCode--1191. K 次串联后最大子数组之和
      • LeetCode--120. 三角形最小路径和
      • LeetCode--121. 买卖股票的最佳时机
      • LeetCode--1218. 最长定差子序列
      • LeetCode--122. 买卖股票的最佳时机 II
      • LeetCode--1220. 统计元音字母序列的数目
      • LeetCode--123. 买卖股票的最佳时机 III
      • LeetCode--124. 二叉树中的最大路径和
      • LeetCode--125. 验证回文串
      • LeetCode--128. 最长连续序列
      • LeetCode--1289. 下降路径最小和 II
      • LeetCode--129. 求根节点到叶节点数字之和
      • LeetCode--1301. 最大得分的路径数目
      • LeetCode--1312. 让字符串成为回文串的最少插入次数
      • LeetCode--134. 加油站
      • LeetCode--135. 分发糖果
      • LeetCode--136. 只出现一次的数字
      • LeetCode--138. 随机链表的复制
      • LeetCode--139. 单词拆分
      • LeetCode--14. 最长公共前缀
      • LeetCode--141. 环形链表
      • LeetCode--142. 环形链表 II
      • LeetCode--143. 重排链表
      • LeetCode--144. 二叉树的前序遍历
      • LeetCode--145. 二叉树的后序遍历
      • LeetCode--146. LRU 缓存
      • LeetCode--148. 排序链表
      • LeetCode--15. 三数之和
      • LeetCode--151. 反转字符串中的单词
      • LeetCode--152. 最大乘积子数组【DP】
      • LeetCode--153. 寻找旋转排序数组中的最小值
      • LeetCode--155. 最小栈
      • LeetCode--1584. 连接所有点的最小费用,最小生成树模板题
      • LeetCode--1594. 矩阵的最大非负积
      • LeetCode--16. 最接近的三数之和
      • LeetCode--160. 相交链表
      • LeetCode--162. 寻找峰值
      • LeetCode--165. 比较版本号
      • LeetCode--169. 多数元素
      • LeetCode--174. 地下城游戏
      • LeetCode--1774. 最接近目标价格的甜点成本
      • LeetCode--179. 最大数
      • LeetCode--1824. 最少侧跳次数
      • LeetCode--188. 买卖股票的最佳时机 IV
      • LeetCode--189. 轮转数组
      • LeetCode--19. 删除链表的倒数第 N 个结点,关于删除链表会遇见的指针问题
      • LeetCode--1937. 扣分后的最大得分
      • LeetCode--1964. 找出到每个位置为止最长的有效障碍赛跑路线
      • LeetCode--198. 打家劫舍
      • LeetCode--199. 二叉树的右视图
      • LeetCode--2. 两数相加
      • LeetCode--20. 有效的括号
      • LeetCode--200. 岛屿数量
      • LeetCode--206. 反转链表
      • LeetCode--207. 课程表
      • LeetCode--208. 实现 Trie (前缀树)
      • LeetCode--209. 长度最小的子数组
      • LeetCode--21. 合并两个有序链表,关于链表的复习
      • LeetCode--210. 课程表 II
      • LeetCode--213. 打家劫舍 II
      • LeetCode--2140. 解决智力问题
      • LeetCode--215. 数组中的第K个最大元素
      • LeetCode--22. 括号生成
      • LeetCode--221. 最大正方形
      • LeetCode--2218. 从栈中取出 K 个硬币的最大面值和
      • LeetCode--224. 基本计算器
      • LeetCode--225. 用队列实现栈
      • LeetCode--226. 翻转二叉树
      • LeetCode--2266. 统计打字方案数
      • LeetCode--2267. 检查是否有合法括号字符串路径
      • LeetCode--227. 基本计算器 II
      • LeetCode--23. 合并 K 个升序链表【堆和分治】
      • LeetCode--230. 二叉搜索树中第 K 小的元素
      • LeetCode--2304. 网格中的最小路径代价
      • LeetCode--232. 用栈实现队列
      • LeetCode--2320. 统计放置房子的方式数
      • LeetCode--2321. 拼接数组的最大分数
      • LeetCode--2328. 网格图中递增路径的数目
      • LeetCode--233. 数字 1 的个数
      • LeetCode--234. 回文链表
      • LeetCode--236. 二叉树的最近公共祖先
      • LeetCode--239. 滑动窗口最大值,关于单调队列的复习
      • LeetCode--24. 两两交换链表中的节点
      • LeetCode--240. 搜索二维矩阵 II
      • LeetCode--2435. 矩阵中和能被 K 整除的路径
      • LeetCode--2466. 统计构造好字符串的方案数
      • LeetCode--25. K 个一组翻转链表
      • LeetCode--2533. 好二进制字符串的数量
      • LeetCode--256. 粉刷房子
      • LeetCode--2606. 找到最大开销的子字符串
      • LeetCode--265. 粉刷房子 II
      • LeetCode--2684. 矩阵中移动的最大次数
      • LeetCode--2787. 将一个数字表示成幂的和的方案数
      • LeetCode--279. 完全平方数【动态规划】
      • LeetCode--283. 移动零
      • LeetCode--287. 寻找重复数
      • LeetCode--2915. 和为目标值的最长子序列的长度
      • LeetCode--295. 数据流的中位数
      • LeetCode--297. 二叉树的序列化与反序列化
      • LeetCode--3. 无重复字符的最长子串
      • LeetCode--300. 最长递增子序列【DP+二分】
      • LeetCode--3082. 求出所有子序列的能量和
      • LeetCode--309. 买卖股票的最佳时机含冷冻期
      • LeetCode--31. 下一个排列
      • LeetCode--3180. 执行操作可获得的最大总奖励 I
      • LeetCode--3186. 施咒的最大总伤害
      • LeetCode--32. 最长有效括号【栈和dp】
      • LeetCode--322. 零钱兑换
      • LeetCode--328. 奇偶链表
      • LeetCode--329. 矩阵中的最长递增路径
      • LeetCode--33. 搜索旋转排序数组【直接二分】
      • LeetCode--337. 打家劫舍 III
      • LeetCode--3393. 统计异或值为给定值的路径数目
      • LeetCode--34. 在排序数组中查找元素的第一个和最后一个位置
      • LeetCode--3418. 机器人可以获得的最大金币数
      • LeetCode--343. 整数拆分
      • LeetCode--347. 前 K 个高频元素
      • LeetCode--347. 前 K 个高频元素Golang中的堆(containerheap)
      • LeetCode--3489. 零数组变换 IV
      • LeetCode--354. 俄罗斯套娃信封问题
      • LeetCode--377. 组合总和 Ⅳ
      • LeetCode--39. 组合总和
      • LeetCode--394. 字符串解码【栈】
      • LeetCode--395. 至少有 K 个重复字符的最长子串
      • LeetCode--4. 寻找两个正序数组的中位数
      • LeetCode--40. 组合总和 II
      • LeetCode--402. 移掉 K 位数字
      • LeetCode--41. 缺失的第一个正数
      • LeetCode--415. 字符串相加
      • LeetCode--416. 分割等和子集_494. 目标和【01背包】
      • LeetCode--42. 接雨水(单调栈和双指针)
      • LeetCode--426. 将二叉搜索树转化为排序的双向链表
      • LeetCode--43. 字符串相乘
      • LeetCode--437. 路径总和 III【前缀和】
      • LeetCode--44. 通配符匹配
      • LeetCode--440. 字典序的第K小数字
      • LeetCode--442. 数组中重复的数据
      • LeetCode--445. 两数相加 II
      • LeetCode--45. 跳跃游戏 II
      • LeetCode--450. 删除二叉搜索树中的节点
      • LeetCode--46. 全排列
      • LeetCode--460. LFU 缓存
      • LeetCode--468. 验证IP地址
      • LeetCode--470. 用 Rand7() 实现 Rand10()
      • LeetCode--474. 一和零
      • LeetCode--48. 旋转图像
      • LeetCode--498. 对角线遍历
      • LeetCode--5. 最长回文子串
      • LeetCode--50. Pow(x, n)
      • LeetCode--509. 斐波那契数
      • LeetCode--516. 最长回文子序列
      • LeetCode--518. 零钱兑换 II
      • LeetCode--529. 扫雷游戏题解C++广搜
      • LeetCode--53. 最大子数组和
      • LeetCode--54. 螺旋矩阵
      • LeetCode--540. 有序数组中的单一元素
      • LeetCode--543. 二叉树的直径
      • LeetCode--55. 跳跃游戏
      • LeetCode--556. 下一个更大元素 III
      • LeetCode--56. 合并区间
      • LeetCode--560. 和为 K 的子数组
      • LeetCode--572. 另一棵树的子树
      • LeetCode--59. 螺旋矩阵 II
      • LeetCode--61. 旋转链表
      • LeetCode--62. 不同路径
      • LeetCode--622. 设计循环队列
      • LeetCode--63. 不同路径 II
      • LeetCode--64. 最小路径和
      • LeetCode--646. 最长数对链
      • LeetCode--662. 二叉树最大宽度
      • LeetCode--673. 最长递增子序列的个数
      • LeetCode--678. 有效的括号字符串
      • LeetCode--679. 24 点游戏
      • LeetCode--69. x 的平方根
      • LeetCode--695. 岛屿的最大面积
      • LeetCode--7. 整数反转
      • LeetCode--70. 爬楼梯
      • LeetCode--704. 二分查找
      • LeetCode--712. 两个字符串的最小ASCII删除和
      • LeetCode--714. 买卖股票的最佳时机含手续费
      • LeetCode--718. 最长重复子数组
      • LeetCode--72. 编辑距离
      • LeetCode--739. 每日温度
      • LeetCode--74. 搜索二维矩阵
      • LeetCode--740. 删除并获得点数
      • LeetCode--746. 使用最小花费爬楼梯
      • LeetCode--75. 颜色分类
      • LeetCode--76. 最小覆盖子串
      • LeetCode--77. 组合
      • LeetCode--78. 子集
      • LeetCode--79. 单词搜索
      • LeetCode--790. 多米诺和托米诺平铺
      • LeetCode--8. 字符串转换整数 (atoi)
      • LeetCode--82. 删除排序链表中的重复元素 II
      • LeetCode--83. 删除排序链表中的重复元素
      • LeetCode--84. 柱状图中最大的矩形【单调栈】
      • LeetCode--85. 最大矩形
      • LeetCode--87. 扰乱字符串
      • LeetCode--879. 盈利计划
      • LeetCode--88. 合并两个有序数组
      • LeetCode--887. 鸡蛋掉落
      • LeetCode--91. 解码方法
      • LeetCode--912. 排序数组
      • LeetCode--918. 环形子数组的最大和
      • LeetCode--92. 反转链表 II
      • LeetCode--93. 复原 IP 地址
      • LeetCode--931. 下降路径最小和
      • LeetCode--94. 二叉树的中序遍历
      • LeetCode--958. 二叉树的完全性检验
      • LeetCode--97. 交错字符串
      • LeetCode--98. 验证二叉搜索树
      • LeetCode--983. 最低票价
      • LeetCode--LCR 140. 训练计划 II
      • NC--311.圆环回原点
      • NC--36进制加法
      • 补充题1. 排序奇升偶降链表
    • Redis
      • Redis基础部分
      • 在用Docker配置Redis哨兵节点的时候出现的错误及其解决
    • SQL学习记录
      • SQL碎片知识
      • 系统
        • MySQL学习笔记1【DQL和DCL】
        • MySQL学习笔记2【函数/约束/多表查询】
        • MySQL学习笔记3【事务】
        • MySQL学习笔记4【存储引擎和索引】
        • MySQL学习笔记5【SQL优化/视图/存储过程/触发器】
        • MySQL学习笔记6【锁】
        • MySQL学习笔记7【InnoDB】
    • x86汇编
      • 学习汇编随手记
    • 微服务相关
      • Nacos与gRPC
      • 【Golangnacos】nacos配置的增删查改,以及服务注册的golang实例及分析
    • 手搓
      • Whalebox(仿Docker)的爆诞
    • 操作系统
      • 操作系统碎片知识
      • MIT6.S081
        • MIT6.S081-lab1
        • MIT6.S081-lab2
        • MIT6.S081-lab3
        • MIT6.S081-lab3前置
        • MIT6.S081-lab4
        • MIT6.S081-lab4前置
        • MIT6.S081-lab5
        • MIT6.S081-lab5前置
        • MIT6.S081-lab7
        • MIT6.S081-lab7前置
        • MIT6.S081-lab8
        • MIT6.S081-lab8前置
        • MIT6.S081-lab9
        • MIT6.S081-环境搭建
    • 消息队列MQ
      • Kafka
    • 算法杂谈
      • 关于二分查找时的边界分类问题
    • 计组笔记
      • 计算机组成原理的学习笔记(1)--概述
      • 计算机组成原理的学习笔记(10)--CPU·其二 组合逻辑控制器和微程序
      • 计算机组成原理的学习笔记(11)--CPU·其三 中断和异常多处理器相关概念
      • 计算机组成原理的学习笔记(12)--总线和IO系统
      • 计算机组成原理的学习笔记(2)--数据的表示和运算·其一
      • 计算机组成原理的学习笔记(3)--数据表示与运算·其二 逻辑门和加减乘
      • 计算机组成原理的学习笔记(4)--数据的表示与运算·其三 补码的乘法以及原码补码的除法
      • 计算机组成原理的学习笔记(4)--数据的表示与运算·其三 补码的乘法以及原码补码的除法
      • 计算机组成原理的学习笔记(6)--存储器·其一 SRAMDRAMROM主存储器的初步认识
      • 计算机组成原理的学习笔记(7)--存储器·其二 容量扩展多模块存储系统外存Cache虚拟存储器
      • 计算机组成原理的学习笔记(8)--指令系统·其一 指令的组成以及数据寻址方式RISK和CISK
      • 计算机组成原理的学习笔记(9)--CPU·其一 CPU的基本概念流水线技术数据通路
由 GitBook 提供支持
在本页
  • 从用户空间陷入
  • 从内核空间陷入
  • 利用页面错误异常
  1. Note
  2. 操作系统
  3. MIT6.S081

MIT6.S081-lab4前置

从用户空间陷入

这部分的内容其实在lab3前置的源码阅读部分就已经讨论过了,但是这个是本章的重点,所以再次来讨论一下:

当我们从用户空间执行ecall指令的时候(一般是系统调用的时候会执行),我们可以在make qemu之后,在user/usys.S中找到这样的代码,以fork为例子:

.global fork
fork:
 li a7, SYS_fork
 ecall
 ret

他先将我们的需要的系统调用的对象(SYS_fork在这里实际上是int常量,表示这个系统调用的数字),存入a7,然后通过执行ecall跳转到我们的陷阱处理的地方(**ecall到底干了什么?**ecall会将我们的user mode切换为supervisor mode,所以这个参数就不需要手动去切换了,并且还会将程序计数器保存到SEPC寄存器中,最后会跳转到STVEC寄存器指向的位置),由于我们此时是从用户空间陷入,所以我们此时stvec寄存器应当是指向用户陷入区的,所以此时是进入到了kernel/trampoline.S的uservec部分,其中,前面都是一些保存寄存器,然后获取参数,当然,我们还会将cpu的id保存到tp寄存器中,之后的myproc()就是依赖于这个tp寄存器来获取proc的,比较重要的一步就是:

csrw satp, t1

我们的trampoline在这一步可以将使用的基地址转移到内核页表,但是我们事实上还需要继续执行后续的指令,不可能因为基地址变化,后面的指令就不执行了,这就是操作系统的神奇,用户页中映射的uservec和内核页中映射的uservec的虚拟地址是相同的!随后便会清空TLB,页表缓存,然后跳转到我们的usertrap了!注意,此时我们调用usertrap之后,并不会返回到当前的位置,而是会经过usertrapret来直接返回到用户态。(usertrap可以看我lab3的前置)

// 将控制权从内核态恢复到用户态,设置陷阱相关寄存器、页表等必要状态。
// 每次用户进程返回用户态执行时,都会调用这个函数。
void
usertrapret(void)
{
  // 依赖于tp寄存器
  struct proc *p = myproc();

  // 我们即将把 stvec 设置为 usertrap,也就是用户态 trap 的入口,
  // 所以在返回用户态之前,要先关中断,防止陷阱跳转期间被打断。
  intr_off();

  // 设置 stvec 为 trampoline.S 中的 uservec 函数地址(用户态 trap 的入口地址)。
  // TRAMPOLINE 是 trampoline 映射到高地址的起始地址,uservec 是 tramp.S 中的偏移。
  uint64 trampoline_uservec = TRAMPOLINE + (uservec - trampoline);
  w_stvec(trampoline_uservec);

  // 把接下来用户态再次 trap 到内核时需要用到的内核信息填入 trapframe
  // 这些值会在 trampoline.S 的 uservec 中被用到

  // 保存当前内核页表的 satp 值,在下一次 trap 回内核时要切回这个页表
  p->trapframe->kernel_satp = r_satp();

  // 保存当前进程在内核中的内核栈顶地址,保证正确的栈结构
  p->trapframe->kernel_sp = p->kstack + PGSIZE;

  // 保存 trap 进入内核的入口函数地址(即 usertrap 函数)
  p->trapframe->kernel_trap = (uint64)usertrap;

  // 保存当前 CPU 的 hartid,用于区分不同核心上的进程
  p->trapframe->kernel_hartid = r_tp();

  // 修改 sstatus 寄存器,设置好用户态返回的相关状态
  // 清除 SPP 位(Supervisor Previous Privilege)= 0,表示下一次 sret 返回到 User 模式
  // 设置 SPIE(使能中断)位,表示回到用户态后允许响应中断
  unsigned long x = r_sstatus();
  x &= ~SSTATUS_SPP; // 设置 SPP 为 0,表示 sret 返回后是用户模式
  x |= SSTATUS_SPIE; // 设置 SPIE 为 1,用户态开启中断
  w_sstatus(x);

  // 设置 sepc 寄存器为 trapframe 中保存的用户态 PC(trap 时被保存在 epc 中)
  // sret 执行后就会跳转回这个地址执行
  w_sepc(p->trapframe->epc);

  // 设置即将切换的页表(用户态页表)
  uint64 satp = MAKE_SATP(p->pagetable);

  // 跳转到 trampoline.S 中的 userret 函数,传入用户页表 satp。
  // userret 会完成:
  //   - 切换 satp(用户页表)
  //   - 恢复用户寄存器(包括 sp、ra、a0~a7 等)
  //   - 执行 sret,sret会将程序计数器设置成sepc寄存器的值,切换为user mode,从而跳转到用户态。
  uint64 trampoline_userret = TRAMPOLINE + (userret - trampoline);
  ((void (*)(uint64))trampoline_userret)(satp);
}

这里最后会跳转到userret,它同样处于trampoline之中,用于真正的恢复用户态的信息,通过执行sret来回到用户态。

之前在编写系统调用的时候还提到过,我们的artint,artaddr,artfd这几个函数从trapframe中寻找第n个系统调用的参数,并且以对应的格式返回,他们都会调用argraw来检索对应的参数:

static uint64
argraw(int n)
{
  // 获取当前进程
  struct proc *p = myproc();
  
  // 根据参数 n 返回对应的系统调用参数
  switch (n) {
  case 0:
    return p->trapframe->a0;
  case 1:
    return p->trapframe->a1
  case 2:
    return p->trapframe->a2;
  case 3:
    return p->trapframe->a3; 
  case 4:
    return p->trapframe->a4;
  case 5:
    return p->trapframe->a5;
  }
  
  panic("argraw");
  
  return -1;
}

我们会发现,这段代码实际上实现地很简单,但是我们还需要注意一点,就是有的时候,我们会传递指针来进行系统调用,内核就需要使用这些指针来读取或写入用户内存,这就会引发问题,而内核实现了安全地将数据传输到用户提供的地址或是安全的从提供的地址处获取数据,比如我们的exec,在传递参数的时候,使用的是传入数组指针的方式,而我们在exec中调用了fetchaddr这个函数:

//   addr - 用户空间中待读取的地址。
//   ip - 指向内核空间的指针,读取的 64 位数据将被存储在这里。
int
fetchaddr(uint64 addr, uint64 *ip)
{
  struct proc *p = myproc();

  // 检查地址是否有效。地址必须在进程的虚拟内存范围内,且能够容纳一个 uint64 类型的数据
  if(addr >= p->sz || addr + sizeof(uint64) > p->sz) 
    return -1;
    
  // 从用户空间拷贝数据到内核空间。如果拷贝失败,返回错误
  if(copyin(p->pagetable, (char *)ip, addr, sizeof(*ip)) != 0)
    return -1;
  return 0;
}

但是这一部分其实并不算复杂的,我们需要回过去看看我们真正的重点--copyin

// 从用户空间拷贝数据到内核空间
//
// 参数:
//   pagetable - 用户进程的页表,用于地址翻译
//   dst       - 目标内核地址,拷贝到这里
//   srcva     - 源地址(用户虚拟地址)
//   len       - 拷贝的字节数
//   本函数会跨页拷贝用户空间的数据。
//   每次调用 walkaddr() 把虚拟地址 srcva 映射为物理地址,
//   然后用 memmove() 拷贝这一页内的内容。
//   如果在任何一步发现地址无效,直接返回 -1。
int
copyin(pagetable_t pagetable, char *dst, uint64 srcva, uint64 len)
{
  uint64 n, va0, pa0;

  while(len > 0){
    // 将 srcva 向下取整到页对齐的地址,得到当前页的起始虚拟地址
    va0 = PGROUNDDOWN(srcva);

    // 使用 walkaddr 将页表中的虚拟地址 va0 映射到物理地址 pa0
    pa0 = walkaddr(pagetable, va0);
    if(pa0 == 0)
      return -1; // 地址无效,说明该页未映射,返回错误

    // 计算从 srcva 开始到当前页末尾还能拷贝多少字节
    n = PGSIZE - (srcva - va0);
    if(n > len)
      n = len; // 如果这一页可拷贝的数据超过需要的长度,截断为 len

    // 从物理地址对应位置拷贝 n 字节数据到内核地址 dst
    // 注意这里 pa0 是页起始物理地址,(srcva - va0) 是页内偏移
    memmove(dst, (void *)(pa0 + (srcva - va0)), n);

    len -= n;

    dst += n;
    srcva = va0 + PGSIZE; // 跨页拷贝,srcva 跳到下一页开头
  }

  return 0;
}

可以看见,我们这里出现的熟悉的walk,但是后面跟了个addr,实际上,这里内部依旧是调用的walk,然后计算出其物理地址而已。

随后就是简单的内存数据拷贝了,将计算出来的用户态物理地址中的数据拷贝到目标的内核地址处。

另外还有一个拷贝字符串的操作函数,叫做copyinstr,也是类似的逻辑,没有太大区别。

从内核空间陷入

之前我们已经对用户态的陷入做了一些介绍,下面我们来细说一下我们还未涉足的领域--从内核空间陷入。

当然,我们在进入内核空间之后,我们的stvec寄存器就会存储指向kernelvec的地址,这个kernelvec,就在kernel/kernelvec.S中,他的功能和我们用户态中断使用的trampoline是一样的,唯一不同的一点是,用户陷入使用的是 trapframe 去保存用户态的寄存器(上下文),而内核态陷入则是使用的栈:

        #
        # 中断或异常发生在 supervisor 模式(内核态)时,从这里进入。
        # 此时使用的是内核栈。
        # 保存寄存器,调用 kerneltrap() 处理陷阱。
        # kerneltrap() 返回后,恢复寄存器,返回原执行点。
        #
.globl kerneltrap       # 声明 kerneltrap 为全局符号(C 函数)
.globl kernelvec        # 声明 kernelvec 为全局符号(stvec 设置为它)
.align 4                # 对齐到 16 字节(2^4)

kernelvec:
        # 为保存寄存器腾出 256 字节栈空间
        addi sp, sp, -256

        # 保存 caller-saved 寄存器到内核栈上
        sd ra, 0(sp)       # 返回地址
        sd sp, 8(sp)       # 栈指针(虽然被改了,仍保存一下)
        sd gp, 16(sp)      # 全局指针
        sd tp, 24(sp)      # 线程指针(hartid)

        sd t0, 32(sp)      # 临时寄存器 t0
        sd t1, 40(sp)
        sd t2, 48(sp)

        sd a0, 72(sp)      # 参数寄存器 a0~a7
        sd a1, 80(sp)
        sd a2, 88(sp)
        sd a3, 96(sp)
        sd a4, 104(sp)
        sd a5, 112(sp)
        sd a6, 120(sp)
        sd a7, 128(sp)

        sd t3, 216(sp)     # 临时寄存器 t3~t6(保存在偏移更后的位置)
        sd t4, 224(sp)
        sd t5, 232(sp)
        sd t6, 240(sp)

        # 调用 C 函数 kerneltrap 来处理中断/异常
        call kerneltrap

        # 恢复寄存器(从栈中加载回原寄存器)
        ld ra, 0(sp)
        ld sp, 8(sp)
        ld gp, 16(sp)
        # 不恢复 tp(线程指针),因为中断可能切换了 CPU 核
        ld t0, 32(sp)
        ld t1, 40(sp)
        ld t2, 48(sp)

        ld a0, 72(sp)
        ld a1, 80(sp)
        ld a2, 88(sp)
        ld a3, 96(sp)
        ld a4, 104(sp)
        ld a5, 112(sp)
        ld a6, 120(sp)
        ld a7, 128(sp)

        ld t3, 216(sp)
        ld t4, 224(sp)
        ld t5, 232(sp)
        ld t6, 240(sp)

        # 栈指针恢复到原位置
        addi sp, sp, 256

        # 使用 sret 指令从中断/异常中返回,回到之前的内核态代码继续执行
        sret

kernelvec会在执行中断的具体逻辑之前之前保存寄存器,随后会跳转到我们的kerneltrap:

void 
kerneltrap()
{
  int which_dev = 0;
  // 读取陷入前的相关寄存器的值
  uint64 sepc = r_sepc();       // sepc 保存的是触发 trap 的指令地址
  uint64 sstatus = r_sstatus(); // sstatus 是当前的特权状态寄存器
  uint64 scause = r_scause();   // scause 表示 trap 的原因(中断/异常 类型)

  // 检查当前是否从 Supervisor 模式进入陷阱(这是必须的)
  if((sstatus & SSTATUS_SPP) == 0)
    panic("kerneltrap: not from supervisor mode");

  // 确保在处理中断时中断已经被关闭,防止嵌套中断
  if(intr_get() != 0)
    panic("kerneltrap: interrupts enabled");

  // 处理具体的设备中断,如果返回 0,说明不是已知的设备中断
  if((which_dev = devintr()) == 0){
    // 无法识别的 trap,打印寄存器信息用于调试
    printf("scause=0x%lx sepc=0x%lx stval=0x%lx\n", scause, r_sepc(), r_stval());
    panic("kerneltrap"); // 触发 panic 终止
  }

  // 如果是时钟中断(which_dev == 2)并且当前存在进程,主动让出 CPU
  if(which_dev == 2 && myproc() != 0)
    yield();

  // yield() 可能会触发新的 trap,需要恢复寄存器供后续使用
  w_sepc(sepc);       // 恢复陷入前的 sepc 值
  w_sstatus(sstatus); // 恢复陷入前的 sstatus 状态
}

我们在这段代码有两个关键函数,devintr和yield,我们的devintr可以检测我们的中断是否为设备中断,如果不是,则说明当前中断是一个异常,直接panic。

而yield就跟注释说的一样,当检测到中断为时钟中断,则会调用yield触发调度,让出cpu,而在之后的某一刻,其中一个线程会让出cpu,从而使得我们当前的线程和kerneltrap恢复,在之后,我们会详细学习这部分内容。

利用页面错误异常

xv6在用户态发生异常时,会panic,而在内核态发生异常,则会导致内核崩溃,而真正的操作系统内核会利用页面错误来实现写时复制(COW)版本的fork。

**写时复制到底是啥?**之前在学习redis,kafka,或是go语言底层的时候,都会遇见写时复制这个词语,之前虽然都分析过很多遍,但是这里会再来解释一遍:

在最开始,我们会让父子进程以只读的状态来共享所有的物理页面,因为我们父子进程最初的状态是完全一样的,所以完全可以这样做!但是当父进程或者子进程尝试写操作的时候,我们的riscv cpu就会触发页面错误异常,为了处理这个异常,我们的内核复制了包含错误地址的页面,而在更新了我们的页表之后,内核就会恢复到导致异常的指令处,此时内核已经更新了相关的页表条目,这样,我们的异常指令在此时将会正确执行。这种策略下,我们可以避免子进程完全拷贝父进程的所有的数据。

除此之外,我们还可以利用页面错误异常来实现惰性加载,当我们调用sbrk分配地址空间的时候,内核会分配地址空间,但是页表中将新地址标记为无效,而在实际使用他们的时候,才会分配内存!

还有一个常用的功能就是从磁盘分页,我在ostep里面看见过这个,如果应用程序需要的内存超出了限度,内核就会换出一些页面,写入磁盘中,并且将他们的PTE标记为无效,如果后面cpu再次读取这个页面,就会触发页面错误,此时,内核可以检查故障地址,如果在磁盘上面,就会分配页面并将磁盘上面的数据读取到内存。

以上功能,xv6均没有实现,而在之后的一个实验中,我们将会亲自去实现写时复制的功能!

补充

supervisor mode多了什么权限?

我们从user mode跳转到supervisor mode多了一些特权:

  1. 读写satp寄存器,也就是修改pagetable指针。

  2. stvec,存储了处理trap的地址。

  3. sepc,保存程序计数器

  4. 可以使用用户不能使用的页表,也就是PTE_U标志位为0的页表

  5. ....


我们之前讨论了很多trap的内容,可能会误以为只有系统调用才会产生trap陷入,事实上,当程序出现了页错误,除以0,设备中断等情况,也会trap陷入,其实,trap的本质就是内核空间和用户空间的切换。我们之前提到过ecall干了三件事,切换mode,保存程序计数器和跳转到STVEC指向的位置。

上一页MIT6.S081-lab4下一页MIT6.S081-lab5

最后更新于2天前