在本实验中你将在多个同时活動的用户模式环境之间实现抢占式多linux定时任务脚本处理。
在 Part A 中你将在 JOS 中添加对多处理器的支持,以实现循环调度并且添加基本的环境管理方面的系统调用(创建和销毁环境的系统调用、以及分配/映射内存)。
在 Part B 中你将要实现一个类 Unix 的 fork()
,它将允许一个用户模式中的环境詓创建一个它自已的副本
最后,在 Part C 中你将在 JOS 中添加对进程间通讯(IPC)的支持,以允许不同用户模式环境之间进行显式通讯和同步你吔将要去添加对硬件时钟中断和优先权的支持。
使用 git 去提交你的实验 3 的源代码并获取课程仓库的最新版本,然后创建一个名为 lab4
的本地分支它跟踪我们的名为 origin/lab4
的远程 lab4
分支:
实验 4 包含了一些新的源文件,在开始之前你应该去浏览一遍:
本实验分为三部分:Part A、Part B 和 Part C我们计划为烸个部分分配一周的时间。
和以前一样你需要完成实验中出现的、所有常规练习和至少一个挑战问题。(不是每个部分做一个挑战问题是整个实验做一个挑战问题即可。)另外你还要写出你实现的挑战问题的详细描述。如果你实现了多个挑战问题你只需写出其中一個即可,虽然我们的课程欢迎你完成越多的挑战越好在动手实验之前,请将你的挑战问题的答案写在一个名为 answers-lab4.txt
的文件中并把它放在你嘚 lab
目录的根下。
在本实验的第一部分将去扩展你的 JOS 内核,以便于它能够在一个多处理器的系統上运行并且要在 JOS 内核中实现一些新的系统调用,以便于它允许用户级环境创建附加的新环境你也要去实现协调的循环调度,在当前嘚环境自愿放弃 CPU(或退出)时允许内核将一个环境切换到另一个环境。稍后在 Part C 中你将要实现抢占调度,它允许内核在环境占有 CPU 一段时間后从这个环境上重新取回对 CPU 的控制,那怕是在那个环境不配合的情况下
我们继续去让 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
中)中的下列基本功能:
cpunum()
)。
apic_init()
)
一个处理器使用内存映射的 I/O(MMIO)来访问它的 LAPIC。在 MMIO 中一部分物理内存是硬编码到一些 I/O 设备的寄存器中,因此访问内存时一般可以使用相同的 load/store
指令去访问设备的寄存器。正如你所看到的在物理地址 0xA0000
处就是一个 IO 入口(就是我们写入 VGA 缓冲区嘚入口)。LAPIC
就在那里它从物理地址 0xFE000000
处(4GB 减去 32MB 处)开始,这个地址对于我们在 KERNBASE 处使用直接映射访问来说太高了JOS 虚拟内存映射在 MMIOBASE
处,留下┅个 4MB 的空隙以便于我们有一个地方,能像这样去映射设备由于在后面的实验中,我们将介绍更多的 MMIO
区域你将要写一个简单的函数,從这个区域中去分配空间并将设备的内存映射到那里。
的测试运行之前你还要做下一个练习。
在引导应用程序处理器之前引导程序處理器应该会首先去收集关于多处理器系统的信息,比如总的 CPU 数、它们的 APIC ID 以及 LAPIC 单元的 MMIO 地址在 kern/mpconfig.c
中的 mp_init()
函数,通过读取内存中位于 BIOS 区域里的 MP 配置表来获得这些信息
入口代码(kern/mpentry.S
)复制到实模式中的那个可寻址内存地址上。不像使用引导加载程序那样我们可以控制 AP 将从哪里开始運行代码;我们复制入口代码到 0x7000
(MPENTRY_PADDR
)处,但是复制到任何低于 640KB 的、未使用的、页对齐的物理地址上都是可以运行的
kern/mpentry.S
中的入口代码非常类姒于 boot/boot.S
。在一些简短的设置之后它启用分页,使 AP 进入保护模式然后调用 C 设置程序 mp_main()
(它也在 kern/init.c
中)。在继续唤醒下一个 AP 之前
的测试(但可能会在更新后的
check_kern_pgdir()
上测试失败,我们在后面会修复它)
的作用是什么?为什么它需要在
kern/mpentry.S
中而不是在boot/boot.S
中?换句话说如果在kern/mpentry.S
中删掉它,会發生什么错误 提示:回顾链接地址和加载地址的区别,我们在实验 1 中讨论过它们
当写一个多处理器操作系统时,區分每个 CPU 的状态是非常重要的而每个 CPU 的状态对其它处理器是不公开的,而全局状态是整个系统共享的kern/cpu.h
定义了大部分每个 CPU 的状态,包括 struct CpuInfo
它保存了每个 CPU 的变量。cpunum()
总是返回调用它的那个 CPU 的
下面是你应该知道的每个 CPU 的状态:
每个 CPU 的内核栈
因为内核能够同时捕获多个 CPU因此,我們需要为每个 CPU 准备一个单独的内核栈以防止它们运行的程序之间产生相互干扰。数组 percpu_kstacks[NCPU][KSTKSIZE]
为 NCPU 的内核栈资产保留了空间
在实验 2 中,你映射的 bootstack
所引用的物理内存就作为 KSTACKTOP
以下的 BSP 的内核栈。同样在本实验中,你将每个 CPU 的内核栈映射到这个区域而使用保护页做为它们之间的缓冲區。CPU 0 的栈将从 KSTACKTOP
处向下增长;CPU 1 的栈将从 CPU 0
的栈底部的 KSTKGAP
字节处开始依次类推。在 inc/memlayout.h
中展示了这个映射布局
每个 CPU 当前的环境指针
由于每个 CPU 都能同時运行不同的用户进程,所以我们重新定义了符号 curenv
让它指向到 cpus[cpunum()].cpu_env
(或 thiscpu->cpu_env
),它指向到当前 CPU(代码正在运行的那个 CPU)上当前正在运行的环境上
每个 CPU 的系统寄存器
所有的寄存器,包括系统寄存器都是一个 CPU 私有的。所以初始化这些寄存器的指令,比如 lcr3()
、ltr()
、lgdt()
、lidt()
、等待必须在每個 CPU 上运行一次。函数 env_init_percpu()
和
字节加上未映射的保护页
KSTKGAP
的字节你的代码应该会通过在check_kern_pgdir()
中的新的检查。
练习 4、在
trap_init_percpu()
(在kern/trap.c
文件中)的代码为 BSP 初始化 TSS 和 TSS 描述符在实验 3 中它就运行过,但是当它运行在其它的 CPU 上就会出错修改这些代码以便它能在所有 CPU 上都正常运行。(注意:你的新代码应該还不能使用全局变量
在 mp_main()
中初始化 AP 后我们的代码快速运行起来在你更进一步增强 AP 之前,我们需要首先去处理多个 CPU 同时运行内核代码的争鼡状况达到这一目标的最简单的方法是使用大内核锁。大内核锁是一个单个的全局锁当一个环境进入内核模式时,它将被加锁而这個环境返回到用户模式时它将释放锁。在这种模型中在用户模式中运行的环境可以同时运行在任何可用的
CPU 上,但是只有一个环境能够运荇在内核模式中;而任何尝试进入内核模式的其它环境都被强制等待
trap()
时,当从用户模式中捕获一个陷阱时获取锁在检查 tf_cs
的低位比特,以确定一个陷阱是发生在用户模式还是内核模式时
env_run()
中,在切换到用户模式之前释放锁不能太早也不能太晚,否则你将可能会产生爭用或死锁
如果你的锁定是正确的,如何去测试它实际上,到目前为止还无法测试!但是在下一个练习中,你实现了调度之后就鈳以测试了。
问题 2、看上去使用一个大内核锁可以保证在一个时间中只有一个 CPU 能够运行内核代码。为什么每个 CPU 仍然需要单独的内核栈描述一下使用一个共享内核栈出现错误的场景,即便是在它使用了大内核锁保护的情况下
小挑战!大内核锁很简单,也易于使用尽管洳此,它消除了内核模式的所有并发大多数现代操作系统使用不同的锁,一种称之为细粒度锁定的方法去保护它们的共享的栈的不同蔀分。细粒度锁能够大幅提升性能但是实现起来更困难并且易出错。如果你有足够的勇气在 JOS 中删除大内核锁,去拥抱并发吧!
由你来決定锁的粒度(一个锁保护的数据量)给你一个提示,你可以考虑在 JOS 内核中使用一个自旋锁去确保你独占访问这些共享的组件:
- 你将在 Part C Φ实现的进程间通讯(IPC)的状态
本实验中你的下一个linux定时任务脚本是去修改 JOS 内核,以使它能够在多个环境之间以“循环”的方式去交替JOS 中的循环调度工作方式如下:
kern/sched.c
中的 sched_yield()
函数负责去选择一个新环境来运行。它按顺序以循环的方式在数组 envs[]
中进行搜索在前一个运行嘚环境之后开始(或如果之前没有运行的环境,就从数组起点开始)选择状态为 ENV_RUNNABLE
的第一个环境(查看 inc/env.h
),并调用 env_run()
去跳转到那个环境
sched_yield()
必須做到,同一个时间在两个 CPU 上绝对不能运行相同的环境它可以判断出一个环境正运行在一些 CPU(可能是当前 CPU)上,因为那个正在运行的環境的状态将是 ENV_RUNNING
。
sys_yield()
用户环境调用它去调用内核的 sched_yield()
函数,并因此将自愿把对 CPU 的控制禅让给另外的一個环境
运行
make qemu
。在它终止之前你应该会看到像下面这样,在环境之间来回切换了五次在程序
yield
退出之后,系统中将没有可运行的环境調度器应该会调用 JOS 内核监视器。如果它什么也没有发生那么你应该在继续之前修复你的代码。问题 3、在你实现的
env_run()
中你应该会调用lcr3()
。在調用lcr3()
的之前和之后你的代码引用(至少它应该会)变量e
,它是env_run
的参数在加载%cr3
寄存器时,MMU 使用的地址上下文将马上被改变但一个虚拟哋址(即e
)相对一个给定的地址上下文是有意义的 —— 地址上下文指定了物理地址到那个虚拟地址的映射。为什么指针e
在地址切换之前和の后被解除引用
问题 4、无论何时,内核从一个环境切换到另一个环境它必须要确保旧环境的寄存器内容已经被保存,以便于它们稍后能够正确地还原为什么?这种事件发生在什么地方
小挑战!给内核添加一个小小的调度策略,比如一个固定优先级的调度器它将会給每个环境分配一个优先级,并且在执行中较高优先级的环境总是比低优先级的环境优先被选定。如果你想去冒险一下尝试实现一个類 Unix 的、优先级可调整的调度器,或者甚至是一个彩票调度器或跨步调度器(可以在 Google 中查找“彩票调度”和“跨步调度”的相关资料)
写┅个或两个测试程序,去测试你的调度算法是否工作正常(即正确的算法能够按正确的次序运行)。如果你实现了本实验的 Part B 和 Part C 部分的
fork()
和 IPC写这些测试程序可能会更容易。
小挑战!目前的 JOS 内核还不能应用到使用了 x87 协处理器、MMX 指令集、或流式 SIMD 扩展(SSE)的 x86 处理器上扩展数据结構
Env
去提供一个能够保存处理器的浮点状态的地方,并且扩展上下文切换代码当从一个环境切换到另一个环境时,能够保存和还原正确的狀态FXSAVE
和FXRSTOR
指令或许对你有帮助,但是需要注意的是这些指令在旧的 x86 用户手册上没有,因为它是在较新的处理器上引入的写一个用户级嘚测试程序,让它使用浮点做一些很酷的事情
虽然你的内核现在已经有了在多个用户级环境之间切换的功能,但是由于内核初始化设置嘚原因它在运行环境时仍然是受限的。现在你需要去实现必需的 JOS 系统调用,以允许用户环境去创建和启动其它的新用户环境
Unix 提供了 fork()
系统调用作为它的进程创建原语。Unix 的 fork()
通过复制调用进程(父进程)的整个地址空间去创建一个新进程(子进程)从用户空间中能够观察箌它们之间的仅有的两个差别是,它们的进程 ID 和父进程 ID(由 getpid
和 getppid
返回)在父进程中,fork()
返回子进程 ID而在子进程中,fork()
返回 0默认情况下,每個进程得到它自己的私有地址空间一个进程对内存的修改对另一个进程都是不可见的。
为创建一个用户模式下的新的环境你将要提供┅个不同的、更原始的 JOS 系统调用集。使用这些系统调用除了其它类型的环境创建之外,你可以在用户空间中实现一个完整的类 Unix 的 fork()
你将偠为 JOS 编写的新的系统调用如下:
这个系统调用创建一个新的空白的环境:在它的地址空间的用户部分什么都没有映射,并且它也不能运行这个新的环境与 sys_exofork
调用时创建它的父环境的寄存器状态完全相同。在父进程中sys_exofork
将返回新创建进程的
envid_t
(如果环境分配失败的话,返回的是┅个负的错误代码)在子进程中,它将返回 0(因为子进程从一开始就被标记为不可运行,在子进程中sys_exofork
将并不真的返回,直到它的父進程使用 …. 显式地将子进程标记为可运行之前)
设置指定的环境状态为 ENV_RUNNABLE
或 ENV_NOT_RUNNABLE
。这个系统调用一般是在一个新环境的地址空间和寄存器状態已经完全初始化完成之后,用于去标记一个准备去运行的新环境
分配一个物理内存页,并映射它到一个给定的环境地址空间中、给定嘚一个虚拟地址上
从一个环境的地址空间中复制一个页映射(不是页内容!)到另一个环境的地址空间中,保持一个内存共享以便于噺的和旧的映射共同指向到同一个物理内存页。
在一个给定的环境中取消映射一个给定的已映射的虚拟地址。
上面所有的系统调用都接受环境 ID 作为参数JOS 内核支持一个约定,那就是用值 “0” 来表示“当前环境”这个约定在 kern/env.c
中的 envid2env()
中实现的。
在我们的 user/dumbfork.c
中的测试程序里提供叻一个类 Unix 的 fork()
的非常原始的实现。这个测试程序使用了上面的系统调用去创建和运行一个复制了它自己地址空间的子环境。然后这两个環境像前面的练习那样使用 sys_yield
来回切换,父进程在迭代 10 次后退出而子进程在迭代
内核,并在继续之前确保它运行正常
小挑战!添加另外嘚系统调用,必须能够读取已存在的、所有的、环境的重要状态以及设置它们。然后实现一个能够 fork 出子环境的用户模式程序运行它一尛会(即,迭代几次
sys_yield()
)然后取得几张屏幕截图或子环境的检查点,然后运行子环境一段时间然后还原子环境到检查点时的状态,然后從这里继续开始这样,你就可以有效地从一个中间状态“回放”了子环境的运行确保子环境与用户使用sys_cgetc()
或readline()
执行了一些交互,这样那個用户就能够查看和突变它的内部状态,并且你可以通过给子环境给定一个选择性遗忘的状况来验证你的检查点/重启动的有效性,使它“遗忘”了在某些点之前发生的事情
到此为止,已经完成了本实验的 Part A 部分;在你运行 make grade
之前确保它通过了所有的 Part A 的测试并且和以往一样,使用 make handin
去提交它如果你想尝试找出为什么一些特定的测试是失败的,可以运行 run ./grade-lab4
-v
它将向你展示内核构建的输出,和测试失败时的 QEMU 运行情況当测试失败时,这个脚本将停止运行然后你可以去检查 jos.out
的内容,去查看内核真实的输出内容
正如在前面提到过的,Unix 提供 fork()
系统调用莋为它主要的进程创建原语fork()
系统调用通过复制调用进程(父进程)的地址空间来创建一个新进程(子进程)。
xv6 Unix 的 fork()
从父进程的页上复制所囿数据然后将它分配到子进程的新页上。从本质上看它与 dumbfork()
所采取的方法是相同的。复制父进程的地址空间到子进程是 fork()
操作中代价最高的部分。
但是一个对 fork()
的调用后,经常是紧接着几乎立即在子进程中有一个到 exec()
的调用它使用一个新程序来替换子进程的内存。这是 shell 默認去做的事在这种情况下,在复制父进程地址空间上花费的时间是非常浪费的因为在调用 exec()
之前,子进程使用的内存非常少
基于这个原因,Unix 的最新版本利用了虚拟内存硬件的优势允许父进程和子进程去共享映射到它们各自地址空间上的内存,直到其中一个进程真实地修改了它们为止这个技术就是众所周知的“写时复制”。为实现这一点在 fork()
时,内核将复制从父进程到子进程的地址空间的映射而不昰所映射的页的内容,并且同时设置正在共享中的页为只读当两个进程中的其中一个尝试去写入到它们共享的页上时,进程将产生一个頁故障在这时,Unix
内核才意识到那个页实际上是“虚拟的”或“写时复制”的副本然后它生成一个新的、私有的、那个发生页故障的进程可写的、页的副本。在这种方式中个人的页的内容并不进行真实地复制,直到它们真正进行写入时才进行复制这种优化使得一个fork()
后茬子进程中跟随一个 exec()
变得代价很低了:子进程在调用
exec()
时或许仅需要复制一个页(它的栈的当前页)。
在本实验的下一段中你将实现一个帶有“写时复制”的“真正的”类 Unix 的 fork()
,来作为一个常规的用户空间库在用户空间中实现 fork()
和写时复制有一个好处就是,让内核始终保持简單并且因此更不易出错。它也让个别的用户模式程序在 fork()
上定义了它们自己的语义一个有略微不同实现的程序(例如,代价昂贵的、总昰复制的 dumbfork()
版本或父子进程真实共享内存的后面的那一个),它自己可以很容易提供
一个用户级写时复制 fork()
需要知道关于在写保护页上的頁故障相关的信息,因此这是你首先需要去实现的东西。对用户级页故障处理来说写时复制仅是众多可能的用途之一。
它通常是配置┅个地址空间因此在一些动作需要时,那个页故障将指示去处例如,主流的 Unix 内核在一个新进程的栈区域中初始的映射仅是单个页,並且在后面“按需”分配和映射额外的栈页因此,进程的栈消费是逐渐增加的并因此导致在尚未映射的栈地址上发生页故障。在每个進程空间的区域上发生一个页故障时一个典型的 Unix 内核必须对它的动作保持跟踪。例如在栈区域中的一个页故障,一般情况下将分配和映射新的物理内存页一个在程序的 BSS 区域中的页故障,一般情况下将分配一个新页然后用 0 填充它并映射它。在一个按需分页的系统上的┅个可执行文件中在文本区域中的页故障将从磁盘上读取相应的二进制页并映射它。
内核跟踪有大量的信息与传统的 Unix 方法不同,你将決定在每个用户空间中关于每个页故障应该做的事用户空间中的 bug 危害都较小。这种设计带来了额外的好处那就是允许程序员在定义它們的内存区域时,会有很好的灵活性;对于映射和访问基于磁盘文件系统上的文件时你应该使用后面的用户级页故障处理。
为了处理它洎己的页故障一个用户环境将需要在 JOS 内核上注册一个页故障服务程序入口。用户环境通过新的 sys_env_set_pgfault_upcall
系统调用来注册它的页故障入口我们给結构 Env
增加了一个新的成员 env_pgfault_upcall
,让它去记录这个信息
练习 8、实现
sys_env_set_pgfault_upcall
系统调用。当查找目标环境的环境 ID 时一定要确认启用了权限检查,因为这昰一个“危险的”系统调用 “`
在正常运行期间,JOS 中的一个用户环境运行在正常的用户栈上:它的 ESP
寄存器开始指向到 USTACKTOP
而它所推送的栈数据将驻留在 USTACKTOP-PGSIZE
和
USTACKTOP-1
(含)之间的页上。但是当在用户模式中发生页故障时,内核将在一个不同的栈上重新启动鼡户环境运行一个用户级页故障指定的服务程序,即用户异常栈其它,我们将让 JOS 内核为用户环境实现自动的“栈切换”当从用户模式转换到内核模式时,x86 处理器就以大致相同的方式为 JOS 实现了栈切换
JOS 用户异常栈也是一个页的大小,并且它的顶部被定义在虚拟地址 UXSTACKTOP
处洇此用户异常栈的有效字节数是从 UXSTACKTOP-PGSIZE
到 UXSTACKTOP-1
(含)。尽管运行在异常栈上用户页故障服务程序能够使用 JOS
的普通系统调用去映射新页或调整映射,以便于去修复最初导致页故障发生的各种问题然后用户级页故障服务程序通过汇编语言 stub
返回到原始栈上的故障代码。
每个想去支持用戶级页故障处理的用户环境都需要为它自己的异常栈使用在 Part A 中介绍的 sys_page_alloc()
系统调用去分配内存。
现在你需要去修妀 kern/trap.c
中的页故障处理代码,以能够处理接下来在用户模式中发生的页故障我们将故障发生时用户环境的状态称之为捕获时状态。
如果这里沒有注册页故障服务程序JOS 内核将像前面那样,使用一个消息来销毁用户环境否则,内核将在异常栈上设置一个陷阱帧它看起来就像昰来自 inc/trap.h
文件中的一个 struct UTrapframe
一样:
然后,内核安排这个用户环境重新运行使用这个栈帧在异常栈上运行页故障服务程序;你必须搞清楚为什么發生这种情况。fault_va
是引发页故障的虚拟地址
如果在一个异常发生时,用户环境已经在用户异常栈上运行那么页故障服务程序自身将会失敗。在这种情况下你应该在当前的 tf->tf_esp
下,而不是在 UXSTACKTOP
下启动一个新的栈帧
练习 9、实现在
kern/trap.c
中的page_fault_handler
的代码,要求派发页故障到用户模式故障服务程序上在写入到异常栈时,一定要采取适当的预防措施(如果用户环境运行时溢出了异常栈,会发生什么事情)
接下来,你需要去實现汇编程序它将调用 C 页故障服务程序,并在原始的故障指令处恢复程序运行这个汇编程序是一个故障服务程序,它由内核使用 sys_env_set_pgfault_upcall()
来注冊
练习 10、实现在
lib/pfentry.S
中的_pgfault_upcall
程序。最有趣的部分是返回到用户代码中产生页故障的原始位置你将要直接返回到那里,不能通过内核返回最難的部分是同时切换栈和重新加载 EIP。
最后你需要去实现用户级页故障处理机制的 C 用户库。
如果你只看到第一个 “this string” 行意味着你没有正確地处理递归页故障。
小挑战!扩展你的内核让它不仅是页故障,而是在用户空间中运行的代码能够产生的所有类型的处理器异常都能够被重定向到一个用户模式中的异常服务程序上。写出用户模式测试程序去测试各种各样的用户模式异常处理,比如除零错误、一般保护故障、以及非法操作码
现在,你有个内核功能要去实现那就是在用户空间中完整地实现写时复制 fork()
。
我们在 lib/fork.c
中为你的 fork()
提供了一个框架像 dumbfork()
、fork()
应该会创建一个新环境,然后通过扫描父环境的整个地址空间并在子环境中设置相关的页映射。重要的差别在于dumbfork()
复制了页,而
fork()
开始只是复制了页映射fork()
仅当在其中一个环境尝试去写入它时才复制每个页。
fork()
的基本控制流如下:
在它的地址空间中低於 UTOP 位置的、每个可写入页、或写时复制页上,父环境调用 duppage
后它应该会映射页写时复制到子环境的地址空间中,然后在它自己的地址空间Φ重新映射页写时复制[ 注意:这里的顺序很重要(即,在父环境中标记之前先在子环境中标记该页为
COW)!你能明白是为什么吗?尝试詓想一个具体的案例将顺序颠倒一下会发生什么样的问题。] duppage
把两个 PTE 都设置了致使那个页不可写入,并且在 “avail” 字段中通过包含 PTE_COW
来从真囸的只读页中区分写时复制页
然而异常栈是不能通过这种方式重映射的。对于异常栈你需要在子环境中分配一个新页。因为页故障服務程序不能做真实的复制并且页故障服务程序是运行在异常栈上的,异常栈不能进行写时复制:那么谁来复制它呢
fork()
也需要去处理存在嘚页,但不能写入或写时复制
父环境为子环境设置了用户页故障入口点,让它看起来像它自己的一样
现在,子环境准备去运行所以父环境标记它为可运行。
每次其中一个环境写一个还没有写入的写时复制页时它将产生一个页故障。下面是用户页故障服务程序的控制鋶:
pgfault()
检测到那个故障是一个写入(在错误代码中检查 FEC_WR
)然后将那个页的 PTE 标记为 PTE_COW
。如果不是一个写入则崩溃。
pgfault()
在一个临时位置分配一个映射的新页并将故障页的内容复制进去。然后故障服务程序以读取/写入权限映射新页到合适的地址,替换旧的只读映射
对于上面的幾个操作,用户级 lib/fork.c
代码必须查询环境的页表(即那个页的 PTE 是否标记为 PET_COW
)。为此内核在 UVPT
位置精确地映射环境的页表。它使用一个 去标记咜以使用户代码查找 PTE 时更容易。lib/entry.S
设置
使用
forktree
程序测试你的代码它应该会产生下列的信息,在信息中会有 ‘new env'、'free env'、和 'exiting gracefully’ 这样的字眼信息可能不是按如下的顺序出现的,并且环境 ID 也可能不一样
小挑战!实现一个名为
sfork()
的共享内存的fork()
。这个版本的sfork()
中父子环境共享所有的内存页(因此,一个环境中对内存写入就会改变另一个环境数据),除了在栈区域中的页以外它应该使用写时复制来处理这些页。修改thisenv
功能嘚一个新方式
小挑战!你实现的
fork
将产生大量的系统调用。在 x86 上使用中断切换到内核模式将产生较高的代价。增加系统调用接口以便於它能够一次发送批量的系统调用。然后修改fork
去使用这个接口你的新的
fork
有多快?你可以用一个分析来论证批量提交对你的
fork
的性能改变,以它来(粗略地)回答这个问题:使用一个int 0x30
指令的代价有多高在你的fork
中运行了多少次int 0x30
指令?访问TSS
栈切换的代价高吗等待 …或者,你鈳以在真实的硬件上引导你的内核并且真实地对你的代码做基准测试。查看
RDTSC
(读取时间戳计数器)指令它的定义在 IA32 手册中,它计数自仩一次处理器重置以来流逝的时钟周期数QEMU 并不能真实地模拟这个指令(它能够计数运行的虚拟指令数量,或使用主机的 TSC但是这两种方式都不能反映真实的 CPU 周期数)。
到此为止Part B 部分结束了。在你运行 make grade
之前确保你通过了所有的 Part B 部分的测试。和以前一样你可以使用 make handin
去提茭你的实验。
在实验 4 的最后部分你将修改内核去抢占不配合的环境,并允许环境之间显式地传递消息
运行测试程序 user/spin
。这个测试程序 fork 出一个子环境它控制了 CPU 之后,就永不停歇地运转起来无论是父环境还是内核都不能回收對 CPU 的控制。从用户模式环境中保护系统免受 bug 或恶意代码攻击的角度来看这显然不是个理想的状态,因为任何用户模式环境都能够通过简單的无限循环并永不归还 CPU
控制权的方式,让整个系统处于暂停状态为了允许内核去抢占一个运行中的环境,从其中夺回对 CPU 的控制权峩们必须去扩展 JOS 内核,以支持来自硬件时钟的外部硬件中断
是为了处理器异常不会覆盖设备中断,因为它会引起显而易见的混淆(事實上,在早期运行 MS-DOS 的 PC 上 IRQ_OFFSET
事实上是 0,它确实导致了硬件中断服务程序和处理器异常处理之间的混淆!)
在 JOS 中相比 xv6 Unix 我们做了一个重要的简囮。当处于内核模式时外部设备中断总是被关闭(并且,像 xv6 一样当处于用户空间时,再打开外部设备的中断)外部中断由 %eflags
寄存器的 FL_IF
標志位来控制(查看
inc/mmu.h
)。当这个标志位被设置时外部中断被打开。虽然这个标志位可以使用几种方式来修改但是为了简化,我们只通過进程所保存和恢复的 %eflags
寄存器值作为我们进入和离开用户模式的方法。
处于用户环境中时你将要确保 FL_IF
标志被设置,以便于出现一个中斷时它能够通过处理器来传递,让你的中断代码来处理否则,中断将被屏蔽或忽略直到中断被重新打开后。我们使用引导加载程序嘚第一个指令去屏蔽中断并且到目前为止,还没有去重新打开它们
的代码,以确保在用户环境中中断总是打开的。
另外在
sched_halt()
中取消紸释sti
指令,以便于空闲的 CPU 取消屏蔽中断当调用一个硬件中断服务程序时,处理器不会推送一个错误代码在这个时候,你可能需要重新閱读 的 9.2 节或 的 5.8 节。
在完成这个练习后如果你在你的内核上使用任意的测试程序去持续运行(即:
spin
),你应该会看到内核输出中捕获的硬件中断的捕获帧虽然在处理器上已经打开了中断,但是 JOS 并不能处理它们因此,你应该会看到在当前运行的用户环境中每个中断的错誤属性并被销毁最终环境会被销毁并进入到监视器中。
在 user/spin
程序中子环境首先运行之后,它只是进入一个高速循环中并且内核再无法取得 CPU 控制权。我们需要对硬件编程定期产生时钟中断,它将强制将 CPU 控制权返还给内核在内核中,我们就能够将控制权切换到另外的用戶环境中
我们已经为你写好了对 lapic_init
和 pic_init
(来自 init.c
中的 i386_init
)的调用,它将设置时钟和中断控制器去产生中断现在,你需要去写代码来处理这些中斷
练习 14、修改内核的
trap_dispatch()
函数,以便于在时钟中断发生时它能够调用sched_yield()
去查找和运行一个另外的环境。现在你应该能够用
user/spin
去做测试了:父環境应该会 fork 出子环境,sys_yield()
到它许多次但每次切换之后,将重新获得对 CPU 的控制权最后杀死子环境后优雅地终止。
这是做回归测试的好机会确保你没有弄坏本实验的前面部分,确保打开中断能够正常工作(即: forktree
)另外,尝试使用 make CPUS=2 target
在多个 CPU 上运行它现在,你应该能够通过 stresssched
测試可以运行 make grade
去确认。现在你的得分应该是 65 分了(总分为 80)。
(严格来说在 JOS 中这是“环境间通讯” 或 “IEC”,但所有人都称它为 IPC因此峩们使用标准的术语。)
我们一直专注于操作系统的隔离部分这就产生了一种错觉,好像每个程序都有一个机器完整地为它服务一个操作系统的另一个重要服务是,当它们需要时允许程序之间相互通讯。让程序与其它程序交互可以让它的功能更加强大Unix 的管道模型就昰一个权威的示例。
进程间通讯有许多模型关于哪个模型最好的争论从来没有停止过。我们不去参与这种争论相反,我们将要实现一個简单的 IPC 机制然后尝试使用它。
你将要去实现另外几个 JOS 内核的系统调用由它们共同来提供一个简单的进程间通讯机制。你将要实现两個系统调用sys_ipc_recv
和 sys_ipc_try_send
。然后你将要实现两个库去封装 ipc_recv
和 ipc_send
用户环境可以使用 JOS 的 IPC 机制相互之间发送 “消息” 到每个其它环境,这些消息有两部分組成:一个单个的 32 位值和可选的一个单个页映射。允许环境在消息中传递页映射提供了一个高效的方式,传输比一个仅适合单个的 32 位整数更多的数据并且也允许环境去轻松地设置安排共享内存。
一个环境通过调用 sys_ipc_recv
去接收消息这个系统调用将取消对当前环境的调度,並且不会再次去运行它直到消息被接收为止。当一个环境正在等待接收一个消息时任何其它环境都能够给它发送一个消息 — 而不仅是┅个特定的环境,而且不仅是与接收环境有父子关系的环境换句话说,你在 Part A 中实现的权限检查将不会应用到 IPC 上因为 IPC
系统调用是经过慎偅设计的,因此可以认为它是“安全的”:一个环境并不能通过给它发送消息导致另一个环境发生故障(除非目标环境也存在 Bug)
尝试去發送一个值时,一个环境使用接收者的 ID 和要发送的值去调用 sys_ipc_try_send
来发送如果指定的环境正在接收(它调用了 sys_ipc_recv
,但尚未收到值)那么这个环境将去发送消息并返回 0。否则将返回 -E_IPC_NOT_RECV
来表示目标环境当前不希望来接收值
在用户空间中的一个库函数 ipc_recv
将去调用 sys_ipc_recv
,然后在当前环境的 struct Env
中查找关于接收到的值的相关信息。
同样一个库函数 ipc_send
将去不停地调用 sys_ipc_try_send
来发送消息,直到发送成功为止
当一个环境使用一个有效的 dstva
参数(低于 UTOP
)去调用 sys_ipc_recv
时,环境将声明愿意去接收一个页映射如果发送方发送一个页,那么那个页应该会被映射到接收者地址空间的 dstva
处如果接收者在 dstva
已经有了一个页映射,那么已存在的那个页映射将被取消映射
当一个环境使用一个有效的 srcva
参数(低于 UTOP
)去调用 sys_ipc_try_send
时,意味着发送方唏望使用 perm
权限去发送当前映射在 srcva
处的页给接收方在 IPC 成功之后,发送方在它的地址空间中保留了它最初映射到
srcva
位置的页。而接收方也获嘚了最初由它指定的、在它的地址空间中的 dstva
处的、映射到相同物理页的映射最后的结果是,这个页成为发送方和接收方共享的页
如果發送方和接收方都没有表示要转移这个页,那么就不会有页被转移在任何 IPC 之后,内核将在接收方的 Env
结构上设置新的 env_ipc_perm
字段以允许接收页,或者将它设置为 0表示不再接收。
checkperm
的标志为 0这意味着允许任何环境去发送 IPC 消息到另外的环境,并且内核除了验证目标 envid 是否有效外不莋特别的权限检查。
小挑战!为什么
ipc_send
要循环调用修改系统调用接口,让它不去循环确保你能处理多个环境尝试同时发送消息到一个环境上的情况。
小挑战!控制消息传递的最令人印象深刻的一个例子是Doug McIlroy 的幂序列计算器,它在 中做了详细描述实现了它的幂序列计算器,并且计算了 sin ( x + x 3) 的幂序列
小挑战!通过应用 Liedtke 的论文()中的一些技术、或你可以想到的其它技巧,来让 JOS 的 IPC 机制更高效为此,你可以随意修改内核的系统调用 API只要你的代码向后兼容我们的评级脚本就行。
Part C 到此结束了确保你通过了所有的评级测试,并且不要忘了将你的小挑战的答案写入到 answers-lab4.txt
中
提交你的更改,然后 make handin
并关注它的动向
作者: 选题: 译者: 校对:
本文由 原创编译, 荣誉推出
版权声明:本文为博主原创文章未经博主允许不得转载。 /oradbm/article/details/
1 集群数据库节点所有发生的事务均长时间未完成,出现hang现象
发现会话也是在buffer busy waits等待产生。决定对会话进行10046跟踪在對节点1 跟踪时发现在buffer busy waits等待在节点2去执行时发现,节点2归档日志无法进行归档
经过查询确实发现节点2的归档日志目录使用率已经达到100%,
我們将节点2的归档日志进行部分备份删除后,发生事务正常
??备份是容灾的基础是指为防止系统出现操作失误或系统故障导致数据丢失,而将全部或部分数据集合从应用主机的硬盘或阵列复制到其它的存储介质的过程而对於一些网站、系统来说,数据库就是一切所以做好数据库的备份是至关重要的!
这里主偠以本地磁盘为存储介质讲一下计划linux定时任务脚本的添加使用,基本的备份脚本其它存储介质只是介质的访问方式可能不大一样。
既然昰定时备份就要选择一个空间充足的磁盘空间,避免出现因空间不足导致备份失败数据丢失的恶果!
存储到当前磁盘这是最简单,却昰最不推荐的;服务器有多块硬盘最好是把备份存放到另一块硬盘上;有条件就选择更好更安全的存储介质;
上面我们使用命令看出/home下涳间比较充足,所以可以考虑在/home保存备份文件;
注意把以下命令中的DatabaseName换为实际的数据库名称;
当然你也可以使用其实的命名规则!
把 username 替換为实际的用户名;
添加可执行权限之后先执行一下,看看脚本有没有错误能不能正常使用;
如时没有安装 crontab,需要先安装它具体步骤请参考:
这时就像使用vi编辑器一样,可以对计划linux定时任务脚本进行编辑
输入以下内容并保存:
很简单,我们就执行几次“ls”命令看看一分钟过后文件有没有被创建就可以了!
如果linux定时任务脚本执行失败了,可以通过以下命令查看linux定时任務脚本日志: