mit6.828,但是还不能做到多进程


到目前为止,lab3是我们的内核能够按顺序完成env_create创建的进程。但是还不能做到多进程同时执行。lab4中我们将实现多进程的调度和进程间通信。

PartA:多处理器支持

要实现多进程,首先要支持多处理器。JOS使用"symmetric multiprocessing"(SMP)模式,,这是一种多处理器模式,在这种模式下,所有的 CPU 都可以平等地访问内存和 I/O 总线等系统资源。

在 SMP 中所有 CPU 的功能都是相同的,但在启动过程中,它们可以分为两种类型:

  • bootstrap processor(BSP) 负责初始化系统和启动操作系统
  • application processors(AP) 只有在操作系统启动和运行后才由 BSP 激活哪个处理器是 BSP 由硬件和 BIOS 决定

在 SMP 系统中,每个 CPU 都有一个Local APIC(LAPIC)单元
LAPIC 单元负责在整个系统中提供中断
LAPIC 还为其连接的 CPU 提供唯一标识符

关于lapic的代码,lab4已经在kern/lapic.c中提供好了,不需要我们自己写这么底层的代码。我们只需要知道:

  • cpunum() 可以获取cpu编号,其原理是读取 lapic id。
  • lapic_startap() 可以唤醒其他ap,其原理是向目标AP发送 STARTUP 中断信号。
  • lapic_init 可以初始化ap,对lapic内置的定时器进行编程,使其每过一段时间产生时钟中断,这是后面实现轮转调度的基础。

CPU以MMIO的方式访问LAPIC,位于0xFE00_0000
距离4GB还有32MB,注意这里是地址的0xFE00_0000

这里的映射工作需要我们的内核完成,这就是exercise 1的工作。

Exercise 1

练习 1. 在 kern/pmap.c 中实现 mmio_map_region。要了解其用法,请查看 kern/lapic.c 中 lapic_init 的开头。

// Reserve size bytes in the MMIO region and map [pa,pa+size) at this
// location.  Return the base of the reserved region.  size does *not*
// have to be multiple of PGSIZE.
//
void *
mmio_map_region(physaddr_t pa, size_t size)
{
	// Where to start the next region.  Initially, this is the
	// beginning of the MMIO region.  Because this is static, its
	// value will be preserved between calls to mmio_map_region
	// (just like nextfree in boot_alloc).
	static uintptr_t base = MMIOBASE;

	// Reserve size bytes of virtual memory starting at base and
	// map physical pages [pa,pa+size) to virtual addresses
	// [base,base+size).  Since this is device memory and not
	// regular DRAM, you'll have to tell the CPU that it isn't
	// safe to cache access to this memory.  Luckily, the page
	// tables provide bits for this purpose; simply create the
	// mapping with PTE_PCD|PTE_PWT (cache-disable and
	// write-through) in addition to PTE_W.  (If you're interested
	// in more details on this, see section 10.5 of IA32 volume
	// 3A.)
	//
	// Be sure to round size up to a multiple of PGSIZE and to
	// handle if this reservation would overflow MMIOLIM (it's
	// okay to simply panic if this happens).
	//
	// Hint: The staff solution uses boot_map_region.
	//
	// Your code here:
	// panic("mmio_map_region not implemented");
	size = ROUNDUP(pa+size, PGSIZE);
	pa = ROUNDDOWN(pa, PGSIZE);
	size -= pa;
	if (base+size >= MMIOLIM) panic("not enough memory");
	boot_map_region(kern_pgdir, base, size, pa, PTE_PCD|PTE_PWT|PTE_W);
	base += size;
	return (void*) (base - size);
}

Application Processor Bootstrap

在启动 AP 之前,BSP 应首先收集有关多处理器系统的信息,如 CPU 总数、其 APIC ID 和 LAPIC 单元的 MMIO 地址。
kern/mpconfig.c 中的 mp_init() 函数通过读取 BIOS 内存区域中的 MP 配置表来获取这些信息。

kern/init.c:boot_aps() 负责启动所有AP。那么怎么启动呢?
回一下,我们最初的cpu(BSP)是怎么启动的,是bootloader按照boot/boot.S启动的。
那么AP也类似,不过,他的代码是kern/mpentry.S
那么BSP要做的事情就是,将kern/mpentry.S代码拷贝到AP(实模式状态)能够寻址到的内存位置。
与bootloader,我们可以控制 AP 开始执行代码的位置;这里,我们将入口代码复制到 0x7000 (MPENTRY_PADDR)(但理论上,任何未使用的、页面对齐的、低于 640KB 的物理地址都可以。)

控制其他AP的入口地址的方法,就是对他们调用 lapic_startap ,该函数负责唤醒这些AP,并指定entry位置,具体不做深究了。专注主线。

lapic_startap会发送IPC:startup来启动其他AP

Exercise 2

  1. 阅读 kern/init.c 中的 boot_aps() 和 mp_main(),以及 kern/mpentry.S 中的汇编代码,确保你理解了 AP 引导过程中的控制流传输。
  2. 修改 kern/pmap.c 中 page_init() 的实现,避免将位于 MPENTRY_PADDR 的页面添加到空闲列表中,这样我们就可以在该物理地址上安全地复制和运行 AP 引导代码。

代码应能通过更新后的 check_page_free_list() 测试(但可能无法通过更新后的 check_kern_pgdir() 测试,我们将尽快修复)。

void
page_init(void)
{
	// LAB 4:
	// Change your code to mark the physical page at MPENTRY_PADDR
	// as in use

	// The example code here marks all physical pages as free.
	// However this is not truly the case.  What memory is free?
	//  1) Mark physical page 0 as in use.
	//     This way we preserve the real-mode IDT and BIOS structures
	//     in case we ever need them.  (Currently we don't, but...)
	//  2) The rest of base memory, [PGSIZE, npages_basemem * PGSIZE)
	//     is free.
	//  3) Then comes the IO hole [IOPHYSMEM, EXTPHYSMEM), which must
	//     never be allocated.
	//  4) Then extended memory [EXTPHYSMEM, ...).
	//     Some of it is in use, some is free. Where is the kernel
	//     in physical memory?  Which pages are already in use for
	//     page tables and other data structures?
	//
	// Change the code to reflect this.
	// NB: DO NOT actually touch the physical memory corresponding to
	// free pages!
	size_t i;
	size_t io_hole_start_page = (size_t)IOPHYSMEM / PGSIZE;
	size_t kernel_end_page = PADDR(boot_alloc(0)) / PGSIZE;	
	for (i = 0; i < npages; i++) {
		// pages[i].pp_ref = 0;
		// pages[i].pp_link = page_free_list;
		// page_free_list = &pages[i];
		if (i == 0) {
			pages[i].pp_ref = 1;
			pages[i].pp_link = NULL;
		} else if (i >= io_hole_start_page && i < kernel_end_page) {
			pages[i].pp_ref = 1;
			pages[i].pp_link = NULL;
		}else if(i == MPENTRY_PADDR / PGSIZE){
			//为mpentry预留1页内存
			pages[i].pp_ref = 1;
			pages[i].pp_link = NULL;
		} else {
			pages[i].pp_ref = 0;
			pages[i].pp_link = page_free_list;
			page_free_list = &pages[i];
		}
	}
}


Per-CPU State and Initialization

JOS在 kern/cpu.h 中定义了 struct CpuInfo,用于抽象每个CPU。

struct CpuInfo {
	uint8_t cpu_id;  // Local APIC ID; index into cpus[] below
	volatile unsigned cpu_status;   // The status of the CPU
	struct Env *cpu_env;   // The currently-running environment.
	struct Taskstate cpu_ts; // Used by x86 to find stack for interrupt
};

CPU的重要属性有:

  1. 内核栈,可以在 lib/memlayout.h 中看到各个CPU内核栈的分布
  2. TSS和TSS描述符,用于记录各个CPU的内核栈位置
  3. 环境指针,用于记录当前运行的环境
  4. 寄存器

Exercise 3

练习 3. 修改 mem_init_mp()(在 kern/pmap.c 中),
映射从 KSTACKTOP 开始的每个 CPU 堆栈,如 inc/memlayout.h 所示。
每个堆栈的大小是 KSTKSIZE 字节加上未映射的保护页 KSTKGAP 字节。
代码应通过 check_kern_pgdir() 中的新检查。

static void
mem_init_mp(void)
{
	// Map per-CPU stacks starting at KSTACKTOP, for up to 'NCPU' CPUs.
	//
	// For CPU i, use the physical memory that 'percpu_kstacks[i]' refers
	// to as its kernel stack. CPU i's kernel stack grows down from virtual
	// address kstacktop_i = KSTACKTOP - i * (KSTKSIZE + KSTKGAP), and is
	// divided into two pieces, just like the single stack you set up in
	// mem_init:
	//     * [kstacktop_i - KSTKSIZE, kstacktop_i)
	//          -- backed by physical memory
	//     * [kstacktop_i - (KSTKSIZE + KSTKGAP), kstacktop_i - KSTKSIZE)
	//          -- not backed; so if the kernel overflows its stack,
	//             it will fault rather than overwrite another CPU's stack.
	//             Known as a "guard page".
	//     Permissions: kernel RW, user NONE
	//
	// LAB 4: Your code here:
	for(int i = 0; i<NCPU;++i){
		boot_map_region(kern_pgdir, 
			KSTACKTOP-(KSTKSIZE+KSTKGAP)*i - KSTKSIZE, 
			KSTKSIZE, 
			PADDR(percpu_kstacks[i]), 
			PTE_W);
	}
	
}

Exercise 4

练习 4. trap_init_percpu() (kern/trap.c) 中的代码初始化了 BSP 的 TSS 和 TSS 描述符。
它在实验 3 中正常工作,但在其他 CPU 上运行时却不正确。
修改代码,使其能在所有 CPU 上运行。(注意:新代码不应再使用全局 ts 变量)

void
trap_init_percpu(void)
{
	// The example code here sets up the Task State Segment (TSS) and
	// the TSS descriptor for CPU 0. But it is incorrect if we are
	// running on other CPUs because each CPU has its own kernel stack.
	// Fix the code so that it works for all CPUs.
	//
	// Hints:
	//   - The macro "thiscpu" always refers to the current CPU's
	//     struct CpuInfo;
	//   - The ID of the current CPU is given by cpunum() or
	//     thiscpu->cpu_id;
	//   - Use "thiscpu->cpu_ts" as the TSS for the current CPU,
	//     rather than the global "ts" variable;
	//   - Use gdt[(GD_TSS0 >> 3) + i] for CPU i's TSS descriptor;
	//   - You mapped the per-CPU kernel stacks in mem_init_mp()
	//   - Initialize cpu_ts.ts_iomb to prevent unauthorized environments
	//     from doing IO (0 is not the correct value!)
	//
	// ltr sets a 'busy' flag in the TSS selector, so if you
	// accidentally load the same TSS on more than one CPU, you'll
	// get a triple fault.  If you set up an individual CPU's TSS
	// wrong, you may not get a fault until you try to return from
	// user space on that 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();
	//在TSS中记录内核栈位置
	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) + i] 作为 CPU i 的 TSS 描述符;
	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 IDT
	lidt(&idt_pd);
}

trap_init_percpu 会被各个AP执行,他们自己初始化自己的TSS描述符。在 mp_init 中被调用


我们当前的代码是在 mp_main() 中初始化 AP 后旋转的。
在让 AP 进一步运行之前,我们需要首先解决多个 CPU 同时运行内核代码时的竞赛条件。

这里我有个问题,为什么不能让多个CPU同时执行内核代码?

最简单的方法就是使用_大内核锁_。
大内核锁是一个单一的全局锁,每当环境进入内核模式时就会被锁定,当环境返回用户模式时就会被释放。
在这种模式下,用户模式下的环境可以在任何可用的 CPU 上并发运行,但内核模式下运行的环境不能超过一个;任何其他试图进入内核模式的环境都会被迫等待。

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

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

Exercise 5

练习 5. 在适当的位置调用 lock_kernel() 和 unlock_kernel(),应用上述的大内核锁。

i386_init()

	// Lab 4 multitasking initialization functions
	pic_init();

	// Acquire the big kernel lock before waking up APs
	// Your code here:
	lock_kernel();	//在唤醒其他AP之前,需要获取大内核锁
	// Starting non-boot CPUs
	boot_aps();

mp_main()

// Setup code for APs
void
mp_main(void)
{
	// We are in high EIP now, safe to switch to kern_pgdir 
	lcr3(PADDR(kern_pgdir));
	cprintf("SMP: CPU %d starting\n", cpunum());

	lapic_init();
	env_init_percpu();
	trap_init_percpu();
	xchg(&thiscpu->cpu_status, CPU_STARTED); // tell boot_aps() we're up

	// Now that we have finished some basic setup, call sched_yield()
	// to start running processes on this CPU.  But make sure that
	// only one CPU can enter the scheduler at a time!
	//
	// Your code here:
	lock_kernel();//获取大内核锁
	sched_yield();//在该AP上运行进程
	// Remove this after you finish Exercise 6
	for (;;);
}

trap()

env_run


循环调度 Round-Robin Scheduling

lab3的时候,我们在 kern/init.c的i386_init()中通过env_create创建多个环境后,使用env_run 运行环境。

现在有了多个处理器后,可以同时env_run多个环境了。因此,我们要做的事情是“让多个CPU以轮转的方式在多个环境之间交替运行”。这个功能由 kern/sched.csched_yield() 实现。它以循环方式依次搜索 envs[] 数组,从之前运行的环境之后开始(如果之前没有运行的环境,则从数组的开头开始),选择找到的第一个状态为 ENV_RUNNABLE(参见 inc/env.h)的环境,并调用 env_run() 跳转到该环境。

sched_yield() 决不能同时在两个 CPU 上运行同一个环境。它可以判断出某个环境当前正在某个 CPU(可能是当前 CPU)上运行,因为该环境的状态将是 ENV_RUNNING

sched_yield 需要注册成系统调用,用户环境可以调用 sched_yield 将控制交还给CPU,让CPU进行调度。

Exercise 6

练习 6. 如上所述,在 sched_yield() 中实现循环调度。不要忘记修改 syscall() 以调度 sys_yield()

确保在 mp_main 中调用 sched_yield()

修改 kern/init.c 来创建三个(或更多!)进程,这些进程都运行 user/yield.c

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

也使用多个 CPUS 进行测试:make qemu CPUS=2.

...
你好,我是环境 00001000。
你好,我是环境 00001001。
你好,我是环境 00001002。
回到环境 00001000,迭代 0。
回到环境 00001001,迭代 0。
回到环境 00001002,迭代 0。
回到环境 00001000,迭代 1。
返回环境 00001001,迭代 1。
回到环境 00001002,迭代 1。
...
收益程序退出后,系统中将没有可运行的环境,调度程序应调用 JOS 内核监控器。如果上述任何情况没有发生,请在继续之前修改代码

void
sched_yield(void)
{
	struct Env *idle;

	// Implement simple round-robin scheduling.
	//
	// Search through 'envs' for an ENV_RUNNABLE environment in
	// circular fashion starting just after the env this CPU was
	// last running.  Switch to the first such environment found.
	//
	// If no envs are runnable, but the environment previously
	// running on this CPU is still ENV_RUNNING, it's okay to
	// choose that environment.
	//
	// Never choose an environment that's currently running on
	// another CPU (env_status == ENV_RUNNING). If there are
	// no runnable environments, simply drop through to the code
	// below to halt the cpu.

	// LAB 4: Your code here.
	int begin = 0;
	if(curenv)//从当前的env的下一个env开始尝试
	{
		begin = ENVX(curenv->env_id) + 1;
	}

	for(int i = 0 ; i < NENV; ++i)//遍历整个 envs 数组寻找 ENV_RUNNABLE 的环境
	{
		int index = (begin + i) % NENV;
		if(envs[index].env_status == ENV_RUNNABLE)
		{
			env_run(&envs[index]);
		}
	}

	if(curenv && curenv->env_status == ENV_RUNNING)//如果之前的环境还没运行结束,即ENV_RUNNING,则继续执行之前的env
	{
		env_run(curenv);
	}
	//实在不行就 drop through to the halt
	// sched_halt never returns
	sched_halt();
}


创建环境的系统调用

内核现在可以让多个CPU在用户环境间切换。
但是,内核中究竟运行哪些进程还是硬编码在 i386_init 中。
为了能够让用户环境创建环境,我们要实现 fork。
在这之前需要准备一些基础设施,即一些与fork相关的系统调用:

sys_exofork
该系统调用创建的新环境几乎是一片空白:其地址空间的用户部分没有任何映射,也无法运行。在调用 sys_exofork 时,新环境的寄存器状态与父环境相同。
在父环境中,sys_exofork 将返回新创建环境的 envid_t(如果环境分配失败,则返回负错误代码)。
但在子环境中,它将返回 0(由于子环境一开始就被标记为不可运行,因此在父环境通过使用 .... 将子环境标记为可运行后,sys_exofork 才会在子环境中实际返回)。
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() 实现。

Exerceise 7

sys_exofork
// Allocate a new environment.
// Returns envid of new environment, or < 0 on error.  Errors are:
//	-E_NO_FREE_ENV if no free environment is available.
//	-E_NO_MEM on memory exhaustion.
static envid_t
sys_exofork(void)
{
	// Create the new environment with env_alloc(), from kern/env.c.
	// It should be left as env_alloc created it, except that
	// status is set to ENV_NOT_RUNNABLE, and the register set is copied
	// from the current environment -- but tweaked so sys_exofork
	// will appear to return 0.

	// LAB 4: Your code here.
	// panic("sys_exofork not implemented");
	struct Env *e;
	int iret = env_alloc(&e, curenv->env_id);		//创建新环境
	if(iret<0) return iret;								//返回错误

	e->env_status = ENV_NOT_RUNNABLE;				//设置状态
	e->env_tf = curenv->env_tf;						//寄存器值和父环境保持一致
	e->env_tf.tf_regs.reg_eax = 0;					//通过eax设置子环境的返回值

	return e->env_id;								//返回syscall,syscall也会通过eax返回父环境
}

sys_env_set_status

// Set envid's env_status to status, which must be ENV_RUNNABLE
// or ENV_NOT_RUNNABLE.
//
// Returns 0 on success, < 0 on error.  Errors are:
//	-E_BAD_ENV if environment envid doesn't currently exist,
//		or the caller doesn't have permission to change envid.
//	-E_INVAL if status is not a valid status for an environment.
static int
sys_env_set_status(envid_t envid, int status)
{
	// Hint: Use the 'envid2env' function from kern/env.c to translate an
	// envid to a struct Env.
	// You should set envid2env's third argument to 1, which will
	// check whether the current environment has permission to set
	// envid's status.

	// LAB 4: Your code here.
	// panic("sys_env_set_status not implemented");
	int ret = 0;
	if(status != ENV_RUNNABLE && status != ENV_NOT_RUNNABLE)	//检查status合规性
	{
		return -E_INVAL;
	}

	struct Env * e;
	ret = envid2env(envid,&e, 1);			//根据envid获取env结构体
	if(ret<0) return E_BAD_ENV;						//返回 -E_BAD_ENV

	e->env_status = ENV_RUNNABLE;
	return 0;
}

sys_page_alloc
// Allocate a page of memory and map it at 'va' with permission
// 'perm' in the address space of 'envid'.
// The page's contents are set to 0.
// If a page is already mapped at 'va', that page is unmapped as a
// side effect.
//
// perm -- PTE_U | PTE_P must be set, PTE_AVAIL | PTE_W may or may not be set,
//         but no other bits may be set.  See PTE_SYSCALL in inc/mmu.h.
//
// Return 0 on success, < 0 on error.  Errors are:
//	-E_BAD_ENV if environment envid doesn't currently exist,
//		or the caller doesn't have permission to change envid.
//	-E_INVAL if va >= UTOP, or va is not page-aligned.
//	-E_INVAL if perm is inappropriate (see above).
//	-E_NO_MEM if there's no memory to allocate the new page,
//		or to allocate any necessary page tables.
static int
sys_page_alloc(envid_t envid, void *va, int perm)
{
	// Hint: This function is a wrapper around page_alloc() and
	//   page_insert() from kern/pmap.c.
	//   Most of the new code you write should be to check the
	//   parameters for correctness.
	//   If page_insert() fails, remember to free the page you
	//   allocated!

	// LAB 4: Your code here.
	// panic("sys_page_alloc not implemented");

	struct Env * e;
	if(envid2env(envid, &e, 1)<0)			//获取环境
	{
		return -E_BAD_ENV;
	}

	if((physaddr_t)(va)>=UTOP || PGOFF(va)) 							//检查va合规性
		return -E_INVAL;

	if((perm &PTE_U) == 0||(perm & PTE_P) == 0 || (perm & ~PTE_SYSCALL) == 1)	//检查Perm合规性
	{	
		return -E_INVAL;
	}

	struct PageInfo * pi = page_alloc(ALLOC_ZERO);		//申请内存页
	if(pi == NULL) return -E_INVAL;						//检查是否还有内存页可供分配

	if(page_insert(e->env_pgdir, pi, va, perm)<0)		//插入页表,如果va已有页则覆盖(page_insert保障)
	{
		page_free(pi);									//如果失败要释放已经申请的page
		return -E_NO_MEM;
	}

	return 0;
} 

sys_page_map

// Map the page of memory at 'srcva' in srcenvid's address space
// at 'dstva' in dstenvid's address space with permission 'perm'.
// Perm has the same restrictions as in sys_page_alloc, except
// that it also must not grant write access to a read-only
// page.
//
// Return 0 on success, < 0 on error.  Errors are:
//	-E_BAD_ENV if srcenvid and/or dstenvid doesn't currently exist,
//		or the caller doesn't have permission to change one of them.
//	-E_INVAL if srcva >= UTOP or srcva is not page-aligned,
//		or dstva >= UTOP or dstva is not page-aligned.
//	-E_INVAL is srcva is not mapped in srcenvid's address space.
//	-E_INVAL if perm is inappropriate (see sys_page_alloc).
//	-E_INVAL if (perm & PTE_W), but srcva is read-only in srcenvid's
//		address space.
//	-E_NO_MEM if there's no memory to allocate any necessary page tables.
static int
sys_page_map(envid_t srcenvid, void *srcva,
	     envid_t dstenvid, void *dstva, int perm)
{
	// Hint: This function is a wrapper around page_lookup() and
	//   page_insert() from kern/pmap.c.
	//   Again, most of the new code you write should be to check the
	//   parameters for correctness.
	//   Use the third argument to page_lookup() to
	//   check the current permissions on the page.

	// LAB 4: Your code here.
	// panic("sys_page_map not implemented");
	struct Env * src_env;
	if(envid2env(srcenvid, &src_env, 1)<0)			//获取环境
		return -E_BAD_ENV;
	
	struct Env * dst_env;	
	if(envid2env(dstenvid, &dst_env, 1)<0)			//获取环境
		return -E_BAD_ENV;
	
	//检查va合规性
	if((physaddr_t)(srcva)>=UTOP || PGOFF(srcva)||(physaddr_t)(dstva)>=UTOP || PGOFF(dstva)) 
		return -E_INVAL;

	//检查Perm合规性
	if((perm &PTE_U) == 0||(perm & PTE_P) == 0 || (perm & ~PTE_SYSCALL) == 1)
		return -E_INVAL;
	
	pte_t *pte;
	struct PageInfo * pp = page_lookup(src_env->env_pgdir, srcva, &pte);
	if(!pp) return -E_INVAL;

	//如果srcenvid只读,但是perm要求写,则返回错误
	if((*pte&PTE_W) == 0 && (perm & PTE_W) == 1)
		return -E_INVAL;
	
	if(page_insert(dst_env->env_pgdir, pp, dstva, perm)<0) 
		return -E_INVAL;

	return 0;
}

sys_page_unmap

将srcenvid中的srcva所在页映射到dstenvid中的dstva中,权限为perm。
Perm有着和 sys_page_alloc 相同的限制,但是,它不能将read-only的页赋予write权限。

返回值:0为成功,<0为错误,错误有:
累了,不翻译了,看代码注释吧:


// Unmap the page of memory at 'va' in the address space of 'envid'.
// If no page is mapped, the function silently succeeds.
//
// Return 0 on success, < 0 on error.  Errors are:
//	-E_BAD_ENV if environment envid doesn't currently exist,
//		or the caller doesn't have permission to change envid.
//	-E_INVAL if va >= UTOP, or va is not page-aligned.
static int
sys_page_unmap(envid_t envid, void *va)
{
	// Hint: This function is a wrapper around page_remove().

	// LAB 4: Your code here.
	// panic("sys_page_unmap not implemented");
	struct Env * e ;
	if(envid2env(envid, &e, 1)<0)
		return -E_BAD_ENV;

	if((physaddr_t)(va)>=UTOP || PGOFF(va)) 				//检查va合规性
		return -E_INVAL;

	page_remove(e->env_pgdir, va);

	return 0;
}

Part B Copy-on-Write Fork

Unix 提供 fork() 系统调用作为主要的进程创建基元。fork()系统调用复制调用进程(父进程)的地址空间,创建一个新进程(子进程)。

不过,在调用 fork() 之后,子进程往往会立即调用 exec(),用新程序替换子进程的内存。例如,shell 通常就是这么做的。在这种情况下,拷贝父进程地址空间所花费的时间基本上是白费的,因为子进程在调用 exec() 之前几乎不会使用它的内存。

因此,后来的 Unix 版本利用虚拟内存硬件,允许父进程和子进程共享映射到各自地址空间的内存,直到其中一个进程实际修改了内存。这种技术被称为 "写时拷贝"(copy-on-write)。 为此,内核会在 fork() 时将父进程的地址空间映射复制到子进程,而不是映射页的内容,同时将现在共享的页标记为只读。 当两个进程中的一个试图写入其中一个共享页面时,该进程就会发生页面错误。此时,Unix 内核会意识到该页面实际上是一个 "虚拟 "或 "写时拷贝 "副本,因此会为发生故障的进程创建一个新的、私有的、可写的页面副本。 这样,单个页面的内容在实际写入之前不会被复制。这种优化使得在子进程中执行 fork() 之后执行 exec() 的成本大大降低:在调用 exec() 之前,子进程可能只需要复制一个页面(堆栈的当前页面)。

我们接下来的目标就是实现写时复制的fork


用户级页面故障处理 User-level page fault handling

为了实现用户级的写时复制 fork(),exercise7做的syscall外,我们还需要实现一些基础设施,即用户级页面故障处理。缺页中断我们在lab3中就解除了,但是把坑留下来了,为了实现fork中,对COW页面的处理,我们不得不面对这个问题了。

Exercise 8 设置页面故障处理程序

为了处理自己的页面故障,用户环境需要向 JOS 内核注册一个页面故障处理程序入口点。用户环境通过新的 sys_env_set_pgfault_upcall 系统调用来注册其页面故障入口点。我们在 Env 结构中添加了一个新成员 env_pgfault_upcall,以记录这一信息。

练习 8. 执行 sys_env_set_pgfault_upcall 系统调用。由于这是一个 "危险 "的系统调用,因此在查找目标环境的环境 ID 时一定要启用权限检查。

实现 sys_env_set_upcall 系统调用。

// Set the page fault upcall for 'envid' by modifying the corresponding struct
// Env's 'env_pgfault_upcall' field.  When 'envid' causes a page fault, the
// kernel will push a fault record onto the exception stack, then branch to
// 'func'.
//
// Returns 0 on success, < 0 on error.  Errors are:
//	-E_BAD_ENV if environment envid doesn't currently exist,
//		or the caller doesn't have permission to change envid.
static int
sys_env_set_pgfault_upcall(envid_t envid, void *func)
{
	// LAB 4: Your code here.
	// panic("sys_env_set_pgfault_upcall not implemented");
	struct Env * e;
	if(envid2env(envid, &e, 1)<0)	// 检查envid是否有误
		return -E_BAD_ENV ;

	e->env_pgfault_upcall = func;	//	设置该环境page fault的handler

	return 0;
}

Exercise 9 调用用户页面故障处理程序

我们要做的是完成内核的 page_fault_handler,page_fault_handler只是页错误处理的第一站,只负责维持用户异常栈,并调用 upcall 去做实际处理。

注释翻译:
当 envid 所指环境发生 page fault 时,kernel 会 push 一个 trap frame 到 exception stack,然后就会执行 handler。
但是 upcall 在运行时开能会触发新的 page fault , 此时就会递归调用 handler , push 一个新的 trapframe 到用户 exception stack 的顶部。
对于我们从页面故障返回的代码(lib/pfentry.S)来说,在 trap-time stack 的顶部留出一个字的抓取空间是很方便的;它可以让我们更容易地恢复 eip/esp。 在非递归情况下,我们不必担心这个问题,因为常规用户栈的顶部是空闲的。 在递归情况下,这意味着我们必须在当前的异常堆栈顶部和新的堆栈帧之间多留一个字,因为 exception stack 就是 trap-time stack
如果没有 page fault upcall,环境没有为其 exception stack 分配页面或无法写入页面,或者异常堆栈溢出,则销毁导致故障的环境。请注意,该级脚本假定您将首先检查页面故障上调,如果没有,则打印下面的 "用户故障 va "信息。 其余三个检查可合并为一个测试。

我们要做的事情:

  1. 判断curenv->env_pgfault_upcall 是否设置
  2. 修改esp,将其切换到异常栈
    1. 对于首次缺页,是直接切换
    2. 对于递归缺页,是在当前tf->tf_esp的下方。
  3. 再异常栈上压入一个UTrapframe
  4. 将eip设置为env_pgfault_upcall

但是如何判断我们是在递归中执行呢?手册这提示了我们:
要测试 tf->tf_esp 是否已在用户异常堆栈中,请检查它是否在 UXSTACKTOP-PGSIZE 和 UXSTACKTOP-1 之间(包括 UXSTACKTOP-1 在内)的范围内。

void
page_fault_handler(struct Trapframe *tf)
{
	uint32_t fault_va;

	// Read processor's CR2 register to find the faulting address
	fault_va = rcr2();	//获取发生页错误的地址

	// Handle kernel-mode page faults.

	// LAB 3: Your code here.
	if ((tf->tf_cs & 3) == 0)
		panic("page_fault_handler():page fault in kernel mode!\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.
	//检查是否有处理页错误的handler
	if(curenv->env_pgfault_upcall)
	{
		uintptr_t stacktop = UXSTACKTOP;
		//检查是否在递归调用handler
		if(tf->tf_esp > UXSTACKTOP-PGSIZE && tf->tf_esp < UXSTACKTOP)
			stacktop = tf->tf_esp;
		
		//预留32位字的scratch space
		uint32_t size = sizeof(struct UTrapframe) + sizeof(uint32_t);
		//检查是否有权限读写exception stack
		user_mem_assert(curenv, (void *)(stacktop-size), size, PTE_U|PTE_W);
		//填充UTrapframe
		struct UTrapframe *utf = (struct UTrapframe *)(stacktop-size);
		utf->utf_fault_va = fault_va;
		utf->utf_err = tf->tf_err;
		utf->utf_regs = tf->tf_regs;
		utf->utf_eip = tf->tf_eip;
		utf->utf_eflags = tf->tf_eflags;
		utf->utf_esp = tf->tf_esp;
		//设置eip和esp,运行handler
		curenv->env_tf.tf_eip = (uintptr_t)curenv->env_pgfault_upcall;
		curenv->env_tf.tf_esp = (uintptr_t)utf;
		env_run(curenv);
	}
	
	// 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);
}

Exercise 10 用户模式页面故障入口点

我们要做的事情是完成一个汇编程序,这个汇编程序仍然不直接处理页错误,而是调用通过 sys_env_set_pgfault_upcall 设定的 _page_handler。并进行恢复工作。
调用完成后,寄存器状态必定和 trap-time state 是不同的,所以我们要做的是:

  1. 恢复寄存器状态
  2. 控制流交还故障处

这里存在的困难在于,我们要让所有寄存器保持中trap-time state,并归还控制流。

  • 我们不能调用 "jmp",因为这要求我们将地址加载到寄存器中,而所有寄存器在返回后都必须具有trap-time value。
  • 我们也不能从异常堆栈调用 "ret",因为如果这样做,%esp 就不是trap-time 值。

因此,一种解决方案是:

  1. 将 trap-time %eip 推送到 trap-time stack
  2. 将栈切换到trap-time stack
  3. ret

练习 10. 实现 lib/pfentry.S 中的 _pgfault_upcall 例程。有趣的部分是返回到用户代码中引起页面故障的原始点。你将直接返回到那里,而无需返回内核。困难的部分是同时切换堆栈和重新加载 EIP。

.text
.globl _pgfault_upcall
_pgfault_upcall:
	// Call the C page fault handler.
	pushl %esp			// function argument: pointer to UTF
	movl _pgfault_handler, %eax
	call *%eax				//调用页错误处理函数
	addl $4, %esp			// pop function argument
	
	movl 28(%esp), %eax				//取第11个参数,eip
	movl 30(%esp), %edx				//去第13个参数,esp
	subl $4, %edx					//在返回函数的栈帧中开辟1个word大小的空间
	movl %edx, 0x30(%esp)			//将esp保存回去
	movl %eax, (%edx)				//将trap-time eip push到trap-time stack中
									//如果这是递归错误,那么eip就会存在32位空白中
							
	// Restore the trap-time registers.  After you do this, you
	// can no longer modify any general-purpose registers.
	// LAB 4: Your code here.
	addl $8, %esp
	popal

	// Restore eflags from the stack.  After you do this, you can
	// no longer use arithmetic operations or anything else that
	// modifies eflags.
	// LAB 4: Your code here.
	addl $4, %esp
	popfl
	// Switch back to the adjusted trap-time stack.
	// LAB 4: Your code here.
	popl %esp
	// Return to re-execute the instruction that faulted.
	// LAB 4: Your code here.
	ret 

Exercise 11

练习 11. 完成 lib/pgfault.c 中的 set_pgfault_handler()。

void
set_pgfault_handler(void (*handler)(struct UTrapframe *utf))
{
	int r;

	if (_pgfault_handler == 0) {
		// First time through!
		// LAB 4: Your code here.
		// panic("set_pgfault_handler not implemented");
		//为当前环境分配异常栈
		if(sys_page_alloc(0, (void *)(UXSTACKTOP - PGSIZE), PTE_U|PTE_W | PTE_P)<0)
			panic("set_pgfault_handler failed.");
		sys_env_set_pgfault_upcall(0, _pgfault_upcall);
	}

	// Save handler pointer for assembly to call.
	_pgfault_handler = handler;
}

做到这里才明白,struct env 中的 env_pgfault_upcall 并不是真正的handler,这个位置的值一定是 Exercise10 中编写的汇编wrapper(_pgfault_upcall)。所有的页错误都会汇集到那里。

sys_env_set_pgfault_upcall 也是和 _pgfault_upcall 捆绑的。

真正提供了功能的是现在要实现的函数 set_pgfault_handler , 真正指向进行页面处理的变量是 _pgfault_handler , 由 set_pgfault_handler 形参控制。

小总结

页面错误时的控制流:

发生页面错误-> CPU将控制流转到IDT中的page fault槽
-> trapentry.S -> trap->trap_dispatch->page_fault_handler
(进入用户态)
->_page_upcall -> _pgfault_handler

发生页错误后,_page_upcall 负责调用 _pgfault_handler,并恢复上下文

测试

debug的时候发现,lib/pgfault.c:set_pgfault_handler 怎么装不上handler。
导致过不了很多测试。来回查了半天,发现 sys_env_set_pgfault_upcall 在做exercise 8的时候忘了给注册到 syscall 里去了。

user/faultread


这个程序没有注册handler,那就会在 page_fault_handler 检查handler合规时失败,打印trapframe后销毁环境。

user/faultdie


这个用户程序的handler打印了引发页错误的地址和错误号。

user/faultalloc

#include <inc/lib.h>

void
handler(struct UTrapframe *utf)
{
	int r;
	void *addr = (void*)utf->utf_fault_va;

	cprintf("fault %x\n", addr);
	if ((r = sys_page_alloc(0, ROUNDDOWN(addr, PGSIZE),
				PTE_P|PTE_U|PTE_W)) < 0)
		panic("allocating at %x in page fault handler: %e", addr, r);
	snprintf((char*) addr, 100, "this string was faulted in at %x", addr);
}

void
umain(int argc, char **argv)
{
	set_pgfault_handler(handler);
	cprintf("%s\n", (char*)0xDeadBeef);
	cprintf("%s\n", (char*)0xCafeBffe);
}

faultalloc 尝试访问两个地址,然后handler中通过 sys_page_alloc 申请这两个地址再访问。

deadbeef这个地址在发生页错误后,通过 handler 申请内存页后成功访问了。
但是 cafebffe 在发生一次页错误后,似乎又发生了一次页错误,因为 cafebffe 在页中正好处于 倒数第二个字节(0xffe),handler又将一长串字符保存到了cafebffe,所以引发了第二次页错误。
第二次handler在申请玩cafec000的内存页后,将一长串字符串保存到了cafec000,然后控制流回到第一次handler处理错误,继续将字符保存到cafebffe的位置,然后将控制流返回到umain的最后一句话,将第一次handler的字符串打出来。
handler第二次保存的字符串应该是被第一次保存的字符串覆盖,没覆盖的地方被尾巴'\0'切断了。


user/faultallocbad

// test user-level fault handler -- alloc pages to fix faults
// doesn't work because we sys_cputs instead of cprintf (exercise: why?)

#include <inc/lib.h>

void
handler(struct UTrapframe *utf)
{
	int r;
	void *addr = (void*)utf->utf_fault_va;

	cprintf("fault %x\n", addr);
	if ((r = sys_page_alloc(0, ROUNDDOWN(addr, PGSIZE),
				PTE_P|PTE_U|PTE_W)) < 0)
		panic("allocating at %x in page fault handler: %e", addr, r);
	snprintf((char*) addr, 100, "this string was faulted in at %x", addr);
}

void
umain(int argc, char **argv)
{
	set_pgfault_handler(handler);
	sys_cputs((char*)0xDEADBEEF, 4);
}

结果是没有出发 handler ,反而是 user_mem_assert 输出了。

faultallocbad 和 faultalloc 的 handler 是一样的,区别在于使用 sys_cputs 打印。sys_cputs 第一件工作就是用 user_mem_assert 确认 0xDEADBEEF 是否使用,还没有机会触发页错误。

by the way :user_mem_assert的检查方式是查页表,看PTE是否合规。这个过程是不会发生页错误的。


Exercise 12 实现写时复制的fork

fork的骨架如下:

  1. fork函数:负责复制自身,并调用duppage复制页映射,设置页错误handler
  2. duppage:负责复制页映射的具体工作
  3. pgfault:页错误handler,当发生对写时复制页进行写操作时,将页面进行实际复制。

fork的具体流程:

  1. 父级程序会使用上面实现的 set_pgfault_handler() 函数安装 pgfault() 作为 C 级页面故障处理程序。
  2. 父环境调用 sys_exofork(),创建子环境。
  3. 父环境将[0~UTOP]的地址空间中所有“可写PTE_W”、“写时复制PTE_COW”页面的映射,通过 duppage 复制到子环境中,然后将写时复制页面重新映射到自己的地址空间(为何?不太清楚)。
  4. 对于 [UXSTACKTOP-PGSIZE, UXSTACKTOP] 的部分则是申请新的页面。
  5. 对于只读页面直接保持原权限复制即可。

发生页错误时,就会触发 pgfault() 然后将PTE_COW的页面用新页替换。

// implement fork from user space

#include <inc/string.h>
#include <inc/lib.h>

// PTE_COW marks copy-on-write page table entries.
// It is one of the bits explicitly allocated to user processes (PTE_AVAIL).
#define PTE_COW		0x800

//
// Custom page fault handler - if faulting page is copy-on-write,
// map in our own private writable copy.
//
static void
pgfault(struct UTrapframe *utf)
{
	void *addr = (void *) utf->utf_fault_va;
	uint32_t err = utf->utf_err;
	int r;

	// Check that the faulting access was (1) a write, and (2) to a
	// copy-on-write page.  If not, panic.
	// Hint:
	//   Use the read-only page table mappings at uvpt
	//   (see <inc/memlayout.h>).

	// LAB 4: Your code here.
	//只在对“写时复制页面”进行“写操作”才处理
	if(!(err & FEC_WR))
	{
		panic("trapno is not FEC_WR.");
	}
	if(!(uvpt[PGNUM(addr)] & PTE_COW))
	{
		panic("fault addr is not COW");
	}


	// Allocate a new page, map it at a temporary location (PFTEMP),
	// copy the data from the old page to the new page, then move the new
	// page to the old page's address.
	// Hint:
	//   You should make three system calls.

	// LAB 4: Your code here.

	// panic("pgfault not implemented");
	addr = ROUNDDOWN(addr, PGSIZE);
	//将当前进程PFTEMP也映射到当前进程addr指向的物理页
	if ((r = sys_page_map(0, addr, 0, PFTEMP, PTE_U|PTE_P)) < 0)
		panic("sys_page_map: %e", r);
	//令当前进程addr指向新分配的物理页
	if ((r = sys_page_alloc(0, addr, PTE_P|PTE_U|PTE_W)) < 0)	
		panic("sys_page_alloc: %e", r);
	//将PFTEMP指向的物理页拷贝到addr指向的物理页
	memmove(addr, PFTEMP, PGSIZE);			
	//解除当前进程PFTEMP映射					
	if ((r = sys_page_unmap(0, PFTEMP)) < 0)					
		panic("sys_page_unmap: %e", r);
}

//
// Map our virtual page pn (address pn*PGSIZE) into the target envid
// at the same virtual address.  If the page is writable or copy-on-write,
// the new mapping must be created copy-on-write, and then our mapping must be
// marked copy-on-write as well.  (Exercise: Why do we need to mark ours
// copy-on-write again if it was already copy-on-write at the beginning of
// this function?)
//
// Returns: 0 on success, < 0 on error.
// It is also OK to panic on error.
//
static int
duppage(envid_t envid, unsigned pn)
{
	int r;

	// LAB 4: Your code here.
	// panic("duppage not implemented");
	void *addr = (void *)(pn * PGSIZE);
	if(uvpt[pn] & PTE_SHARE)
	{
		sys_page_map(0, addr, envid, addr, PTE_SYSCALL);	
	}
	else if ((uvpt[pn]&PTE_W)|| (uvpt[pn] & PTE_COW))
	{
		if ((r = sys_page_map(0, addr, envid, addr, PTE_COW|PTE_U|PTE_P)) < 0)
			panic("sys_page_map:%e", r);
		if ((r = sys_page_map(0, addr, 0, addr, PTE_COW|PTE_U|PTE_P)) < 0)
			panic("sys_page_map:%e", r);
	}
	else
	{
		sys_page_map(0, addr, envid, addr, PTE_U|PTE_P);	//对于只读的页,只需要拷贝映射关系即可
	}
	return 0;
}

//
// User-level fork with copy-on-write.
// Set up our page fault handler appropriately.
// Create a child.
// Copy our address space and page fault handler setup to the child.
// Then mark the child as runnable and return.
//
// Returns: child's envid to the parent, 0 to the child, < 0 on error.
// It is also OK to panic on error.
//
// Hint:
//   Use uvpd, uvpt, and duppage.
//   Remember to fix "thisenv" in the child process.
//   Neither user exception stack should ever be marked copy-on-write,
//   so you must allocate a new page for the child's user exception stack.
//
envid_t
fork(void)
{
	// LAB 4: Your code here.
	// panic("fork not implemented");
	extern void _pgfault_upcall(void);
	set_pgfault_handler(pgfault);
	envid_t envid = sys_exofork();
	if (envid == 0) {				//子进程将走这个逻辑
		thisenv = &envs[ENVX(sys_getenvid())];
		return 0;
	}
	if (envid < 0) {
		panic("sys_exofork Failed, envid: %e", envid);
	}

	uint32_t  addr = 0;
	while(addr < USTACKTOP)
	{
		if((uvpd[PDX(addr)] & PTE_P) && (uvpt[PGNUM(addr)]&PTE_P) && (uvpt[PGNUM(addr)] &PTE_U))
		{
			duppage(envid, PGNUM(addr));
		}
		addr += PGSIZE;
	}

	int r;
	//为子环境的异常栈申请内存页
	if((r=sys_page_alloc(envid, (void *)(UXSTACKTOP-PGSIZE), PTE_P|PTE_W|PTE_U))<0)
		panic("sys_page_alloc: %e", r);
	//为子环境设置pgfault_upcall
	if((r= sys_env_set_pgfault_upcall(envid, _pgfault_upcall))<0)
		panic("sys_env_set_pgfault_upcall: %e",r);
	//设置子环境的运行状态
	if ((r = sys_env_set_status(envid, ENV_RUNNABLE)) < 0)
		panic("sys_env_set_status: %e", r);
	return envid;
}

// Challenge!
int
sfork(void)
{
	panic("sfork not implemented");
	return -E_INVAL;
}


C 部分:抢占式多任务和进程间通信(IPC

看看 user/spin.c

尝试在命令行跑 make run-spin 会发现,父进程fork之后再也无法执行了。这是因为我们的内核目前还没有从未完成的进程中抢回控制的能力。
我们在启动加载程序的第一条指令中就屏蔽了中断,到目前为止,我们还从未重新启用过中断。
接下来的任务,我们要完善外部中断的管理,


Exercise 13

在 trap.c:trap_init() 中定义外部设备中断的handler

	//初始化外部中断的中断向量
	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.c:env_alloc,在用户环境运行前开启外部设备中断,在注释提示处添加语句:

// Enable interrupts while in user mode.
	// LAB 4: Your code here.
	// 开启用户环境的外部设备中断
	e->env_tf.tf_eflags |= FL_IF;

修改 kern/sched.c:sched_halt,将提示处的sti语句注释取消掉,sti 指令是开中断,如手册中所述,我们在 bootloader 中第一条指令 cli 就屏蔽了中断,到目前为止还没有重新开启中断。
sched_halt 这个让CPU陷入自旋,等待被timer打断。不开中断是不可能做到被抢断的。

完成了这些我们再次尝试 make run-spin


Exercise 14

目前我们已经在中断向量表中添加了接受timer信号的中断描述符,timer中断发生后,控制流会来到trap,然后发往 trap_dispatch,但是 trap_dispatch 中还没有对应的hander接应,所以现在要在 trap_dispatch 中处理timer的中断信号。

	// Handle clock interrupts. Don't forget to acknowledge the
	// interrupt using lapic_eoi() before calling the scheduler!
	// LAB 4: Your code here.
	if(tf->tf_trapno == IRQ_OFFSET + IRQ_TIMER)
	{
		cprintf("Timer interrupt on irq 0\n");
		lapic_eoi();
		sched_yield();
	}

lapic_eoi() 函数的作用是在接收到中断请求并处理完成后,向本地高级可编程中断控制器(Local Advanced Programmable Interrupt Controller, LAPIC)发送一个 EOI 命令,通知 LAPIC 中断处理已完成。这是为了释放中断控制器的资源,以便处理下一个中断。

这个时候我们再次尝试 make run-spin ,会发现程序可以正常执行了:


进程间通信(IPC)

在 kern/syscall.c 中实现 sys_ipc_try_send 。
按照注释进行一系列检查后将 srcva 所在的 pg ,映射到 dstva 所在的地址。

// Try to send 'value' to the target env 'envid'.
// If srcva < UTOP, then also send page currently mapped at 'srcva',
// so that receiver gets a duplicate mapping of the same page.
//
// The send fails with a return value of -E_IPC_NOT_RECV if the
// target is not blocked, waiting for an IPC.
//
// The send also can fail for the other reasons listed below.
//
// Otherwise, the send succeeds, and the target's ipc fields are
// updated as follows:
//    env_ipc_recving is set to 0 to block future sends;
//    env_ipc_from is set to the sending envid;
//    env_ipc_value is set to the 'value' parameter;
//    env_ipc_perm is set to 'perm' if a page was transferred, 0 otherwise.
// The target environment is marked runnable again, returning 0
// from the paused sys_ipc_recv system call.  (Hint: does the
// sys_ipc_recv function ever actually return?)
//
// If the sender wants to send a page but the receiver isn't asking for one,
// then no page mapping is transferred, but no error occurs.
// The ipc only happens when no errors occur.
//
// Returns 0 on success, < 0 on error.
// Errors are:
//	-E_BAD_ENV if environment envid doesn't currently exist.
//		(No need to check permissions.)
//	-E_IPC_NOT_RECV if envid is not currently blocked in sys_ipc_recv,
//		or another environment managed to send first.
//	-E_INVAL if srcva < UTOP but srcva is not page-aligned.
//	-E_INVAL if srcva < UTOP and perm is inappropriate
//		(see sys_page_alloc).
//	-E_INVAL if srcva < UTOP but srcva is not mapped in the caller's
//		address space.
//	-E_INVAL if (perm & PTE_W), but srcva is read-only in the
//		current environment's address space.
//	-E_NO_MEM if there's not enough memory to map srcva in envid's
//		address space.
static int
sys_ipc_try_send(envid_t envid, uint32_t value, void *srcva, unsigned perm)
{
	// LAB 4: Your code here.
	// panic("sys_ipc_try_send not implemented");
	int r;
	struct Env * env;

	// 检查 envid 是否合规
	if((r=envid2env(envid, &env ,0)) < 0) 
	{
		return -E_BAD_ENV ;
	}

	// 检查目标env是否准备接受
	if(env->env_ipc_recving == 0)
	{
		return -E_IPC_NOT_RECV;
	}

	if (srcva < (void*)UTOP) {
		pte_t *pte;
		struct PageInfo *pg = page_lookup(curenv->env_pgdir, srcva, &pte);
		
		// 检查 srcva 是否 page-aligned.
		if(!PGOFF(srcva))
		{
			return -E_INVAL;
		}
		// 检查权限
		if((*pte & perm & (PTE_W|PTE_P|PTE_U))!=(perm & (PTE_W|PTE_P|PTE_U)))
		{
			return -E_INVAL;
		}
		// 如果来源环境没有映射pg页
		if(!pg)
		{
			return -E_INVAL;
		}
		// 如果perm要求写权限,但是srcva没有写权限
		if ((perm & PTE_W) && !(*pte & PTE_W))
		{
			return -E_INVAL;
		}
		// 如果目标环境以有效dstva参数调用 sys_ipc_recv,说明目标环境愿意接受页面映射
		if (env->env_ipc_dstva < (void*)UTOP) {
			// 将当前环境的 pg 页 映射到目标环境的dstva上
			if((r = page_insert(env->env_pgdir, pg, env->env_ipc_dstva, perm))<0)
			{
				return -E_NO_MEM;
			}
			env->env_ipc_perm = perm;
		}

	}
	// 标记目标环境为 未准备接收
	env->env_ipc_recving = 0;
	// 将目标环境的 IPC发送方 设置为当前环境
	env->env_ipc_from = curenv->env_id;
	// 发送 message 的 value
	env->env_ipc_value = value; 
	// 设置目标环境为可运行
	env->env_status = ENV_RUNNABLE;
	// 设置目标环境的eax
	env->env_tf.tf_regs.reg_eax = 0;
	return 0;
}

sys_ipc_recv 则是设置env的与IPC相关的成员,关键是env_ipc_recving=1,标记为准备接受数据。
然后调用 sched_yield 交出cpu,等待sender发送数据

static int
sys_ipc_recv(void *dstva)
{
	// LAB 4: Your code here.
	// panic("sys_ipc_recv not implemented");
	// 检查 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;

	// 交出控制流,等待数据输入
	sched_yield();

	return 0;
}

然后不要忘了在 syscall 的 switch 中加上相关调用的分支:

		case SYS_ipc_try_send:
			ret = sys_ipc_try_send((envid_t) a1, (uint32_t) a2, (void *) a3, (unsigned int) a4);
			return ret;
		case SYS_ipc_recv:
			ret = sys_ipc_recv((void*)(a1));
			return ret;

接着去用户的lib/ipc.c 中实现相应库函数。


// Receive a value via IPC and return it.
// If 'pg' is nonnull, then any page sent by the sender will be mapped at
//	that address.
// If 'from_env_store' is nonnull, then store the IPC sender's envid in
//	*from_env_store.
// If 'perm_store' is nonnull, then store the IPC sender's page permission
//	in *perm_store (this is nonzero iff a page was successfully
//	transferred to 'pg').
// If the system call fails, then store 0 in *fromenv and *perm (if
//	they're nonnull) and return the error.
// Otherwise, return the value sent by the sender
//
// Hint:
//   Use 'thisenv' to discover the value and who sent it.
//   If 'pg' is null, pass sys_ipc_recv a value that it will understand
//   as meaning "no page".  (Zero is not the right value, since that's
//   a perfectly valid place to map a page.)
int32_t
ipc_recv(envid_t *from_env_store, void *pg, int *perm_store)
{
	// LAB 4: Your code here.
	// panic("ipc_recv not implemented");
	// 检查pg是否为空
	if(pg == NULL)
	{
		pg=(void *) -1;
	}
	//接收 message
	int r = sys_ipc_recv(pg);
	if(r<0)
	{
		if(from_env_store) *from_env_store = 0;
		if(perm_store) *perm_store = 0;
		return r;
	}
	// 保存发送者的envid
	if(from_env_store) *from_env_store = thisenv->env_ipc_from;
	// 保存发送来的页面的权限
	if(perm_store) *perm_store = thisenv->env_ipc_perm;
	// 返回message的value
	return thisenv->env_ipc_value;
}

// Send 'val' (and 'pg' with 'perm', if 'pg' is nonnull) to 'toenv'.
// This function keeps trying until it succeeds.
// It should panic() on any error other than -E_IPC_NOT_RECV.
//
// Hint:
//   Use sys_yield() to be CPU-friendly.
//   If 'pg' is null, pass sys_ipc_try_send a value that it will understand
//   as meaning "no page".  (Zero is not the right value.)
void
ipc_send(envid_t to_env, uint32_t val, void *pg, int perm)
{
	// LAB 4: Your code here.
	// panic("ipc_send not implemented");
	// 如果pg为NULL, 要提供给sys_ipc_try_send一个能表达“no page”的值,0是有效的地址
	if(pg==NULL)
	{
		pg = (void *)-1;
	}
	int r;
	//不停尝试发送消息直到成功
	while(1)
	{
		r = sys_ipc_try_send(to_env, val, pg, perm);
		if (r == 0) {		//发送成功
			return;
		} else if (r == -E_IPC_NOT_RECV) {	//接收环境未准备接收
			sys_yield();
		}else{
			panic("ipc_send() fault:%e\n", r);
		}
	}
}

相关内容