Lab 4: Preemptive Multitasking

tags: mit-6.828, os


概述

本文是lab4的实验报告,主要围绕进程相关概念进行介绍。主要将四个知识点:

  1. 开启多处理器。现代处理器一般都是多核的,这样每个CPU能同时运行不同进程,实现并行。需要用锁解决多CPU的竞争。介绍了spin locksleep lock,并给出了spin lock的实现。
  2. 实现进程调度算法。
  3. 实现写时拷贝fork(进程创建)。
  4. 实现进程间通信

Part A: Multiprocessor Support and Cooperative Multitasking

该部分做三件事:

  1. 使JOS支持多CPU
  2. 实现系统调用允许普通进程创建新的进程
  3. 实现协作式进程调度

Multiprocessor Support

我们将使JOS支持"symmetric multiprocessing" (SMP),这是一种所有CPU共享系统资源的多处理器模式。在启动阶段这些CPU将被分为两类:

  1. 启动CPU(BSP):负责初始化系统,启动操作系统。
  2. 应用CPU(AP):操作系统启动后由BSP激活。

    哪一个CPU是BSP由硬件和BISO决定,到目前位置所有JOS代码都运行在BSP上。

    在SMP系统中,每个CPU都有一个对应的local APIC(LAPIC),负责传递中断。CPU通过内存映射IO(MMIO)访问它对应的APIC,这样就能通过访问内存达到访问设备寄存器的目的。LAPIC从物理地址0xFE000000开始,JOS将通过MMIOBASE虚拟地址访问该物理地址。

Exercise 1

实现kern/pmap.c中的mmio_map_region()。

解决:可以参照boot_map_region()

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:
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,它们的LAPIC ID以及LAPIC MMIO地址。mp_init()函数从BIOS中读取这些信息。具体代码在mp_init()中,该函数会在进入内核后被i386_init()调用,主要作用就是读取mp configuration table中保存的CPU信息,初始化cpus数组,ncpu(总共多少可用CPU),bootcpu指针(指向BSP对应的CpuInfo结构)

真正启动AP的是在boot_aps()中,该函数遍历cpus数组,一个接一个启动所有的AP,当一个AP启动后会执行kern/mpentry.S中的代码,然后跳转到mp_main()中,该函数为当前AP设置GDT,TTS,最后设置cpus数组中当前CPU对应的结构的cpu_status为CPU_STARTED。更多关于SMP的知识可以参考:https://pdos.csail.mit.edu/6.828/2018/readings/ia32/MPspec.pdfhttps://wenku.baidu.com/view/615ea3c6aa00b52acfc7ca97.html

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结构。

每个CPU如下信息是当前CPU私有的:

  1. 内核栈:内核代码中的数组percpu_kstacks[NCPU][KSTKSIZE]为每个CPU都保留了KSTKSIZE大小的内核栈。从内核线性地址空间看CPU 0的栈从KSTACKTOP开始,CPU 1的内核栈将从CPU 0栈后面KSTKGAP字节处开始,以此类推,参见inc/memlayout.h。
  2. TSS和TSS描述符:每个CPU都需要单独的TSS和TSS描述符来指定该CPU对应的内核栈。
  3. 进程结构指针:每个CPU都会独立运行一个进程的代码,所以需要Env指针。
  4. 系统寄存器:比如cr3, gdt, ltr这些寄存器都是每个CPU私有的,每个CPU都需要单独设置。

到目前为止CpuInfo和Env关系可以总结如下:

Exercise 3:

修改mem_init_mp(),将内核栈线性地址映射到percpu_kstacks处的物理地址处。

解决:本质上是修改kern_pdir指向的页目录和页表,按照inc/memlayout.h中的结构进行映射即可。

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

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和CUP 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 5

在前面提的位置添加加锁和释放锁的代码。比较简单就不贴代码了。

Round-Robin Scheduling

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

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

Exercise 6

实现sched_yield()函数。

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. Make sure curenv is not null before
// dereferencing it.
//
// 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 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();
}

需要注意:当前CPU在envs数组中找了一圈后没找到合适的Env去执行,需要重新执行之前运行的进程,否则当前CPU就会进入停机状态。

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:拷贝页表,使指定进程共享当前进程相同的映射关系。本质上是修改特定进程的页目录和页表。
  5. sys_page_unmap:解除页映射关系。本质上是修改指定用户环境的页目录和页表。

Exercise 7:

实现上述所有的系统调用:

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

Part B: Copy-on-Write Fork

实现fork()有多种方式,一种是将父进程的内容全部拷贝一次,这样的话父进程和子进程就能做到进程隔离,但是这种方式非常耗时,需要在物理内存中复制父进程的内容。

另一种方式叫做写时拷贝的技术(copy on write),父进程将自己的页目录和页表复制给子进程,这样父进程和子进程就能访问相同的内容。只有当一方执行写操作时,才复制这一物理页。这样既能做到地址空间隔离,又能节省了大量的拷贝工作。我画了个图来比较这两种fork方式:

想要实现写时拷贝的fork()需要先实现用户级别的缺页中断处理函数。

User-level page fault handling

为了实现用户级别的页错误处理函数,进程需要注册页错误处理函数,需要实现一个sys_env_set_pgfault_upcall()系统调用提供支持。

Exercise 8:

实现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。

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结构如下:

                    <-- 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 9:

按照上面的描述实现page_fault_handler()。

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"); // 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);
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;
curenv->env_tf.tf_esp = (uintptr_t)utr;
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);
}

User-mode Page Fault Entrypoint

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

Exercise 10:

实现lib/pfentry.S中的_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 // LAB 4: Your code here.
// 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 //跳过utf_fault_va和utf_err
movl 40(%esp), %eax //保存中断发生时的esp到eax
movl 32(%esp), %ecx //保存终端发生时的eip到ecx
movl %ecx, -4(%eax) //将中断发生时的esp值亚入到到原来的栈中
popal
addl $4, %esp //跳过eip // 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:

完成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.
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");;
}
sys_env_set_pgfault_upcall(0, _pgfault_upcall); //系统调用,设置进程的env_pgfault_upcall属性
} // Save handler pointer for assembly to call.
_pgfault_handler = handler;
}

踩坑:

user_mem_check()中的cprintf()需要去掉,不然faultregs这个测试可能会过不了,坑啊~

缺页处理小结:

  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()设置缺页处理函数。
  2. 调用sys_exofork()系统调用,在内核中创建一个Env结构,复制当前用户环境寄存器状态,UTOP以下的页目录还没有建立,新创建的进程还不能直接运行。
  3. 拷贝父进程的页表和页目录到子进程。对于可写的页,将对应的PTE的PTE_COW位设置为1。
  4. 为子进程设置_pgfault_upcall。
  5. 将子进程状态设置为ENV_RUNNABLE。

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

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

Exercise 12:

实现lib/fork.c中的fork, duppage and pgfault。

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_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;
} 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)

Handling Clock Interrupts

目前程序一旦进入用户模式,除非发生中断,否则CPU永远不会再执行内核代码。我们需要开启时钟中断,强迫进入内核,然后内核就可以切换另一个进程执行。

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

Exercise 14:

修改trap_dispatch(),使时钟中断发生时,切换到另一个进程执行。

	if (tf->tf_trapno == IRQ_OFFSET + IRQ_TIMER) {
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);
}
}
}

IPC原理可以总结为下图:

总结

本lab还是围绕进程这个概念来展开的。主要介绍了四部分:

  1. 支持多处理器。现代处理器一般都是多核的,这样每个CPU能同时运行不同进程,实现并行。需要用锁解决多CPU的竞争。 CPU和进程在内核中的数据结构如下图所示:
  2. 实现进程调度算法。 一种是非抢占式式的,另一种是抢占式的,借助时钟中断实现,时钟中断到来时,内核调用sched_yield()选择另一个Env结构执行。
  3. 实现写时拷贝fork(进程创建)。fork()是库函数,会调用sys_exofork(void)这个系统调用,该系统调用在内核中为子进程创建一个新的Env结构,然将父进程的寄存器状态复制给该Env结构,复制页表,对于PTE_W为1的页目录,复制的同时,设置PTE_COW标志。为父进程和子进程设置缺页处理函数,处理逻辑:当缺页中断发生是因为写写时拷贝的地址,分配一个新的物理页,然后将该虚拟地址映射到新的物理页。

    原理总结如下:
  4. 实现进程间通信。本质还是进入内核修改Env结构的的页映射关系。原理总结如下:

具体代码在:https://github.com/gatsbyd/mit_6.828_jos

如有错误,欢迎指正(_):

15313676365

MIT-6.828-JOS-lab4:Preemptive Multitasking的更多相关文章

  1. MIT6.828 Lab4 Preemptive Multitasking(下)

    Lab4 Preemptive Multitasking(下) lab4的第二部分要求我们实现fork的cow.在整个lab的第一部分我们实现了对多cpu的支持和再多系统环境中的切换,但是最后分析的时 ...

  2. MIT 6.828 JOS学习笔记2. Lab 1 Part 1.2: PC bootstrap

    Lab 1 Part 1: PC bootstrap 我们继续~ PC机的物理地址空间 这一节我们将深入的探究到底PC是如何启动的.首先我们看一下通常一个PC的物理地址空间是如何布局的:        ...

  3. MIT 6.828 | JOS | 关于虚拟空间和物理空间的总结

    Question: 做lab过程中越来越迷糊,为什么一会儿虚拟地址是4G 物理地址也是4G ,那这有什么作用呢? 解决途径: 停下来,根据当前lab的进展,再回头看上学期操作系统的ppt & ...

  4. MIT 6.828 JOS学习笔记0. 写在前面的话

    0. 简介 操作系统是计算机科学中十分重要的一门基础学科,是一名计算机专业毕业生必须要具备的基础知识.但是在学习这门课时,如果仅仅把目光停留在课本上一些关于操作系统概念上的叙述,并不能对操作系统有着深 ...

  5. MIT 6.828 JOS学习笔记17. Lab 3.1 Part A User Environments

    Introduction 在这个实验中,我们将实现操作系统的一些基本功能,来实现用户环境下的进程的正常运行.你将会加强JOS内核的功能,为它增添一些重要的数据结构,用来记录用户进程环境的一些信息:创建 ...

  6. MIT 6.828 JOS学习笔记7. Lab 1 Part 2.2: The Boot Loader

    Lab 1 Part 2 The Boot Loader Loading the Kernel 我们现在可以进一步的讨论一下boot loader中的C语言的部分,即boot/main.c.但是在我们 ...

  7. MIT 6.828 JOS学习笔记18. Lab 3.2 Part B: Page Faults, Breakpoints Exceptions, and System Calls

    现在你的操作系统内核已经具备一定的异常处理能力了,在这部分实验中,我们将会进一步完善它,使它能够处理不同类型的中断/异常. Handling Page Fault 缺页中断是一个非常重要的中断,因为我 ...

  8. MIT 6.828 JOS学习笔记16. Lab 2.2

    Part 3 Kernel Address Space JOS把32位线性地址虚拟空间划分成两个部分.其中用户环境(进程运行环境)通常占据低地址的那部分,叫用户地址空间.而操作系统内核总是占据高地址的 ...

  9. MIT 6.828 JOS学习笔记15. Lab 2.1

    Lab 2: Memory Management lab2中多出来的几个文件: inc/memlayout.h kern/pmap.c kern/pmap.h kern/kclock.h kern/k ...

  10. MIT 6.828 JOS学习笔记10. Lab 1 Part 3: The kernel

    Lab 1 Part 3: The kernel 现在我们将开始具体讨论一下JOS内核了.就像boot loader一样,内核开始的时候也是一些汇编语句,用于设置一些东西,来保证C语言的程序能够正确的 ...

随机推荐

  1. 使用ImageMagick 在图片上绘制粗斜体的中文也许是一个错误。

    测试发现: ImageMagick使用中文字体,在图片上绘制带粗或斜体的中文,看不到效果. 如果使用英文字体,绘制粗或斜体的英文,99%都有效果. 今天无意看到一篇文章提到: convert -lis ...

  2. hdu 1789 Doing HomeWork Again (贪心算法)

    题目链接:http://acm.hdu.edu.cn/showproblem.php?pid=1789 /*Doing Homework again Time Limit: 1000/1000 MS ...

  3. AngularJS 项目里使用echarts 2.0 实现地图功能

    项目中有一页是显示全国地图, echarts官网的地图实例里,有一个模拟迁徙的实例,比较符合项目需求.所以大部分配置项是参考此实例. angular 就不过多介绍了, Google出品的mvc(或者说 ...

  4. Apache 的 ab 压测工具快速使用

    ab 是一个 httpd 自带的很好用的压力测试工具,它是 apache bench 命令的缩写.ab 命令会创建多个并发访问线程,模拟多个访问者同时对某一 URL 地址进行访问.可以用来测试 apa ...

  5. sklearn5_preprocessing数据标准化

    sklearn实战-乳腺癌细胞数据挖掘(博主亲自录制视频) https://study.163.com/course/introduction.htm?courseId=1005269003& ...

  6. spectrogram函数做短时傅里叶分析

    整理自:http://blog.sina.com.cn/s/blog_6163bdeb0102dwfw.html 今天偶人发现原来matlab自带了短时傅里叶变换的分析函数,老版本的matlab是sp ...

  7. javascript私有静态成员

    就私有静态成员而言,指的是成员具有如下属性:1.以同一个构造函数创建的所有对象共享该成员.2.构造函数外部不可访问该成员. //构造函数 var Gadget = (function(){ //静态变 ...

  8. linq中let关键字学习

    linq中let关键字就是对子查询的一个别名,let子句用于在查询中添加一个新的局部变量,使其在后面的查询中可见. linq中let关键字实例 1.传统下的子查询与LET关键字的区别     C# 代 ...

  9. TTPRequest 提示#import <libxml/HTMLparser.h>找不到 的解决方法

    本文永久地址为http://www.cnblogs.com/ChenYilong/p/3984251.html ,转载请注明出处. ASIHTTPRequest 或者AFNetwork提示的#impo ...

  10. Zookeeper笔记之quota

    一.节点配额概述 zookeeper中可以往节点存放数据,但是一般来说存放数据总是要有个度量的对吧,不然空间就那么大,如果某个节点将空间全占用了其它节点没得用了,所以zookeeper提供了一个对节点 ...