MIT6.828-OS lab4:Preemptive Multitasking 记录

github:https://github.com/viktorika/mit-os-lab

Introduction

在这个lab,您将在多个同时活动的用户态environment中实现抢占式多任务处理。

在partA中,您将为JOS添加多处理器支持,实现循环调度,并添加基本的environment管理系统调用(创建和销毁environment以及分配/映射内存的调用)。

在partB中,您将实现一个类似Unix的fork(),它允许用户态environment创建其自身的副本。

最后,在partC,您将添加对进程间通信(IPC)的支持,从而允许不同的用户态environment进行通信和同步。 您还将添加对硬件时钟中断和抢占的支持。

Getting Started

使用Git提交您的Lab 3源代码,获取课程存储库的最新版本,然后创建一个名为lab4的本地分支。

实验4包含许多新的源文件,在开始之前,应浏览其中的一些文件:

lab要求

本lab分为A,B和C三个部分。我们在时间表中为每个部分分配了一周的时间。

和以前一样,您将需要执行lab中描述的所有常规练习以及至少一个challenge problem。 (您不需要每个部分都做一个挑战问题,而整个lab只需要做一个。)此外,您将需要编写一份关于所实施挑战问题的简短描述。 如果您实施了多个challenge problem,则只需在本文中描述其中一个problem,当然,欢迎您完成更多的问题。 在进行工作之前,将文章放在lab目录顶层的Answers-lab4.txt文件中。

 

Part A: Multiprocessor Support and Cooperative Multitasking

在lab的第一部分,您将首先扩展JOS以使其在多处理器系统上运行,然后实现一些新的JOS内核系统调用,以允许用户级environment创建其他新environment。 您还将实现协作循环调度,当当前environment自愿放弃CPU(或退出)时,允许内核从一种environment切换到另一种environment。 在partC的后面,您将实现抢占式调度,该调度使内核可以在经过一定时间后重新从environment里获得对CPU的控制,即使environment不合作也是如此。

 

Multiprocessor Support

我们准备让JOS支持对称多重处理(SMP),一个多处理器模块,其中所有CPU均具有对系统资源(例如内存和IO总线)的同等访问权限。尽管所有CPU在SMP中在功能上都是相同的,但是在启动过程中,它们可以分为两种类型:引导处理器(BSP)负责初始化系统并启动操作系统;并且只有在操作系统启动并运行后,应用处理器(APs)才能由BSP激活。哪个处理器是BSP,这取决于硬件和BIOS。到目前为止,所有现有的JOS代码都已在BSP上运行。

在SMP系统中,每个CPU都有一个随附的本地APIC(LAPIC)单元。LAPIC单元负责在整个系统中传递中断。LAPIC还为其连接的CPU提供唯一标识符。在这个lab里,我们利用LAPIC单元的以下基本功能(在kern / lapic.c中)。

•     读取LAPIC标识符(APIC ID)以了解我们的代码当前在哪个CPU上运行(请参阅cpunum())。

•     从BSP发送STARTUP处理器中断(IPI)到AP以启动其他CPU(请参阅lapic_startrap())。

•     在partC中,我们对LAPIC的内置计时器进行编程以触发时钟中断以支持抢先式多任务处理(请参阅apic_init())。

处理器使用内存映射的I / O(MMIO)访问其LAPIC。 在MMIO中,一部分物理内存被硬连线到某些I / O设备的寄存器,因此通常用于访问内存的相同加载/存储指令可用于访问设备寄存器。 您已经在物理地址0xA0000处看到一个IO孔(我们使用它来写入CGA display buffer)。 LAPIC位于一个从物理地址0xFE000000(比4GB小32MB)开始的孔中,因此对于我们来说,使用我们在KERNBASE的常规直接映射进行访问的空间过高。 JOS虚拟内存映射在MMIOBASE上留下了4MB的空间,因此我们有一个映射这样的设备的地方。 由于以后的lab会引入更多的MMIO区域,因此您将编写一个简单的函数从该区域分配空间并将设备内存映射到该区域。

Exercise 1.在kern / pmap.c中实现mmio_map_region。 要查看其用法,请查看kern / lapic.c中lapic_init的开头。 在运行mmio_map_region的测试之前,您还必须做下一个练习。

这个练习就很简单,跟前面lab2的差不多,说明也很充分了。直接上代码吧。

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:
    // 检查是否溢出MMIOLIM
    size_t align_size = ROUNDUP(size, PGSIZE);
    if(base + align_size > MMIOLIM)
        panic("mmio_map_region: overflow MMIOLIM");
    boot_map_region(kern_pgdir, base, size, pa, PTE_W|PTE_PCD|PTE_PWT);
    uintptr_t result = base;
    base += align_size;
    return (void *)result;
}

Application Processor Bootstrap

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

boot_aps()函数(在kern / init.c中)驱动AP引导过程。 AP在real mode下启动,就像boot loader在boot / boot.S中启动的方式一样,因此boot_aps()将AP入口代码(kern / mpentry.S)复制到可在real mode下寻址的内存位置。 与boot loader不同,我们可以控制AP在何处开始执行代码。 我们将输入代码复制到0x7000(MPENTRY_PADDR),小于640KB的任何未使用的页面对齐的物理地址都可以使用。

之后,boot_aps()通过向相应AP的LAPIC单元发送STARTUP IPI以及一个初始CS:IP地址(该AP应开始运行其入口代码)开始一个接一个地激活AP。 kern / mpentry.S中的输入代码与boot / boot.S的输入代码非常相似。 进行一些简短的设置后,它将使AP进入启用分页的protect mode,然后调用C设置例程mp_main()(也在kern / init.c中)。 boot_aps()等待AP在其结构CpuInfo的cpu_status字段中发信号通知CPU_STARTED标志,然后继续唤醒下一个。

Exercise 2.在kern / init.c中阅读boot_aps()和mp_main(),并在kern / mpentry.S中阅读汇编代码。 确保您了解AP引导过程中的控制流转移。 然后,在kern / pmap.c中修改您对page_init()的实现,以避免将MPENTRY_PADDR中的页面添加到空闲列表中,以便我们可以安全地在该物理地址复制并运行AP引导程序代码。 您的代码应通过更新的check_page_free_list()测试(但可能无法通过更新的check_kern_pgdir()测试,我们将尽快修复)。

根据这段描述大致看一下代码,改page_init实际上是很简单的。。。就是多一个虚拟页需要标记,修改如下。

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!

    // 已经使用的区间,左闭右开
    // 0用于实模式IDT和BIOS结构
    // 还有空洞是不能分配的
    // 空洞后一直到下一个可分配的物理内存之前都是不可用的
    // 这个地方我踩了好久,一开始不知道怎么辨别kernel这里有多少个地方被用了,结果看大神的直接把前面的都认为不可用才恍然大大悟
    physaddr_t nextfree_paddr = PADDR((pde_t *)boot_alloc(0));
    physaddr_t used_interval[3][2] = {{0, PGSIZE}, {MPENTRY_PADDR, MPENTRY_PADDR + PGSIZE}, {IOPHYSMEM, nextfree_paddr}};
    const int kUsed_interval_length = 3;
    int used_interval_pointer = 0;
    int i;
    for(i = 0; i < npages; ++i){
        while(used_interval_pointer < kUsed_interval_length && used_interval[used_interval_pointer][1] <= page2pa(pages + i))
            used_interval_pointer++;

        //空闲-----used_interval_pointer越界或者当前页面的物理地址小于used_interval[0]
        if(used_interval_pointer >= kUsed_interval_length || page2pa(pages + i) < used_interval[used_interval_pointer][0]){
            pages[i].pp_ref = 0;
            pages[i].pp_link = page_free_list;
            page_free_list = pages + i;
        }
        else{
        //否则就是在used_interval的其中一个区间,也就是已经被使用了
            pages[i].pp_ref = 1;
            pages[i].pp_link = NULL;
        }
    }
}

Question

1.将kern / mpentry.S与boot / boot.S并排比较。 kern / mpentry.S就像内核中的所有其他内容一样经过编译和链接以在KERNBASE之上运行,宏MPBOOTPHYS的目的是什么? 为什么在kern / mpentry.S中有必要,但在boot / boot.S中却没有必要? 换句话说,如果在kern / mpentry.S中省略了它,那会出什么问题? 
Hint:回忆一下我们在实验1中讨论的链接地址和加载地址之间的区别。

answer:mpentry_start, mpentry_end是LMA,内核在启动AP之前将这个地址映射到了MPENTRY_PADDR(VMA),AP启动还没进protect mode之前只能使用物理地址,而实际上这时候BSP已经到kernel状态,gdt的地址都是线性地址,所以需要一个宏来转换成物理地址。boot.S还没进protect mode,访问的都是物理地址。

Per-CPU State and Initialization

编写多处理器OS时,区分每个处理器专用的每个CPU的状态和整个系统共享的全局状态非常重要。 kern / cpu.h定义了大多数每个CPU的状态,包括存储每CPU变量的struct CpuInfo结构。 cpunum()总是返回调用它的CPU的ID,该ID可用作cpus的数组的索引。 另外,宏thiscpu是当前CPU的结构CpuInfo的简写。

这是您应注意的每个CPU的状态:

*        CPU的kernel栈。

          因为多个CPU可以同时trap到kernel中,所以我们需要为每个处理器使用单独的kernel栈,以防止它们干扰彼此的执行。
          数组percpu_kstacks [NCPU] [KSTKSIZE]为NCPU的kernel栈保留了空间。

          在lab2中,您映射了boot栈称为KSTACKTOP下方的BSP kernel栈的物理内存。 同样,在本lab中,您将把每个CPU的kernel栈映射到该区域,其中guard page充当它们之间的缓冲区。 CPU 0的栈仍将从KSTACKTOP增长; CPU 1的栈将从CPU 0的堆栈底部开始的KSTKGAP字节开始,依此类推。 inc / memlayout.h显示了映射布局。

*        CPU TSS和TSS描述符。

          还需要每个CPU的TSS段,以指定每个CPU的kernel栈所在的位置。 CPU i的TSS存储在cpus [i] .cpu_ts中,并且相应的TSS描述符在GDT条目gdt [(GD_TSS0 >> 3)+ i]中定义。 在kern / trap.c中定义的全局ts变量将不再有用。

*        CPU当前environment指针。

          由于每个CPU可以同时运行不同的用户进程,因此我们将符号curenv重新定义为引用cpus [cpunum()]。cpu_env(或thiscpu-> cpu_env),它指向当前在当前CPU上执行的environment( 代码正在运行的environment)。

*        CPU系统寄存器。

          所有寄存器,包括系统寄存器,都是CPU专用的。 因此,初始化这些寄存器的指令,例如lcr3(),ltr(),lgdt(),lidt()等,必须在每个CPU上执行一次。 为此,定义了函数env_init_percpu()和trap_init_percpu()。

Exercise 3. 修改mem_init_mp()(在kern / pmap.c中)以映射从KSTACKTOP开始的每个CPU的栈,如inc / memlayout.h中所示。 每个栈的大小为KSTKSIZE字节加上未映射的guard page的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:
    int i;
    for(i = 0; i < NCPU; ++i){
        unsigned end = KSTACKTOP - i * (KSTKSIZE + KSTKGAP);
        unsigned start = end - KSTKSIZE;
        boot_map_region(kern_pgdir, start, KSTKSIZE, PADDR(percpu_kstacks + i), PTE_W);
    }
}

Exercise 4.trap_init_percpu()(kern / trap.c)中的代码初始化BSP的TSS和TSS描述符。 它在lab3中工作,但是在其他CPU上运行时不正确。 更改代码,使其可以在所有CPU上使用。 (注意:您的新代码不应再使用全局ts变量。)

完成上述练习后,使用make qemu CPUS = 4(或make qemu-nox CPUS = 4)在4个CPU的QEMU中运行JOS,您应该看到如下输出:

这个也很简单,要注意一下TSS段选择器也要修改。代码如下。

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()
    //  
    // 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_id = thiscpu->cpu_id;
    thiscpu->cpu_ts.ts_esp0 = KSTACKTOP - cpu_id * (KSTKSIZE + KSTKGAP);
    thiscpu->cpu_ts.ts_ss0 = GD_KD;

    // Initialize the TSS slot of the gdt.
    gdt[(GD_TSS0 >> 3) + cpu_id] = SEG16(STS_T32A, (uint32_t) (&thiscpu->cpu_ts), sizeof(struct Taskstate), 0); 
    gdt[(GD_TSS0 >> 3) + cpu_id].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_id << 3));

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

Locking

在mp_main()中初始化AP之后,我们当前的代码spins(就是一直在循环)。 在让AP进一步运行之前,我们需要首先解决多个CPU同时运行kernel代码时的竞争条件。 实现此目的的最简单方法是使用big kernel锁。 big kernel锁是单个全局锁,每当environment进入内核态时都会持有,并在environment返回用户态时释放。 在此模型中,用户态下的environment可以在任何可用的CPU上同时运行,但是内核态下只能运行一个environment。 任何其他尝试进入内核态的environment都必须等待。

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

*       在i386_init()中,在BSP唤醒其他CPU之前获取锁。

*       在mp_main()中,在初始化AP之后获取锁,然后调用sched_yield()开始在此AP上运行environment。

*       在trap()中,从用户态trap时获取锁。 要确定trap是在用户态下还是内核态下发生的,请检查tf_cs的低位。

*       在env_run()中,在切换到用户态之前立即释放锁。 不要太早或太晚地这样做,否则会遇到资源竞争或死锁。

Exercise 5.如上所述,通过在适当的位置调用lock_kernel()和unlock_kernel()来应用big kernel锁。

如何测试您的锁定是否正确? 您暂时不能! 但是您可以在下一个练习中实现调度程序之后测试。

这代码就不贴了,就把上面几个地方说的全部改一遍。没难度。

Question

2.使用bit kernel锁似乎可以保证一次只有一个CPU可以运行kernel代码。 为什么每个CPU仍需要单独的kernel栈? 描述使用共享kernel栈会出错的情况,即使使用了big kernel锁也是如此。

answer:中断发生后我们需要把参数压到TSS段再上的锁,上锁之前已经是在内核态了,如果多个进程发生中断那么同时共享kernel栈就会出问题。big kernel锁只是保证kernel共享的数据结构不被几个cpu同时修改。

--------------------------------------------------------------------------------------------------------------------------------

TODO   challenge

--------------------------------------------------------------------------------------------------------------------------------
 

Round-Robin Scheduling

在本lab中,您的下一个任务是更改JOS内核,以便它可以“轮循”方式在多个environment之间切换。 JOS中的循环调度工作方式如下:

*       新的kern / sched.c中的函数sched_yield()负责选择要运行的新environment。 它以循环方式依次搜索envs []数组,从先前运行的environment之后开始(如果没有先前运行的environment,则从数组的开头开始),选择状态为ENV_RUNNABLE的第一个environment(请参见 inc / env.h),然后调用env_run()运行该environment。

*       sched_yield()绝对不能在两个CPU上同时运行相同的environment。 它可以表明某个environment当前正在某些CPU(可能是当前CPU)上运行,因为该环境的状态为ENV_RUNNING。

*       我们为您实现了一个新的系统调用sys_yield(),用户environment可以调用该系统调用来调用内核的sched_yield()函数,从而自动将CPU放弃运行其他environment。

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

修改kern / init.c以创建三个(或更多!)environment,这些environment都运行程序user / yield.c。 在终止之前,您应该看到environment在彼此之间来回切换了五次,如下所示:

在yield程序退出之后,系统中将没有可运行的environment,调度程序应调用JOS内核监视器。 如果以上任何一种都没有发生,请在继续操作之前先修复您的代码。

这个练习我是调得真久,上一个lab有几个地方没做好,导致这里错误很多。。。。首先处理上一个lab的问题。第一个问题,系统调用的中断是有返回值的是有返回值的。。。需要将结果存到trapframe结构里的eax寄存器,这样恢复上下文的时候就带返回值了。

        case T_SYSCALL:{
            uint32_t eax = tf->tf_regs.reg_eax;
            uint32_t ebx = tf->tf_regs.reg_ebx;
            uint32_t ecx = tf->tf_regs.reg_ecx;
            uint32_t edx = tf->tf_regs.reg_edx;
            uint32_t edi = tf->tf_regs.reg_edi;
            uint32_t esi = tf->tf_regs.reg_esi;
            tf->tf_regs.reg_eax = syscall(eax, edx, ecx, ebx, edi, esi);
            break;
        }

第二个是lib/libmain.c文件上一个lab不知道为什么我漏了做这个部分。。。。调试完发现这个问题后补上。

void
libmain(int argc, char **argv)
{
    // set thisenv to point at our Env structure in envs[].
    // LAB 3: Your code here.
    thisenv = envs + ENVX(sys_getenvid());

    // save the name of the program so that panic() can use it
    if (argc > 0)
        binaryname = argv[0];

    // call user main routine
    umain(argc, argv);

    // exit gracefully
    exit();
}

然后才到这次练习需要做的内容。。。首先实现sched_yield函数.

// Choose a user environment to run and run it.
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 curenv_index = (curenv?(curenv - envs + 1):0);
    int offset;
    for(offset = 0; offset < NENV; ++offset){
        idle = envs + ((curenv_index + offset) % NENV);
        if(idle && ENV_RUNNABLE == idle->env_status){
            env_run(idle);
            return;
        }
    }
    if(curenv && ENV_RUNNING == curenv->env_status)
        env_run(curenv);
    else
        // sched_halt never returns
        sched_halt();
}

除了多注意curenv的可能是空就没难度了。然后增加一个系统调用。

// Dispatches to the correct kernel function, passing the arguments.
int32_t
syscall(uint32_t syscallno, uint32_t a1, uint32_t a2, uint32_t a3, uint32_t a4, uint32_t a5)
{
    // Call the function corresponding to the 'syscallno' parameter.
    // Return any appropriate return value.
    // LAB 3: Your code here.
    switch(syscallno){
        case SYS_cputs:
            sys_cputs((char *)a1, a2);
            return 0;
        case SYS_cgetc:
            return sys_cgetc();
        case SYS_getenvid:
            return sys_getenvid();
        case SYS_env_destroy:
            return sys_env_destroy(a1);
        case SYS_yield:
            sys_yield();
            return 0;
        default:
            return -E_INVAL;
    }   
}

真是痛苦的调试过程。。。。

Question

3.在实现env_run()时,您应该调用lcr3()。 在调用lcr3()前后,您的代码(至少应该)引用变量e(即env_run的参数)。 加载%cr3寄存器后,MMU使用的寻址上下文将立即更改。 但是虚拟地址(即e)相对于给定的地址上下文才有含义-----地址上下文指定了虚拟地址映射到的物理地址。 为什么在寻址开关之前和之后都可以解除对指针e的引用?

answer:这是因为envs数组也被映射到用户页表里,UTOP的映射都和kernel一致。

4.每当kernel从一种environment切换到另一种environment时,必须确保保存了旧environment的寄存器,以便以后可以正确还原它们。 为什么? 这在哪里发生?

answer:因为需要保存上下文。当你需要再次运行这个environment的时候恢复上下文才能正确运行,这个在发生中断的时候把上下文压到了TSS段,然后在把这些数据存到了env对应的trapframe结构里,最后处理完中断就把这个trapframe恢复,或者执行别的environment,当再次执行这个environment时把env结构里的trapframe拿出来恢复。

--------------------------------------------------------------------------------------------------------------------------------

TODO   challenge

--------------------------------------------------------------------------------------------------------------------------------

 

System Calls for Environment Creation

尽管您的kernel现在可以在多个用户态的environment中运行和切换,但仍限于kernel最初设置的运行environment。 现在,您将实现必要的JOS系统调用,以允许用户environment创建和启动其他新的用户environment。

Unix提供fork()系统调用作为其进程创建原语。 Unix fork()复制调用进程(父进程)的整个地址空间,以创建一个新进程(子进程)。 从用户空间可观察到的两个唯一区别是它们的进程ID和父进程ID(由getpid和getppid返回)。 在父进程中,fork()返回子进程的进程ID,而在子进程中,fork()返回0。默认情况下,每个进程都有其自己的私有地址空间,并且另一个进程对内存的修改对其他进程都不可见。

您将提供一组不同的,更原始的JOS系统调用集,以创建新的用户态environment。 通过这些系统调用,您还可以完全在用户空间中实现类似Unix的fork()。 您将为JOS编写的新系统调用如下:

sys_exofork:

                        该系统调用创建了一个几乎空白的新environment:在其地址空间的用户部分中未映射任何内容,并且该environment不可运行。 调用sys_exofork时,新environment将与父environment具有相同的寄存器状态。 在父进程中,sys_exofork将返回新创建的environment的envid_t(如果environment分配失败,则返回负错误代码)。 但是,在子进程中,它将返回0。(由于该子进程开始时标记为不可运行,因此sys_exofork不会真正返回该子进程,直到父进程标记该子进程允许该操作。)

sys_env_set_status:

                        将指定environment的状态设置为ENV_RUNNABLE或ENV_NOT_RUNNABLE。 一旦其地址空间和寄存器状态已完全初始化,此系统调用通常用于标记准备运行的新environment。

sys_page_alloc:

                        分配一页物理内存,并将其映射到给定environment的地址空间中的给定虚拟地址。

sys_page_map:

                        将页面映射(而不是页面的内容!)从一个environment的地址空间复制到另一个environment,保留共享内存部分,以便新映射和旧映射都引用同一物理内存页面。

sys_page_unmap

                        取消映射在给定environment中映射到给定虚拟地址的页面。

对于上面所有接受environmentID的系统调用,JOS内核都支持以下约定:值0表示“当前environment”。 此约定由kern / env.c中的envid2env()实现。

我们在测试程序user / dumbfork.c中提供了一个类似于Unix的fork()的非常原始的实现。 该测试程序使用上述系统调用来创建和运行带有其自身地址空间副本的子environment。 然后,像前面的练习一样,使用sys_yield在两个environment之间来回切换。 父进程在10次迭代后退出,而子进程在20次迭代后退出。

exercise7.在kern / syscall.c中实现上述系统调用。 您将需要用到kern / pmap.c和kern / env.c中的各种函数,尤其是envid2env()。 现在,无论何时调用envid2env(),都要在checkperm参数中传递1。 确保检查所有无效的系统调用参数,在这种情况下返回-E_INVAL。 使用user / dumbfork测试您的JOS内核,并在继续之前确保其工作正常。

这个练习实际上还是很容易的。。但是自从我做到lab4,我就感觉调试越来越难,每次都怀疑是不是前面某个地方出错。这个练习就把这几个函数实现了再增加系统调用的分发函数就好。

// 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.
    struct Env *new_env;
    int result = env_alloc(&new_env, curenv->env_id);
    if(result < 0)
        return result;
    //状态设置为not runnable,寄存器信息修改同父进程,父进程返回子进程id,子进程返回0    
    new_env->env_tf = curenv->env_tf;
    new_env->env_status = ENV_NOT_RUNNABLE;
    new_env->env_tf.tf_regs.reg_eax = 0;
    return new_env->env_id;
}
// 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.
    if(status < ENV_FREE || status > ENV_NOT_RUNNABLE)
        return -E_INVAL;
    struct Env *env;
    int result = envid2env(envid, &env, 1);
    if(result < 0)
        return result;
    env->env_status = status;
    return 0;
}
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 *env;
    int result = envid2env(envid, &env, 1);
    if(result < 0)
        return result;
    if((unsigned)va >= UTOP || (unsigned)va & (PGSIZE - 1))
        return -E_INVAL;
    if((perm & (PTE_U | PTE_P)) != (PTE_U | PTE_P) || (perm & (~(PTE_P | PTE_U | PTE_W | PTE_AVAIL))))
        return -E_INVAL;
    struct PageInfo *new_page = page_alloc(ALLOC_ZERO);
    if(!new_page)
        return -E_NO_MEM;
    result = page_insert(env->env_pgdir, new_page, va, perm);
    if(result < 0)
        page_free(new_page);
    return result;
}
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 *src_env, *dst_env;
    int result = envid2env(srcenvid, &src_env, 1);
    if(result < 0)
        return result;
    result = envid2env(dstenvid, &dst_env, 1);
    if(result < 0)
        return result;
    if((unsigned)srcva >= UTOP || (unsigned)srcva & (PGSIZE - 1) ||
        (unsigned)dstva >= UTOP || (unsigned)dstva & (PGSIZE - 1))
        return -E_INVAL;
    pte_t *page_table_entry;
    struct PageInfo *page_info = page_lookup(src_env->env_pgdir, srcva, &page_table_entry);
    if(!page_info)
        return -E_INVAL;
    if((perm & (PTE_U | PTE_P)) != (PTE_U | PTE_P) || (perm & (~(PTE_P | PTE_U | PTE_W | PTE_AVAIL))))
        return -E_INVAL;
    if((perm & PTE_W) && !(*page_table_entry & PTE_W))
        return -E_INVAL;
    result = page_insert(dst_env->env_pgdir, page_info, dstva, perm);
    if(result < 0)
        page_free(page_info);
    return result;
}
// 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.
    struct Env *env;
    int result = envid2env(envid, &env, 1);
    if(result < 0)
        return result;
    if((unsigned)va >= UTOP || (unsigned)va & (PGSIZE - 1))
        return -E_INVAL;
    page_remove(env->env_pgdir, va);
    return 0;
}
// Dispatches to the correct kernel function, passing the arguments.
int32_t
syscall(uint32_t syscallno, uint32_t a1, uint32_t a2, uint32_t a3, uint32_t a4, uint32_t a5)
{
    // Call the function corresponding to the 'syscallno' parameter.
    // Return any appropriate return value.
    // LAB 3: Your code here.
    switch(syscallno){
        case SYS_cputs:
            sys_cputs((char *)a1, a2);
            return 0;
        case SYS_cgetc:
            return sys_cgetc();
        case SYS_getenvid:
            return sys_getenvid();
        case SYS_env_destroy:
            return sys_env_destroy(a1);
        case SYS_yield:
            sys_yield();
            return 0;
        case SYS_exofork:
            return sys_exofork();
        case SYS_env_set_status:
            return sys_env_set_status(a1, a2);
        case SYS_page_alloc:
            return sys_page_alloc(a1, (void *)a2, a3);
        case SYS_page_map:
            return sys_page_map(a1, (void *)a2, a3, (void *)a4, a5);
        case SYS_page_unmap:
            return sys_page_unmap(a1, (void *)a2);
        default:
            return -E_INVAL;
    }
}

--------------------------------------------------------------------------------------------------------------------------------

TODO   challenge

--------------------------------------------------------------------------------------------------------------------------------

至此完成了实验的A部分; 使用make grade检查。 如果您想弄清楚为什么某个特定的测试用例失败,请运行./grade-lab4 -v,它将向您显示内核构建的输出和QEMU针对每个测试的运行,直到测试失败。 当测试失败时,脚本将停止,然后您可以检查jos.out以查看内核实际打印的内容。

 

Part B: Copy-on-Write Fork

如前所述,Unix提供fork()系统调用作为其进程创建原语。 fork()系统调用复制调用进程(父进程)的地址空间以创建一个新进程(子进程)。

xv6 Unix通过将父进程页面的所有数据复制到为子进程分配的新页面中来实现fork()。 这基本上与dumbfork()所采用的方法相同。 将父进程的地址空间复制到子进程是fork()操作最昂贵的部分。

但是,对fork()的调用通常在子进程中几乎立即会调用exec(),这用新进程替换了子进程的内存。 例如,这就是shell通常执行的操作。 在这种情况下,复制父地址空间所花费的时间大大浪费了,因为子进程在调用exec()之前将占用很少的内存。

因此,Unix的更高版本利用虚拟内存硬件允许父进程和子进程共享映射到其各自地址空间的内存,直到其中一个进程实际对其进行修改为止。这种技术称为写时复制(copy-on-write)。为此,在fork()上,kernel会将地址空间映射从父进程复制到子进程,而不是映射页面的内容,同时将当前共享的页面标记为只读。当两个进程之一尝试写入这些共享页面之一时,该进程将出现页面错误。此时,Unix kernel意识到该页面实际上是“虚拟”副本或“copy-on-write”副本,因此它为该错误过程创建了该页面的新的,私有的,可写的副本。这样,各个页面的内容实际上不会被复制,直到它们被实际写入为止。这种优化使子进程中的fork()和exec()便宜得多:子进程在调用exec()之前可能只需要复制一页(其栈的当前页)。

在本lab的下一部分中,您将实现一个“适当的”类Unix的fork(),并带有写时复制功能,作为用户空间库例程。 在用户空间中实现fork()和copy-on-write支持的好处是kernel保持简单得多,因此更可能是正确的。 它还允许单个用户态程序为fork()定义自己的语义。 想要一个稍有不同的实现的程序(例如,昂贵的始终复制版本,例如dumbfork(),或者父子进程随后实际共享内存的版本)可以轻松提供自己的程序。

 

User-level page fault handling

用户级别的写时复制fork()需要了解受写保护页面上的页面错误,因此这是您首先要实现的。 copy-on-write只是用户级页面错误处理的多种可能之一。

设置地址空间是很常见的,以便页面错误指示何时需要执行某些操作。 例如,大多数Unix kernel最初只在新进程的栈区域中映射单个页面,然后在该进程的栈消耗增加时“按需”分配和映射其他栈页面,并在尚未映射的栈地址上引起页面错误。 典型的Unix kernel必须跟踪在进程空间的每个区域中发生页面错误时应采取的措施。 例如,栈区域中的故障通常会分配并映射新的物理内存页面。 程序的BSS区域中的故障通常将分配一个新页面,并用零填充并映射它。 在具有按需分页可执行文件的系统中,文本区域中的错误将从磁盘读取二进制文件的相应页面,然后将其映射。

这是kernel要跟踪的很多信息。 代替传统的Unix方法,采用决定如何处理用户空间中每个页面错误,而这些错误对破坏性的影响较小。 这种设计的附加好处是允许程序在定义其内存区域时具有极大的灵活性。 您稍后将使用用户级页面错误处理来映射和访问基于磁盘的文件系统上的文件。

Setting the Page Fault Handler

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

Exercise 8.实现sys_env_set_pgfault_upcall系统调用。 查找目标environment的environment ID时,请确保启用权限检查,因为这是“危险的”系统调用。

这个绝对是这次lab里最简单的一个没有之一。

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

Normal and Exception Stacks in User Environments

在正常执行期间,JOS中的用户environment将在普通用户栈上运行:其ESP寄存器开始指向USTACKTOP,而其推送的栈数据位于USTACKTOP-PGSIZE和USTACKTOP-1(含)之间的页面上。 但是,当在用户态下发生页面错误时,kernel将在另一个栈(即用户异常栈)上运行指定的用户级页面错误处理程序,从而重新启动用户environment。 本质上,我们将使JOS kernel代表用户environment实现自动“栈切换”,与x86处理器已经在从用户态转换到内核态时的JOS实现栈切换的方式几乎相同!

JOS用户异常栈的大小也为一页,并且其顶部定义为虚拟地址UXSTACKTOP,因此用户异常栈的有效字节从UXSTACKTOP-PGSIZE到UXSTACKTOP-1(含)。 在此异常栈上运行时,用户级页面错误处理程序可以使用JOS的常规系统调用来映射新页面或调整映射,以修复最初导致页面错误的任何问题。 然后,用户级页面错误处理程序通过汇编语言存根返回到原始栈上的错误代码。

希望支持用户级页面错误处理的每个用户environment都将需要使用A部分中引入的sys_page_alloc()系统调用为其自身的异常栈分配内存。

Invoking the User Page Fault Handler

现在,您将需要更改kern / trap.c中的页面错误处理代码,以从用户态处理页面错误,如下所示。 我们将发生故障时的用户environment状态称为trap-time状态。

如果没有注册页面错误处理程序,则JOS kernel会像以前一样通过一条消息破坏用户environment,否则,内核会在异常栈上设置一个trap frame,该trap frame看起来像来自inc / trap.h的UTrapframe结构:

然后,kernel安排用户environment在具有此栈帧的异常栈上运行的页面错误处理程序中恢复执行。 您必须弄清楚如何做到这一点。 fault_va是导致页面错误的虚拟地址。

如果发生异常时用户异常栈上的用户environment已经在运行,则页面错误处理程序本身已发生故障。 在这种情况下,您应该在当前tf-> tf_esp下而不是在UXSTACKTOP下开始新的栈帧。 您应该首先push一个空的32位字,然后再push一个UTrapframe结构。

要测试用户异常栈上是否已经存在tf-> tf_esp,请检查它是否在UXSTACKTOP-PGSIZE和UXSTACKTOP-1(包括UXSTACKTOP-PGSIZE)之间的范围内。

Exercise 9. 在kern / trap.c中的page_fault_handler中实现将页面错误分派到usermode handler。 写入异常栈时,请确保采取适当的预防措施。 (如果用户environment在异常栈上的空间不足,会发生什么?)

这个练习要理解好需要做些什么,上面有说注册页面错误之前需要用系统调用分配一页给异常栈,所以到中断处理的时候我们无需分配任何内存,直接访问。剩下的结合代码跟注释理解吧。

    if(curenv->env_pgfault_upcall){
        struct UTrapframe *utrapframe;
        unsigned curenv_esp = curenv->env_tf.tf_esp;
        if(curenv_esp >= UXSTACKTOP-PGSIZE && curenv_esp < UXSTACKTOP){
            //已经处理过一次页面异常了
            //push空的32位字和UTrapframe
            utrapframe = (struct UTrapframe *)(curenv_esp - 4 - sizeof(struct UTrapframe));
        }
        else
            //第一次处理,push UTrapframe
            utrapframe = (struct UTrapframe *)(UXSTACKTOP - sizeof(struct UTrapframe));
        //保证UTrapframe的地址是可写的
        user_mem_assert(curenv, (void*)utrapframe, sizeof(struct UTrapframe), PTE_W);
        //utrapframe是异常栈帧,压栈是为了保存之前的状态以便还原
        utrapframe->utf_fault_va = fault_va;
        utrapframe->utf_err = tf->tf_err;
        utrapframe->utf_regs = tf->tf_regs;
        utrapframe->utf_eip = tf->tf_eip;
        utrapframe->utf_eflags = tf->tf_eflags;
        utrapframe->utf_esp = tf->tf_esp;

        //修改当前栈指针
        curenv->env_tf.tf_eip = (unsigned)curenv->env_pgfault_upcall;
        curenv->env_tf.tf_esp = (unsigned)utrapframe;
        //重新执行env
        env_run(curenv);
    }
    else{
        // 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);
    }

这样用户environment可以调用自己的page fault函数了,至于为什么需要这样压栈,处理完是怎么返回的暂时我们还不知道。

User-mode Page Fault Entrypoint 

接下来,您需要实现汇编例程,该例程将负责调用C页面错误处理程序并按照原始错误指令恢复执行。 该汇编例程是将使用sys_env_set_pgfault_upcall()在内核中注册的处理程序。

Exercise 10. 在lib / pfentry.S中实现_pgfault_upcall例程。 有趣的部分是返回到导致页面错误的用户代码中的原始点。 您将直接返回那里,而无需返回内核。 困难的部分是同时切换栈并重新加载EIP。

又得写汇编,头疼,大概是这个样子。

    // Now the C page fault handler has returned and you must return
    // to the trap time state.
    // Push trap-time %eip onto the trap-time stack.
    //  
    // Explanation:
    //   We must prepare the trap-time stack for our eventual return to
    //   re-execute the instruction that faulted.
    //   Unfortunately, we can't return directly from the exception stack:
    //   We can't call 'jmp', since that requires that we load the address
    //   into a register, and all registers must have their trap-time
    //   values after the return.
    //   We can't call 'ret' from the exception stack either, since if we
    //   did, %esp would have the wrong value.
    //   So instead, we push the trap-time %eip onto the *trap-time* stack!
    //   Below we'll switch to that stack and call 'ret', which will
    //   restore %eip to its pre-fault value.
    //  
    //   In the case of a recursive fault on the exception stack,
    //   note that the word we're pushing now will fit in the
    //   blank word that the kernel reserved for us.
    //  
    // Throughout the remaining code, think carefully about what
    // registers are available for intermediate calculations.  You
    // may find that you have to rearrange your code in non-obvious
    // ways as registers become unavailable as scratch space.
    //  
    // LAB 4: Your code here.
    movl 0x28(%esp), %eax  //eip
    subl $0x4, 0x30(%esp)  //原本的esp-4,用于ret
    movl 0x30(%esp), %ebx  //获得扩大后的原本的esp
    movl %eax, (%ebx)      //填充ret地址

    // 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

此时明白之前为什么需要压4位空字,就是用来存返回地址的。

最后,您需要实现用户级页面错误处理机制的C用户库。

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.
        //第一次要分配页面给异常stack使用
        if(sys_page_alloc(0, (void *)(UXSTACKTOP - PGSIZE), PTE_P|PTE_U|PTE_W) < 0)
            panic("set_pgfault_handler: sys_page_alloc failed");
        //然后设置一下入口为_pgfault_upcall
        if(sys_env_set_pgfault_upcall(0, _pgfault_upcall) < 0)
            panic("set_pgfault_handler: sys_env_set_pgfault_upcall failed");
    }   

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

按照上述例子一个个测试。

确保您了解为什么user / faultalloc和user / faultallocbad行为不同。这个答案也很简单,falutallocabad是通过sys_cputs系统调用来打印的,然后回顾自己写的这个系统调用,里面会先有个user_mem_assert检查,检查到没有权限就会销毁environment。

--------------------------------------------------------------------------------------------------------------------------------

TODO   challenge

--------------------------------------------------------------------------------------------------------------------------------

Implementing Copy-on-Write Fork

现在,您有了相关的kernel的工具,可以完全在用户空间中实现copy-on-write fork()。

我们在lib / fork.c中为您的fork()提供了一个框架。 像dumbfork()一样,fork()应该创建一个新environment,然后扫描父environment的整个地址空间,并在子environment中设置相应的页面映射。 关键区别在于,dumbfork()复制了页面的内容,而fork()最初仅复制页面映射。 仅当其中一种environment尝试写入页面时,fork()才会复制每个页面的内容。

fork()的基本控制流程如下:

        1.父environment使用您在上面实现的set_pgfault_handler()函数将pgfault()设置为C-level页面错误处理程序。

        2.父environment调用sys_exofork()创建一个子environment。

        3.对于位于其UTOP下地址空间中的每个可写或copy-on-write页面,父environment调用duppage,应将页面映射到子地址空间后后重新映射到自己的页面里。 duppage设置两个PTE,以使该页面不可写,并在“ avail”字段中包含PTE_COW,以区分copy-on-write页面和真正的只读页面。

        但是,异常栈不会以这种方式重新映射。 相反,您需要在子environment中为异常栈分配一个新页面。 由于页面错误处理程序将进行真正的复制,并且页面错误处理程序在异常栈上运行,因此无法将异常栈copy-on-write:所以谁可以复制它?

        fork()还需要处理存在的页面,但这些页面不可写或copy-on-write。

        4.父environment为子environment设置用户页面错误入口点,使其看起来像自己的子environment。

        5.子environment现在可以运行了,因此父environment将其标记为可运行。

每次其中一个environment写入尚未写入的copy-on-write页面,都会发生页面错误。

下面是用户页面错误处理程序的控制流:

        1.kernel将页面错误传播到_pgfault_upcall,它调用fork()的pgfault()处理程序。

        2. pgfault()检查错误是否是写操作(检查错误代码中的FEC_WR),以及页面的PTE是否标记为PTE_COW。 如果没有,则panic。

        3. pgfault()分配一个映射到临时位置的新页面,并将有问题的页面内容复制到其中。 然后,故障处理程序将新页面映射到具有读/写权限的适当地址,以代替旧的只读映射。

Exercise 12. 在lib / fork.c中实现fork,duppage和pgfault。

这个内容还是挺多的。。。。最近因为工作都没时间搞了,隔了很久又重新看了一遍之前写的函数。。结合上面说的流程外加c文件里的注释基本上可以解决问题。。。但是还是有部分坑点要自己处理。

envid_t
fork(void)
{
    // LAB 4: Your code here.
    //根据文档的描述,duppage好像不应该处理除了可写或者copy-on-write的部分,但这里都交给duppage一块处理了
    set_pgfault_handler(pgfault);
    envid_t envid = sys_exofork();
    if(envid < 0)
        //失败
        panic("fork: sys_exofork error");
    else if(!envid)
        //子进程
        thisenv = &envs[ENVX(sys_getenvid())];
    else{
        //父进程
        unsigned addr;
        for(addr = UTEXT; addr < USTACKTOP; addr+= PGSIZE){
            if((uvpd[PDX(addr)] & PTE_P) && (uvpt[PGNUM(addr)] & PTE_P) && (uvpt[PGNUM(addr)] & PTE_U)) 
                duppage(envid, PGNUM(addr));
        }

        if(sys_page_alloc(envid, (void *)(UXSTACKTOP - PGSIZE), PTE_P|PTE_U|PTE_W))
            panic("fork: sys_page_alloc error");
        extern void _pgfault_upcall();
        sys_env_set_pgfault_upcall(envid, _pgfault_upcall);
        if(sys_env_set_status(envid, ENV_RUNNABLE))
            panic("fork: sys_env_set_status error");
    }   
    return envid;
}
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 | PTE_COW)){
        //如果是可写或者是copy-on-write
        //先对子进程映射
        if(sys_page_map(0, addr, envid, addr, PTE_COW|PTE_U|PTE_P))
            panic("duppage: sys_page_map child error");
        //重新映射父进程
        if(sys_page_map(0, addr, 0, addr, PTE_COW|PTE_U|PTE_P))
            panic("duppage: sys_page_map remap parent error");
    }
    else{
        //如果是其他情况则直接把拷贝父进程
        if(sys_page_map(0, addr, envid, addr, PTE_U|PTE_P))
            panic("duppage: other sys_page_map error");
    }
    return 0;
}

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) && (uvpd[PDX(addr)] & PTE_P) && (uvpt[PGNUM(addr)] & PTE_P) && (uvpt[PGNUM(addr)] & PTE_COW)))
        panic("pgfault: page cow check failed");

    // 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.
    //   No need to explicitly delete the old page's mapping.

    // LAB 4: Your code here.
    addr = ROUNDDOWN(addr, PGSIZE);
    if(sys_page_alloc(0, PFTEMP, PTE_P|PTE_U|PTE_W))
        panic("pgfault: sys_page_alloc error");

    memmove(PFTEMP, addr, PGSIZE);
    if(sys_page_map(0, PFTEMP, 0, addr, PTE_P|PTE_U|PTE_W))
        panic("pgfault: sys_page_map error");

    if(sys_page_unmap(0, PFTEMP))
        panic("pgfault: sys_page_unmap error");
}

--------------------------------------------------------------------------------------------------------------------------------

TODO   challenge

--------------------------------------------------------------------------------------------------------------------------------

part B到此结束,你可以使用make grade来评分了。

 

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

在lab 4的最后一部分中,您将修改kernel去抢占不合作的environment,并允许environment之间显式地传递消息。

Clock Interrupts and Preemption

运行user/spin测试程序。 该测试程序fork一个子environment中,该子environment一旦获得CPU的控制权,就会在紧密的循环中永久旋转。 父environment和kernel都无法重新获得CPU。就保护系统免受用户态environment中的错误或恶意代码的影响而言,这显然不是理想的情况,因为任何用户态environment都可能会使整个系统停止运行通过这种进入无限循环的方法。 为了允许kernel抢占正在运行的environment,并从中强行夺回CPU的控制权,我们必须扩展JOS kernel以支持时钟硬件的外部硬件中断。

Interrupt discipline

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

在inc / trap.h中,IRQ_OFFSET定义为十进制32。因此IDT条目32-47对应于IRQ 0-15。 例如,时钟中断为IRQ0。因此,IDT [IRQ_OFFSET + 0](即IDT [32])包含kernel中时钟中断处理程序例程的地址。 选择此IRQ_OFFSET的目的是使设备中断不会与处理器异常重叠,因为处理器异常显然会引起混乱。 (实际上,在运行MS-DOS的PC的早期,IRQ_OFFSET实际上为零,这确实在处理硬件中断和处理处理器异常之间引起了巨大的混乱!)

与xv6 Unix相比,在JOS中,我们进行了关键简化。 在kernel中时,始终禁用外部设备中断(与xv6类似,在用户空间中时,则启用外部设备中断)。 外部中断由%eflags寄存器的FL_IF标志位控制(请参见inc / mmu.h)。 当该位置1时,使能外部中断。 尽管可以通过多种方式修改该位,但是由于我们的简化,我们仅在进入和退出用户态时通过保存和恢复%eflags寄存器的过程来处理该位。

您必须确保在用户environment运行时在用户environment中设置了FL_IF标志,以便在中断到达时将其传递给处理器并由您的中断代码进行处理。 否则,将屏蔽或忽略中断,直到重新启用中断。 我们使用引导加载程序的第一条指令屏蔽了中断,到目前为止,我们还没有解决过重新启用它们的问题。

Exercise 13.修改kern / trapentry.S和kern / trap.c来初始化IDT中的适当条目,并为IRQ 0到15提供处理程序。然后修改kern / env.c中env_alloc()中的代码以确保该用户environment始终在启用中断的情况下运行。

调用硬件中断处理程序时,处理器从不push错误代码或检查IDT条目的描述符特权级别(DPL)。

完成此练习后,如果您使用运行时间很短(例如,spin)的任何测试程序来运行kernel,则应该看到kernel打印trap frame以了解硬件中断。 现在在处理器中启用了中断,但是JOS尚未处理它们,因此您应该看到它将每个中断错误地分配给了当前正在运行的用户environment并销毁了它。 最终,它应该跑完environment后销毁进入monitor。

这个如果前面几个实验没写错,那么其实是很简单的,不然就会调到哭。(本人前面共计前面写错了3个地方,然后因为工作繁忙调通花了大量时间。。。)

首先在trapentry.S增加入口。

TRAPHANDLER_NOEC(irq_timer, IRQ_TIMER + IRQ_OFFSET)
TRAPHANDLER_NOEC(irq_kbd, IRQ_KBD + IRQ_OFFSET)
TRAPHANDLER_NOEC(irq_serial, IRQ_SERIAL + IRQ_OFFSET)
TRAPHANDLER_NOEC(irq_spurious, IRQ_SPURIOUS + IRQ_OFFSET)
TRAPHANDLER_NOEC(irq_ide, IRQ_IDE + IRQ_OFFSET)
TRAPHANDLER_NOEC(irq_error, IRQ_ERROR + IRQ_OFFSET)

trap.c文件也增加入口。

void irq_timer();
void irq_kbd();
void irq_serial();
void irq_spurious();
void irq_ide();
void irq_error();

env_alloc函数增加IF标记。

    // Enable interrupts while in user mode.
    // LAB 4: Your code here.
    e->env_tf.tf_eflags |= FL_IF;

Handling Clock Interrupts

在user/spin程序中,子进程首次运行后,它只是循环旋转,kernel再也无法获得控制权。 我们需要对硬件进行编程以定期生成时钟中断,这将迫使控制权回到kernel,在kernel中我们可以将控制权切换到其他用户environment。

我们为您编写的对lapic_init和pic_init的调用(来自init.c中的i386_init)设置了时钟,并设置了中断控制器以生成中断。 现在,您需要编写代码来处理这些中断。

Exercise 14.修改内核的trap_dispatch()函数,以便在发生时钟中断时调用sched_yield()来查找并运行其他environment。

现在,您应该能够使user/spin测试正常工作。最终进程正常退出。

这是进行回归测试的好时机。 确保您没有通过启用中断来破坏该lab以前可以工作的任何部分(例如forktree)。 另外,请尝试使用make CPUS = 2 target与多个CPU一起运行。 您现在还应该能够通过压测。 运行make grade看是否正确。 您现在应该在本练习中获得65/75分的总成绩。

代码如下,先确认中断,然后再调sched_yield()。

        case IRQ_OFFSET + IRQ_TIMER: {
            // Handle clock interrupts. Don't forget to acknowledge the
            // interrupt using lapic_eoi() before calling the scheduler!
            // LAB 4: Your code here.
            lapic_eoi();
            sched_yield();
            break;
        }

Inter-Process communication (IPC)

我们一直专注于操作系统的隔离方面,即它给人的感觉是每个程序都拥有一台机器。 操作系统的另一项重要服务是允许程序在需要时相互通信。 让程序与其他程序交互可能会非常强大。 Unix管道模型就是典型的例子。

进程间通信的模型很多。 即使在今天,仍然存在关于哪种模型最好的争论。 我们不会参加那个辩论。 相反,我们将实现一个简单的IPC机制,然后尝试一下。

IPC in JOS

您将实现一些其他的JOS kernel系统调用,这些调用共同提供了一种简单的进程间通信机制。 您将实现两个系统调用sys_ipc_recv和sys_ipc_try_send。 然后,您将实现两个库包装器ipc_recv和ipc_send。

用户environment可以使用JOS的IPC机制相互发送的“消息”包括两个组件:单个32位值,以及可选的单个页面映射。 允许environment在消息中传递页面映射可以提供一种有效的方式来传输比单个32位整数更多的数据,并且还允许environment轻松地实现共享内存。

Sending and Receiving Messages

要接收消息,environment调用sys_ipc_recv。 此系统调用将对当前environment进行调度,并且在收到消息之前不会再次运行它。 当environment正在等待接收消息时,任何其他environment都可以向其发送消息-不仅是特定environment,而且不仅仅是与接收environment具有父/子关系的environment。 换句话说,您在A部分中实现的权限检查将不适用于IPC,因为IPC系统调用经过了精心设计,因此是“安全的”:一个environment不能仅仅通过向其发送消息而导致另一个environment发生故障。

要尝试发送值,environment会使用接收者的environment ID和要发送的值来调用sys_ipc_try_send。 如果指定的environment实际上正在接收(它已调用sys_ipc_recv并且尚未获得值),则发送发送消息并返回0。否则,发送返回-E_IPC_NOT_RECV以指示目标environment当前不希望接收值 。

用户空间中的库函数ipc_recv将负责调用sys_ipc_recv,然后在当前environment的结构Env中查找有关接收到的值的信息。

同样,库函数ipc_send将负责重复调用sys_ipc_try_send,直到发送成功。

Transferring Pages

当environment使用有效的dstva参数(位于UTOP之下)调用sys_ipc_recv时,该environment表明它愿意接收页面映射。 如果发送方发送一个页面,则该页面应映射到接收方地址空间中的dstva。 如果接收者已经在dstva上映射了页面,则之前的页将被取消映射。

当environment使用有效的srcva(位于UTOP之下)调用sys_ipc_try_send时,这意味着发送方希望将当前映射到srcva的页面发送给接收方,并具有权限perm。 成功完成IPC之后,发送方将其页面的原始映射保留在其地址空间中的srcva处,但是接收方还将在接收方的地址空间中由接收方最初指定的dstva处获得此物理页面的映射。 结果,该页面在发送者和接收者之间共享。

如果发送方或接收方均未指示应传送页面,则不会传送任何页面。 在完成任何IPC之后,内核会将接收者的Env结构中的新字段env_ipc_perm设置为接收到的页面的权限;如果未接收到任何页面,则将其设置为零。

Implementing IPC

Exercise 15.在kern / syscall.c中实现sys_ipc_recv和sys_ipc_try_send。 在实现它们之前,请先阅读它们的注释,因为它们必须协同工作。 在这些例程中调用envid2env时,应将checkperm标志设置为0,这意味着允许任何environment将IPC消息发送到任何其他environment,并且除了验证目标envid有效之外,kernel不执行任何特殊权限检查。

这两函数照着注释弄其实不难实现。

static int 
sys_ipc_try_send(envid_t envid, uint32_t value, void *srcva, unsigned perm)
{
    // LAB 4: Your code here.
    struct Env *target_env;
    int result = envid2env(envid, &target_env, 0); 
    if(result < 0)
        return result;
    if(!target_env->env_ipc_recving || target_env->env_ipc_from)
        return -E_IPC_NOT_RECV;
    if(srcva && (unsigned)srcva < UTOP){
        if((unsigned)srcva & (PGSIZE - 1)) 
            return -E_INVAL;
        if((perm & (PTE_U | PTE_P)) != (PTE_U | PTE_P) || (perm & (~(PTE_P | PTE_U | PTE_W | PTE_AVAIL))))
            return -E_INVAL;
        unsigned check_perm = PTE_U | PTE_P;
        if(perm & PTE_W)
            check_perm |= PTE_W;
        if(user_mem_check(curenv, srcva, PGSIZE, check_perm) < 0)
            return -E_INVAL;
        if(target_env->env_ipc_dstva){
            //如果对方设置了这个才表明要接受内存映
            struct PageInfo *page_info;
            pte_t *pte;
            if (!(page_info = page_lookup(curenv->env_pgdir, srcva, &pte)))
                return -E_INVAL;
            result = page_insert(target_env->env_pgdir, page_info, target_env->env_ipc_dstva, perm);
            if(result < 0)
                return -E_NO_MEM;
            target_env->env_ipc_perm = perm;
        }   
    }   
    target_env->env_ipc_value = value;
    target_env->env_ipc_from = curenv->env_id;
    //得告诉内核该进程可以进行调度了
    target_env->env_status = ENV_RUNNABLE;
    //避免在下一次调度之前有新的进程通过该IPC与它通信
    target_env->env_ipc_recving = false;
    //receive系统调用成功的情况下不会切回用户态, 用户态的返回值这里设置为0
    target_env->env_tf.tf_regs.reg_eax = 0;
    return 0;
}
static int
sys_ipc_recv(void *dstva)
{
    // LAB 4: Your code here.
    if(dstva && (unsigned)dstva < UTOP && (unsigned)dstva & (PGSIZE - 1))
        return -E_INVAL;
    curenv->env_ipc_recving = true;
    curenv->env_ipc_from = 0;
    curenv->env_ipc_dstva = dstva;
    curenv->env_status = ENV_NOT_RUNNABLE;
    sched_yield();
    return 0;
}

当然还得在dispatch里添加这两个入口。就不贴了。

然后在lib / ipc.c中实现ipc_recv和ipc_send函数。这两个就基本是用系统调用的,简单的再封了一层。

int32_t
ipc_recv(envid_t *from_env_store, void *pg, int *perm_store)
{
    // LAB 4: Your code here.
    int result = sys_ipc_recv(pg);
    if(from_env_store)
        *from_env_store = (result<0?0:thisenv->env_ipc_from);
    if(perm_store)
        *perm_store = (result<0?0:thisenv->env_ipc_perm);
    if(result < 0)
        return result;
    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_recv 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.
    unsigned failed_cnt = 0;
    while(sys_ipc_try_send(to_env, val, pg, perm) < 0){ 
        failed_cnt++;
        if(!(failed_cnt & 0xff))
            //失败到一定次数后放弃CPU
            sys_yield();
    }
}

使用user / pingpong和user / primes函数来测试IPC机制。 您可能会发现阅读user / primes.c很有趣,可以看到幕后所有的分支和IPC。

最后make grade。

--------------------------------------------------------------------------------------------------------------------------------

TODO   challenge

--------------------------------------------------------------------------------------------------------------------------------

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值