mit6.828笔记 - lab4 Part A:多处理器支持和协同多任务处理

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

为了不迷失方向,我们来看一下与 lab4 合并后 kern/init.c : i386_init() 有什么变化:

void
i386_init(void)
{
	// Initialize the console.
	// Can't call cprintf until after we do this!
	cons_init();

	cprintf("6828 decimal is %o octal!\n", 6828);

	// Lab 2 memory management initialization functions
	mem_init();

	// Lab 3 user environment initialization functions
	env_init();
	trap_init();

	// Lab 4 multiprocessor initialization functions
	mp_init();
	lapic_init();

	// Lab 4 multitasking initialization functions
	pic_init();

	// Acquire the big kernel lock before waking up APs
	// Your code here:

	// Starting non-boot CPUs
	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*

	// Schedule and run the first user environment!
	sched_yield();
}

可以看到,在 trap_init 和 ENV_CREATE 之间,多了 mp_init、lapic_init、pic_init、boot_aps。
最后不在是 调用 env_run 而是调用 shed_yield(),这个函数看起来是用于调度多个进程的。

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 ,MMIO 的物理地址位于 0xFE00_0000,距离4GB还有32MB
注意哦,这是物理地址,如果我们通过目前虚拟地址空间布局顶部物理映射区来访问的话,那就是这个样子:
kernbase + 0xFE00_0000 = 0xF000_0000+0xFE00_0000
显然,这是个无法访问的虚拟地址,为了访问这个区域,JOS的虚拟空间布局设置了专门的 MMIO 区域,而 exercise1 的工作就是完成这部分的映射。

Exercise 1

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

// 在 MMIO 区域保留大小字节,并将 [pa,pa+size) 映射到该位置。 
// 返回保留区域的基数。size 不一定是 PGSIZE 的倍数。
//
void *
mmio_map_region(physaddr_t pa, size_t size)
{
	// 下一区域的起始位置。 最初,这是 MMIO 区域的起点。 
	// 因为它是静态的,所以在调用 mmio_map_region 时它的值会被保留
	//(就像 boot_alloc 中的 nextfree 一样)。
	static uintptr_t base = MMIOBASE;

	// 从 base 开始预留大小字节的虚拟内存,
	// 并将物理页 [pa,pa+size) 映射到虚拟地址 [base,base+size) 上。 
	// 由于这是设备内存,而不是普通的 DRAM,因此必须告诉 CPU,缓存访问该内存是不安全的。 
	// 幸运的是,页表提供了用于此目的的位,只需在创建映射时,
	// 在 PTE_W 之外再加上 PTE_PCD|PTE_PWT(禁用缓存和写透)即可
	//(如果您对这方面的更多细节感兴趣,请参阅 IA32 第 3A 卷第 10.5 节)。
	//
	// 务必将大小取整为 PGSIZE 的倍数,并处理该预留值是否会溢出 MMIOLIM(如果发生这种情况,只需panic即可)。
	//
	// 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);
}

这个函数在 lapic_init 的开头被调用。


应用处理器引导程序

在启动 AP 之前,BSP 应首先收集有关多处理器系统的信息,如 CPU 总数、其 APIC ID 和 LAPIC 单元的 MMIO 地址。
kern/mpconfig.c 中的 mp_init() 函数通过读取 BIOS 内存区域中的 MP 配置表来获取这些信息。
kern/mpconfig.c 的所有代码都是最终都是服务于这个 mp_init(),他的输出就是完成了 kern/mpconfig.c 开头的几个变量的赋值:

// Initialized in mpconfig.c
extern struct CpuInfo cpus[NCPU];
extern int ncpu;                    // Total number of CPUs in the system
extern struct CpuInfo *bootcpu;     // The boot-strap processor (BSP)
extern physaddr_t lapicaddr;        // Physical MMIO address of the local APIC

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

image.png

注意BSP 唤醒 AP 的那句, lapic_startap 的第二个参数,制定了AP的首个执行地址,即 0x7000

从 lab1 BSP的启动到目前为止的情况如下:

image.png

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];
		}
	}
}

我们来看看手册中的问题:

尝试比较一下 kern/mpentry.Sboot/boot.S
思考这样一个问题:
kern/mpentry.S 与内核中的其他内容一样,都是编译并链接到 KERNBASE 上运行的,那么宏 MPBOOTPHYS 的作用是什么?
为什么在 kern/mpentry.S 中需要宏 MPBOOTPHYS而在 boot/boot.S 中不需要?
如果在 kern/mpentry.S 中省略它,会出现什么问题?

我们知道 BSP 启动的时候是以物理地址,0x0000_7C00为起点执行的,然后通过 kernel.ld 将 entry.S 编译出的代码链接到这个地址。
但是想想,0x0000_7C00 这个地址和 0x0000_7000 好像有点重合,如果将 mpentry_start (mpentry.S 中 入口点的标号) 链接到这的话,肯定是冲突的。
在vscode 中全局搜索 mpentry_start ,果然没有在链接脚本出现
image.png
如果没有猜错,他大概位于内核的代码段,某个很高的虚拟地址上面,通过objdump -t 看看 mpentry_start 这个符号:

image.png

果然如此啊,这就意味着 mpentry.S 被编译链接后,其中的变量和标号都是虚拟地址,而且还很高。但是AP需要的是物理地址,所以需要

#define MPBOOTPHYS(s) ((s) - mpentry_start + MPENTRY_PADDR)

这么一个宏,用来将虚拟地址转化为物理地址。当然这都是为了编写代码方便,也可以通过链接脚本将 mpentry.S连接到某个不冲突的物理地址的low memory 区域。
然后在 lapic_startup 的时候指定 AP 从这个地址启动。


各 CPU 状态和初始化

通过之前对 boot_aps和mp_main()的学习,我们发现 AP 其实和 BSP 区别不大,只不过AP醒来的时候,内存布局都已经让BSP处理好了。但是,每个AP都需要自己的内存栈、为了在中断、权限转移时保存环境,需要自己的TSS记录自己的内存栈位置,这需要我们修改一些代码。
这就是接下来练习3、练习4要做的事情,不过在那之前,先来了解下 JOS 是如何记录CPU信息的:
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);
	}
	
}

注意,这个 percpu_kstacks 是在 mpconfig.c 中定义的:
image.png
由于是个为初始化的全局变量,应该是位于kernel的 .bss 段
image.png
确实如此。

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.
	// 为每个CPU设置一个 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_main 中被调用

image.png


到现在为止,所有的CPU都启动完毕了,BSP顺着 i386_init 在完成 boot_aps 后差不多该 ENV_CREATE 了,各个 AP 也该顺着 mp_main 进入 shed_yield ,由进程调度分配用户进程执行了。
想一想,多个CPU同时执行,如果不做任何措施,肯定在读写数据时产生冲突。
如果各个CPU都在用户态还好,因为用户态下只能读写虚拟地址空间中的用户区域,而且各个进程的虚拟地址空间通过页表隔离,实际上操作的是不同的物理页。
但是内核态就不一样了,所有虚拟地址空间的内核区域的映射相同的,如果有多个cpu同时在进入内核势必产生冲突。

我们必须要处理竞争条件。最简单粗暴的办法就是使用"big kernel lock","big kernel lock"是一个全局锁,进程从用户态进入内核后获取该锁,退出内核释放该锁。这样就能保证只有一个CPU在执行内核代码,但缺点也很明显就是一个CPU在执行内核代码时,另一个CPU如果也想进入内核,就会处于等待的状态。

kern/spinlock.h 声明了大内核锁,即 kernel_lock,长这个样子

// Mutual exclusion lock.
struct spinlock {
	unsigned locked;       // Is the lock held?

#ifdef DEBUG_SPINLOCK
	// For debugging:
	char *name;            // Name of lock.
	struct CpuInfo *cpu;   // The CPU holding the lock.
	uintptr_t pcs[10];     // The call stack (an array of program counters)
	                       // that locked the lock.
#endif
};

好家伙,本质上就是一个整形,它还提供了 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()
image.png

env_run
image.png


循环调度 Round-Robin Scheduling

lab3的时候,我们在 kern/init.c -> i386_init()中通过 env_create 创建进程后,使用 env_run 运行一个进程,进程结束后,操作系统也就退出了。
这距离一个操作系统还是远远不够的,为了能够让操作系统运行多个进程,我们需要实现进程调度,给所用用户进程分配时间片,当某个进程得到时间片时,就可以运行;从CPU视角来看,则是有多个CPU以轮转的方式在多个进程之间交替运行。

这个功能由 kern/sched.csched_yield() 实现。它以循环方式依次搜索 envs[] 数组,从之前运行的环境之后开始(如果之前没有运行的环境,则从数组的开头开始),选择找到的第一个状态为 ENV_RUNNABLE(参见 inc/env.h)的环境,并调用 env_run() 跳转到该环境。

sched_yield() 决不能同时在两个 CPU 上运行同一个环境。struct env 的 status 字段来判断,这个进程是否正在某个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;

	// 实施简单的循环调度。
	//
	// 在 “envs ”中循环搜索 ENV_RUNNABLE 环境,从 CPU 最后运行的环境之后开始。 
	// 切换到找到的第一个此类环境。
	//
	// 如果没有可运行的环境,但该 CPU 先前运行的环境仍为 ENV_RUNNING,则可以选择该环境。
	//
	// 切勿选择当前正在其他 CPU 上运行的环境(env_status == ENV_RUNNING)。
	// 如果没有可运行的环境,则直接跳转到下面的代码,停止 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]);//env_run仍然是回归用户态的唯一出口
		}
	}

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

kern/syscall.c : syscall 中补充对 sys_yield 的调用
image.png

修改 mp_main, 在 初始化完毕后调用 sys_yield
image.png

为了测试效果,修改 kern/init.c : i386_init , 创建多个用户进程 user/yield.c

image.png

make qemu CPUS = 2 测试效果
image.png


创建环境的系统调用

到现在为止,内核现在可以让多个CPU在多个进程间切换。
但是,内核中究竟运行哪些进程还是硬编码在 i386_init 中。
为了能够让用户进程创建进程,我们要实现 fork。

关于 fork
Unix 提供 fork() 系统调用作为创建进程的基本方法。Unix fork() 复制调用进程(父进程)的整个地址空间,创建一个新进程(子进程)。从用户空间观察到的两个进程之间的唯一区别是它们的进程 ID 和父进程 ID(由 getpidgetppid 返回)。在父进程中,fork() 返回的是子进程的进程 ID,而在子进程中,fork() 返回的是 0。默认情况下,每个进程都有自己的私有地址空间,两个进程对内存的修改对对方都不可见。
除了其他创建环境的方式外,JOS 还可以在用户空间完全使用类似 Unix 的 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

练习 7. 在 `kern/syscall.c` 中实现上述系统调用,并确保 `syscall()` 调用它们。
您将需要使用 `kern/pmap.c` 和 `kern/env.c` 中的各种函数,尤其是 `envid2env()`。
目前,无论何时调用 `envid2env()`,都要在 `checkperm` 参数中传递 1。
一定要检查任何无效的系统调用参数,在这种情况下返回 `-E_INVAL`。
使用 `user/dumbfork` 测试你的 JOS 内核,确保它能正常工作后再继续。
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)
{
	// 使用 kern/env.c 中的 env_alloc() 创建新环境。
	// 除了将状态设置为 ENV_NOT_RUNNABLE,
	// 以及从当前环境复制寄存器集之外,
	// 它应该保持 env_alloc 创建时的状态,
	// 但 sys_exofork 似乎会返回 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
// 将 envid 的 env_status 设置为 status,
// 即 ENV_RUNNABLE 或 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)
{
	// 提示:使用 kern/env.c 中的 “envid2env ”函数
	// 将 envid 转换为 struct Env。
	// 应将 envid2env 的第三个参数设置为 1,
	// 它将检查当前环境是否有权限设置 envid 的状态。

	// LAB 4: Your code here.
	// panic("sys_env_set_status not implemented");
	if(status != ENV_RUNNABLE && status != ENV_NOT_RUNNABLE){
		return -E_INVAL;
	}
	struct Env *e;
	if(envid2env(envid, &e, true) < 0){
		return -E_BAD_ENV;
	}
	e->env_status = status;
	return 0;
}

sys_page_alloc
// 分配一页内存,并将其映射到权限为
// 'perm' 位于'envid'的地址空间。
// 该页的内容设置为 0。
// 如果在'va'处已经映射了一个页面,该页面将作为副作用被取消映射。
//
// perm -- PTE_U | PTE_P 必须设置,PTE_AVAIL | PTE_W 可以设置,也可以不设置、
// 但不能设置其他位。 参见 inc/mmu.h 中的 PTE_SYSCALL。
//
// 成功时返回 0,错误时返回 <0。 错误是
// -E_BAD_ENV 如果环境 envid 当前不存在,或者调用者没有权限更改 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.
	// 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


// 将 srcenvid 地址空间中位于'srcva'的内存页
// 映射到 dstenvid 地址空间中位于'dstva'的内存页,
// 并授予权限'perm'。
// Perm 与 sys_page_alloc 中的限制相同,
// 只是它也不能对只读页面授予写访问权限。
//
// 成功时返回 0,错误时返回 <0。 错误包括
// -E_BAD_ENV 如果 srcenvid 和/或 dstenvid 当前不存在,或者调用者没有权限更改其中一个。
// -E_INVAL 表示 srcva >= UTOP 或 srcva 未进行页面对齐,或 dstva >= UTOP 或 dstva 未进行页面对齐。
// -E_INVAL 表示 srcva 没有映射到 srcenvid 的地址空间。
// -E_INVAL 如果 perm 不合适(参见 sys_page_alloc)。
// -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.
	// 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;

	//如果srcva 在 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

// 在 “envid ”的地址空间中,解映射位于 “va ”的内存页。
// 如果没有映射到内存页,函数将自动成功。
//
// 成功时返回 0,错误时返回 <0。 错误是
// -E_BAD_ENV 如果环境 envid 当前不存在,或者调用者没有权限更改 envid。
// -E_INVAL 如果 va >= UTOP,或者 va 不是页面对齐的。
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;
}
补充syscall

完事之后一定记得在 kern/syscall.c : syscall 里补充上这几个 syscall:

image.png

否则在 make run-dumbfork 就会在用户进程进行syscall的时候失败:
image.png

修改好之后就正常了

posted @ 2023-12-08 10:47  toso  阅读(107)  评论(0编辑  收藏  举报