MIT 6.828 Lab04 : Preemptive Multitasking


在本实验中,我们将在多个同时活动的用户模式环境中实施抢占式多任务处理。

  • PartA:
    • 为 JOS 增添多处理器支持特性。
    • 实现 round-robin scheduling循环调度。
    • 添加一个基本的环境(进程)管理系统调用(创建和销毁环境,分配和映射内存)。
  • PartB:
    • 实现一个类Unix的fork(),其允许一个用户模式的环境能创建一份它自身的拷贝。
  • PartC:
    • 支持进程间通信(inter-process communication, IPC)
    • 支持硬件时钟中断和抢占

Part A:Multiprocessor Support and Cooperative Multitasking

🚩 不要搞混淆:multiprocessor support 是针对JOS 这个操作系统,让它可以支持多处理器,而不是说某个处理器管理其他处理器

Multiprocessor Support

我们将让 JOS 支持对称多处理器(symmetric multiprocessing,SMP),这是一种多处理器模型,其中所有CPU都具有对系统资源(如内存和I / O总线)的等效访问。虽然所有CPU在SMP中功能相同,但在引导过程中它们可分为两种类型:

  • 引导处理器(BSP):负责初始化系统和引导操作系统;
  • 应用程序处理器(AP):只有在操作系统启动并运行后,BSP才会激活应用程序处理器。

具体哪个处理器是BSP是由硬件和BIOS系统决定的。到目前为止,我们完成的JOS code都在BSP上运行。哪一个CPU是BSP由硬件和BISO决定,到目前位置所有JOS代码都运行在BSP上。
在SMP系统中,每个CPU都有一个对应的local APIC(LAPIC),负责传递中断。CPU通过内存映射IO(MMIO)访问它对应的APIC,这样就能通过访问内存达到访问设备寄存器的目的。LAPIC从物理地址0xFE000000开始,JOS将通过MMIOBASE虚拟地址访问该物理地址。

在SMP系统中,每个CPU都有一个附带的本地APIC(LAPIC)单元。

APIC:Advanced Programmable Interrupt Controller高级可编程中断控制器 。APIC 是装置的扩充组合用来驱动 Interrupt 控制器 [1] 。在目前的建置中,系统的每一个部份都是经由 APIC Bus 连接的。"本机 APIC" 为系统的一部份,负责传递 Interrupt 至指定的处理器;举例来说,当一台机器上有三个处理器则它必须相对的要有三个本机 APIC。自 1994 年的 Pentium P54c 开始Intel 已经将本机 APIC 建置在它们的处理器中。实际建置了 Intel 处理器的电脑就已经包含了 APIC 系统的部份。

LAPIC单元负责在整个系统中传递中断。 LAPIC还为其连接的CPU提供唯一标识符。 在本实验中,我们使用LAPIC单元的以下基本功能(在kern/lapic.c中):

  • 根据LAPIC识别码(APIC ID)区别我们的代码运行在哪个CPU上。(cpunum()
  • 从BSP向APs发送STARTUP处理器间中断(IPI)去唤醒其他的CPU。(lapic_startap()
  • 在Part C,我们编写LAPIC的内置定时器来触发时钟中断,以支持抢占式多任务(pic_init())。

LAPIC的 hole 开始于物理地址0xFE000000(4GB之下的32MB),但是这地址太高我们无法访问通过过去的直接映射(虚拟地址0xF0000000映射0x0,即只有256MB)。但是JOS虚拟地址映射预留了4MB空间在MMIOBASE处。

![]()
虚拟内存图
/*
 * Virtual memory map:                                Permissions
 *                                                    kernel/user
 *
 *    4 Gig -------->  +------------------------------+
 *                     |                              | RW/--
 *                     ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 *                     :              .               :
 *                     :              .               :
 *                     :              .               :
 *                     |~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~| RW/--
 *                     |                              | RW/--
 *                     |   Remapped Physical Memory   | RW/--
 *                     |                              | RW/--
 *    KERNBASE, ---->  +------------------------------+ 0xf0000000      --+
 *    KSTACKTOP        |     CPU0's Kernel Stack      | RW/--  KSTKSIZE   |
 *                     | - - - - - - - - - - - - - - -|                   |
 *                     |      Invalid Memory (*)      | --/--  KSTKGAP    |
 *                     +------------------------------+                   |
 *                     |     CPU1's Kernel Stack      | RW/--  KSTKSIZE   |
 *                     | - - - - - - - - - - - - - - -|                 PTSIZE
 *                     |      Invalid Memory (*)      | --/--  KSTKGAP    |
 *                     +------------------------------+                   |
 *                     :              .               :                   |
 *                     :              .               :                   |
 *    MMIOLIM ------>  +------------------------------+ 0xefc00000      --+
 *                     |       Memory-mapped I/O      | RW/--  PTSIZE
 * ULIM, MMIOBASE -->  +------------------------------+ 0xef800000
 *                     |  Cur. Page Table (User R-)   | R-/R-  PTSIZE
 *    UVPT      ---->  +------------------------------+ 0xef400000
 *                     |          RO PAGES            | R-/R-  PTSIZE
 *    UPAGES    ---->  +------------------------------+ 0xef000000
 *                     |           RO ENVS            | R-/R-  PTSIZE
 * UTOP,UENVS ------>  +------------------------------+ 0xeec00000
 * UXSTACKTOP -/       |     User Exception Stack     | RW/RW  PGSIZE
 *                     +------------------------------+ 0xeebff000
 *                     |       Empty Memory (*)       | --/--  PGSIZE
 *    USTACKTOP  --->  +------------------------------+ 0xeebfe000
 *                     |      Normal User Stack       | RW/RW  PGSIZE
 *                     +------------------------------+ 0xeebfd000
 *                     |                              |
 *                     |                              |
 *                     ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 *                     .                              .
 *                     .                              .
 *                     .                              .
 *                     |~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~|
 *                     |     Program Data & Heap      |
 *    UTEXT -------->  +------------------------------+ 0x00800000
 *    PFTEMP ------->  |       Empty Memory (*)       |        PTSIZE
 *                     |                              |
 *    UTEMP -------->  +------------------------------+ 0x00400000      --+
 *                     |       Empty Memory (*)       |                   |
 *                     | - - - - - - - - - - - - - - -|                   |
 *                     |  User STAB Data (optional)   |                 PTSIZE
 *    USTABDATA ---->  +------------------------------+ 0x00200000        |
 *                     |       Empty Memory (*)       |                   |
 *    0 ------------>  +------------------------------+                 --+

Exercise 01

	void *ret = (void *)base;
	size = ROUNDUP(size,PGSIZE); //非常关键的一步,传入的size不一定刚好是一页
	pa = ROUNDDOWN(pa,PGSIZE);
	
	if (base + size > MMIOLIM || base + size <base)
	{
	    panic ("mmio_map_region() overflow!\n");
	}
	
	boot_map_region(kern_pgdir,base,size,pa,PTE_W|PTE_PCD|PTE_PWT);
	base += size;//base 是static的,需要维护!
	return ret;//return 起始位置
}

Application Processor Bootstrap

在启动APs之前,BSP应该先收集关于多处理器系统的配置信息,比如CPU总数,CPUs的APIC ID,LAPIC单元的MMIO地址等。 kern/mpconfig.c中的mp_init()函数通过读取驻留在BIOS内存区域中的MP配置表来检索此信息。 也就是说在出厂时,厂家就将此计算机的处理器信息写入了BIOS中,其有一定的规范,也就是kern/mpconfig.cstruct mp定义的。

boot_aps()(在kern / init.c中)函数驱动了AP引导过程。 AP以实模式启动,非常类似于 bootloader 在boot/boot.S中启动的方式,因此boot_aps()将AP进入代码(kern / mpentry.S)复制到可在实模式下寻址的内存位置。与 bootloader 不同,我们可以控制 AP 开始执行代码的位置; 我们将 entry 代码复制到0x7000(MPENTRY_PADDR),(但其实任何未使用的,页面对齐的物理地址低于640KB都可以)。

之后,boot_aps函数通过发送STARTUP的IPI(处理器间中断)信号到AP的 LAPIC 单元来一个个地激活AP。在kern/mpentry.S中的入口代码跟boot/boot.S中的代码类似。在一些简短的配置后,它使AP进入开启分页机制的保护模式,调用C语言的setup函数mp_main。boot_aps 等待AP在其结构CpuInfo的cpu_status字段中发出CPU_STARTED标志信号,然后再唤醒下一个。

整理:

  1. i386_init() -->mp_init() 读取初始配置中的CPU信息 -->boot_aps() -->mp_main()
  2. AP 以实模式启动,kern/mpentry.S中的代码处理后,进入有分页机制的保护模式

Exercise 02

添加结果:

Question

  1. Compare kern/mpentry.S side by side with boot/boot.S. Bearing in mind that kern/mpentry.S is compiled and linked to run above KERNBASE just like everything else in the kernel, what is the purpose of macro MPBOOTPHYS? Why is it necessary in kern/mpentry.S but not in boot/boot.S? In other words, what could go wrong if it were omitted in kern/mpentry.S?
    Hint: recall the differences between the link address and the load address that we have discussed in Lab 1.

宏MPBOOTPHYS是为求得变量的物理地址,例如MPBOOTPHYS(gdtdesc)得到GDT的物理地址。

🚩 boot.S中,由于尚没有启用分页机制,所以我们能够指定程序开始执行的地方以及程序加载的地址;但是,在mpentry.S的时候,由于主CPU已经处于保护模式下了,因此是不能直接指定物理地址的,给定线性地址,映射到相应的物理地址是允许的。

Per-CPU State and Initialization

JOS使用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
};

cpunum()总是返回调用它的CPU的ID,宏thiscpu提供了更加方便的方式获取当前代码所在的CPU对应的CpuInfo结构。

// Maximum number of CPUs
#define NCPU  8

在多处理器OS中区分每个CPU私有和共享的处理器状态十分重要。

  • Per-CPU kernel stack.
  • Per-CPU TSS and TSS descriptor
  • Per-CPU current environment pointer
  • Per-CPU system registers.

**1. Per-CPU kernel stack **

避免互相干扰,需要为每个CPU都准备一个kernal stack,所以在内存中,仍然从KSTACKTOP往下,中间间隔一个KSTKGAP的距离

**2.Per-CPU TSS and TSS descriptor **

TSS和TSS描述符:每个CPU都需要单独的TSS和TSS描述符来指定该CPU对应的内核栈。

3. Per-CPU current environment pointer

进程结构指针:每个CPU都会独立运行一个进程的代码,所以需要Env指针。

4. Per-CPU system registers.

系统寄存器:比如cr3, gdt, ltr这些寄存器都是每个CPU私有的,每个CPU都需要单独设置。

Exercise 03

重新为多CPU 分为kernal stack

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 - i*(KSTKSIZE +KSTKGAP),KSTKSIZE,PADDR(percpu_kstacks[i]),PTE_W);
	}
}

Exercise 04

不是对所有的CPU进行init,实际上此时代码执行发生在不同的CPU上,只需要对自身CPU进行初始化即可。即使用thiscpu->cpu_ts代替全局变量 ts 。

	// LAB 4: Your code here:

        //用thiscpu->cpu_ts代替ts即可
        
	// Setup a TSS so that we get the right stack
	// when we trap to the kernel.
	thiscpu->cpu_ts.ts_esp0 = KSTACKTOP;
	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] = SEG16(STS_T32A, (uint32_t) (&thiscpu->cpu_ts),
					sizeof(struct Taskstate) - 1, 0);
	gdt[GD_TSS0 >> 3].sd_s = 0;

	// Load the TSS selector (like other segment selectors, the
	// bottom three bits are special; we leave them 0)
	ltr(GD_TSS0);

	// Load the IDT
	lidt(&idt_pd);
}

Locking

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

锁的数据结构在kern/spinlock.h中:

struct spinlock {
	unsigned locked;       // Is the lock held?
};

这是一种spin-locks。让我们来看看自旋锁的实现原理
我们最容易想到的获取自旋锁的代码如下:

21 void
22 acquire(struct spinlock *lk)
23 {
24    for(;;) {
25      if(!lk->locked) {
26          lk->locked = 1;
27          break;
28      }
29    }
30 }

但是这种实现是有问题的,假设两个CPU同时执行到25行,发现lk->locked是0,那么会同时获取该锁。问题出在25行和26行是两条指令。

我们的获取锁,释放锁的操作在kern/spinlock.c中:

void
spin_lock(struct spinlock *lk)
{
	// The xchg is atomic.
	// It also serializes, so that reads after acquire are not
	// reordered before it. 
	while (xchg(&lk->locked, 1) != 0)			//原理见:https://pdos.csail.mit.edu/6.828/2018/xv6/book-rev11.pdf  chapter 4
		asm volatile ("pause");
}

void
spin_unlock(struct spinlock *lk)
{
	// The xchg instruction is atomic (i.e. uses the "lock" prefix) with
	// respect to any other instruction which references the same memory.
	// x86 CPUs will not reorder loads/stores across locked instructions
	// (vol 3, 8.2.2). Because xchg() is implemented using asm volatile,
	// gcc will not reorder C statements across the xchg.
	xchg(&lk->locked, 0);
}


static inline uint32_t
xchg(volatile uint32_t *addr, uint32_t newval)
{
    uint32_t result;
    // The + in "+m" denotes a read-modify-write operand.
    asm volatile("lock; xchgl %0, %1"
         : "+m" (*addr), "=a" (result)
         : "1" (newval)
         : "cc");
    return result;
}

对于spin_lock()获取锁的操作,使用xchgl这个原子指令,xchg()封装了该指令,交换lk->locked和1的值,并将lk-locked原来的值返回。如果lk-locked原来的值不等于0,说明该锁已经被别的CPU申请了,继续执行while循环吧。因为这里使用的xchgl指令,从addr指向的位置读数据保存到result,然后将newval写到该位置,但是原子的,相当于之前25和26行的结合,所以也就不会出现上述的问题。对于spin_unlock()释放锁的操作,直接将lk->locked置为0,表明我已经用完了,这个锁可以被别人获取了。
至于为什么spin_lock()的while循环中,需要加asm volatile ("pause");?可以参考
https://c9x.me/x86/html/file_module_x86_id_232.html, pause指令相当于一个带延迟的noop指令(that is, it performs essentially a delaying noop operation),主要是为了减少能耗。

还有另一类称作sleep lock的锁类型。例如在一个双核的机器上有两个线程(线程A和线程B),它们分别运行在CPU 1和CPU 2上。假设线程A想要某个sleep lock,而此时这个锁正被线程B所持有,那么线程A就会被阻塞(blocking),CPU1 会在此时进行上下文切换将线程A置于等待队列中,此时CPU 1就可以运行其他的任务(例如另一个线程C)而不必进行忙等待。而spin lock则不是,如果线程A获取spin lock,那么线程A就会一直在 CPU 1上进行忙等待并不停的进行锁请求,直到得到这个锁为止。
jos中没有实现sleep lock。

有了获取锁和释放锁的函数,我们看下哪些地方需要加锁,和释放锁:

  1. i386_init()中,BSP唤醒其它AP前需要获取内核锁。
  2. mp_main()中,AP需要在执行sched_yield()前获取内核锁。
  3. trap()中,需要获取内核锁,因为这是用户态进入内核的唯一入口。
  4. env_run()中,需要释放内核锁,因为该函数使用iret指令,从内核返回用户态。

Exercise 05

// i386_init()
// Your code here:
lock_kernel();
boot_aps();

// mp_main()
// Your code here:
lock_kernel();
sched_yield();

// trap()
// LAB 4: Your code here.
lock_kernel();
assert(curenv); 

// env_run()
lcr3(PADDR(e->env_pgdir));
unlock_kernel();
env_pop_tf(&(e->env_tf));

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.
  1. 因为在_alltraps到 lock_kernel()的过程中,进程已经切换到了内核态,但并没有上内核锁,此时如果有其他CPU进入内核,如果用同一个内核栈,则_alltraps中保存的上下文信息会被破坏,所以即使有大内核栈,CPU也不能用用同一个内核栈。同样的,解锁也是在内核态内解锁,在解锁到真正返回用户态这段过程中,也存在上述这种情况。fang92

  2. 如果内核栈中留下不同 CPU 之后需要使用的数据,可能会造成混乱

Round-Robin Scheduling (RR轮转式)

现要JOS内核需要让CPU能在进程之间切换。目前先实现一个非抢占式的进程调度,需要当前进程主动让出CPU,其他进程才有机会在当前CPU运行。具体实现如下:

  1. 实现sched_yield(),该函数选择一个新的进程运行,从当前正在运行进程对应的Env结构下一个位置开始循环搜索envs数组,找到第一个cpu_status为ENV_RUNNABLE的Env结构,然后调用env_run()在当前CPU运行这个新的进程。
  2. 我们需要实现一个新的系统调用sys_yield(),使得用户程序能在用户态通知内核,当前进程希望主动让出CPU给另一个进程。

Exercise 06

	// LAB 4: Your code here.
	int start = 0;
	int j;
	if (curenv) {
		start = ENVX(curenv->env_id) + 1;	//从当前Env结构的后一个开始
	}
	for (int i = 0; i < NENV; i++) {		//遍历所有Env结构
		j = (start + i) % NENV;
		if (envs[j].env_status == ENV_RUNNABLE) {
			env_run(&envs[j]);
		}
	}
	if (curenv && curenv->env_status == ENV_RUNNING) {		//这是必须的,假设当前只有一个Env,如果没有这个判断,那么这个CPU将会停机
		env_run(curenv);
	}
	// sched_halt never returns
	sched_halt();
debug到深夜

写完excercise6之后,始终提示有kern/env.c 处有env_creat() error !

一开始茫然无措,之前运气好也没有出过太多的bug,有bug也就再检查一遍代码就可以发现一些显而易见的低级错误。但这次我反复看了很久也没看出问题。。然后突然想到之前内核里已经加了backtrace命令,可以查看栈里信息和对应函数,欣喜若狂! 一连串查出2个大错误:

  1. kern/env.c 中的env_init()函数中env_free_list 一直是NULL,链接方法有问题
  1. kern/trapentry.S 中的 _alltraps 写错了!!导致一直报错
成功!

Question

  1. In your implementation of env_run() you should have called lcr3(). Before and after the call to lcr3(), your code makes references (at least it should) to the variable e, the argument to env_run. Upon loading the %cr3 register, the addressing context used by the MMU is instantly changed. But a virtual address (namely e) has meaning relative to a given address context–the address context specifies the physical address to which the virtual address maps. Why can the pointer e be dereferenced both before and after the addressing switch?

因为当前是运行在系统内核中的,而每个进程的页表中都是存在内核映射的。每个进程页表中虚拟地址高于UTOP之上的地方,只有UVPT不一样,其余的都是一样的,只不过在用户态下是看不到的。所以虽然这个时候的页表换成了下一个要运行的进程的页表,但是映射也没变,还是依然有效的。

  1. Whenever the kernel switches from one environment to another, it must ensure the old environment’s registers are saved so they can be restored properly later. Why? Where does this happen?

因为不保存下来就无法正确地恢复到原来的环境。用户进程之间的切换,会调用系统调用sched_yield();用户态陷入到内核态,可以通过中断、异常、系统调用;这样的切换之处都是要在系统栈上建立用户态的TrapFrame,在进入trap()函数后,语句curenv->env_tf = *tf;将内核栈上需要保存的寄存器的状态实际保存在用户环境的env_tf域中。

System Calls for Environment Creation

尽管现在内核有能力在多进程之前切换,但是仅限于内核创建的用户进程。目前JOS还没有提供系统调用,使用户进程能创建新的进程。
Unix提供fork()系统调用创建新的进程,fork()拷贝父进程的地址空间和寄存器状态到子进程。父进程从fork()返回的是子进程的进程ID,而子进程从fork()返回的是0。父进程和子进程有独立的地址空间,任何一方修改了内存,不会影响到另一方。

本小节需要实现如下系统调用:

  1. sys_exofork():
    创建一个新的进程,用户地址空间没有映射,不能运行,寄存器状态和父环境一致。在父进程中sys_exofork()返回新进程的envid,子进程返回0。

  2. sys_env_set_status:

    设置一个特定进程的状态为ENV_RUNNABLE或ENV_NOT_RUNNABLE。

  3. sys_page_alloc:

    为特定进程分配一个物理页,映射指定线性地址va到该物理页。

  4. sys_page_map:

    拷贝页表,使指定进程共享当前进程相同的映射关系。本质上是修改特定进程的页目录和页表。共享同样的地址空间,而不是拷贝page的内容!

  5. sys_page_unmap:

    解除页映射关系。

Exercise 07

  • 补充5个系统调用函数

sys_exofork(void):

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.
	struct Env *e;
	int ret = env_alloc(&e, curenv->env_id);    //分配一个Env结构
	if (ret < 0) {
		return ret;
	}
	e->env_tf = curenv->env_tf;			//寄存器状态和当前进程一致
	e->env_status = ENV_NOT_RUNNABLE;   //目前还不能运行
	e->env_tf.tf_regs.reg_eax = 0;		//新的进程从sys_exofork()的返回值应该为0
	return e->env_id;
}

sys_env_set_status(envid_t envid, int status):

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.
	if (status != ENV_NOT_RUNNABLE && status != ENV_RUNNABLE) return -E_INVAL;

	struct Env *e;
	int ret = envid2env(envid, &e, 1);
	if (ret < 0) {
		return ret;
	}
	e->env_status = status;
	return 0;
}

sys_page_alloc(envid_t envid, void *va, int perm):

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.
	struct Env *e; 									//根据envid找出需要操作的Env结构
	int ret = envid2env(envid, &e, 1);
	if (ret) return ret;	//bad_env

	if ((va >= (void*)UTOP) || (ROUNDDOWN(va, PGSIZE) != va)) return -E_INVAL;		//一系列判定
	int flag = PTE_U | PTE_P;
	if ((perm & flag) != flag) return -E_INVAL;

	struct PageInfo *pg = page_alloc(1);			//分配物理页
	if (!pg) return -E_NO_MEM;
	ret = page_insert(e->env_pgdir, pg, va, perm);	//建立映射关系
	if (ret) {
		page_free(pg);
		return ret;
	}

	return 0;
}

sys_page_map(envid_t srcenvid, void *srcva,envid_t dstenvid, void *dstva, int perm):

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.
	struct Env *se, *de;
	int ret = envid2env(srcenvid, &se, 1);
	if (ret) return ret;	//bad_env
	ret = envid2env(dstenvid, &de, 1);
	if (ret) return ret;	//bad_env

	//	-E_INVAL if srcva >= UTOP or srcva is not page-aligned,
	//		or dstva >= UTOP or dstva is not page-aligned.
	if (srcva >= (void*)UTOP || dstva >= (void*)UTOP || 
		ROUNDDOWN(srcva,PGSIZE) != srcva || ROUNDDOWN(dstva,PGSIZE) != dstva) 
		return -E_INVAL;

	//	-E_INVAL is srcva is not mapped in srcenvid's address space.
	pte_t *pte;
	struct PageInfo *pg = page_lookup(se->env_pgdir, srcva, &pte);
	if (!pg) return -E_INVAL;

	//	-E_INVAL if perm is inappropriate (see sys_page_alloc).
	int flag = PTE_U|PTE_P;
	if ((perm & flag) != flag) return -E_INVAL;

	//	-E_INVAL if (perm & PTE_W), but srcva is read-only in srcenvid's
	//		address space.
	if (((*pte&PTE_W) == 0) && (perm&PTE_W)) return -E_INVAL;

	//	-E_NO_MEM if there's no memory to allocate any necessary page tables.
	ret = page_insert(de->env_pgdir, pg, dstva, perm);
	return ret;

}

sys_page_unmap(envid_t envid, void *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.
	struct Env *env;
	int ret = envid2env(envid, &env, 1);
	if (ret) return ret;

	if ((va >= (void*)UTOP) || (ROUNDDOWN(va, PGSIZE) != va)) return -E_INVAL;
	page_remove(env->env_pgdir, va);
	return 0;
}
  • 再syscall.c里面的syscall()函数补充上述系统调用
...
		case SYS_page_alloc:
    			ret = sys_page_alloc(a1, (void *) a2, a3);
    			break;
		case SYS_page_map:
    			ret = sys_page_map(a1, (void *) a2, a3, (void *) a4, a5);
    			break;
		case SYS_page_unmap:
    			ret = sys_page_unmap(a1, (void *) a2);
    			break;
		case SYS_exofork:
    			ret sys_exofork();
    			break;
		case SYS_env_set_status:
    			ret sys_env_set_status(a1, a2);
    			break;
...

检验:

kern/init.c中 ENV_CREATE(user_dumbfork, ENV_TYPE_USER); 成功输出,且parent 进程exit after 10 iterations, while child exits after 20.

Part A 成功!

Part A 小结

多任务并行,需要由多处理器来支持。如何由引导处理器(BSP)加载应用处理器(APs)是PartA的一个重要环节。应用处理器的启动代码与BSP的启动代码最大的一个区别是:此时的BSP工作在保护模式,以虚拟地址的形式进行寻址,在启动APs时需要由物理地址变换为虚拟地址来加载页目录等操作。启动多处理器后,需要记录各个CPU的Info。因为不能让多个CPU同时进入内核,因此很重要的一点是实现内核的互斥访问。内核互斥与循环调度很容易理解,这个Part最难的一部分在于fork system call 的实现,fork实现了用户环境创建新的用户进程,以区别于之前只是在内建环境之间切换。 fork()的实现,创建一个环境并且进行环境复制(tf),以至于孩子进程也像调用了sys_exofork,并且其返回0(从而可以区分父子进程)

Part B: Copy-on-Write Fork (写时复制Fork)

实现fork()有多种方式,一种是将父进程的内容全部拷贝一次,这样的话父进程和子进程就能做到进程隔离,但是这种方式的缺点在于耗时,且不一定有用。因为fork()函数后面大概率会紧跟在子进程中调用exec()函数,替代原来子进程的内存空间为新的程序。所以如果fork时即复制,那么有很大概率白费功夫,因为在fork() 和 exec() 之前需要用mem的情况很少。

另一种方式叫做Copy-on-Write Fork,父进程将自己的页目录和页表复制给子进程,并同时将shared-pages 改为 read-only。这样父进程和子进程就能访问相同的内容。只有当一方执行写操作时,发生 page fault,然后生成新的可写的page进行复制这一页。这样既能做到地址空间隔离,又能节省了大量的拷贝工作——很可能fork()后紧跟exec()的进程只需要copy 1 页(current page of the stack)。

想要实现写时拷贝的fork()需要先实现用户级别的缺页中断处理函数,to know about page faults on write-proteced pages.

User-level page fault handling

与传统的Unix方法不同,我们将决定如何处理用户空间中的每一个页面错误,其中bug的破坏性更小。这种设计的另一个好处是允许程序在定义其内存区域时具有极大的灵活性;稍后将使用用户级页面错误处理来映射和访问基于磁盘的文件系统上的文件。

为了处理自己的page faults,用户程序需要向JOS Kernel —— register a page fault handler entrypoint. 到这里代码中为Env新加了一个属性:env_pgfault_upcall来记录注册信息。

struct Env 更新
struct Env {
	struct Trapframe env_tf;	// Saved registers
	struct Env *env_link;		// Next free Env
	envid_t env_id;			// Unique environment identifier
	envid_t env_parent_id;		// env_id of this env's parent
	enum EnvType env_type;		// Indicates special system environments
	unsigned env_status;		// Status of the environment
	uint32_t env_runs;		// Number of times environment has run
	int env_cpunum;			// The CPU that the env is running on

	// Address space
	pde_t *env_pgdir;		// Kernel virtual address of page dir

	// Exception handling
	void *env_pgfault_upcall;	// Page fault upcall entry point

	// Lab 4 IPC
	bool env_ipc_recving;		// Env is blocked receiving
	void *env_ipc_dstva;		// VA at which to map received page
	uint32_t env_ipc_value;		// Data value sent to us
	envid_t env_ipc_from;		// envid of the sender
	int env_ipc_perm;		// Perm of page mapping received
};

Exercise 08

实现sys_env_set_pgfault_upcall(envid_t envid, void *func)系统调用。该系统调用为指定的用户环境设置env_pgfault_upcall。缺页中断发生时,会执行env_pgfault_upcall指定位置的代码。当执行env_pgfault_upcall指定位置的代码时,栈已经转到异常栈,并且压入了UTrapframe结构。

static int
sys_env_set_pgfault_upcall(envid_t envid, void *func)
{
	// LAB 4: Your code here.
	struct Env *env;
	int ret;
	if ((ret = envid2env(envid, &env, 1)) < 0) {
		return ret;
	}
	env->env_pgfault_upcall = func;
	return 0;
}

Normal and Exception Stacks in User Environments

当缺页中断发生时,内核会返回用户模式来处理该中断。我们需要一个用户异常栈,来模拟内核异常栈。JOS的用户异常栈被定义在虚拟地址UXSTACKTOP。

到目前为止出现了三个栈:

  [KSTACKTOP-KSTKSIZE,  KSTACKTOP) 
  内核态系统栈

  [UXSTACKTOP - PGSIZE, UXSTACKTOP )
  用户态错误处理栈

  [UTEXT, USTACKTOP)
  用户态运行栈

内核态系统栈是运行内核相关程序的栈,在有中断被触发之后,CPU会将栈自动切换到内核栈上来,而内核栈是在kern/trap.c的trap_init_percpu()中设置的。

Invoking the User Page Fault Handler

缺页中断发生时会进入内核的trap(),然后分配page_fault_handler()来处理缺页中断。在该函数中应该做如下几件事:

  1. 判断curenv->env_pgfault_upcall是否设置,如果没有设置也就没办法修复,直接销毁该进程。
  2. 修改esp,切换到用户异常栈。
  3. 在栈上压入一个UTrapframe结构。
  4. 将eip设置为curenv->env_pgfault_upcall,然后回到用户态执行curenv->env_pgfault_upcall处的代码。
UTrapframe 结构
inc/trap.h 中的 UTrapframe

struct UTrapframe {
	/* information about the fault */
	uint32_t utf_fault_va;	/* va for T_PGFLT, 0 otherwise */
	uint32_t utf_err;
	/* trap-time return state */
	struct PushRegs utf_regs;
	uintptr_t utf_eip;
	uint32_t utf_eflags;
	/* the trap-time stack to return to */
	uintptr_t utf_esp;
} __attribute__((packed));

对应的栈结构:(注意还是倒叙压入!)

                    <-- UXSTACKTOP
trap-time esp
trap-time eflags
trap-time eip
trap-time eax       start of struct PushRegs
trap-time ecx
trap-time edx
trap-time ebx
trap-time esp
trap-time ebp
trap-time esi
trap-time edi       end of struct PushRegs
tf_err (error code)
fault_va            <-- %esp when handler is run

Exercise 09

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 in kernel mode, fault address %d\n", fault_va);
	}

// LAB 4: Your code here.
	if (curenv->env_pgfault_upcall) 
	{
		uintptr_t stacktop = UXSTACKTOP;
		if (UXSTACKTOP - PGSIZE < tf->tf_esp && tf->tf_esp < UXSTACKTOP) 
		{
			stacktop = tf->tf_esp;
		}
		uint32_t size = sizeof(struct UTrapframe) + sizeof(uint32_t);
		user_mem_assert(curenv, (void *)stacktop - size, size, PTE_U | PTE_W);
		struct UTrapframe *utr = (struct UTrapframe *)(stacktop - size);//在栈上压入一个UTrapframe结构
		utr->utf_fault_va = fault_va;
		utr->utf_err = tf->tf_err;
		utr->utf_regs = tf->tf_regs;
		utr->utf_eip = tf->tf_eip;
		utr->utf_eflags = tf->tf_eflags;
		utr->utf_esp = tf->tf_esp;				//UXSTACKTOP栈上需要保存发生缺页异常时的%esp和%eip

		curenv->env_tf.tf_eip = (uintptr_t)curenv->env_pgfault_upcall;  //将eip置为curenv->env_pgfault_upcall,然后回到用户态执行curenv->env_pgfault_upcall处的代码
		curenv->env_tf.tf_esp = (uintptr_t)utr;  //修改esp 切换到用户异常栈
		env_run(curenv);			//重新进入用户态
	}

User-mode Page Fault Entrypoint

现在需要实现lib/pfentry.S中的_pgfault_upcall函数,该函数会作为系统调用sys_env_set_pgfault_upcall()的参数。

Exercise 10

实现lib/pfentry.S中的_pgfault_upcall函数。

⏰: 需要对照着UTrapframe 来写;

同时,这里的数字加减单位是字节(8位),例如utf_fault_va是 uint32_t,那么就是4个字节32位

.text
.globl _pgfault_upcall
_pgfault_upcall:
	// Call the C page fault handler.
	pushl %esp			// function argument: pointer to UTF  //esp:栈顶指针
	movl _pgfault_handler, %eax    
	call *%eax                    //调用页处理函数
	addl $4, %esp			// pop function argument

	// LAB 4: Your code here.
	
	//需要对照着UTrapframe 来写
	//这里的数字加减单位是字节(8位),例如utf_fault_va是 uint32_t,那么就是4个字节32位
	addl $8, %esp			//esp+8 -> PushRegs,跳过utf_fault_va和utf_err,esp是栈顶,把栈顶往上减少
	movl 40(%esp), %eax 	//保存中断发生时的esp到eax   //都需要对照uft的堆栈结构
	movl 32(%esp), %ecx 	//保存终端发生时的eip到ecx,因为esp+0x20 即是 utf_eip
	movl %ecx, -4(%eax) 	//将中断发生时的esp值压入到到原来的栈中
	popal
	addl $4, %esp			//跳过eip

	// Restore the trap-time registers.  After you do this, you
	// can no longer modify any general-purpose registers.
	// LAB 4: Your code here.
	
	

	// 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.
	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.
	lea -4(%esp), %esp		//因为之前压入了eip的值但是没有减esp的值,所以现在需要将esp寄存器中的值减4
	ret

Exercise 11

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");
		int r = sys_page_alloc(0, (void *)(UXSTACKTOP-PGSIZE), PTE_W | PTE_U | PTE_P);	//为当前进程分配异常栈
		if (r < 0) 
		{
			panic("set_pgfault_handler:sys_page_alloc failed!\n");
		}
		sys_env_set_pgfault_upcall(0, _pgfault_upcall);		//系统调用,设置进程的env_pgfault_upcall属性
	}
	

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

测试+debug

  1. Run user/faultread (make run-faultread). ✔
  1. Run user/faultdie. (❌)

一开始是失败的!错误提示信息如下所示:

  1. Run user/faultalloc. (❌)

一开始是失败的!错误提示信息如下所示:

这个跟书上的要求不太一样

  1. Run user/faultallocbad. ✔
Debug

经过backtrace+海王搜索法,找了快半个小时,终于找到原来是syscall.c里面没有给 SYS_env_set_pgfault_upcall添加到switch中进行调度!!!

加上之后:

  • Run user/faultdie.
  • Run user/faultalloc.
make grade

image-20200813091620689

缺页处理小结:

  1. 引发缺页中断,执行内核函数链:trap()->trap_dispatch()->page_fault_handler()
  2. page_fault_handler()切换栈到用户异常栈,并且压入UTrapframe结构,然后调用curenv->env_pgfault_upcall(系统调用sys_env_set_pgfault_upcall()设置)处代码。又重新回到用户态。
  3. 进入_pgfault_upcall处的代码执行,调用_pgfault_handler(库函数set_pgfault_handler()设置)处的代码,最后返回到缺页中断发生时的那条指令重新执行。

Implementing Copy-on-Write Fork

到目前已经可以实现用户级别的写时拷贝fork函数了。fork流程如下:

  1. 使用set_pgfault_handler()设置缺页处理函数_pgfault_handler = handler

  2. 调用sys_exofork()系统调用,在内核中创建一个Env结构,复制当前用户环境寄存器状态,UTOP以下的页目录还没有建立,新创建的进程还不能直接运行。

  3. 拷贝父进程的页表和页目录到子进程。对于可写的页,将对应的PTE的PTE_COW位设置为1。

  4. 为子进程设置_pgfault_upcall。

    sys_env_set_pgfault_upcall()中 为env->env_pgfault_upcall 设置了pgfault的时候的处理函数func

  5. 将子进程状态设置为ENV_RUNNABLE。

[Note] 此处的顺序十分重要——父进程在子进程之后对 COW 页进行 mark 。

缺页处理函数pgfault()流程如下:

  1. 如果发现错误是因为写造成的(错误码是FEC_WR)并且该页的PTE_COW是1,则进行执行第2步,否则直接panic。
  2. 分配一个新的物理页,并将之前出现错误的页的内容拷贝到新的物理页,然后重新映射线性地址到新的物理页。

Clever mapping tricks

官网材料:https://pdos.csail.mit.edu/6.828/2018/labs/lab4/uvpt.html

简而言之,这个trick的巧妙之处在于,我们定义page directory, page table,page frame的时候本能的想到了下图:

img

但是他们本质上都是一串格式相同的数字罢了,谁规定的page directory只能是page directory了!如果我们将指针放入页面目录,该指针指向索引 V (page directory)的自身,那么根据页目录查询情况:kern_pgdir[PDX(kern_pgdir)]寻找到的页面(这里的角色是page table)仍然是V指向的页面。

当我们尝试翻译一个虚拟地址与PDX和PTX等于V,以下三个箭头离开我们在页面目录。因此,该虚拟页面将转换为包含页面目录的页面。在JOS,V 为 0x3BD,因此 UVPD 的虚拟地址为 (0x3BD<<<22)|(0x3BD<<12)

现在,如果我们尝试使用 PDX = V 转换虚拟地址,但使用任意的 PTX!= V 转换,则从 CR3 中跟随三个箭头将比平常一个级别向上一个级别(而不是上一例中的两个级别),这就是在页表中。因此,PDX+V 的虚拟页面集形成了一个 4MB 区域,其页面内容(就处理器而言)是页面表本身。在 Jos 中,V 为 0x3BD,因此 UVPT 的虚拟地址为 (0x3BD<<<22)。

🚩uvpt[pagenumber]

可以直接访问第pagenumber项页表条目。

Exercise 12

特别注意:

duppage()中复制的可能情况:

  • 对于表示为PTE_SHARE的页,拷贝映射关系,并且两个进程都有读写权限
  • 对于UTOP以下的可写的或者写时拷贝的页,拷贝映射关系的同时,需要同时标记当前进程和子进程的页表项为PTE_COW
  • 对于只读的页,只需要拷贝映射关系即可
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) && (uvpt[PGNUM(addr)] & PTE_COW))) { //只有因为写操作写时拷贝的地址这种情况,才可以抢救。否则一律panic
		panic("pgfault():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.
	addr = ROUNDDOWN(addr, PGSIZE);
	if ((r = sys_page_map(0, addr, 0, PFTEMP, PTE_U|PTE_P)) < 0)		//将当前进程PFTEMP也映射到当前进程addr指向的物理页
		panic("sys_page_map: %e", r);
	if ((r = sys_page_alloc(0, addr, PTE_P|PTE_U|PTE_W)) < 0)	//令当前进程addr指向新分配的物理页
		panic("sys_page_alloc: %e", r);
	memmove(addr, PFTEMP, PGSIZE);								//将PFTEMP指向的物理页拷贝到addr指向的物理页
	if ((r = sys_page_unmap(0, PFTEMP)) < 0)					//解除当前进程PFTEMP映射
		panic("sys_page_unmap: %e", r);
}

static int
duppage(envid_t envid, unsigned pn)
{
	int r;

	// LAB 4: Your code here.
 	void *addr = (void*) (pn * PGSIZE);
	if ((uvpt[pn] & PTE_W) || (uvpt[pn] & PTE_COW)) { //对于UTOP以下的可写的或者写时拷贝的页,拷贝映射关系的同时,需要同时标记当前进程和子进程的页表项为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;
}

envid_t
fork(void)
{
	// LAB 4: Your code here.
	extern void _pgfault_upcall(void);
	set_pgfault_handler(pgfault);	//设置缺页处理函数
	envid_t envid = sys_exofork();	//系统调用,只是简单创建一个Env结构,复制当前用户环境寄存器状态,UTOP以下的页目录还没有建立
	if (envid == 0) {				//子进程将走这个逻辑
		thisenv = &envs[ENVX(sys_getenvid())];
		return 0;
	}
	if (envid < 0) {
		panic("sys_exofork: %e", envid);
	}

	uint32_t addr;
	for (addr = 0; addr < USTACKTOP; addr += PGSIZE) {
		if ((uvpd[PDX(addr)] & PTE_P) && (uvpt[PGNUM(addr)] & PTE_P) //为什么uvpt[pagenumber]能访问到第pagenumber项页表条目:https://pdos.csail.mit.edu/6.828/2018/labs/lab4/uvpt.html
			&& (uvpt[PGNUM(addr)] & PTE_U)) {
			duppage(envid, PGNUM(addr));	//拷贝当前进程映射关系到子进程
		}
	}
	int r;
	if ((r = sys_page_alloc(envid, (void *)(UXSTACKTOP-PGSIZE), PTE_P | PTE_W | PTE_U)) < 0)	//为子进程分配异常栈
		panic("sys_page_alloc: %e", r);
	sys_env_set_pgfault_upcall(envid, _pgfault_upcall);		//为子进程设置_pgfault_upcall

	if ((r = sys_env_set_status(envid, ENV_RUNNABLE)) < 0)	//设置子进程为ENV_RUNNABLE状态
		panic("sys_env_set_status: %e", r);
	return envid;
}

Part C: Preemptive Multitasking and Inter-Process communication (IPC)

Interrupt discipline

目前程序一旦进入用户模式,除非发生中断,否则CPU永远不会再执行内核代码。为了避免CPU资源被恶意抢占,需要开启时钟中断,强迫进入内核,然后内核就可以切换另一个进程执行。

外部中断(即,设备中断)被称为IRQs。有16个可能的IRQ,编号为0到15。从IRQ编号到IDT条目的映射不是固定的。picirq.c中的pic_init通过IRQ_OFFSET+15将IRQ 0-15映射到IDT条目IRQ_OFFSET。

External interrupts are controlled by the FL_IF flag bit of the %eflags register (see inc/mmu.h).外部中断由%eflags 的FL_IF flag位控制。设置了该位,外部中断启用。 bootloader的第一条指令是屏蔽外部中断,不签位置还没有开启中断。

Exercise 13

修改kern/trapentry.S和kern/trap.c以初始化IDT中的相应条目,并为IRQ 0到15提供处理程序。然后修改kern/env.c中env_alloc()中的代码,以确保用户环境始终在启用中断的情况下运行.还要取消sched_halt()中sti指令的注释,以便空闲CPU取消掩码中断。

  1. 修改Trapentry.s,当调用硬件中断处理时,处理器不会传入错误代码,因此我们需要调用TRAPHANDLER_NOEC宏。

  2. 修改trap.c, 注册IDT。

  3. IDT表项中的每一项都初始化为中断门,这样在发生任何中断/异常的时候,陷入内核态的时候,CPU都会将%eflags寄存器上的FL_IF标志位清0,关闭中断;切换回用户态的时候,CPU将内核栈中保存的%eflags寄存器弹回%eflags寄存器,恢复原来的状态。You will have to ensure that the FL_IF flag is set in user environments when they run so that when an interrupt arrives, it gets passed through to the processor and handled by your interrupt code.

  4. 🚩 易错!!在env_allco中加入以下代码, 同时取消 sched_halt()sti的注释,使能中断。

    //STI 置中断允许位.

Handling Clock Interrupts

lapic_init()和pic_init()设置时钟中断控制器产生中断。需要写代码来处理中断。

具体地:lapic_init设定中断号,设置时钟以及中断控制器生成中断等。 pic_init初始化 8259A 中断控制器。(但还是不太能理解具体干什么)

Exercise14

// Handle clock interrupts. Don't forget to acknowledge the
// interrupt using lapic_eoi() before calling the scheduler!
// LAB 4: Your code here.
if(IRO_OFFSET + IRO_TIMER)
{
    // 回应8259A CPU已经接收中断。
    //处理始终中断
        lapic_eoi();
        sched_yield();
        return;
}

Inter-Process communication (IPC)

到目前为止,我们都在做隔离的事情。操作系统另一个重要的内容是允许程序相互交流。

IPC in JOS

我们将要实现sys_ipc_recv()和sys_ipc_try_send()这两个系统调用,来实现进程间通信。并且实现两个包装函数ipc_recv()和 ipc_send()。
JOS中进程间通信的“消息”包含两部分:

  1. 一个32位的值。
  2. 可选的页映射关系。

Sending and Receiving Messages

sys_ipc_recv()和sys_ipc_try_send()是这么协作的:

  1. 当某个进程调用sys_ipc_recv()后,该进程会阻塞(状态被置为ENV_NOT_RUNNABLE),直到另一个进程向它发送“消息”。当进程调用sys_ipc_recv()传入dstva参数时,表明当前进程准备接收页映射。
  2. 进程可以调用sys_ipc_try_send()向指定的进程发送“消息”,如果目标进程已经调用了sys_ipc_recv(),那么就发送数据,然后返回0,否则返回-E_IPC_NOT_RECV,表示目标进程不希望接受数据。当传入srcva参数时,表明发送进程希望和接收进程共享srcva对应的物理页。如果发送成功了发送进程的srcva和接收进程的dstva将指向相同的物理页。

Exercise 15

实现sys_ipc_recv()和sys_ipc_try_send()。包装函数ipc_recv()和 ipc_send()。

static int
sys_ipc_try_send(envid_t envid, uint32_t value, void *srcva, unsigned perm)
{
	// LAB 4: Your code here.
	struct Env *rcvenv;
	int ret = envid2env(envid, &rcvenv, 0);
	if (ret) return ret;
	if (!rcvenv->env_ipc_recving) return -E_IPC_NOT_RECV;

	if (srcva < (void*)UTOP) {
		pte_t *pte;
		struct PageInfo *pg = page_lookup(curenv->env_pgdir, srcva, &pte);

		//按照注释的顺序进行判定
		if (debug) {
			cprintf("sys_ipc_try_send():srcva=%08x\n", (uintptr_t)srcva);
		}
		if (srcva != ROUNDDOWN(srcva, PGSIZE)) {		//srcva没有页对齐
			if (debug) {
				cprintf("sys_ipc_try_send():srcva is not page-alligned\n");
			}
			return -E_INVAL;
		}
		if ((*pte & perm & 7) != (perm & 7)) {  //perm应该是*pte的子集
			if (debug) {
				cprintf("sys_ipc_try_send():perm is wrong\n");
			}
			return -E_INVAL;
		}
		if (!pg) {			//srcva还没有映射到物理页
			if (debug) {
				cprintf("sys_ipc_try_send():srcva is not maped\n");
			}
			return -E_INVAL;
		}
		if ((perm & PTE_W) && !(*pte & PTE_W)) {	//写权限
			if (debug) {
				cprintf("sys_ipc_try_send():*pte do not have PTE_W, but perm have\n");
			}
			return -E_INVAL;
		}		
		
		if (rcvenv->env_ipc_dstva < (void*)UTOP) {
			ret = page_insert(rcvenv->env_pgdir, pg, rcvenv->env_ipc_dstva, perm); //共享相同的映射关系
			if (ret) return ret;
			rcvenv->env_ipc_perm = perm;
		}
	}
	rcvenv->env_ipc_recving = 0;					//标记接受进程可再次接受信息
	rcvenv->env_ipc_from = curenv->env_id;
	rcvenv->env_ipc_value = value; 
	rcvenv->env_status = ENV_RUNNABLE;
	rcvenv->env_tf.tf_regs.reg_eax = 0;
	return 0;
}

static int
sys_ipc_recv(void *dstva)
{
	// LAB 4: Your code here.
	if (dstva < (void *)UTOP && dstva != ROUNDDOWN(dstva, PGSIZE)) {
		return -E_INVAL;
	}
	curenv->env_ipc_recving = 1;
	curenv->env_status = ENV_NOT_RUNNABLE;
	curenv->env_ipc_dstva = dstva;
	sys_yield();
	return 0;
}
int32_t
ipc_recv(envid_t *from_env_store, void *pg, int *perm_store)
{
	// LAB 4: Your code here.
	if (pg == NULL) {
		pg = (void *)-1;
	}
	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;
	}
	if (from_env_store)
		*from_env_store = thisenv->env_ipc_from;
	if (perm_store)
		*perm_store = thisenv->env_ipc_perm;
	return thisenv->env_ipc_value;
}

void
ipc_send(envid_t to_env, uint32_t val, void *pg, int perm)
{
	// LAB 4: Your code here.
	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():%e", r);
		}
	}
}

<Oh NO!!怎么会这样子!!>

认真寻找又是两三个小时后,,,发现是lib/duppage()写错了!😭

错误版

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);		//对于表示为PTE_SHARE的页,拷贝映射关系,并且两个进程都有读写权限
	} 
	else if ((uvpt[pn] & PTE_W) || (uvpt[pn] & PTE_COW)) 
	    { //对于UTOP以下的可写的或者写时拷贝的页,拷贝映射关系的同时,需要同时标记当前进程和子进程的页表项为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;
}

正确版

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_W) || (uvpt[pn] & PTE_COW)) { //对于UTOP以下的可写的或者写时拷贝的页,拷贝映射关系的同时,需要同时标记当前进程和子进程的页表项为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;
}

成功啦!

posted @ 2020-08-18 15:42  Cindy's  阅读(667)  评论(0编辑  收藏  举报