📖
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--3363. 最多可收集的水果数目
      • 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. Inspect a user-process page table
  • 2. Speed up system calls
  • 3. Print a page table
  • 4. Use superpages
  1. Note
  2. 操作系统
  3. MIT6.S081

MIT6.S081-lab3

注:本篇lab的前置知识在《MIT6.S081-lab3前置》

1. Inspect a user-process page table

实验要求是解释我们通过输入pgtbltest输出的包含的内容及其权限位,我的输出如下:

print_pgtbl starting
va 0x0 pte 0x21FC885B pa 0x87F22000 perm 0x5B
va 0x1000 pte 0x21FC7C17 pa 0x87F1F000 perm 0x17
va 0x2000 pte 0x21FC7807 pa 0x87F1E000 perm 0x7
va 0x3000 pte 0x21FC74D7 pa 0x87F1D000 perm 0xD7
va 0x4000 pte 0x0 pa 0x0 perm 0x0
va 0x5000 pte 0x0 pa 0x0 perm 0x0
va 0x6000 pte 0x0 pa 0x0 perm 0x0
va 0x7000 pte 0x0 pa 0x0 perm 0x0
va 0x8000 pte 0x0 pa 0x0 perm 0x0
va 0x9000 pte 0x0 pa 0x0 perm 0x0
va 0xFFFF6000 pte 0x0 pa 0x0 perm 0x0
va 0xFFFF7000 pte 0x0 pa 0x0 perm 0x0
va 0xFFFF8000 pte 0x0 pa 0x0 perm 0x0
va 0xFFFF9000 pte 0x0 pa 0x0 perm 0x0
va 0xFFFFA000 pte 0x0 pa 0x0 perm 0x0
va 0xFFFFB000 pte 0x0 pa 0x0 perm 0x0
va 0xFFFFC000 pte 0x0 pa 0x0 perm 0x0
va 0xFFFFD000 pte 0x0 pa 0x0 perm 0x0
va 0xFFFFE000 pte 0x21FD08C7 pa 0x87F42000 perm 0xC7
va 0xFFFFF000 pte 0x2000184B pa 0x80006000 perm 0x4B
print_pgtbl: OK

后面的输出有问题,但是实验要求是解释用户页表,刚好用户页表的信息也能正常输出就不需要管了。

pgtbltest的main函数主体如下:

int
main(int argc, char *argv[])
{
  print_pgtbl();
    //下面执行不了
  ugetpid_test();
  print_kpgtbl();
  superpg_test();
  printf("pgtbltest: all tests succeeded\n");
  exit(0);
}

我们这里只有一个函数是我们需要关心的,那么我们可以来看看print_pgtbl干了些什么:

void
print_pgtbl()
{
  printf("print_pgtbl starting\n");
    // 打印前10页的页表条目
  for (uint64 i = 0; i < 10; i++) {
    print_pte(i * PGSIZE);
  }
  uint64 top = MAXVA/PGSIZE;
    // 打印后10页的页表条目
  for (uint64 i = top-10; i < top; i++) {
    print_pte(i * PGSIZE);
  }
  printf("print_pgtbl: OK\n");
}

我们可以看到,这里分别打印了前十页页表条目和后十页页表条目,然后我们跟踪到print_pte

void
print_pte(uint64 va)
{	//通过系统调用,获取我们的条目
    pte_t pte = (pte_t) pgpte((void *) va);
    printf("va 0x%lx pte 0x%lx pa 0x%lx perm 0x%lx\n", va, pte, PTE2PA(pte), PTE_FLAGS(pte));
}

为了更深刻的理解这个pgpte,我们可以追踪系统调用来弄清楚输出的东西到底是啥,此处,我找到了系统调用pgpte的入口,让我们看看他干了些什么:

int
sys_pgpte(void)
{
  uint64 va;
  struct proc *p;  
	//获取当前进程
  p = myproc();
   // 获取参数,就是当前的虚拟地址偏移
  argaddr(0, &va);
    //通过当前进程的用户页表取执行系统调用。
  pte_t *pte = pgpte(p->pagetable, va);
  if(pte != 0) {
      return (uint64) *pte;
  }
  return 0;
}

随后的内容,让我大失所望,结果就是个walk

pte_t*
pgpte(pagetable_t pagetable, uint64 va) {
  return walk(pagetable, va, 0);
}

walk函数的具体功能,我已经在上一篇文章中讲过,那么我们直接开始解释吧!

事实上,这些输出实在是一大堆,16进制也不好看,所以我们来重点分析第一个!

va 0x0 pte 0x21FC885B pa 0x87F22000 perm 0x5B

虚拟地址 (va 0x0):

  • va 0x0 表示虚拟地址是 0x0。

页表项 (pte 0x21FC885B):

  • pte 0x21FC885B 是该虚拟地址对应的页表项值。页表项包含了虚拟地址到物理地址的映射关系,以及相关的权限信息,我们还可以将其分为两部分:

    • 物理页框地址(Physical Page Frame Address):pte[63:10]

    • 权限位和控制位:pte[9:0]

对于 0x21FC885B,我们可以按位来分析:

0x21FC885B -> 0010 0001 1111 1100 1000 1000 0101 1011(二进制表示)

  • 物理页框地址(假设是 pte[63:10]):0x87F22000(即将高 22 位作为物理页框地址)

  • 权限位和控制位(假设是 pte[9:0]):

    • 有效位(PTE_V):1,表示该页表项有效。

    • 读写权限(PTE_R):1,表示该页是可读的,通过下一位我们可以发现是不可写的。

    • 用户访问权限(PTE_X):1,表示可以执行。

    • 执行权限(PTE_U):1,表示该用户态可以访问。

      其他倒不是特别重要。

物理地址 (pa 0x87F22000):

  • pa 0x87F22000 是映射后的物理地址。这表示虚拟地址 0x0 被映射到了物理地址 0x87F22000。也就是说,当程序访问 0x0 时,实际访问的是物理地址 0x87F22000。

权限位 (perm 0x5B):

  • perm 0x5B 是页表项的权限位,表示该页的访问权限。

0x5B -> 0101 1011(二进制表示)

根据上文,我们可以直接得出:

  • 有效位(PTE_V):1,表示该页表项有效。

  • 读写权限(PTE_R):1,表示该页是可读的,通过下一位我们可以发现是不可写的。

  • 用户访问权限(PTE_X):1,表示可以执行。

  • 执行权限(PTE_U):1,表示该用户态可以访问。

结合位掩码 0x5B,表示该页是有效的、可读的,但可执行,并且用户态可以访问。


2. Speed up system calls

简单来说,就是为添加一个用户可读的页表,来存储我们的pid,使得可以快速访问,首先,我们先来观察我们的实际的函数是如何调用的:

// 测试用户态系统调用 ugetpid() 是否返回与 getpid() 一致的进程 ID
void
ugetpid_test()
{
  int i;
  printf("ugetpid_test starting\n");
  testname = "ugetpid_test";
  // 多个数据测试
  for (i = 0; i < 64; i++) {
    int ret = fork();  // 创建一个子进程

    if (ret != 0) {  // 父进程执行的逻辑
      wait(&ret);    // 等待子进程结束,并获取退出状态
      if (ret != 0)  // 如果子进程非正常退出,测试失败
        exit(1);
      continue;      // 继续循环,创建下一个子进程
    }
    // 检查内核态 getpid() 与用户态 ugetpid() 返回的 PID 是否一致
    if (getpid() != ugetpid())
      err("missmatched PID");
    exit(0);
  }
  printf("ugetpid_test: OK\n");
}

而进入到我们的ugetpid可以发现,他就是在我们的用户页的对应的索引位置找到的pid的信息,以此来实现加速,甚至我们的pid页所在的位置,已经给我们规定好了。

int
ugetpid(void)
{
  struct usyscall *u = (struct usyscall *)USYSCALL;
  return u->pid;
}

我们进入到USYSCALL定义的地方:

#define USYSCALL (TRAPFRAME - PGSIZE)

struct usyscall {
  int pid;  // Process ID
};

显然,我们的usyscall结构体仅仅为我们准备了pid,并且USYSCALL的在页表中映射的位置已经规定好了,剩下就是我们去实现了!

下面开始!

通过hint可以发现,我们可以通过在proc结构体字段里面加上我们需要的结构体usyscall,也就是我们缓存的结构体,在proc.h中的结构体中加上如下字段:

  struct usyscall *syscall;    // lab3 getpid

然后,当我们创建进程的时候,我们肯定需要初始化这个结构体,一般来说,为进程分配页表时,会为其添加各种页表映射(包括trampoline,trapframe),所以作为附带品,我们的结构体也应当分配一个页,然后映射到内存中,由于在用户页的偏移已经规定好了,所以这并不是问题,所以在allocproc中:

  // 实验部分
  if ((p->syscall = (struct usyscall*)kalloc()) == 0) {
    freeproc(p);
    release(&p->lock);
    return 0;
  }
  // 写入数值
  p->syscall->pid = p->pid;

显然,虽然我们可以直接创建一个页,再将结构体写入页中,但是这样显然比较麻烦,所以直接将页分配给结构体即可。

到了这里,确实已经分配好页了,但是还没有映射,在我们的proc_pagetable中还需要建立映射关系:

  if(mappages(pagetable, USYSCALL, PGSIZE,
              (uint64)(p->syscall), PTE_R | PTE_U) < 0) {
    uvmunmap(pagetable, TRAPFRAME, 1, 0);
    uvmfree(pagetable, 0);
    return 0;
  }

这里使用的字段,都是之前规定好的,所以无需再去考虑,然后我们还需要考虑这个新分配的页的销毁!这里真的是个很麻烦的过程,找来找去真的累人。

proc_freepagetable()

void
proc_freepagetable(pagetable_t pagetable, uint64 sz)
{
  uvmunmap(pagetable, TRAMPOLINE, 1, 0);
  uvmunmap(pagetable, TRAPFRAME, 1, 0);
  // 取消映射并回收页表
  uvmunmap(pagetable, USYSCALL, 1, 0);
  uvmfree(pagetable, sz);
}

freeproc()

static void
freeproc(struct proc *p)
{
  // 释放我们的syscall这一页表
  if(p->syscall)
    kfree((void*)p->syscall);
  p->syscall = 0;
  if(p->trapframe)
    kfree((void*)p->trapframe);
  p->trapframe = 0;
  if(p->pagetable)
    proc_freepagetable(p->pagetable, p->sz);
  p->pagetable = 0;
  p->sz = 0;
  p->pid = 0;
  p->parent = 0;
  p->name[0] = 0;
  p->chan = 0;
  p->killed = 0;
  p->xstate = 0;
  p->state = UNUSED;
}

这样,我们的speed up完成了!

3. Print a page table

实验要求,简而言之就是需要我们使用kpgtbl系统调用的时候,需要我们去完善其中的一个函数vmprint,需要我们以下面的格式打印页表信息:

page table 0x0000000087f22000
 ..0x0000000000000000: pte 0x0000000021fc7801 pa 0x0000000087f1e000
 .. ..0x0000000000000000: pte 0x0000000021fc7401 pa 0x0000000087f1d000
 .. .. ..0x0000000000000000: pte 0x0000000021fc7c5b pa 0x0000000087f1f000
 .. .. ..0x0000000000001000: pte 0x0000000021fc70d7 pa 0x0000000087f1c000
 .. .. ..0x0000000000002000: pte 0x0000000021fc6c07 pa 0x0000000087f1b000
 .. .. ..0x0000000000003000: pte 0x0000000021fc68d7 pa 0x0000000087f1a000
 ..0xffffffffc0000000: pte 0x0000000021fc8401 pa 0x0000000087f21000
 .. ..0xffffffffffe00000: pte 0x0000000021fc8001 pa 0x0000000087f20000
 .. .. ..0xffffffffffffd000: pte 0x0000000021fd4c13 pa 0x0000000087f53000
 .. .. ..0xffffffffffffe000: pte 0x0000000021fd00c7 pa 0x0000000087f40000
 .. .. ..0xfffffffffffff000: pte 0x000000002000184b pa 0x0000000080006000

啥意思啊?简而言之,就是将每一级的页表,给他按照树的形式打印出来,详细的要求为:第一行显示 的 vmprint 参数。之后,每个 PTE 都有一行,包括引用树中更深处的页表页面的 PTE。每个 PTE 行都缩进了一个数字, " .." 表示其 depth 在树中。 每个 PTE 行显示其虚拟地址、pte 位和 从 PTE 中提取的物理地址。 不要打印无效的 PTE。 在上面的示例中, 顶级页表页包含条目 0 和 255 的映射。 下一个 条目 0 的 level down 仅映射了索引 0,而 bottom-level 对于该索引 0 映射了一些条目。

下面开始实验,首先,我们找到对应的函数vmprint,根据hint,我们需要从freewalk寻找相关的线索,freewalk:

// 递归释放页表中所有的非叶子页表项所指向的页表,最终释放自身。
// 这个函数不能用于释放包含实际映射(即叶子节点)的页表。
void
freewalk(pagetable_t pagetable)
{
  // 页表有 2^9 = 512 个页表项(PTE)
  for(int i = 0; i < 512; i++){
    pte_t pte = pagetable[i];
    // 如果该页表项有效 (PTE_V) 且不是叶子节点(没有 R/W/X 权限)
    if((pte & PTE_V) && (pte & (PTE_R | PTE_W | PTE_X)) == 0){
      // 当前页表项指向的是下一级页表
      uint64 child = PTE2PA(pte);             // 获取子页表的物理地址
      freewalk((pagetable_t)child);           // 递归释放子页表
      pagetable[i] = 0;                        // 清空当前页表项
    } 
    // 如果是有效的叶子节点(即具有 R/W/X 权限)
    else if(pte & PTE_V){
      // 不允许释放包含实际映射的叶子节点
      panic("freewalk: leaf");
    }
  }
  // 释放当前这一页页表
  kfree((void*)pagetable);
}

众所周知,我们就是需要去寻找每一个PTE,并且将持有这些PTE的页表的虚拟地址打印出来,并且输出其PTE,以及物理地址,而这里有用的信息是什么?我们可以看见,它通过遍历每一个页表项,去查询我们的每一个页表,然后检验其是否有效,是否为叶子节点,如果是叶子节点,那么就可以将其清空,如果不是,递归到下一层。

很显然,我们需要借鉴他的递归思路,遍历思路,以及检查叶子节点的方法,于是:

我们的vmprint还需要另外构造一个函数,总体如下:

void
vmprint(pagetable_t pagetable) {
  printf("page table %p\n", pagetable);
  PRINT(pagetable, 2, 0);
}

我们额外需要构造的PRINT函数:

// pagetable: 当前层级的页表指针
// level: 当前层级(2是顶层,0是页表最低层)
// va: 当前页表项对应的虚拟地址起始值
void PRINT(pagetable_t pagetable, int level, uint64 va) {
  uint64 sz;
  // 根据当前层级计算该层级每个页表项的虚拟地址的跨度
  if(level == 2) {
    sz = 512 * 512 * PGSIZE;
  } else if(level == 1) {
    sz = 512 * PGSIZE; 
  } else {
    sz = PGSIZE; 
  }
  // 遍历该页表的512个PTE
  for(int i = 0; i < 512; i++, va += sz) {
    pte_t pte = pagetable[i];
    // 无效,不管他
    if((pte & PTE_V) == 0)
      continue;
	// 树形结构(假)
    for(int j = level; j < 3; j++) {
      printf(" ..");
    }
    // 打印当前页表项的信息:
    // - 虚拟地址 va(从 pagetable 起开始累加)
    // - 页表项内容(pte)
    // - 对应的物理地址(通过 PTE2PA 提取)
    printf("%p: pte %p pa %p\n", 
           (pagetable_t)va, 
           (pagetable_t)pte, 
           (pagetable_t)PTE2PA(pte));
    // 如果该页表项不是叶子节点(即没有 R/W/X 权限),说明它指向下一级页表
    // 递归进入下一级页表打印,否则就不管他,直接走就行。
    if((pte & (PTE_R | PTE_W | PTE_X)) == 0) {
      PRINT((pagetable_t)PTE2PA(pte), level - 1, va);
    }
  }
}

4. Use superpages

这个实验真的比较难下手,虽然看提示还是能够写出来一点,但是有的东西我真的难写出来😶‍🌫️,这个实验在后面的很多都是参考了别人的博客的。

首先,superpage,其实就是在需求的内存比较大的时候,直接分配一个大页,也不是很多个页,而我们正要去实现这个大页分配的功能。

随后,我们可以查看hint中提醒我们去看看super_test:

// 测试超级页是否能够正确分配、读写,并在子进程中继承
void
superpg_test()
{
  int pid;
  printf("superpg_test starting\n");
  testname = "superpg_test"; // 设置当前测试的名字,用于错误报告中识别
  // 尝试向用户空间申请 N 字节内存(N = 2MB),返回的是当前堆的结束位置
  char *end = sbrk(N);
  if (end == 0 || end == (char*)0xffffffffffffffff)
    err("sbrk failed");  // 分配失败则报错并退出
  // 将堆的结束地址对齐到 2MB 边界(用于保证是超级页起始地址)
  uint64 s = SUPERPGROUNDUP((uint64) end);
  // 检查这个地址 s 是否是有效的超级页(映射正确、可读写、PTE 一致等)
  supercheck(s);
  // 创建子进程,验证页表继承是否正确
  if((pid = fork()) < 0) {
    err("fork"); // 创建失败
  } else if(pid == 0) {
    // 子进程中再次检查超级页是否仍然有效
    supercheck(s);
    exit(0);
  } else {
    int status;
    wait(&status); // 等待子进程退出
    if (status != 0) {
      exit(0); // 子进程出错则退出
    }
  }
  // 如果父子进程都验证成功,则测试通过
  printf("superpg_test: OK\n");  
}

显然,这里主要是通过sbrk分配了2MB的大页,如果单纯去使用PAGESIZE,势必会分配很多页,但是只需要使用一个超级页,便可以分配完成,然后我们可以来看看supercheck干了些什么:

// 检查以 s 为起点的超级页(2MB)内所有页的 PTE 是否一致、有效、具有读写权限;并测试读写是否正确
void supercheck(uint64 s) {
  pte_t last_pte = 0;  // 用于记录上一页的 PTE 值(用于比较是否一致)
  // 遍历整个超级页(2MB = 512 个 4KB 页 = 512 * PGSIZE)
  for (uint64 p = s; p < s + 512 * PGSIZE; p += PGSIZE) {
    // 获取虚拟地址 p 对应的页表项
    pte_t pte = (pte_t) pgpte((void *) p);
    // 页表项为空,说明没有映射,报错
    if (pte == 0)
      err("no pte");
    // 如果不是第一次检查,比较当前页表项是否和前一页一致
    if ((uint64) last_pte != 0 && pte != last_pte) {
      err("pte different");
    }
    // 检查页表项是否设置了有效位、读权限、写权限
    if ((pte & PTE_V) == 0 || (pte & PTE_R) == 0 || (pte & PTE_W) == 0) {
      err("pte wrong");
    }
    // 更新 last_pte
    last_pte = pte;
  }
  // 向超级页的每个页写入一个整数值(每页写一个 4 字节的整数 i)
  for (int i = 0; i < 512; i += PGSIZE) {
    *(int*)(s + i) = i;
  }
  // 读取验证:检查写入的值是否被正确保存在内存中
  for (int i = 0; i < 512; i += PGSIZE) {
    if (*(int*)(s + i) != i)
      err("wrong value");
  }
}

通过以上函数,我们可以获得的信息:

  1. 超级页的尺寸为512 * PGSIZE。

  2. 我们的检测实际上就是去分配一个超级页,看看是否能够正常使用这个超级页。

  3. 持有超级页的进程fork子进程的时候,必须分配超级页。

  4. 能够合理分配和释放一个超级页。

以上,就是本次实验的要求。

开始!

根据题目,我们可以知道,要从sbrk入手,我们先来回忆一下sbrk到底干了些啥

uint64
sys_sbrk(void)
{
  uint64 addr;
  int n;

  argint(0, &n);
  addr = myproc()->sz;
  if(growproc(n) < 0)
    return -1;
  return addr;
}

是不是大概能够会想起之前的源码的介绍了?我们通过growproc来为进程增加内存空间:

int
growproc(int n)
{
  uint64 sz;
  struct proc *p = myproc();
  // 获取当前进程的虚拟内存大小(sz表示进程的堆顶)
  sz = p->sz;
  if(n > 0){
    // 如果 n > 0,表示需要增加内存
    // 调用 uvmalloc 分配从 sz 到 sz + n 的内存
    if((sz = uvmalloc(p->pagetable, sz, sz + n, PTE_W)) == 0) {
      return -1;  // 分配失败,返回 -1
    }
  } else if(n < 0){
    // 如果 n < 0,表示需要减少内存
    sz = uvmdealloc(p->pagetable, sz, sz + n);  // 释放 sz 到 sz + n 之间的内存
  }
  // 更新进程的虚拟内存大小(sz)
  p->sz = sz;
  return 0;  // 返回 0 表示成功
}

此处就是我们需要做一定修改的地方,需要为他添加一定的逻辑,使得当申请的内存超过一定大小可以分配超级页,超级页作为一页,肯定不能继续沿用之前所用的普通的page链表,所以在此处做修改之前,先要把我们的准备工作给做好,就是添加一个全局的空闲的超级页链表,但是在此之前,我们还需要去看一个东西,就是普通空闲链表是如何分配的,来看看:

void
kinit()
{
  // 加锁,保证链表插入正常
  initlock(&kmem.lock, "kmem");
  // 记住这里的两个参数
  freerange(end, (void*)PHYSTOP);
}

void
freerange(void *pa_start, void *pa_end)
{
  char *p;
  // 对齐
  p = (char*)PGROUNDUP((uint64)pa_start);
  // 初始化所有的空闲内存为链表
  for(; p + PGSIZE <= (char*)pa_end; p += PGSIZE)
    kfree(p);
}

我们可以看到,我们的空闲链表的内存来自于[end, PHYSTOP]这段空间,end就是内核已经使用的空间的结尾,而PHTSTOP就是内存的上限,在memlayout中定义。

既然如此,我们的超级页也应当在这段空间里面分配,并且我们还需要一定程度上对kinit进行修改,使得可以添加超级页的空闲链表。

在memlayout中:

// 这里是程序的起始位置
#define KERNBASE 0x80000000L
// 分配的最大的空间,这里规定了最大内存范围
#define PHYSTOP (KERNBASE + 128*1024*1024)
// 这里最多提供12个超级页,虽然实际上是无法提供这么多的,因为内核代码占了一部分。
// 防止分配太多把普通页表的空间占完了
#define SUPERBASE (KERNBASE + 512 * PGSIZE * 12)

我们添加了一个SUPERBASE字段,表示在普通页的空间中分配12个超级页。

随后,我们根据规定的范围去修改我们的kinit相关的函数:

struct {
  struct spinlock lock;
  struct run *freelist;
} kmem, supermem;
// 为链表新增超级页实例

void
kinit()
{
  initlock(&kmem.lock, "kmem");
  freerange(end, (void*)SUPERBASE);
  superinit();
  // 这里的end是内核结束的地方,之后直到SUPERBASE都是普通页表可以分配的空间
  // 而SUPERBASE到PHYSTOP是超级页表可以分配的空间
}

void
freerange(void *pa_start, void *pa_end)
{
  char *p;
  p = (char*)PGROUNDUP((uint64)pa_start);
  for(; p + PGSIZE <= (char*)pa_end; p += PGSIZE)
    kfree(p);
}

// 新增的init函数,初始化超级页的空闲链表
void superinit(){
  initlock(&supermem.lock, "supermem");
  char *p;
  p = (char*)SUPERPGROUNDUP((uint64)SUPERBASE);
  for(; p + SUPERPGSIZE <= (char*)PHYSTOP; p += SUPERPGSIZE)
    superfree(p);
}

// 超级页的释放,直接根据kfree照葫芦画瓢就行了。
void superfree(void *pa) {
  struct run *r;
  // 改成超级页的尺寸,定义在SUPERPGSIZE, 这里爆红是正常的
  if(((uint64)pa % SUPERPGSIZE) != 0 || (char*)pa < SUPERBASE || (uint64)pa >= PHYSTOP)
    panic("superfree");
  // memset,将内存为都置1,表示处于空闲中
  memset(pa, 1, SUPERPGSIZE);
  
  r = (struct run*)pa;
  // 加锁,保证链表插入顺序
  acquire(&supermem.lock);
  r->next = supermem.freelist;
  supermem.freelist = r;
  release(&supermem.lock);
}

接下来,我们还需要为超级页添加实现分配的函数,总体的superpage的流程,大多都需要以普通页的分配释放映射为参考,而这里,其实就是kalloc换个皮:

void* superalloc(void) {
  struct run *r;
  // 获取锁
  acquire(&supermem.lock);
  r = supermem.freelist;
  if(r)
    // 更新空闲页链表
    supermem.freelist = r->next;
  release(&supermem.lock);

  if(r)
    memset((char*)r, 5, SUPERPGSIZE); // fill with junk
  return (void*)r; 
}

这样,最底层需要被用到的函数就完成了,而sbrk在中间还会调用growproc,在其中调用vmmalloc为我们的进程分配内存,此处,就需要修改我们的uvmalloc,当需要的内存过大时,分配超级页,但是值得注意的一点是,我们分配过程中肯定需要进行内存对齐,由于超级页实在是太大了,肯定会非常浪费空间,所以我们应当先将对齐需要浪费的内存空间给利用起来。

uint64
uvmalloc(pagetable_t pagetable, uint64 oldsz, uint64 newsz, int xperm)
{
  char *mem;
  uint64 a; 
  int sz; 
    
  if(newsz < oldsz)
    return oldsz;
    
  oldsz = PGROUNDUP(oldsz);
  // 利用超级页对其所浪费的空间
  for(a = oldsz; a < SUPERPGROUNDUP(oldsz) && a < newsz; a += sz){
    sz = PGSIZE; 
    mem = kalloc(); 
    if(mem == 0){
      uvmdealloc(pagetable, a, oldsz); 
      return 0;
    }
#ifndef LAB_SYSCALL
    memset(mem, 0, sz);
#endif
    if(mappages(pagetable, a, sz, (uint64)mem, PTE_R|PTE_U|xperm) != 0){
      kfree(mem);
      uvmdealloc(pagetable, a, oldsz);
      return 0;
    }
  }
  // 第二步:尽可能使用超级页批量分配,提升性能并减少页表开销
  // 之所以加上 a + SUPERPGSIZE < newsz 的条件,是为了尽可能少地浪费内存
  for(; a + SUPERPGSIZE < newsz; a += sz){
    sz = SUPERPGSIZE; 
    mem = superalloc(); 
    if(mem == 0){
      break;
    }
#ifndef LAB_SYSCALL
    memset(mem, 0, sz);
#endif
    if(mappages(pagetable, a, sz, (uint64)mem, PTE_R|PTE_U|xperm) != 0){
      superfree(mem);
      uvmdealloc(pagetable, a, oldsz);
      return 0;
    }
  }
  // 第三步:对剩下不足一个超级页的部分用普通页补齐
  for(; a < newsz; a += sz){
    sz = PGSIZE;
    mem = kalloc();
    if(mem == 0){
      uvmdealloc(pagetable, a, oldsz);
      return 0;
    }
#ifndef LAB_SYSCALL
    memset(mem, 0, sz);
#endif
    if(mappages(pagetable, a, sz, (uint64)mem, PTE_R|PTE_U|xperm) != 0){
      kfree(mem);
      uvmdealloc(pagetable, a, oldsz);
      return 0;
    }
  }
  // 返回新分配完成后的地址空间大小
  return newsz;
}

值得注意的是,uvmdealloc也需要被重新修改,现在当我们分配一个超级页之后,使用的mappages依旧是原来的样子,他现在只能做到对普通页进行映射,所以我们还需要去修改mappages,使得它能够映射超级页,与此同时,还需要一个superwalk函数:

// 超级页映射只需要走到第1级,不需要走到最底层,这里也是我看别人博客才了解到的。
// 所以这里就没有必要用循环了。
pte_t* superwalk(pagetable_t pagetable, uint64 va, int alloc)
{
  // 检查虚拟地址是否越界
  if(va >= MAXVA)
    panic("superwalk");
  // 获取第2级页表中的页表项
  pte_t *pte = &pagetable[PX(2, va)];
  if(*pte & PTE_V) {
    // 如果该页表项有效(页表存在),进入第1级页表
    pagetable = (pagetable_t)PTE2PA(*pte); // 获取下一层页表的物理地址
    return &pagetable[PX(1, va)];          // 返回第1级页表中对应的 PTE 指针
  } else {
    // 页表项无效,页表不存在,需要根据 alloc 决定是否分配新的页表页
    // 注意:即使是映射超级页,它的页表结构也是由普通页组成的
    if(!alloc || (pagetable = (pde_t*)kalloc()) == 0)
      return 0;  // 分配失败或不允许分配,返回 NULL
    memset(pagetable, 0, PGSIZE); // 清零新分配的页表页
    *pte = PA2PTE(pagetable) | PTE_V; // 设置上层页表项为新分配页的地址,并置有效位
    // 返回第0级页表中的项
    return &pagetable[PX(0, va)];
  }
}




// - pagetable:进程页表
// - va:要映射的起始虚拟地址,必须页对齐
// - size:映射的大小,必须是页大小的倍数
// - pa:起始物理地址,决定是否使用超级页
// - perm:页表项权限位(如 PTE_W、PTE_U 等)
int
mappages(pagetable_t pagetable, uint64 va, uint64 size, uint64 pa, int perm)
{
  uint64 a, last;
  uint64 pgsize;
  pte_t *pte;

  // 根据物理地址是否超过 SUPERBASE 判断是否使用超级页
  if (pa >= SUPERBASE)
    pgsize = SUPERPGSIZE;
  else
    pgsize = PGSIZE; 

  // 检查虚拟地址是否页对齐
  if((va % pgsize) != 0)
    panic("mappages: va not aligned");

  // 检查 size 是否是页大小的整数倍
  if((size % pgsize) != 0)
    panic("mappages: size not aligned");

  if(size == 0)
    panic("mappages: size");

  a = va;
  last = va + size - pgsize;

  // 遍历每一页并映射
  for(;;){
    // 1. 根据页大小选择 walk 方式,获取/创建对应的页表项地址
    if(pgsize == PGSIZE && (pte = walk(pagetable, a, 1)) == 0)
      return -1;
    if(pgsize == SUPERPGSIZE && (pte = superwalk(pagetable, a, 1)) == 0)
      return -1;

    // 2. 如果已经存在映射,说明重复映射,报错
    if(*pte & PTE_V)
      panic("mappages: remap");

    // 3. 设置页表项,包含物理地址 + 权限 + 有效位
    *pte = PA2PTE(pa) | perm | PTE_V;

    // 4. 判断是否完成整个映射区间
    if(a == last)
      break;

    // 5. 前进到下一页
    a += pgsize;
    pa += pgsize;
  }

  return 0;
}

我们走到这一步,回到我们刚刚留下的一个问题,uvmdealloc用来处理释放内存,我们需要修改它,使得其支持超级页的释放,其中,uvmdealloc调用了uvmunmap函数,用于取消映射和释放页表,所以我们的核心就放在了他的上面:

//   pagetable:进程的页表
//   va:要解除映射的起始虚拟地址
//   npages:要解除映射的页数
//   do_free:是否释放物理页
void uvmunmap(pagetable_t pagetable, uint64 va, uint64 npages, int do_free)
{
  uint64 a;
  pte_t *pte;
  int sz;
  // 检查虚拟地址必须页对齐
  if((va % PGSIZE) != 0)
    panic("uvmunmap: not aligned");
  // 遍历每一页,解除映射
  for(a = va; a < va + npages * PGSIZE; a += sz){
    sz = PGSIZE;
    // 获取页表项(不分配页表,所以 alloc=0)
    if((pte = walk(pagetable, a, 0)) == 0)
      panic("uvmunmap: walk");
    // 检查页表项是否有效(是否映射)
    if((*pte & PTE_V) == 0) {
      printf("va=%ld pte=%ld\n", a, *pte);
      panic("uvmunmap: not mapped");
    }
    // 检查该页表项是否为叶子节点(是否真正映射了物理页)
    if(PTE_FLAGS(*pte) == PTE_V)
      panic("uvmunmap: not a leaf");
    // 取得对应物理地址
    uint64 pa = PTE2PA(*pte);
    // 如果是超级页(物理地址 >= SUPERBASE),则说明是 2MB 映射
    // 需要额外调整步长,使得下一次循环跳过整个超级页范围
    if (pa >= SUPERBASE){
      a += SUPERPGSIZE;
      a -= sz; // 因为循环里还会加 sz(4KB),这里先减去以抵消
    }
    // 如果需要释放物理页帧
    if(do_free){
      uint64 pa = PTE2PA(*pte);
      if (pa >= SUPERBASE) 
        superfree((void*)pa); // 释放超级页
      else 
        kfree((void*)pa);     // 释放普通页
    }
    // 清空页表项
    *pte = 0;
  }
}

此时,我们很多事情都已经干完了,接下来,回到提示,看看我们少干了些什么,我们会发现,我们的fork的条件似乎并未满足,在fork的时候,我们是通过uvmcopy来复制页表,所以此时修改ivmcopy的逻辑,使其能够分配超级页。

// 复制用户页表的内容:
// 把 old 页表中从 0 到 sz 的映射复制到 new 页表中。
// 如果对应的是超级页则使用 superalloc,否则使用 kalloc。
// 复制时连同数据内容一起复制(即数据页克隆)。
// 返回 0 表示成功,-1 表示失败(并清理 new 页表中已分配的页)。
int
uvmcopy(pagetable_t old, pagetable_t new, uint64 sz)
{
  pte_t *pte;
  uint64 pa, i;
  uint flags;
  char *mem;
  int szinc;

  // 遍历 old 页表中从 0 到 sz 的所有虚拟地址
  for(i = 0; i < sz; i += szinc){
    szinc = PGSIZE;
    // 找到 old 页表中当前虚拟地址 i 对应的页表项
    // 由于此时使用的页已经被正确映射
    // 所以普通的walk也可以找到超级页的pte
    if((pte = walk(old, i, 0)) == 0)
      panic("uvmcopy: pte should exist");
    if((*pte & PTE_V) == 0)
      panic("uvmcopy: page not present");
    // 提取物理地址与标志位
    pa = PTE2PA(*pte);
    flags = PTE_FLAGS(*pte);
    // 判断是普通页还是超级页
    if(pa >= SUPERBASE) {
      // 超级页大小设置为 2MB
      szinc = SUPERPGSIZE;
      // 分配新的超级页
      if((mem = superalloc()) == 0)
        goto err;
    }
    // 普通页
    else if((mem = kalloc()) == 0)
      goto err;
    // 拷贝数据内容(从旧物理页拷贝到新分配的物理页)
    memmove(mem, (char*)pa, szinc);
    // 映射到新页表中
    if(mappages(new, i, szinc, (uint64)mem, flags) != 0){
      if(szinc == SUPERPGSIZE)
        superfree(mem);
      else
        kfree(mem);
      goto err;
    }
  }
  return 0;
 err:
  // 若出错,释放 new 页表中已分配的页
  uvmunmap(new, 0, i / PGSIZE, 1);
  return -1;
}

至此,完成。。。

另外想说一下超级页的好处:

我们知道,超级页减少了一级的映射,所以自然而然,分配的页面也会更少,管理这些页面的消耗也会减少,同时,我们在上一篇文章中提过,TLB可以缓存我们的翻译结果,由于我们超级页映射的范围非常大,所以缓存的命中率是更高的,如果对这部分的缓存量很大,且在内存中很分散,我们就可以节省了很多缓存的条目,注意,TLB是缓存了我们的翻译的页号,而不是缓存的数据,所以无论你是4kb的页还是2mb的页,都会作为一个条目被缓存,这样就节省了缓存的条目!

这个超级页的实验还是很难的,虽然根据提示我也能做出来一些,但是我一个人还是没办法完成的,总结一下:

超级页的实验主要是迫使我们去真正的理解普通的页是如何分配的,如何映射,如何释放的,然后让我们自己去重新实现一遍逻辑,对于超级页而言,我觉得难想的就是映射,我还以为也要映射三级,但是事实上根本不用,因为超级页本身就是已经涵盖了两级的内容,无需继续walk到最后一层页表,这就是理解上的问题了,这个实验去做一遍还是很不错的,虽然也参考了别人的博客,但总归是自己敲了一遍,接下来几天就要all in 期中复习了,虽然想继续下一个课程,但是只能先暂缓了。

参考文献:

上一页MIT6.S081-lab2下一页MIT6.S081-lab3前置

最后更新于2天前

[。

MIT 6.1810]Lab3 page table