锁、轮转调度 学习笔记

130 阅读5分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第16天,点击查看活动详情 

接着上次实验的过程,又堆了好久的博客没有发出去,今天再来填一填坑,最近实验实现的内容主要包括锁,和轮转调度

我们当前的代码在mp_main()中初始化 AP 后旋转。在让 AP 进一步获取之前,我们需要首先解决多个 CPU 同时运行内核代码时的竞争条件。实现此目的的最简单方法是使用大内核锁。大内核锁是一个全局锁,每当环境进入内核模式时,就会保持该全局锁,并在环境返回到用户模式时释放。在此模型中,用户模式下的环境可以在任何可用的 CPU 上并发运行,但内核模式下只能运行一个环境。尝试进入内核模式的任何其他环境都被迫等待。

kern/spinlock.h 声明大内核锁,即kernel_lock .它还提供lock_kernel()和 unlock_kernel(),用于获取和释放锁的快捷方式。您应该在四个位置应用大内核锁:

  • 在 i386_init()中,在 BSP 唤醒其他 CPU 之前获取锁。i386_init()
  • 在 mp_main()中,在初始化 AP 后获取锁,然后调用 sched_yield()以开始在此 AP 上运行环境。
  • 在 trap()中,从用户模式捕获时获取锁。要确定陷阱是在用户模式下还是在内核模式下发生,请检查 tf_cs的低位。
  • 在 env_run()中,在切换到用户模式之前释放锁。不要太早或太晚这样做,否则你会经历竞争或死锁。

Exercise 5. Apply the big kernel lock as described above, by calling lock_kernel() and unlock_kernel() at the proper locations.

How to test if your locking is correct? You can’t at this moment! But you will be able to after you implement the scheduler in the next exercise.

这里的代码很简单,直接按照上述提示在指定调用前后添加函数用于获取或者释放锁即可。其中注意在调用 sched_yield()前获取锁,sched_yield()函数将在下文进行解释。

Question

  1. It seems that using the big kernel lock guarantees that only one CPU can run the kernel code at a time. Why do we still need separate kernel stacks for each CPU? Describe a scenario in which using a shared kernel stack will go wrong, even with the protection of the big kernel lock.

这个问题需要考虑我们上锁的位置,由于我们知道大内核锁只对于内核是独占的但是对于用户环境是共享的,那么在从用户切换到内核(获取锁)和从内核切换到用户(释放锁)的过程中实际上我们是不持有锁的,也就是说如果只有一个内核堆栈的话,此时数据是可能会被其他处理器修改的。(例如中断时,从_alltraps到lock_kernel()的过程中,可能会导致从用户态进入内核态的现场被破坏,或者恢复用户环境时导致内核环境被其他处理器修改而获取到错误的环境数据。)

轮询调度

本实验中的下一个任务是更改 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放弃到不同的环境。

Exercise 6. Implement round-robin scheduling in sched_yield() as described above. Don’t forget to modify syscall() to sys_yield() dispatch .

void
sched_yield(void)
{
	// LAB 4: Your code here.
	size_t idx = 0;
	if (curenv) {
		idx = ENVX(curenv->env_id) + 1;
	}

	for (size_t i = 0; i < NENV; i++) {
		size_t index = (idx + i) % NENV;
		if (envs[index].env_status == ENV_RUNNABLE) {
			env_run(&envs[index]);
		}
	}

	if(curenv && curenv->env_status == ENV_RUNNING) {
		env_run(curenv);
	}
	// sched_halt never returns
	sched_halt();
}

首先通过ENVX获取当前运行环境的id号并向后+1,以便轮询查找到下一个要运行的新环境。然后循环访问查找到下一个状态为 ENV_RUNNABLE的环境,通过env_run进行启动。之后注意判断 ENV_RUNNING在当前环境运行的状态,避免发生在两个不同的CPU运行相同的环境的情况,最后调用sched_halt()退出环境。

注意:在完成Exercise 6之后移除mp_main()中的循环等待,这里已经是不必要的了。在完成轮询之后,我们已经可以确定下一个运行的用户环境了(或者继续执行当前环境)。(此处不移除for应该也不会出现bug)

Make sure to invoke sched_yield() in mp_main().

修改 kern/init.c 以创建三个(或更多)环境,这些环境都运行程序 user/yield.c。

运行 make qemu在终止之前,您应该看到环境在彼此之间来回切换五次,如下所示。

还可以使用多个 CPU 进行测试: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.
...

这里直接调用ENV_CREATE宏初始化三次用户环境即可。

yield 程序退出后,系统中将没有可运行的环境,调度程序应调用 JOS 内核监视器。如果未发生上述任何情况,请在继续操作之前修复代码。