🎉Introduction
在本实验中,我们将在多个同时活动的用户模式环境中实现抢占式多任务处理。
在 Part A 中,我们将一起向 JOS 添加多处理器支持,实现循环调度,并添加基本的环境管理系统调用:
- 创建和销毁环境
- 分配/映射内存的调用
在 Part B 中,我们将实现一个类似 Unix 系统的的 fork() 函数,它允许在用户模式的环境下创建自身的副本。
最后,在 Part C 中,我们将添加对进程间通信(IPC)的支持,允许不同的用户模式下的环境显式地相互通信和同步;还将添加对硬件时钟中断和抢占的支持。
🎐Multiprocessor Support and Cooperative Multitasking
在 Part A 中,将首先扩展 JOS 以在多处理器系统上运行,然后实现一些新的 JOS 内核系统调用以允许用户级环境创建额外的新环境。还将实现协作循环调度,允许内核在当前环境自愿放弃 CPU(或退出)时从一个环境切换到另一个环境。在后面的 Part C 中,将实现抢占式调度,它允许内核在经过一定时间后从环境中重新控制 CPU,即使没有环境的合作。
🌭Multiprocessor Support
我们将使 JOS 支持对称多处理器(SMP),这是一种多处理器模型,在该模型中,所有 CPU 对系统资源(如内存和 I/O 总线)具有同等的访问权限。虽然在 SMP 中所有 CPU 的功能都是相同的,但在引导过程中,它们可以分为两种类型:
- 引导处理器(BSP)负责初始化系统和引导操作系统
- 应用处理器(AP)只有在操作系统启动并运行后才由 BSP 激活
哪个处理器是 BSP 由硬件和 BIOS 决定。到目前为止,所有现有的 JOS 代码都已在 BSP 上运行。
在 SMP 系统中,每个 CPU 都有一个相应的本地 APIC(LAPIC)单元。LAPIC 单元负责在整个系统中传递中断;LAPIC 还为其连接的 CPU 提供唯一标识符。在本实验中,我们使用了 LAPIC 单元的以下基本功能(在 kern/lapic.c 中):
- 读取 LAPIC 标识符(APIC ID)来告诉我们的代码当前运行在哪个 CPU 上(参见 cpunum())
- 将启动处理器间中断(IPI)从 BSP 发送到 APs 以启动其他 CPU(请参阅 lapic_startap())
- 在 Part C 中,我们对 LAPIC 的内置计时器进行编程,以触发时钟中断,从而支持抢占式多任务处理(参见 apic_init())
处理器使用内存映射 I/O(MMIO)访问其 LAPIC。在 MMIO 中,物理内存的一部分硬连接到一些 I/O 设备寄存器,因此通常用于访问内存的 load/store 指令可以用于访问设备寄存器。
在物理地址 0xA0000 处已经有了一个 I/O 孔(我们使用它来写入 VGA 显示缓冲区)。LAPIC 位于一个从物理地址 0xFE000000(离 4GB 有 32MB 的距离)开始的 IO 孔中,所以它太高了,我们无法使用 KERNBASE 中常用的直接映射进行访问。
JOS 虚拟内存映射在 MMIOBASE 上留下了 4MB 的空间,所以我们有一个地方可以映射这样的设备。因为后面的实验引入了更多的 MMIO 区域,所以您将编写一个简单的函数来分配该区域的空间,并将设备内存映射到该区域。
Exercise 1。
🛺Application Processor Bootstrap
在启动 APs 之前,BSP 应该首先收集有关多处理器系统的信息,例如:
- CPU 的总数
- CPU 的 APIC ID 和 LAPIC 单元的 MMIO 地址
kern/mpconfig.c 中的 mp_init() 函数通过读取驻留在 BIOS 内存区域中的 MP 配置表来检索此信息。
boot_aps() 函数(在 kern/init.c 中)驱动 AP 引导过程。APs 以实模式启动,与 boot/boot.S 中 bootloader 的启动方式非常相似,因此 boot_aps() 将 AP 条目代码(kern/mpentry.S)复制到实模式下可寻址的内存位置。与 bootloader 不同的是,我们可以控制 AP 从何处开始执行代码;我们将入口代码复制到 0x7000(MPENTRY_PADDR),且低于 640KB 的任何未使用的、页对齐的物理地址都可以工作。
之后,boot_aps() 通过向相应 AP 的 LAPIC 单元发送 STARTUP IPIS,以及 AP 应该开始运行其入口代码的初始 CS:IP 地址(在本例中是 MPENTRY_PADDR),一个接一个地激活 AP。kern/mpentry.S 中的条目代码与 boot/boot.S 中的条目代码非常相似。经过一些简短的设置后,它将 AP 置于启用分页的保护模式,然后调用 C 设置程序 mp_main()(也在 kern/init.c 中)。boot_aps() 等待 AP 在其 CpuInfo 结构体的 CPU_status 字段中发出 CPU_STARTED 标志的信号,然后再唤醒下一个。
Exercise 2。
Question 1。
🚛Per-CPU State and Initialization
在编写多处理器操作系统时,区分各个处理器专用的 CPU 状态和整个系统共享的全局状态是很重要的。kern/cpu.h 定义了大多数的 CPU 状态,包括:
- 存储 CPU 变量的 CpuInfo 结构体
- cpunum() 总是返回调用它的 CPU 的 ID,这个 ID 可以用作 cpus 数组的索引
- 宏 thiscpu 是当前 CPU 的 CpuInfo 结构体的缩写
以下是您应该注意的各个 CPU 的状态:
- Per-CPU kernel stack:因为多个 CPU 可以同时进入内核,所以我们需要为每个处理器提供一个单独的内核堆栈,以防止它们干扰彼此的执行。数组 percpu_kstacks[NCPU][KSTKSIZE] 为 NCPU 个 CPU 保留堆栈空间。在 Lab2 中,我们将 bootstack 所指的物理内存映射为 KSTACKTOP 下面的 BSP 内核堆栈。类似地,在这个实验中,我们将把每个 CPU 的内核堆栈映射到这个区域,并使用保护页作为它们之间的缓冲区。CPU 0 的堆栈仍将从 KSTACKTOP 向下增长;CPU 1 的堆栈将从 CPU 0 堆栈底部以下的 KSTKGAP 字节开始,依此类推。inc/memlayout.h 中展示了映射布局
- Per-CPU TSS and TSS descriptor:为了指定每个 CPU 的内核堆栈所在的位置,还需要一个 CPU 任务状态段(TSS)。CPU i 的 TSS 存储在 cpus[i].cpu_ts 中,相应的 TSS 描述符在 GDT 条目 gdt[(GD_TSS0 >> 3) + i] 中定义。在 kern/trap.c 中定义的全局 ts 变量将不再使用
- Per-CPU current environment pointer:由于每个 CPU 可以同时运行不同的用户进程,因此我们重新定义了 curenv 符号,以引用 cpus[cpunum()].cpu_env(或 thiscpu->cpu_env),它指向当前 CPU(运行代码的CPU)上当前执行的环境
- Per-CPU system registers:所有寄存器,包括系统寄存器,都是 CPU 专用的。因此,初始化这些寄存器的指令,如 lcr3()、ltr()、lgdt()、lidt() 等,必须在每个 CPU 上执行一次。函数 env_init_percpu() 和 trap_init_percpu() 就是为此而定义的
🛹Locking
我们当前的代码在 mp_main() 中初始化 AP 后自旋。在让 AP 进一步工作之前,我们需要首先解决多个 CPU 同时运行内核代码时的竞争条件。
实现这一点的最简单方法是使用一个内核锁。内核锁是一个全局锁,它在环境进入内核模式时保持,在环境返回用户模式时释放。在这个模型中,处于用户模式的环境可以在任何可用的 CPU 上并发运行,但是不能有多个环境在内核模式下运行;任何其他尝试进入内核模式的环境都将被迫等待。
kern/spinlock.h 声明了内核锁,即 kernel_lock。它还提供 lock_kernel() 和 unlock_kernel(),这是获取和释放锁的方式。您应该在四个位置应用大内核锁:
- 在 i386_init() 中,在 BSP 唤醒其他 CPU 之前获取锁
- 在 mp_main() 中,在初始化 AP 后获取锁,然后调用 sched_yield() 开始在此 AP 上运行环境
- 在 trap() 中,从用户模式捕获时获取锁;要确定 trap 是发生在用户模式还是内核模式,请检查 tf_cs 的低位
- 在 env_run() 中,在切换到用户模式之前释放锁
🚈Round-Robin Scheduling
下一个任务是更改 JOS 内核,以便它能够以循环的方式在多个环境之间切换;JOS 中的循环调度工作如下:
- kern/sched.c 中的函数 sched_yield() 负责选择要运行的新环境;它以循环方式依次搜索 envs[] 数组,从之前运行的环境(如果没有以前运行的环境,则在数组的开头)开始,选择找到的第一个状态为 ENV_RUNNABLE 的环境,并调用 env_run() 跳转到该 env 中
- sched_yield() 决不能同时在两个 CPU 上运行同一个环境;它可以判断某个环境当前正在某个 CPU(可能是当前CPU)上运行,因为该环境的状态将是 ENV_RUNNING
- 我们为您实现了一个新的系统调用 sys_yield(),用户环境可以调用它来调用内核的 sched_yield() 函数,从而自动地将 CPU 交给不同的环境
Exercise 6。
Question 3。
Question 4。
🧈System Calls for Environment Creation
尽管您的内核现在能够在多个用户级环境之间运行和切换,但它仍然局限于内核最初设置的运行环境。现在,您将实现必要的 JOS 系统调用,以允许用户环境创建和启动其他新的用户环境。
Unix 提供 fork() 系统调用作为其进程创建原语。Unix fork() 复制调用进程(父进程)的整个地址空间以创建新进程(子进程)。从用户空间中观察到的两个进程之间的唯一区别是它们的进程 ID 和父进程 ID(由 getpid 和 getppid 返回)。在父进程中,fork() 返回子进程 ID,而在子进程中,fork() 返回 0。默认情况下,每个进程都有自己的私有地址空间,并且两个进程对内存的修改对另一个进程都不可见。
将提供一组不同的、更原始的 JOS 系统调用来创建新的用户模式环境。通过这些系统调用,您将能够完全在用户空间中实现类 Unix 的 fork(),以及其他类型的环境创建。将为 JOS 编写的新系统调用如下:
- sys_exofork:这个系统调用创建了一个几乎是空白的新环境;在地址空间的用户部分没有映射任何内容,并且它是不可运行的。在 sys_exofork 调用时,新环境将具有与父环境相同的寄存器状态。在父级中,sys_exofork 将返回新创建环境的 envid_t(如果环境分配失败,则返回负错误代码);但是,在子级中,它将返回 0
- sys_env_set_status:将指定环境的状态设置为 ENV_RUNNABLE 或 ENV_NOT_RUNNABLE;此系统调用通常用于在地址空间和寄存器状态完全初始化后,将新环境标记为可以运行
- sys_page_alloc:分配物理内存页并将其映射到给定环境地址空间中的给定虚拟地址
- sys_page_map:复制页面映射(而不是页面内容!)从一个环境的地址空间到另一个环境,保留内存共享,以便新映射和旧映射都引用相同的物理内存页
- sys_page_unmap:取消映射在给定环境中给定虚拟地址处映射的页
对于上面所有接受环境 ID 的系统调用,JOS 内核支持值为 0 表示“当前环境”的约定。该约定由 kern/env.c 中的 envid2env() 实现。
我们在测试程序 user/dumbfork.c 中提供了一个非常原始的类 Unix fork() 的实现。这个测试程序使用上面的系统调用来创建和运行一个具有自己地址空间副本的子环境。然后,这两个环境使用 sys_yield 在上一个练习中来回切换。父级在10次迭代后退出,而子级在20次迭代后退出。
Exercise 7。
🧵Copy-on-Write Fork
上面我们已经讲过,Unix 提供 fork() 系统调用作为其主要进程创建原语。fork() 系统调用复制调用进程(父进程)的地址空间以创建新进程(子进程)。
xv6 Unix 通过将父进程内存也中的所有数据复制到子进程的内存页中来实现 fork()。这与 dumbfork() 采用的方法基本相同。将父地址空间复制到子地址空间是 fork() 操作中最昂贵的部分。
但是,在子进程中,对 fork() 的调用通常紧接着对 exec() 的调用,这将用新的程序替换子进程的内存。例如, shell 通常就是这样做的。在这种情况下,复制父进程的地址空间所花费的时间在很大程度上是浪费的,因为子进程在调用 exec() 之前只会使用很少的被复制过来的内存。
因此,Unix 的更高版本利用虚拟内存硬件,允许父进程和子进程共享实际的物理内存却映射到各自地址空间的虚拟内存中,直到某个进程实际修改它为止。这种技术称为写时复制。
因此,在 fork() 中,内核将地址空间映射从父进程复制到子进程,而不是复制映射页的内容,同时将现在共享的页标记为只读。当两个进程中的一个试图写入其中一个共享页时,该进程将出现页错误。这样做,Unix 内核将会意识到页面实际上是一个“虚拟”或“写时复制”的副本,因此它为出错的进程创建一个新的、私有的、可写的页面副本。这样,在实际写入之前,单个页面的内容实际上不会被复制。
这种优化使得 fork() 和 exec() 在子进程中的开销大大降低:子对象在调用 exec() 之前可能只需要复制一个页面(其堆栈的当前页面)。
在 Part B 中,将实现一个类 Unix 的 fork(),并将 copy-on-write 作为一个用户态的库的一个特性。在用户空间中实现 fork() 和 copy-on-write 支持的好处是内核仍然简单得多。它还允许各个用户态程序为 fork() 定义自己的语义。如果一个程序想要一个稍微不同的实现(例如,像 dumbfork() 这样的昂贵实现),那么将会很容易。
🛺User-level page fault handling
用户级的支持写时复制的 fork() 需要知道写保护页时发生的页错误,所以这是我们首先要实现的。写时拷贝只是用户级页面错误处理的许多可能用途之一。
通常会设置一个地址空间,以便页错误可以指示何时需要执行什么操作。例如,大多数 Unix 内核最初只映射一个存放新进程堆栈区域的内存页,然后随着进程堆栈消耗的增加并导致访问尚未映射的地址出现页错误时,按需分配和映射其他堆栈页面。
典型的 Unix 内核必须跟踪在进程空间的每个区域中发生页面错误时要执行的操作。例如:
- 堆栈区域中的错误通常会分配和映射新的物理内存页
- 程序的 BSS 区域中的错误通常会分配一个新页面,用零填充它,然后映射它
- 在具有按需分页可执行文件的系统中,文本区域中的错误将从磁盘中读取二进制文件的相应页,然后将其映射
内核需要跟踪的大量信息。您将决定如何处理用户空间中的每个页错误,而不是采用传统的 Unix 方法,因为在用户空间中,bug 的破坏性较小。这种设计的另一个好处是允许程序在定义其内存区域时具有极大的灵活性;后面的实验中将使用用户级页面错误处理来映射和访问基于磁盘的文件系统上的文件。
🚕Setting the Page Fault Handler
为了处理用户态的页面错误,用户环境需要向 JOS 内核注册一个页面错误处理程序入口点。用户环境通过新的 sys_env_set_pgfault_upcall 系统调用注册其页面错误入口点。我们在 Env 结构中添加了一个新成员 env_pgfault_upcall 来记录此信息。
Exercise 8。
🚚Normal and Exception Stacks in User Environments
在正常执行期间,JOS 中的用户环境将在正常用户堆栈上运行:它的 ESP 寄存器开始指向 USTACKTOP,它推送的堆栈数据驻留在 USTACKTOP-PGSIZE 和 USTACKTOP-1(含)之间的页面上。
但是,当在用户模式下发生页错误时,内核将重新启动用户环境,在不同的堆栈(即用户异常堆栈)上运行指定的用户级页面错误处理程序。本质上,我们将使 JOS 内核代表用户环境实现自动“堆栈切换”,就像 x86 处理器在从用户模式转换到内核模式时已经代表 JOS 实现堆栈切换一样!
JOS 用户异常堆栈的大小也是一页,并且其顶部被定义为位于虚拟地址 UXSTACKTOP,因此用户异常堆栈的有效字节是从 UXSTACKTOP-PGSIZE 到 UXSTACKTOP-1(含)。在这个异常堆栈上运行时,用户级页面错误处理程序可以使用 JOS 的常规系统调用来映射新页面或调整映射,以便修复最初导致页错误的问题。然后,用户级页面错误处理程序通过汇编语言存根返回到原始堆栈上的错误代码。
每个想要支持用户级页面错误处理的用户环境都需要使用在 Part A 中介绍的 sys_page_alloc() 系统调用为自己的异常堆栈分配内存。
🦼Invoking the User Page Fault Handler
您现在需要更改 kern/trap.c 中的页面错误处理代码,以从用户模式处理页面错误,如下所示。我们将故障发生时用户环境的状态称为 trap-time 状态。
如果没有注册页面错误处理程序,那么 JOS 内核会像以前一样用一条消息破坏用户环境。否则,内核会在异常堆栈上设置一个 trap 帧,看起来像 inc/trap.h 中的 UTrapframe 结构体:
<-- UXSTACKTOPtrap-time esptrap-time eflagstrap-time eiptrap-time eax start of struct PushRegstrap-time ecxtrap-time edxtrap-time ebxtrap-time esptrap-time ebptrap-time esitrap-time edi end of struct PushRegstf_err (error code)fault_va <-- %esp when handler is run
然后,内核安排用户环境恢复执行,页面错误处理程序在此堆栈框架的异常堆栈上运行;您必须弄清楚如何实现这一点。fault_va 是导致页面错误的虚拟地址。
如果发生异常时用户环境已在用户异常堆栈上运行,则页面错误处理程序本身已出错。在这种情况下,您应该在当前 tf->tf_esp 下而不是在 UXSTACKTOP 下启动新的堆栈帧。您应该首先推送一个 32 位的空字,然后推送一个结构 UTrapframe。
要测试 tf->tf_esp 是否已经在用户异常堆栈上,请检查它是否在 UXSTACKTOP-PGSIZE 和 UXSTACKTOP-1 之间(包括UXSTACKTOP-1)。
Exercise 9。
🚅User-mode Page Fault Entrypoint
接下来,您需要实现汇编程序,该程序将负责调用 C 代码中的页错误处理程序,并在原始错误指令处继续执行。此程序将使用 sys_env_set_pgfault_upcall() 向内核注册的处理程序。
Exercise 10。
最后,需要实现 C 用户库端的用户级页面错误处理机制。
Exercise 11。
🚊Testing
运行 make grade,可以通过如下测试样例:
faultread: OK (0.9s)faultwrite: OK (1.0s)faultdie: OK (1.8s)faultregs: OK (1.2s)faultalloc: OK (1.6s)faultallocbad: OK (2.4s)faultnostack: OK (1.8s)faultbadhandler: OK (1.2s)faultevilhandler: OK (1.7s)
🚛Implementing Copy-on-Write Fork
现在,您拥有了完全在用户空间中实现写时复制 fork() 的内核功能。
我们在 lib/fork.c 中为 fork() 提供了一个框架。与 dumbfork() 一样,fork() 应该创建一个新的环境,然后扫描父环境的整个地址空间,并在子环境中设置相应的页映射。关键区别在于,dumbfork() 复制页面,fork() 最初只复制页面映射。fork() 将只在其中一个环境尝试写入一个页时复制它。
fork() 的基本控制流如下:
- 父级使用上面实现的 set_pgfault_handler() 函数将 pgfault() 注册为页错误处理程序
- 父级调用 sys_exofork() 来创建子环境
- 对于 UTOP 下面地址空间中的每个可写或可以写时复制的内存页,父级调用 duppage,它应将内存页映射到子级的地址空间,然后在其自己的地址空间中重新映射一次(需要注意的是:在标记父进程的页之前将子进程的页标记为 COW)。duppage 设置两个 PTE,使页面不可写,并在 avail 字段中包含 PTE_COW,以区分写内存页是副本还是只读页面。但是,异常堆栈不会以这种方式重新映射;相反,您需要在子级中为异常堆栈分配一个新的页。由于页错误处理程序将执行实际的复制,并且页错误处理程序在异常堆栈上运行,因此不能使异常堆栈在写时复制。fork() 还需要处理存在但不可写或写时复制的页
- 父进程将子进程的用户页错误入口点设置为自己的
- 父进程将子进程标记为 runnable
每次其中一个环境在其尚未进行写的写时复制页上写入数据时,都会出现一个页错误。以下是用户页错误处理程序的控制流:
- 内核将页错误传播到调用 fork() 的 pgfault() 处理程序的 _pgfault_upcall
- pgfault() 检查该错误是否为写入错误(检查错误代码 FEC_WR)以及该页的 PTE 是否标记为 PTE_COW;如果没有,则引发 panic
- pgfault() 分配新页,并将出错页的内容复制到其中
- 然后,错误处理程序将新页映射到具有读/写权限的适当地址,而不是旧的只读映射
用户级 lib/fork.c 代码必须查阅环境中的页表,以执行上面的几个操作(例如,页的 PTE 被标记为PTE_COW)。内核将环境中的页表映射到 UVPT,正是出于这个目的。它使用了一个映射技巧,使得查找用户代码的 pte 变得容易。lib/entry.S 设置了 uvpt 和 uvpd,这样就可以方便地在 lib/fork.c 中查找页表信息。
Exercise 12。
🧥Preemptive Multitasking and Inter-Process communication (IPC)
在 Lab4 的最后一部分,将修改内核以实现抢占式环境,并允许环境显式地相互传递消息。
🎒Clock Interrupts and Preemption
运行 user/spin 测试程序。这个测试程序派生出一个子环境,一旦它接收到 CPU 的控制,子环境就永远在一个紧密的循环中旋转。无论是父环境还是内核都无法恢复 CPU。
在用户模式环境中保护系统不受 bug 或恶意代码的影响,这显然不是一个理想的情况,因为任何用户模式环境都可以通过进入一个无限循环而永远不返回 CPU,从而使整个系统停止。为了让内核能够抢占运行环境,从中强行夺回对 CPU 的控制,我们必须扩展 JOS 内核以支持来自时钟硬件的外部硬件中断。
🧣Interrupt discipline
外部中断(即设备中断)称为 IRQ,其有 16 种可能的 IRQ,编号从 0 到 15。从 IRQ 编号到 IDT 条目的映射不是固定的。picirq.c 中的 pic_init 通过 IRQ_OFFSET+15 将 IRQs 0-15 映射到 IDT 条目 IRQ_OFFSET。
在 inc/trap.h 中,IRQ_OFFSET 定义为十进制的 32。因此,IDT 条目 32-47 对应于 IRQs 0-15。例如,时钟中断是 IRQ 0。因此,IDT[IRQ_OFFSET+0](即 IDT[32])包含内核中时钟中断处理程序例程的地址。选择这个 IRQ_OFFSET 是为了使设备中断不会与处理器异常重叠。
在 JOS 中,与 xv6 Unix 相比,我们做了一个关键的简化。外部设备中断在内核中总是禁用的(和 xv6 一样,在用户空间中也是启用的)。外部中断由 %eflags 寄存器的 FL_IF 标志位控制(参见 inc/mmu.h)。设置此位时,启用外部中断。虽然可以通过多种方式修改位,但由于我们的简化,我们将在进入和离开用户模式时,通过保存和恢复 %eflags 寄存器的过程来处理它。
当用户环境运行时,您必须确保 FL_IF 标志在用户环境中设置,以便当中断到达时,它通过处理器并由中断代码处理。否则,中断将被屏蔽或忽略,直到中断被重新启用。我们用 boot loader 的第一条指令屏蔽了中断,到目前为止,我们还没来得及重新启用它们。
Exercise 13。
🧤Handling Clock Interrupts
在 user/spin 程序中,在第一次运行子环境之后,它只是在一个循环中旋转,内核再也没有得到控制权。我们需要对硬件进行编程,定期生成时钟中断,这将迫使控制返回到内核,在那里我们可以将控制切换到不同的用户环境。
我们为您编写的对 lapic_init 和 pic_init(来自 init.c 中的 i386_init)的调用设置了时钟和中断控制器来生成中断。现在需要编写代码来处理这些中断。
Exercise 14。
👞Inter-Process communication (IPC)
我们一直在关注操作系统对于各种环境的隔离方面,它制造了一种错觉,即每个程序都有一台独立的机器。操作系统的另一项重要服务是允许程序在需要时相互通信。让程序与其他程序交互是非常强大的,Unix 管道模型就是一个典型的例子。
进程间通信有许多模型。即使在今天,关于哪种模式是最好的仍然存在争论。现在我们将实现一个简单的 IPC 机制。
🧦IPC in JOS
您将实现几个 JOS 内核系统调用,这些调用共同提供一个简单的进程间通信机制。
您将实现两个系统调用,sys_ipc_recv 和 sys_ipc_try_send。然后您将实现两个库函数 ipc_recv 和 ipc_send。
用户环境可以使用 JOS 的 IPC 机制相互发送的“消息”由两个部分组成:
- 一个 32 位的值
- 一个可选的页面映射
允许环境在消息中传递页映射提供了一种高效的方法,可以传输比单个 32 位整数更多的数据,还允许环境轻松地设置共享内存。
👗Sending and Receiving Messages
为了接收消息,环境调用 sys_ipc_recv。此系统调用取消调度当前环境的,并且在收到消息之前不会再次运行它。当一个环境正在等待接收消息时,任何其他环境都可以向它发送消息——包括特定的环境,也包括与接收环境有父子关系的环境。
换言之,在 Part A 中实现的权限检查将不用于 IPC,因为 IPC 系统调用经过精心设计,因此是安全的:一个环境不能通过向另一个环境发送消息而导致其发生故障(除非目标环境有 bug)。
为了发送一个值,调用环境调用 sys_ipc_try_send,向其传递接收方的环境 id 和要发送的值:
- 如果指定的环境正在等待消息传递(即,调用了 sys_ipc_recv,但尚未获得值),那么 send 将传递消息并返回 0
- 否则,send 将返回 -E_IPC_NOT_RECV 以指示目标环境当前不希望接收值
用户空间中的库函数 ipc_recv 将负责调用 sys_ipc_recv,然后在当前环境的 Env 结构体中查找有关接收值的信息。
类似地,库函数 ipc_send 将负责反复调用 sys_ipc_try_send,直到发送成功。
🥻Transferring Pages
当一个环境使用一个有效的 dstva 参数(在 UTOP 下面)调用 sys_ipc_recv 时,该环境表示它愿意接收一个页面映射。如果发送方发送一个页,那么该页应该映射到接收方地址空间中的 dstva。如果接收者已经在 dstva 映射了一个页面,那么上一个页面将被取消映射。
当环境使用有效的 srcva(在 UTOP 下面)调用 sys_ipc_try_send 时,这意味着发送方希望将当前映射在 srcva 的页面发送给接收方,并且具有 perm 权限。在成功的 IPC 之后,发送方在其地址空间中保留 srcva 处的页的原始映射,但是接收方也在接收方的地址空间中,在接收方最初指定的 dstva 处获得相同物理页的映射。因此,此页面将在发送方和接收方之间共享。
如果发送方或接收方都没有指明要传输某个页面,则不传输任何页面。
在任何 IPC 之后,内核将接收器 env 结构中的新字段 env_ipc_perm 设置为接收到的页面的权限,如果没有接收到页面,则设置为零。
👜Implementing IPC
🎫Exercise and Question
🚓No1
在 kern/pmap.c 中实现 mmio_map_region。要了解如何使用它,请查看 kern/lapic.c 中 lapic_init()。在运行 mmio_map_region 测试之前,还必须完成下一个练习。
在 MMIO 区域中保留 size 字节大小的空间,并在此位置映射 [pa,pa+size);返回保留区域的基地址;注意 size 不是 PGSIZE 的倍数。
void* mmio_map_region(physaddr_t pa, size_t size) {// 下一个映射的开始地址. 其被初始化为 MMIO 的开始地址.// 因为这是一个 static 的变量, 所以其值将会// 在每次调用 mmio_map_region 时保留// (就像 boot_alloc 中的 nextfree 一样).static uintptr_t base = MMIOBASE;// 保留开始于 base 的 size 字节内存并且// 将物理地址 [pa,pa+size) 映射到虚拟地址// [base,base+size). 因为现在的内存并不是// 通常意义上的 DRAM, 所以你需要告诉 CPU 这不是// 安全的缓存空间. 幸运的是, 页表提供了标志位// 来解决这个问题; 简单来说使用 PTE_PCD|PTE_PWT// (cache-disable and write-through) 代替 PTE_W// 来映射内存.//// 将 size 向上取整确保其是 PGSIZE 的整数倍并且// 处理超出 MMIOLIM 的问题 (引发 panic 即可).//// 提示: 使用 boot_map_region 函数.//// Your code here:void* ret = (void*)base;size_t round_size = ROUNDUP(size, PGSIZE);if (base + round_size >= MMIOLIM) {panic("mmio_map_region reservation overflow\n");}boot_map_region(kern_pgdir, base, round_size, pa, PTE_W | PTE_PCD | PTE_PWT);base += round_size;return ret;}
🚒No2
阅读 kern/init.c 中的 boot_aps() 和 mp_main(),以及 kern/mpentry.S 中的汇编代码。确保您了解 aps 引导过程中的控制流传输。 然后在 kern/pmap.c 中修改 page_init() 的实现,以避免将 MPENTRY_PADDR 处的页面添加到空闲列表中,这样我们就可以在该物理地址安全地复制并运行 AP 引导代码。 完成的代码应该通过 Lab4 更新后的 check_page_free_list() 测试样例(但不能通过 check_kern_pgdir() 测试)。
首先整理一下程序运行过程,此过程一直都运行在 CPU0,即 BSP 上,在保护模式下运行。
- i386_init 调用了 boot_aps(),在 BSP 中引导其他 CPU 启动
- boot_aps 调用 memmove 将每个 CPU 的 boot 代码加载到固定位置
最后调用 lapic_startap 执行其 bootloader 启动对应的 CPU
🍔代码阅读
i386_init 调用了 boot_aps(),在 BSP 中引导其他 CPU 启动: ```c void i386_init(void) { extern char edata[], end[];
// 初始化控制台 console. // 在完成之前不能使用 cprintf 函数! cons_init();
// Lab 2 完成的内存管理初始化函数 mem_init();
// Lab 3 完成的用户环境初始化函数 env_init(); trap_init();
// Lab 4 完成的多处理器初始化函数 mp_init(); lapic_init();
// Lab 4 完成的多任务初始化函数 pic_init();
// Acquire the big kernel lock before waking up APs // Your code here:
// 开始引导 APs boot_aps();
if defined(TEST)
// Don’t touch — used by grading script! ENV_CREATE(TEST, ENV_TYPE_USER);
else
// Touch all you want. ENV_CREATE(user_primes, ENV_TYPE_USER);
endif // TEST*
// 调度并运行第一个用户环境! sched_yield(); }
boot_aps 调用 memmove 将每个 CPU 的 boot 代码加载到固定位置:```c// 当 boot_aps 启动给定的 CPU 时, is booting a given CPU,// 它将 mpentry.S 应加载的每个 CPU 堆栈指针传递给这个变量中的CPU.void* mpentry_kstack;// 启动 APs.static void boot_aps(void) {extern unsigned char mpentry_start[], mpentry_end[];void* code;struct CpuInfo* c;// 将入口点写入在 MPENTRY_PADDR 处的未使用的内存code = KADDR(MPENTRY_PADDR);memmove(code, mpentry_start, mpentry_end - mpentry_start);// 依次启动一个 APfor (c = cpus; c < cpus + ncpu; c++) {if (c == cpus + cpunum()) // 已经启动了对应的 AP.continue;// 告诉 mpentry.S 使用哪个堆栈mpentry_kstack = percpu_kstacks[c - cpus] + KSTKSIZE;// 从 mpentry_start 处启动 CPUlapic_startap(c->cpu_id, PADDR(code));// 等待在 mp_main() 完成初始化操作while (c->cpu_status != CPU_STARTED);}}
🧇修改 page_init()
boot loader 的地址 0x7000 在低地址段为第 7 页,其页数小于 npage_basemem = 160;所以需要修改代码如下:
void page_init(void) {// 1) Mark physical page 0 as in use.pages[0].pp_ref = 1;// LAB 4:// Change your code to mark the physical page at MPENTRY_PADDR// as in use// 声明 i 和 mp_sizesize_t i, mp_size;// 通过 extern 找到 mpentry 的开始地址和结束地址extern unsigned char mpentry_start[], mpentry_end[];// 计算 mpentry 的大小mp_size = ROUNDUP(mpentry_end - mpentry_start, PGSIZE);// 2) The rest of base memory, [PGSIZE, npages_basemem * PGSIZE)// is free.// 在 MPENTRY_PADDR 之前的应为空闲for (i = 1; i < MPENTRY_PADDR / PGSIZE; i++) {pages[i].pp_ref = 0;pages[i].pp_link = page_free_list;page_free_list = &pages[i];}// mpentry 的空间标志为占用for (; i < (MPENTRY_PADDR + mp_size) / PGSIZE; i++) {pages[i].pp_ref = 1;}// mpentry 之后的空间标志为空闲for (; i < npages_basemem; i++) {pages[i].pp_ref = 0;pages[i].pp_link = page_free_list;page_free_list = &pages[i];}// 3) Then comes the IO hole [IOPHYSMEM, EXTPHYSMEM), which must// never be allocated.for (; i < EXTPHYSMEM / PGSIZE; i++) {pages[i].pp_ref = 1;}// 4) Then extended memory [EXTPHYSMEM, ...).physaddr_t first_free_addr = PADDR(boot_alloc(0));for (; i < first_free_addr / PGSIZE; i++) {pages[i].pp_ref = 1;}// mark other pages as freefor (; i < npages; i++) {pages[i].pp_ref = 0;pages[i].pp_link = page_free_list;page_free_list = &pages[i];}}
🦽Q1
将 kern/mpentry.S 与 boot/boot.S 进行比较,记住 kern/mpentry.S 是编译并链接到 KERNBASE 之上运行的,就像内核中的其他东西一样。 宏 MPBOOTPHYS 的目的是什么?为什么在 kern/mpentry.S 中需要它而在 boot/boot.S 中不需要?换句话说,如果在kern/mpentry.S中省略它,会出什么问题? 提示:回想一下在 Lab1 中讨论过的链接地址和加载地址之间的区别。
宏 MPBOOTPHYS 是为求得变量的物理地址,例如 MPBOOTPHYS(gdtdesc) 得到 GDT 的物理地址。
boot.S 中,由于尚没有启用分页机制,所以我们能够指定程序开始执行的地方以及程序加载的地址;但是,在 mpentry.S 中,由于主 CPU 已经处于保护模式下了,因此是不能直接指定物理地址的,必须进行转换。
🚄No3
修改 mem_init_mp()(在 kern/pmap.c 中)以映射从 KSTACKTOP 开始的每个 CPU 堆栈,如 inc/memlayout.h 中所示。 每个堆栈的大小是 KSTKSIZE 字节加上未映射保护页的 KSTKGAP 字节。您的代码应该通过新的 check_kern_pgdir()。
修改 kern_pgdir 中的映射来支持 SMP;将各个 CPU 的堆栈映射到 [KSTACKTOP-PTSIZE, KSTACKTOP)。
static void mem_init_mp(void) {// 从 KSTACKTOP 开始映射 NCPU 个 CPU 的堆栈.//// 对于 CPU i, 使用 percpu_kstacks[i] 作为其内核堆栈的物理地址.// CPU i 的内核堆栈从虚拟地址 kstacktop_i = KSTACKTOP - i * (KSTKSIZE + KSTKGAP)// 开始向下增长, 并且分成两部分就像之前在 mem_init 中编写的那样:// * [kstacktop_i - KSTKSIZE, kstacktop_i)// -- 有物理内存支持// * [kstacktop_i - (KSTKSIZE + KSTKGAP), kstacktop_i - KSTKSIZE)// -- 无物理内存支持; 所以如果内核栈溢出,// 将会引发段错误而不是覆盖其他 CPU 的堆栈.// 也被成为 "guard page".// Permissions: kernel RW, user NONE//// LAB 4: Your code here:for (int i = 0; i < NCPU; i++) {uintptr_t kstacktop_i = KSTACKTOP - i * (KSTKSIZE + KSTKGAP);boot_map_region(kern_pgdir, kstacktop_i - KSTKSIZE, KSTKSIZE,PADDR(percpu_kstacks[i]), PTE_W);}}
🚉No4
trap_init_percpu()(kern/trap.c)中的代码初始化 BSP 的 TSS 和 TSS 描述符。它在 BSP 上可以工作,但在 AP上运行时不正确。 更改代码,使其可以在所有CPU上工作(注意:新代码不应再使用全局 ts 变量)。
初始化并加载每个 CPU 的 TSS 和 IDT。
void trap_init_percpu(void) {// 提示:// - 宏定义 "thiscpu" 总是指向当前正在运行的 CPU 的// CpuInfo 结构体;// - 当前 CPU 的 ID 可以通过 cpunum() 或者 thiscpu->cpu_id 获取;// - 使用 "thiscpu->cpu_ts" 作为当前 CPU 的 TSS 代替 ts 变量;// - 使用 gdt[(GD_TSS0 >> 3) + i] 作为 CPU i 的 TSS 描述符;// - 我们已经在 mem_init_mp() 中映射了 CPU 的内核栈;// - 将 cpu_ts.ts_iomb 初始化来阻止未授权的 env 进行 IO//// ltr 在 TSS 选择器中设置了一个 'busy' 标志,// 因此如果不小心在多个 CPU 上加载了相同的 TSS,// 则会出现 triple fault. 如果您将某个 CPU 的 TSS 设置错误,// 则在尝试从该 CPU 上的用户空间返回之前,可能不会出现错误.//// LAB 4: Your code here:// Setup a TSS so that we get the right stack// when we trap to the kernel.int cpu_num = cpunum();thiscpu->cpu_ts.ts_esp0 = KSTACKTOP - cpu_num * (KSTKSIZE + KSTKGAP);thiscpu->cpu_ts.ts_ss0 = GD_KD;thiscpu->cpu_ts.ts_iomb = sizeof(struct Taskstate);// Initialize the TSS slot of the gdt.gdt[(GD_TSS0 >> 3) + cpu_num] = SEG16(STS_T32A, (uint32_t)(&thiscpu->cpu_ts),sizeof(struct Taskstate) - 1, 0);gdt[(GD_TSS0 >> 3) + cpu_num].sd_s = 0;// Load the TSS selector (like other segment selectors, the// bottom three bits are special; we leave them 0)ltr(GD_TSS0 + (cpu_num << 3));// Load the IDTlidt(&idt_pd);}
🪂No5
如上所述,通过在适当的位置调用 lock_kernel() 和 unlock_kernel() 来应用内核锁。
//i386_initlock_kernel();boot_aps();//mp_mainlock_kernel();sched_yield();//trapif ((tf->tf_cs & 3) == 3) {lock_kernel();assert(curenv);......}//env_runlcr3(PADDR(curenv->env_pgdir));unlock_kernel();env_pop_tf(&(curenv->env_tf));
🛰Q2
big kernel lock 已经确保每次仅仅一个 CPU 能运行内核代码, 为什么我们仍然需要为每个 CPU 设定一个内核栈?
因为每个 CPU 进入内核,其压栈的数据可能不一样,同时此 CPU 下一次再进入内核时可能需要用到之前的 data。
🚏No6
在 sched_yield() 中实现 round-robin 调度算法。不要忘记修改 syscall() 触发 sys_yield()。 确保你已经在 mp_main() 中调用了 sched_yield()。 修改 kern/init.c 来创建三个运行 user/yield.c 二进制文件的 env。 运行 make qemu. 你应该看到下面的场景:
Test also with several CPUS: make qemu CPUS=2....Hello, I am environment 00001000.Hello, I am environment 00001001.Hello, I am environment 00001002.Back in environment 00001000, iteration 0.Back in environment 00001001, iteration 0.Back in environment 00001002, iteration 0.Back in environment 00001000, iteration 1.Back in environment 00001001, iteration 1.Back in environment 00001002, iteration 1....
在 yield 程序退出后,系统中将没有可运行的环境,调度器应该调用 JOS 内核监视器。如果没有发生上述任何情况,请在继续之前修复代码。
🚌sys_yield()
选择可以运行的 env 并运行它。
void sched_yield(void) {struct Env* idle;// 实现简单的循环轮转算法.//// 从该 CPU 上一次运行的环境之后开始,// 在 'envs' 中以循环方式搜索可运行的环境.// 切换到第一个找到的这样的环境.//// 如果没有环境是可运行的,// 但是之前在这个 CPU 上运行的环境仍然是 ENV_RUNNING 的,// 那么可以选择那个环境.//// 永远不要选择当前正在另一个 CPU 上运行的环境// (env_status==ENV_RUNNING).// 如果没有可运行的环境, 只需转到下面的代码就可以停止 CPU.// LAB 4: Your code here.idle = thiscpu->cpu_env;envid_t start_id = idle ? ENVX(idle->env_id) : 0;for (int i = 0; i < NENV; i++) {envid_t next_id = (start_id + i) % NENV;if (envs[next_id].env_status == ENV_RUNNABLE) {env_run(&envs[next_id]);return;}}if (envs[start_id].env_status == ENV_RUNNING &&envs[start_id].env_cpunum == cpunum()) {env_run(&envs[start_id]);return;}// sched_halt never returnssched_halt();}
🚘sys_call.c
kern/syscall.c 中的 syscall 中加入一个 case:
case SYS_yield:sys_yield();return 0;
🛴init.c
init 中创建用户环境:
ENV_CREATE(user_yield, ENV_TYPE_USER);ENV_CREATE(user_yield, ENV_TYPE_USER);ENV_CREATE(user_yield, ENV_TYPE_USER);
出现下面的结果:
Physical memory: 131072K available, base = 640K, extended = 130432Kcheck_page_free_list() succeeded!check_page_alloc() succeeded!check_page() succeeded!check_kern_pgdir() succeeded!check_page_free_list() succeeded!check_page_installed_pgdir() succeeded![00000000] new env 00001000[00000000] new env 00001001[00000000] new env 00001002SMP: CPU 0 found 2 CPU(s)enabled interrupts: 1 2SMP: CPU 1 startingHello, I am environment 00001000.Hello, I am environment 00001001.Back in environment 00001000, iteration 0.Hello, I am environment 00001002.Back in environment 00001001, iteration 0.Back in environment 00001000, iteration 1.Back in environment 00001002, iteration 0.Back in environment 00001001, iteration 1.Back in environment 00001000, iteration 2.Back in environment 00001002, iteration 1.Back in environment 00001001, iteration 2.Back in environment 00001000, iteration 3.Back in environment 00001002, iteration 2.Back in environment 00001001, iteration 3.Back in environment 00001000, iteration 4.Back in environment 00001002, iteration 3.All done in environment 00001000.[00001000] exiting gracefully[00001000] free env 00001000Back in environment 00001001, iteration 4.Back in environment 00001002, iteration 4.All done in environment 00001001.All done in environment 00001002.[00001001] exiting gracefully[00001001] free env 00001001[00001002] exiting gracefully[00001002] free env 00001002No runnable environments in the system!Welcome to the JOS kernel monitor!K>
🏳Q3
在 env_run() 的实现中,应该调用 lcr3()。在调用 lcr3() 之前和之后,代码引用了变量 e(env_run 的参数),加载 %cr3 寄存器后,MMU 使用的寻址上下文立即更改。 为什么指针 e 在寻址开关前后都能被解引用?
因为当前是运行在系统内核中的,而每个进程的页表中都是存在内核映射的。每个进程页表中虚拟地址高于 UTOP 之上的地方,只有 UVPT 不一样,其余的都是一样的,只不过在用户态下是看不到的。所以虽然这个时候的页表换成了下一个要运行的进程的页表,但是 curenv 的地址没变,映射也没变,还是依然有效的。
🌏Q4
每当内核从一个环境切换到另一个环境时,它必须确保保存旧环境的寄存器,以便以后可以正确地恢复它们。为什么?这在哪里发生?
因为不保存下来就无法正确地恢复到原来的环境。
用户进程之间的切换,会调用系统调用 sched_yield();用户态陷入到内核态,可以通过中断、异常、系统调用;这样的切换之处都是要在系统栈上建立用户态的 TrapFrame,在进入 trap() 函数后,语句 curenv->env_tf = *tf; 将内核栈上需要保存的寄存器的状态实际保存在用户环境的 env_tf 域中。
🏕No7
在 kern/syscall.c 中实现上述系统调用,并确保 syscall() 调用它们。您需要使用 kern/pmap.c 和 kern/env.c 中的各种函数,特别是 envid2env()。现在,无论何时调用 envid2env(),都在 checkperm 参数中传递 1。 确保您检查了任何无效的系统调用参数,在这种情况下返回 -E_INVAL。 使用 user/dumbfork 测试您的 JOS 内核。
🎃sys_exofork
分配新的环境(进程)。
返回新环境的 envid 当发生错误时返回小于 0 的值,这些错误是:
- -E_NO_FREE_ENV:如果无法分配新的环境空间
-E_NO_MEM:内存耗尽
static envid_t sys_exofork(void) {// 使用 kern/env.c 中的 env_alloc() 创建新的环境.// 在使用 env_alloc 创建 env 时, 将 status 设置为// ENV_NOT_RUNNABLE, 并且从现在的 env 中复制寄存器and the register set is copied// 到新的环境中 -- 但是调整寄存器的值使其返回 0.// LAB 4: Your code here.int ret_code = 0;struct Env* newenv;// 创建新进程if ((ret_code = env_alloc(&newenv, curenv->env_id)) < 0) {return ret_code;}// 设置状态、设置寄存器值newenv->env_status = ENV_NOT_RUNNABLE;newenv->env_tf = curenv->env_tf;// 通过 eax 设置环境的返回值newenv->env_tf.tf_regs.reg_eax = 0;return newenv->env_id;}
🧧sys_env_set_status
设置 envid 所属环境的 env_status 为 status,status 必须是 ENV_RUNNABLE 或者 ENV_NOT_RUNNABLE。
在成功时返回 0,失败时返回小于 0 的值,错误有:-E_BAD_ENV:如果环境的 envid 现在不存在,或者调用者没有权限进行更改
-E_INVAL:如果不是上面列出的两个可设置状态
static int sys_env_set_status(envid_t envid, int status) {// 提示: 使用 kern/env.c 中的 'envid2env' 函数通过// envid 找到真正的 Env.// 应该将 envid2env 的第三个参数设置为 1, 这样将会// 检查当前环境是否有权限设置设置 Env 的状态.// LAB 4: Your code here.int ret_code = 0;struct Env* env;// 验证 statusif (status != ENV_RUNNABLE && status != ENV_NOT_RUNNABLE) {return -E_INVAL;}// 找到对应的 Envif ((ret_code = envid2env(envid, &env, 1)) < 0) {return -E_BAD_ENV;}// 设置 statusenv->env_status = status;return ret_code;}
🎡sys_page_alloc
在 envid 对应的地址空间以权限 perm 映射一页内存到虚拟地址 va 处:
页内容应该被设置为 0
- 如果一个内存页已经被映射到 va,则应该取消那一页的映射
- perm:perm 必需中 PTE_U | PTE_P 必须被设置,PTE_AVAIL | PTE_W 为可选项,但是不能设置其他位,可以在 inc/mmu.h 中查看 PTE_SYSCALL
成功时返回 0,失败时返回小于 0,错误有:
- -E_BAD_ENV:如果环境 envid 不存在,或者调用者没有权限进行更改
- -E_INVAL:如果 va 大于等于 UTOP 或者 va 不是页对齐的
- -E_INVAL:如果 perm 的值不正确
-E_NO_MEM:如果没有内存分配新的页,或者分配新的页表
static int sys_page_alloc(envid_t envid, void* va, int perm) {// 提示: 调用 kern/pmap.c 中的 page_alloc() 和// page_insert() 完成这个函数.// 大多数代码都需要检查参数是否正确.// 如过 page_insert() 执行失败, 记得释放你分配的内存!// LAB 4: Your code here.int ret_code = 0;struct Env* env;struct PageInfo* pp;// 获得 Envif ((ret_code = envid2env(envid, &env, 1)) < 0) {return -E_BAD_ENV;}// 检查虚拟地址if ((uintptr_t)va > UTOP || PGOFF(va)) {return -E_INVAL;}// 验证权限if ((perm & PTE_SYSCALL) == 0 || perm & ~PTE_SYSCALL) {return -E_INVAL;}// 分配内存pp = page_alloc(ALLOC_ZERO);if (!pp) {return -E_NO_MEM;}// 插入页表if (page_insert(env->env_pgdir, pp, va, perm) < 0) {// 执行失败释放内存page_free(pp);return -E_NO_MEM;}return 0;}
🛒sys_page_map
映射 srcenvid 环境中在虚拟地址 srcva 处的内存页以权限 perm 到 dstenvid 环境中的虚拟地址 dstva 处。perm 与在 sys_page_alloc 中有相同的要求,只是它也不能授予对只读页的写访问权。
成功时返回 0,失败时返回小于 0,错误有:-E_BAD_ENV:如果 srcenvid 以及 dstenvid 对应的 Env 当前不存在,或者调用者没有权限进行更改
- -E_INVAL:如果 srcva >= UTOP 或者 srcva 不是页对齐的,或者 dstva >= UTOP 或者 dstva 不是页对齐的
- -E_INVAL:如果 srcva 不在 srcenvid 的地址空间中
- -E_INVAL:如果 perm 的值不正确
- -E_INVAL:如果 perm & PTE_W,但是 srcva 在 srcenvid 的地址空间中是只读的
-E_NO_MEM:如果没有内存分配必要的页表
static int sys_page_map(envid_t srcenvid,void* srcva,envid_t dstenvid,void* dstva,int perm) {// 提示: 使用 kern/pmap.c 中的函数 page_lookup() 和// page_insert() 完成函数编写.// 大多数代码都需要检查参数是否正确.// 使用 page_lookup() 的第三个参数检查内存页的权限.// LAB 4: Your code here.pte_t* pte;struct Env *srcenv, *dstenv;struct PageInfo* srcpp;// 获得 env 检查权限if (envid2env(srcenvid, &srcenv, 1) < 0 ||envid2env(dstenvid, &dstenv, 1) < 0) {return -E_BAD_ENV;}// 验证地址是否正确if ((uintptr_t)srcva >= UTOP || PGOFF(srcva) || (uintptr_t)dstva >= UTOP ||PGOFF(dstva)) {return -E_INVAL;}// 验证权限是否符合 SYS_CALLif ((perm & PTE_SYSCALL) == 0 || (perm & ~PTE_SYSCALL)) {return -E_INVAL;}// 验证 srcva 对应的 page 是否存在srcpp = page_lookup(srcenv->env_pgdir, srcva, &pte);if (!srcpp) {return -E_INVAL;}// 验证只读页是否加入了写的操作if ((perm & PTE_W) && !(*pte & PTE_W)) {return -E_INVAL;}// 映射页if (page_insert(dstenv->env_pgdir, srcpp, dstva, perm) < 0) {return -E_NO_MEM;}return 0;}
👕sys_page_unmap
取消在 envid 的地址空间中映射在虚拟地址 va 的空间。如果本身就没有页被映射,那么函数默认成功。
成功时返回 0,失败时返回小于 0,错误有:-E_BAD_ENV:如果环境 envid 不存在,或者调用者没有权限进行更改
-E_INVAL:如果 va 大于等于 UTOP 或者 va 不是页对齐的
static int sys_page_unmap(envid_t envid, void* va) {// 提示: 调用 page_remove() 函数完成.// LAB 4: Your code here.struct Env* env;if (envid2env(envid, &env, 1) < 0) {return -E_BAD_ENV;}if ((uintptr_t)va > UTOP || PGOFF(va)) {return -E_INVAL;}page_remove(env->env_pgdir, va);return 0;}
👘syscall
添加几个 case: ```c case SYS_exofork: return sys_exofork();
case SYS_env_set_status: return sys_env_set_status(a1, a2);
case SYS_page_alloc: return sys_page_alloc(a1, (void*)a2, a3);
case SYS_page_map: return sys_page_map(a1, (void)a2, a3, (void)a4, a5);
case SYS_page_unmap: return sys_page_unmap(a1, (void*)a2);
接着运行 make grade 既可以通过 dumbfork 测试:```bashmake[1]: Leaving directory '/home/wangjq/MIT/lab'dumbfork: OK (2.2s)(Old jos.out.dumbfork failure log removed)Part A score: 5/5
🏗No8
实现 sys_env_set_pgfault_upcall 系统调用。确保寻找通过环境 ID 寻找环境时有正确的权限检查,因为这是一个风险极大的系统调用。
通过修改 Env 的 env_pgfault_upcall 字段,设置 envid 对应的出现页错误的处理函数(在 syscall 函数中添加 case 就不再展示了)。
当 envid 出现页错误时,内核将会将错误记录压入异常栈,然后调转到 func 函数中。
成功时返回 0,失败时返回小于 0,错误有:
-E_BAD_ENV:如果环境 envid 不存在,或者调用者没有权限进行更改
static int sys_env_set_pgfault_upcall(envid_t envid, void* func) {// LAB 4: Your code here.struct Env* env;if (envid2env(envid, &env, 1) < 0) {return -E_BAD_ENV;}env->env_pgfault_upcall = func;return 0;}
🕌No9
在 kern/trap.c 的 page_fault_handler 中实现将页错误分派给用户模式处理程序所需的代码。在写入异常堆栈时,请务必采取适当的预防措施。(如果用户环境耗尽了异常堆栈上的空间怎么办?)
我们已经处理了内核模式的页错误异常,所以在后面的部分,页面错误就发生在用户模式下。
环境如果存在 page fault upcall,则调用环境的 page fault upcall;在用户异常堆栈(UXSTACKTOP 下面)上设置一个页面错误堆栈的框架,然后跳转到 curenv->env_pgfault_upcall。
Page fault upcall 可能会导致另一个页面错误,在这种情况下,我们递归地跳转到 page fault upcall,将另一个页面错误堆栈帧推到用户异常堆栈的顶部。
对于从 page fault(lib/pfentry.S)返回的代码来说,在 trap 时堆栈的顶部有一个字的暂存空间是很方便的;它允许我们更容易地恢复 eip/esp:
- 在非递归情况下,我们不必担心这一点,因为常规用户堆栈的顶部是空闲的
- 在递归情况下,这意味着我们必须在异常堆栈的当前顶部和新堆栈帧之间留一个额外的字,因为异常堆栈是 trap-time 堆栈
如果没有 page fault upcall,出现环境没有为其异常堆栈分配页或无法写入,异常堆栈溢出等情况,会破坏导致错误的环境。请注意,grade 脚本假定您首先检查 page fault upcall,如果没有,则打印 user fault_va 消息。
总结一下:
- 当正常执行过程中发生了页错误,那么栈的切换是:用户运行栈—>内核栈—>异常栈
而如果在异常处理程序中发生了也错误,那么栈的切换是:异常栈—>内核栈—>异常栈
void page_fault_handler(struct Trapframe* tf) {uint32_t fault_va;// Read processor's CR2 register to find the faulting addressfault_va = rcr2();// Handle kernel-mode page faults.// LAB 3: Your code here.if ((tf->tf_cs & 0x3) == 0) {panic("Page fault in kernel code, halt\n");}// Hints:// user_mem_assert() and env_run() are useful here.// To change what the user environment runs, modify 'curenv->env_tf'// (the 'tf' variable points at 'curenv->env_tf').// LAB 4: Your code here.struct UTrapframe* utf;if (curenv->env_pgfault_upcall) {// 判断是否由异常栈引发异常if (tf->tf_esp >= UXSTACKTOP - PGSIZE && tf->tf_esp < UXSTACKTOP) {// 如果从异常栈引发异常, tf->ts_esp 则为异常栈的 esp 指针// 留出 4 个字节的空白以及 UTrapframe 结构体的空间得到 utf 开始地址utf = (struct UTrapframe*)(tf->tf_esp - sizeof(struct UTrapframe) - 4);} else {// 如果从用户栈引发异常则直接从异常栈栈顶直接留出 UTrapframe 结构体的空间utf = (struct UTrapframe*)(UXSTACKTOP - sizeof(struct UTrapframe));}// 验证留出 utf 的空间后异常栈会不会满user_mem_assert(curenv, (const void*)utf, sizeof(struct UTrapframe),PTE_P | PTE_W);// 构造 UTrapframe 结构体utf->utf_fault_va = fault_va;utf->utf_err = tf->tf_trapno;utf->utf_regs = tf->tf_regs;utf->utf_eflags = tf->tf_eflags;// 保存用户堆栈或者异常堆栈返回后的执行信息utf->utf_eip = tf->tf_eip;utf->utf_esp = tf->tf_esp;// curenv->env_tf 的 tf_eip, tf_esp 的设置跳转到异常处理代码执行curenv->env_tf.tf_eip = (uintptr_t)curenv->env_pgfault_upcall;curenv->env_tf.tf_esp = (uintptr_t)utf;// 执行当前环境env_run(curenv);return;}// Destroy the environment that caused the fault.cprintf("[%08x] user fault va %08x ip %08x\n", curenv->env_id, fault_va,tf->tf_eip);print_trapframe(tf);env_destroy(curenv);}
🏦No10
在 lib/pfentry.S 中实现 _pgfault_upcall 程序。关注如何返回到导致页面错误的用户代码中的原始点;您将直接返回那里,而不必返回内核,最困难的部分是同时切换堆栈和重新加载 EIP。
Page fault upcall 入口点;在这里我们要求内核重定向到用户空间中造成页错误的地方(查看 pgfault.c 中的 sys_set_pgfault_handler 函数)。
当页错误确实发生时,内核将会将 ESP 指针切换到用户异常栈,然后将 UTrapframe 入栈。
trap-time esptrap-time eflagstrap-time eiputf_regs.reg_eax...utf_regs.reg_esiutf_regs.reg_ediutf_err (error code)utf_fault_va <-- %esp
如果这是一个递归的错误,内核将会在 trap-time 上保留一个空白的字节,以便我们恢复时临时使用。
我们通过全局指针变量 _pgfault_handler 调用 C 代码以解决页错误。
在汇编代码中首先给出了对 C 代码的调用:
.text.globl _pgfault_upcall_pgfault_upcall:// 调用 C 代码pushl %esp // 函数参数入栈: 指向 UTFmovl _pgfault_handler, %eaxcall *%eaxaddl $4, %esp // 函数参数弹栈
现在 C 代码的页错误处理函数已将完成执行,我们需要返回其之前的状态。
要完成的具体工作是将 trap-time 的 %eip 放到 trap-time 栈帧中,之所以要这么做是因为我们不能直接从异常栈中返回:
- 不能使用 jmp 命令,因为需要我们加载地址到寄存器中,但是所有的寄存器的值在 trap-time 中必须被保留
- 不能使用 ret 命令,因为如果这么做了 %esp 将会由一个错误的值,不能跳回到用户栈
所以我们将 trap-time 的 %eip 放到 trap-time 栈帧,完成上面的工作后我们在使用 ret 命令跳回到原先的环境中(这里因为存在递归页错误所以可能返回错误栈,而我们需要借助栈帧完成对 eip 的暂存,所以可以解释上面为什么在异常中引发异常开辟 utf 空间要空出一个字节来)。
// LAB 4: Your code here.// 根据栈的结构 0x28(%esp) 指向 tf_eipmovl 0x28(%esp), %eax// 根据栈的结构 0x30(%esp) 指向 tf_espmovl 0x30(%esp), %edx// 在要返回函数的栈帧中向下开辟一个字节空间subl $4, %edxmovl %edx, 0x30(%esp)// 将 tf_eip 移动到原先的栈帧中movl %eax, (%edx)// 还原其他寄存器的值. 完成之后不能修改寄存器.// LAB 4: Your code here.addl $8, %esppopal// 还原 eflags. 完成之后不能修改 eflags 的值// LAB 4: Your code here.addl $4, %esppopfl// 切换栈帧.// LAB 4: Your code here.popl %esp// 返回接着执行的地方.// LAB 4: Your code here.ret
🏰No11
完成 lib/pgfault.c 中的 set_pgfault_handler() 函数。
设置页错误处理函数。如果没有页错误处理函数,则 _pgfault_handler 是 0;当我们第一次注册处理函数时,我们需要分配一个异常栈:
- 大小为 PGSIZE
- 栈顶在 UXSTACKTOP
并且告诉内核在页错误发生时调用汇编语言中的 _pgfault_upcall。
void set_pgfault_handler(void (*handler)(struct UTrapframe* utf)) {int r;if (_pgfault_handler == 0) {// First time through!// LAB 4: Your code here.sys_page_alloc(sys_getenvid(), (void*)UXSTACKTOP - PGSIZE, PTE_SYSCALL);sys_env_set_pgfault_upcall(sys_getenvid(), _pgfault_upcall);}// Save handler pointer for assembly to call._pgfault_handler = handler;}
⛺No12
在 lib/fork.c 中实现 fork、duppage 和 pgfault。 用 forktree 程序测试代码。
🚐pgfault()
自定义 page fault handler,如果发生页错误的是写时复制页,那么映射一个可写的副本。
static void pgfault(struct UTrapframe* utf) {void* addr = (void*)utf->utf_fault_va;uint32_t err = utf->utf_err;int r;// LAB 4: Your code here.// 检查时写入页错误中断if (!(err & FEC_WR)) {panic("The trapno is not FEC_WR.\n");}// 检查时写时复制页if (!((uvpd[PDX(addr)] & PTE_P) && (uvpt[PGNUM(addr)] & PTE_P) &&(uvpt[PGNUM(addr)] & PTE_COW))) {panic("Neither the fault is a write nor COW page.\n");}// LAB 4: Your code here.uintptr_t addr_start = ROUNDDOWN(addr, PGSIZE);envid_t envid = sys_getenvid();// 将新分配的页映射在一个临时地址 PFTEMPif ((r = sys_page_alloc(envid, (void*)PFTEMP, PTE_P | PTE_W | PTE_U)) < 0) {panic("Page allocation fault: %e\n", r);}// 复制页的内容到临时页中memcpy((void*)PFTEMP, (const void*)addr_start, PGSIZE);// 将这个页映射到 addr_start 即子进程正在写入的页if ((r = sys_page_map(envid, (void*)PFTEMP, envid, addr_start,PTE_P | PTE_W | PTE_U)) < 0) {panic("Page map fault: %e\n", r);}// 取消这个页在临时地址的映射if ((r = sys_page_unmap(envid, (void*)PFTEMP)) < 0) {panic("Page unmap fault: %e\n", r);}}
🚔duppage()
将虚拟页 pn (地址是:pn*PGSIZE)映射到 envid 中相同的虚拟地址处。
如果页是可写的或者是写时复制的,先在 envid 处创建写时复制映射,再在当前环境中重新映射一此。
成功时返回 0,不成功时返回 <0。
static int duppage(envid_t envid, unsigned pn) {int r;uintptr_t va = pn * PGSIZE;// LAB 4: Your code here.if (uvpt[pn] & PTE_W || uvpt[pn] & PTE_COW) {if ((r = sys_page_map(thisenv->env_id, (void*)va, envid, (void*)va,PTE_P | PTE_U | PTE_COW)) < 0) {return r;}if ((r = sys_page_map(thisenv->env_id, (void*)va, thisenv->env_id,(void*)va, PTE_P | PTE_U | PTE_COW)) < 0) {return r;}} else {if ((r = sys_page_map(thisenv->env_id, (void*)va, envid, (void*)va,PTE_P | PTE_U)) < 0) {return r;}}return 0;}
🛵fork()
用户级别的写时复制 fork()。步骤大概如下:
- 设置页错误处理函数
- 创建子进程
- 映射页空间为写时复制页
- 然后设置子进程 RUNNABLE 并返回
为父进程返回 envid,子进程返回 0,错误时返回 <0。
提示:
- 使用 uvpd、uvpt、duppage
- 为子进程设置 thisenv
用户异常栈不能被标记为写时复制,所以你需要分配新的一页
envid_t fork(void) {// LAB 4: Your code here.int r;envid_t envid;// 得到 _pgfault_upcall 的地址extern void _pgfault_upcall(void);// 设置当前环境的页错误处理函数set_pgfault_handler(pgfault);// 创建进程envid = sys_exofork();if (envid < 0) {panic("sys_exofork: %e", envid);}// 如果时子进程则直接返回if (envid == 0) {thisenv = &envs[ENVX(sys_getenvid())];return 0;}// 进行可写页和写时复制页的映射for (int pn = PGNUM(UTEXT); pn < PGNUM(USTACKTOP); pn++) {if ((uvpd[pn >> 10] & PTE_P) && (uvpt[pn] & PTE_P)) {if ((r = duppage(envid, pn)) < 0) {return r;}}}// 为错误栈分配内存if ((r = sys_page_alloc(envid, (void*)(UXSTACKTOP - PGSIZE),PTE_U | PTE_P | PTE_W)) < 0) {return r;}// 为子进程添加页错误处理映射if ((r = sys_env_set_pgfault_upcall(envid, _pgfault_upcall)) < 0) {return r;}// 设置子进程启动if ((r = sys_env_set_status(envid, ENV_RUNNABLE)) < 0) {return r;}return envid;}
💈No13
修改 kern/trapentry.S 和 kern/trap.c 以初始化 IDT 中的相应条目,并为 IRQs 0-15 提供处理程序。然后修改 kern/env.c 中 env_alloc() 中的代码,以确保用户环境总是在启用中断的情况下运行。 还要取消对 sched_halt() 中的 sti 指令的注释,以便空闲 CPU 取消掩码中断。 在完成这个练习之后,如果使用任何运行很长时间(例如 spin)的测试程序运行内核,您应该会看到内核打印硬件中断的陷阱帧。虽然中断现在已经在处理器中启用,但是 JOS 还没有处理它们,所以您应该看到它将每个中断错误地赋给当前正在运行的用户环境,并将其销毁。
🛸inc/trap.h
首先我们现在这个文件中查看,需要定义的 IRQ 中断:
// Hardware IRQ numbers. We receive these as (IRQ_OFFSET+IRQ_WHATEVER)#define IRQ_TIMER 0#define IRQ_KBD 1#define IRQ_SERIAL 4#define IRQ_SPURIOUS 7#define IRQ_IDE 14#define IRQ_ERROR 19
⛵kern/trapentry.S
在 trapentry.S 文件中注册中断的入口点:
TRAPHANDLER_NOEC(irq_error_handler, IRQ_OFFSET+IRQ_ERROR)TRAPHANDLER_NOEC(irq_ide_handler, IRQ_OFFSET+IRQ_IDE)TRAPHANDLER_NOEC(irq_kbd_handler, IRQ_OFFSET+IRQ_KBD)TRAPHANDLER_NOEC(irq_serial_handler, IRQ_OFFSET+IRQ_SERIAL)TRAPHANDLER_NOEC(irq_spurious_handler, IRQ_OFFSET+IRQ_SPURIOUS)TRAPHANDLER_NOEC(irq_timer_handler, IRQ_OFFSET+IRQ_TIMER)
🚤kern/trap.c
在 IDT 中进行设置:
void irq_error_handler();void irq_kbd_handler();void irq_ide_handler();void irq_timer_handler();void irq_spurious_handler();void irq_serial_handler();SETGATE(idt[IRQ_OFFSET + IRQ_ERROR], 0, GD_KT, irq_error_handler, 3);SETGATE(idt[IRQ_OFFSET + IRQ_IDE], 0, GD_KT, irq_ide_handler, 3);SETGATE(idt[IRQ_OFFSET + IRQ_KBD], 0, GD_KT, irq_kbd_handler, 3);SETGATE(idt[IRQ_OFFSET + IRQ_SERIAL], 0, GD_KT, irq_serial_handler, 3);SETGATE(idt[IRQ_OFFSET + IRQ_SPURIOUS], 0, GD_KT, irq_spurious_handler, 3);SETGATE(idt[IRQ_OFFSET + IRQ_TIMER], 0, GD_KT, irq_timer_handler, 3);
🛥env_alloc()
修改 kern/env.c 中 env_alloc() 中的代码,以确保用户环境总是在启用中断的情况下运行,通过 FL_IF 标志位设置即可:
// Enable interrupts while in user mode.// LAB 4: Your code here.e->env_tf.tf_eflags |= FL_IF;
⛴sched_halt()
取消对 sched_halt() 中的 sti 指令的注释,以便空闲 CPU 取消掩码中断:
asm volatile("movl $0, %%ebp\n""movl %0, %%esp\n""pushl $0\n""pushl $0\n"// Uncomment the following line after completing exercise 13"sti\n""1:\n""hlt\n""jmp 1b\n":: "a"(thiscpu->cpu_ts.ts_esp0));
🧻No14
修改内核的 trap_dispatch() 函数,以便在发生时钟中断时调用 sched_yield() 来查找并运行不同的环境。 现在可以通过 user/spin 测试样例了:父环境分叉子环境,sys_yield() 给它几次执行机会,但是在每种情况下,在一个时间片之后重新获得对 CPU 的控制,最后杀死子环境并优雅地终止。
在 trap_dispatch 中添加一个 case:
case IRQ_OFFSET + IRQ_TIMER:lapic_eoi();sched_yield();return;
运行 make grade,应该获得 65 分:
Score: 65/80GNUmakefile:207: recipe for target 'grade' failedmake: *** [grade] Error 1
🧹No15
在 kern/syscall.c 中实现 sys_ipc_recv 和 sys_ipc_try_send。在实现它们之前,请阅读这两个命令的注释,因为它们必须一起工作。 在其中调用 envid2env 时,应该将 checkperm 标志设置为 0,这意味着允许任何环境向任何其他环境发送 IPC 消息,内核除了验证目标 envid 是否有效之外,不进行任何特殊的权限检查。 然后在 lib/ipc.c 中实现 ipc_recv 和 ipc_send 函数。
🩰sys_ipc_recv()
阻塞程序知道接收到值。使用 Env 结构体的 env_ipc_recving 和 env_ipc_dstva 字段记录你想要接收的值,将环境标记为 not runnable,然后放弃 CPU。
如果 dstva 小于 UTOP, 我们将会接受一页数据;dstva 是被发送的页应该映射的虚拟地址。
这个函数只在 error 时返回,但是系统调用需要在成功时返回 0。
错误时返回 <0,错误有:
-E_INVAL:dstva < UTOP 但是不是页对齐的
static int sys_ipc_recv(void* dstva) {// LAB 4: Your code here.// 检查 dstva 是否合法if ((uintptr_t)dstva < UTOP && PGOFF(dstva) != 0) {return -E_INVAL;}// 设置 IPC 的初始值// 标识正在等待接收消息curenv->env_ipc_recving = 1;// 记录想要映射页的虚拟地址curenv->env_ipc_dstva = dstva;curenv->env_ipc_value = 0;curenv->env_ipc_from = 0;curenv->env_ipc_perm = 0;// 设置 Env 状态curenv->env_status = ENV_NOT_RUNNABLE;// 重新调度任务 放弃 CPUsched_yield();return 0;}
👑sys_ipc_try_send()
尝试发送 value 到目标环境 envid。
如果 srcva 小于 UTOP,并且已经 有页映射在 srcva 处,那么接收者也将会映射相同的页面。
如果因为目标 env 不是被阻塞并等待 IPC 导致发送失败,返回 -E_IPC_NOT_RECV;但是发送也可能因为其他原因失败。
发送成功时,目标 env 的各个字段将使用如下方法设置:env_ipc_recving:设置为 0 阻塞消息接收
- env_ipc_from:设置为发送方 envid
- env_ipc_value:设置为 value 参数
- env_ipc_perm:如果映射了页设置为 perm,否则为 0
目标环境重新设置为 RUNNIABLE,函数返回 0(想一想如何为接收者返回值)。
如果发送者发送了一个 page 但是接收者不想要一个页,将不会有页面被映射,但是页不会发生错误。
本函数只在下面这些情况返回 error:
- -E_BAD_ENV:如果环境 envid 当前不存在(不检查权限)
- -E_IPC_NOT_RECV:如果环境 envid 没有被 sys_ipc_recv 阻塞,或者另一个环境已经发送了数据
- -E_INVAL:srcva 小于 UTOP,但是没有页对齐
- -E_INVAL:srcva 小于 UTOP,但是权限不合适
- -E_INVAL:srcva 小于 UTOP 但是没有映射在调用者的地址空间
- -E_INVAL:如果 perm & PTE_W,但是 srcva 在当前地址空间中是只读的
-E_NO_MEM:没有足够的空间在 envid 的地址空间映射 srcva
static int sys_ipc_try_send(envid_t envid,uint32_t value,void* srcva,unsigned perm) {// LAB 4: Your code here.int r;pte_t* pte;struct Env* env;struct PageInfo* pp;// 各种参数验证if ((r = envid2env(envid, &env, 0)) < 0) {return -E_BAD_ENV;}if (env->env_ipc_recving == 0 || env->env_ipc_from != 0) {return -E_IPC_NOT_RECV;}if ((uintptr_t)srcva < UTOP && PGOFF(srcva) != 0) {return -E_INVAL;}if ((uintptr_t)srcva < UTOP) {static int sys_ipc_try_send(envid_t envid,uint32_t value,void* srcva,unsigned perm) {// LAB 4: Your code here.int r;pte_t* pte;struct Env* env;struct PageInfo* pp;if ((r = envid2env(envid, &env, 0)) < 0) {return -E_BAD_ENV;}if (env->env_ipc_recving == 0 || env->env_ipc_from != 0) {return -E_IPC_NOT_RECV;}if ((uintptr_t)srcva < UTOP && PGOFF(srcva) != 0) {return -E_INVAL;}if ((uintptr_t)srcva < UTOP) {if ((perm & PTE_SYSCALL) == 0 || perm & ~PTE_SYSCALL) {return -E_INVAL;}}if ((uintptr_t)srcva < UTOP &&!(pp = page_lookup(curenv->env_pgdir, srcva, &pte))) {return -E_INVAL;}if ((uintptr_t)srcva < UTOP && (perm & PTE_W) != 0 && (*pte & PTE_W) == 0) {return -E_INVAL;}if ((uintptr_t)srcva < UTOP && (uintptr_t)env->env_ipc_dstva != UTOP) {if ((r = page_insert(env->env_pgdir, pp, env->env_ipc_dstva, perm)) < 0) {return -E_NO_MEM;}}env->env_ipc_recving = 0;env->env_ipc_value = value;env->env_ipc_from = curenv->env_id;env->env_ipc_perm = perm;env->env_status = ENV_RUNNABLE;env->env_tf.tf_regs.reg_eax = 0;return 0;}}if ((uintptr_t)srcva < UTOP &&!(pp = page_lookup(env->env_pgdir, srcva, &pte))) {return -E_INVAL;}if ((uintptr_t)srcva < UTOP && (perm & PTE_W) != 0 && (*pte & PTE_W) == 0) {return -E_INVAL;}if ((uintptr_t)srcva < UTOP && env->env_ipc_dstva != UTOP) {// 符合条件映射页if ((r = page_insert(env->env_pgdir, pp, env->env_ipc_dstva, perm)) < 0) {return -E_NO_MEM;}}// 设置 ipc 参数的值env->env_ipc_recving = 0;env->env_ipc_value = value;env->env_ipc_from = curenv->env_id;env->env_ipc_perm = perm;// 记得设置 env 开始运行env->env_status = ENV_RUNNABLE;// 设置接收者的返回值env->env_tf.tf_regs.reg_eax = 0;return 0;}
🧢ipc_recv()
接受 IPC 消息并返回接收到的值。
如果 pg 不是 NULL,那么地址发送者发送的数据将被映射到这里。
如果 from_env_store 不是 NULL,那么发送者的 envid 将被存储在这里。
如果 perm_store 不是 NULL,那么发送的权限将被存储在这里。
如果系统调用失败,将 0 存储在 fromenv 和 perm,然后返回。
提示:使用 thisenv 得到当前环境
如果 pg 是 null,给 sys_ipc_recv 传递一个值,让他明白不进行映射
int32_t ipc_recv(envid_t* from_env_store, void* pg, int* perm_store) {// LAB 4: Your code here.int r;// 是否映射页if (pg) {r = sys_ipc_recv(pg);} else {r = sys_ipc_recv((void*)UTOP);}// 向外存储 env_ipc_from 与 env_ipc_permif (from_env_store) {*from_env_store = thisenv->env_ipc_from;}if (perm_store) {*perm_store = thisenv->env_ipc_perm;}// 处理失败的情况if (r < 0) {if (from_env_store) {*from_env_store = 0;}if (perm_store) {*perm_store = 0;}return r;}// 返回传递的值return thisenv->env_ipc_value;}
⛑ipc_send()
将 val(如果 pg 不为空,则发送带有 perm 的 pg)到 toenv;此函数一直尝试,直到成功为止。
除了 -E_IPC_NOT_RECV 以外的任何错误都应该 panic。
提示:使用 sys_yield() 不要一直占用 CPU
如果 pg 为空,则传递给 sys_ipc_try_send 一个值,它将理解为 no page
void ipc_send(envid_t to_env, uint32_t val, void* pg, int perm) {// LAB 4: Your code here.int r;while (true) {if (pg) {r = sys_ipc_try_send(to_env, val, pg, perm);} else {r = sys_ipc_try_send(to_env, val, (void*)UTOP, perm);}if (r == 0) {return;}if (r != -E_IPC_NOT_RECV) {panic("IPC send fault: %e.\n", r);}sys_yield();}}
此时我们本实验可以得到满分。
