MIT6.828 LAB4_PartA Multiprocessor Support and Cooperative Multitasking


简介

激动人心的时刻,我们终于走到了Lab4,OS开始实现进程调度了~

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

  • PartA:

    • 为 JOS 增添多处理器支持特性。
    • 实现 round-robin scheduling循环调度。
    • 添加一个基本的环境(进程)管理系统调用(创建和销毁环境,分配和映射内存)。
  • PartB:

    • 实现一个类Unix的fork(),其允许一个用户模式的环境能创建一份它自身的拷贝。
  • PartC:

    • 支持进程间通信(inter-process communication, IPC)
    • 支持硬件时钟中断和抢占

    PartA:Multiprocessor Support and Cooperative Multitasking(多处理器支持和协作式多任务处理)

    扩展 JOS 使其能运行在多处理器上,实现新的系统调用使用户程序能创建新的进程,实现协作式循环调度。

    多处理器支持

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

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

在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处,我们需要分配映射空间。

2.1.1. Exercise 1.

Implement mmio_map_region in kern/pmap.c. To see how this is used, look at the beginning of lapic_init in kern/lapic.c. You'll have to do the next exercise, too, before the tests for mmio_map_region will run.
  // ret -> MMIOBASE 暂存
    void *ret = (void *)base;
    size = ROUNDUP(size, PGSIZE);
    if (base + size > MMIOLIM || base + size < base) {
        panic("mmio_map_region reservation overflow\n");
    }
    
    boot_map_region(kern_pgdir, base, size, pa, PTE_W|PTE_PCD|PTE_PWT);
    // base 为static!
    base += size;
    return ret;

2.2. 应用处理器(APs)引导程序

在启动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标志信号,然后再唤醒下一个。

2.2.1. Exercise 2

  • Read boot_aps() and mp_main() in kern/init.c, and the assembly code in kern/mpentry.S. Make sure you understand the control flow transfer during the bootstrap of APs.
  • Then modify your implementation of page_init() in kern/pmap.c to avoid adding the page at MPENTRY_PADDR to the free list, so that we can safely copy and run AP bootstrap code at that physical address.

整理一下程序运行过程,此过程一直都运行在CPU0,即BSP上,工作在保护模式。

  • i386_init调用了boot_aps(),也就是在BSP中引导其他CPU开始运行
  • boot_aps调用memmove将每个CPU的boot代码加载到固定位置
  • 最后调用lapic_startap执行其bootloader启动对应的CPU

bootloader的地址0x7000在低地址段为第5页,其页数小于npage_basemem: 160,所以将之前代码的前半部分修改为以下代码。

  size_t i;
    for (i = 1; i < MPENTRY_PADDR/PGSIZE; i++) {
        pages[i].pp_ref = 0;
        pages[i].pp_link = page_free_list;
        page_free_list = &pages[i];
    }zheng
    // boot APs entry code
    extern unsigned char mpentry_start[], mpentry_end[];
    size_t size = mpentry_end - mpentry_start;
    size = ROUNDUP(size, PGSIZE);
    for(;i<(MPENTRY_PADDR+size)/PGSIZE; i++) {
        pages[i].pp_ref = 1;
    }
    
    for (; i < npages_basemem; i++) {
        pages[i].pp_ref = 0;
        pages[i].pp_link = page_free_list;
        page_free_list = &pages[i];
    }

编译执行后,panic 在check_kern_pgdir

check_page_free_list() succeeded!
check_page_alloc() succeeded!
check_page() succeeded!
kernel panic on CPU 0 at kern/pmap.c:986: assertion failed: check_va2pa(pgdir, base + KSTKGAP + i) == PADDR(percpu_kstacks[n]) + i

2.2.2. Question

  1. 将kern/mpentry.S与boot/boot.S并排比较。 请记住,就像内核中的其他内容一样,kern/mpentry.S被编译、链接并运行在KERNBASE之上,宏MPBOOTPHYS的目的是什么? 为什么这在在kern/mpentry.S很关键?换句话说,如果在kern/mpentry.S中省略了什么可能会出错?提示:回忆链接地址与加载地址的区别。

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

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

2.3. Per-CPU State and Initialization

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

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

2.3.1. Exercise 3

Modify mem_init_mp() (in kern/pmap.c) to map per-CPU stacks starting at KSTACKTOP, as shown in inc/memlayout.h. The size of each stack is KSTKSIZE bytes plus KSTKGAP bytes of unmapped guard pages. Your code should pass the new check in check_kern_pgdir().
  1. 这个部分较简单,直接给代码。 一开始比较犹豫是否需要再映射一次CPU0的stack,毕竟之上已经映射过一次了。但发现网上的代码都再映射了一次。

    // LAB 4: Your code here:
     size_t i;
     size_t kstacktop_i;
     for(i = 0; i < NCPU; i++) {
         kstacktop_i = KSTACKTOP - i * (KSTKSIZE + KSTKGAP);
         boot_map_region(kern_pgdir, 
                         kstacktop_i - KSTKSIZE, 
                         KSTKSIZE,
                         PADDR(&percpu_kstacks[i]), 
                         PTE_W );
     
     }

2.结果。

check_page_free_list() succeeded!
check_page_alloc() succeeded!
check_page() succeeded!
check_kern_pgdir() succeeded!
check_page_free_list() succeeded!
check_page_installed_pgdir() succeeded!
SMP: CPU 0 found 1 CPU(s)
enabled interrupts: 1 2
[00000000] new env 00001000

Exercise 4

The code in trap_init_percpu() (kern/trap.c) initializes the TSS and TSS descriptor for the BSP. It worked in Lab 3, but is incorrect when running on other CPUs. Change the code so that it can work on all CPUs. (Note: your new code should not use the global ts variable any more.)

重用Lab3中的代码,将所有的全局ts替换为cpus[i].cpu_ts就行了。

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

   size_t i = cpunum();
    
    // Setup a TSS so that we get the right stack
    // when we trap to the kernel.
    thiscpu->cpu_ts.ts_esp0 = KSTACKTOP - i*(KSTKSIZE + KSTKGAP);
    thiscpu->cpu_ts.ts_ss0 = GD_KD;
    thiscpu->cpu_ts.ts_iomb = sizeof(struct Taskstate);
    // Initialize the TSS slot of the gdt.
    gdt[GD_TSS0 >> 3] = SEG16(STS_T32A, (uint32_t) (&thiscpu->cpu_ts),
                            sizeof(struct Taskstate) - 1, 0);
    gdt[GD_TSS0 >> 3].sd_s = 0;

执行结果如下。但是为什么会启动三次?发现经过Exercise 5 之后就没有重启多次的现象了,难道是因为没有Locking,导致在内核中发生了某种错误? 注释掉mp_main中的lock_kernel后输出明显增多。全注释之后,发现确实是这样!

...
check_page_free_list() succeeded!
check_page_installed_pgdir() succeeded!
SMP: CPU 0 found 4 CPU(s)
enabled interrupts: 1 2
SMP: CPU 1 starting
SMP: CPU 2 starting
SMP: CPU 3 starting
[00000000] new env 00001000
6828 decimal is XXX octal!
Physical memory: 131072K available, base = 640K, extended = 130432K
...
enabled interrupts: 1 2
SMP: CPU 1 starting
SMP: CPU 2 starting
SMP: CPU 3 starting
[00000000] new env 00001000

Locking

在mp_main函数中初始化AP后,代码就会进入自旋(APs在mp_main()中最后执行for (;;);,而BSP在sched_yield中调用了sched_halt();)。这个解释了Exercise 4中为什么启动了所有APs后停住了。

Exercise 5

Apply the big kernel lock as described above, by calling lock_kernel() and unlock_kernel() at the proper locations.
//i386_init
lock_kernel();
boot_aps();

//mp_main
lock_kernel();
sched_yield();

//trap
if ((tf->tf_cs & 3) == 3) {
    lock_kernel();
    assert(curenv);
    ......
}
//env_run
lcr3(PADDR(curenv->env_pgdir));
unlock_kernel();
env_pop_tf(&(curenv->env_tf));

Question

  1. big kernel lock似乎已经确保每次仅仅一个CPU能运行内核代码, 为什么我们仍然需要为每个CPU设定一个内核栈

因为每个CPU进入内核,其压栈的数据可能不一样,同时此CPU下一次再进入内核时可能需要用到之前的data。

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

Round-Robin Scheduling(循环调度)

Exercise 6

Implement round-robin scheduling in sched_yield() as described above. Don't forget to modify syscall() to
dispatch sys_yield().
  1. sched_yield代码如下所示。经过最后PART_A疑问部分第三问的分析,实际上我们可以去掉return这条语句。

    // LAB 4: Your code here.
     
     struct Env *now = thiscpu->cpu_env;
     int32_t startid = (now) ? ENVX(now->env_id): 0;
     int32_t nextid;
     size_t i;
     // 当前没有任何环境执行,应该从0开始查找
     for(i = 0; i < NENV; i++) {
         nextid = (startid+i)%NENV;
         if(envs[nextid].env_status == ENV_RUNNABLE) {
                 env_run(&envs[nextid]);
                 return;
             }
     }
     
     // 循环一圈后,没有可执行的环境
     if(envs[startid].env_status == ENV_RUNNING && envs[startid].env_cpunum == cpunum()) {
         env_run(&envs[startid]);
     }
     
     // sched_halt never returns
     sched_halt();
  2. kern/syscall.c中的syscall中加入一个case

    case SYS_yield: 
         sys_yield();
         break;
  3. init中创建用户环境

    ENV_CREATE(user_yield, ENV_TYPE_USER);
    ENV_CREATE(user_yield, ENV_TYPE_USER);
    ENV_CREATE(user_yield, ENV_TYPE_USER);
  4. make qemu 与 make qemu CPUS=2都可以出现以下结果。

    check_page_installed_pgdir() succeeded!
    SMP: CPU 0 found 1 CPU(s)
    enabled interrupts: 1 2
    ...
    Hello, I am environment 00001000.
    ...
    Hello, I am environment 00001003.
    Back in environment 00001000, iteration 0.
    ...
    Back in environment 00001000, iteration 1.
    ...
    Back in environment 00001002, iteration 3.
    ...
    Back in environment 00001000, iteration 4.
    All done in environment 00001000.
    [00001000] exiting gracefully
    [00001000] free env 00001000
    Back in environment 00001001, iteration 4.
    All done in environment 00001001.
    ...
    All done in environment 00001003.
    [00001003] exiting gracefully
    [00001003] free env 00001003
    No runnable environments in the system!
    Welcome to the JOS kernel monitor!
    

    Question

  5. Good Question. 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不一样,其余的都是一样的,只不过在用户态下是看不到的。所以虽然这个时候的页表换成了下一个要运行的进程的页表,但是curenv的地址没变,映射也没变,还是依然有效的。
  6. 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域中。

Challenge

Add a less trivial scheduling policy to the kernel, such as a fixed-priority scheduler that allows each environment to be assigned a priority and ensures that higher-priority environments are always chosen in preference to lower-priority environments.
思路: 在env的结构体中加入一个成员env_priority, 如果实现静态的优先级,直接在env_create中进行初始化就好了。 若需要动态变化,需要实现一个修改优先级的系统调用。 最后在sched_yield()中扫描整个envs数组,得到优先级最高并且states为runable的环境。

System Calls for Environment Creation

尽管现在我们的内核已经实现了在多个用户环境之间切换,但还是局限于内核最初建立的环境。以下我们要实现允许用户环境去创建并启动新的用户环境。
Unix使用fork实现,其copy了父进程的整个地址空间,唯一可见的不同在于父子进程的ID不一样。父进程中的fork返回子进程号,子进程中的fork返回0。By default, each process gets its own private address space, and neither process's modifications to memory are visible to the other.

需要实现以下几个系统调用

系统调用功能
sys_exofork创建一个新环境, 设定运行状态为ENV_NOT_RUNNABLE,拷贝父进程的寄存器值,置返回值eax为0。
sys_env_set_status修改环境的status
sys_page_alloc分配一个物理页,并插入到页表中(映射到虚拟地址上)
sys_page_map将源进程中的某个虚拟地址对应的页(注意不是拷贝页里面的内容!)映射到目标进程的某个虚拟地址上。
sys_page_unmap解除给定虚拟地址上的的映射

Exercise 7

Implement the system calls described above in kern/syscall.c and make sure syscall() calls them. For now, whenever you call envid2env(), pass 1 in the checkperm parameter.
  1. sys_exofork,将子进程的Trap Frame – env_tf中的eax寄存器设置为0,就可以实现系统调用sys_exofork给子进程返回0:

    // LAB 4: Your code here.
     struct Env *newenv;
     int32_t ret = 0;
     if ((ret = env_alloc(&newenv, curenv->env_id)) < 0) {
         // 两个函数的返回值是一样的
         return ret;
     }
     newenv->env_status = ENV_NOT_RUNNABLE;
     newenv->env_tf = curenv->env_tf;
     // newenv的返回值为0
     newenv->env_tf.tf_regs.reg_eax = 0;
     
     return newenv->env_id;
  2. sys_env_set_status

    // LAB 4: Your code here.
     int ret = 0;
     struct Env *env;
     if (status != ENV_RUNNABLE || status != ENV_NOT_RUNNABLE) 
         return -E_INVAL;
     
     if ((ret = envid2env(envid, &env, 1)) < 0) 
         return -E_BAD_ENV;
    
     env->env_status = status;
     return 0;
  3. sys_page_alloc

    // LAB 4: Your code here.
     int ret = 0;
     struct Env *env;
     
     if ((ret = envid2env(envid, &env, 1)) < 0) 
         return -E_BAD_ENV;
     
     if((uintptr_t)va >= UTOP || PGOFF(va))
         return -E_INVAL;
     if((perm & PTE_SYSCALL) == 0)
         return -E_INVAL;
     if (perm & ~PTE_SYSCALL)
         return -E_INVAL;
     
     struct PageInfo *pp = page_alloc(ALLOC_ZERO);
     if(!pp) 
         return -E_NO_MEM;
     
     if (page_insert(env->env_pgdir, pp, va, perm) < 0)
         return -E_NO_MEM;
    
     return 0;
  4. sys_page_map

    // LAB 4: Your code here.
     int ret = 0;
     struct Env *srcenv, *dstenv;
     struct PageInfo *srcpp, *dstpp;
     pte_t *pte;
     if ((envid2env(srcenvid, &srcenv, 1) < 0 )|| ( envid2env(dstenvid, &dstenv, 1) < 0)) 
         return -E_BAD_ENV;
     if ((uintptr_t)srcva >= UTOP || PGOFF(srcva) || (uintptr_t)dstva >= UTOP || PGOFF(dstva))
         return -E_INVAL;
     if ( (perm & PTE_SYSCALL)==0 || (perm & ~PTE_SYSCALL))
         return -E_INVAL;
     if (!(srcpp = page_lookup(srcenv->env_pgdir, srcva, &pte)))
         return -E_INVAL;
     if ((perm & PTE_W) && ((*pte & PTE_W) == 0))
         return -E_INVAL;
     if (page_insert(dstenv->env_pgdir, srcpp, dstva, perm) < 0)
         return -E_NO_MEM;
    
     return 0;
  5. sys_page_unmap

    // LAB 4: Your code here.
     int ret = 0;
     struct Env *env;
     
     if ((ret = envid2env(envid, &env, 1)) < 0) 
         return -E_BAD_ENV;
     if ((uintptr_t)va >= UTOP || PGOFF(va))
         return -E_INVAL;
     page_remove(env->env_pgdir, va);
     return 0;

结果,运行make run-dumbfork, 而usr/umbfork.c的逻辑是父进程创建1个子进程,然后每次打印1条信息后交出控制权,并且让父进程重复10次而子进程重复20次。但是出现了以下问题。

  1. 此时只打印了子进程,没有打印父进程的内容!
  2. 根据输出内容,1000应该是父进程,1001是子进程,为什么最后显示1000完成了! 注意,此处的1000并不对应进程id。取低十位应该 id 应该分别是0和1。
enabled interrupts: 1 2
[00000000] new env 00001000
[00001000] new env 00001001
0: I am the child!
1: I am the child!
...
11: I am the child!
12: I am the child!
[00001000] exiting gracefully
[00001000] free env 00001000
MISSING '0: I am the parent.'
MISSING '9: I am the parent.'
MISSING '.00001001. exiting gracefully'
MISSING '.00001001. free env 00001001'

一开始感觉是sched_yield的锅,但是在以上的练习中,确实实现了任务的切换啊!做到此处我觉得十分有必要分析一下dumbfork的实现过程。首先内核将使用我们的寄存器状态的副本对子进程进行初始化,这样子进程就像也调用了sys_exofork() - 除此之外,在子进程中对sys_exofork()的这种“假”调用将返回0而不是子进程的envid。

  • 首先从dumbfork开始Debug,在sys_exofork的调用后打印envid,发现竟然是 0!返回值为零就会进入以下代码段。

    if (envid == 0) {
          thisenv = &envs[ENVX(sys_getenvid())];
          return 0;
    }

    我的妈呀,分析分析,最后发现是因为我在syscall中没有return ! 而我又默认返回值为0,所以一直执行fork会返回0,心态爆炸。 不过经过这一波,还是学习到了不少新知识,fork原来这么神奇!!

加上return后,break也可以相应去掉,就能过PartA了。

case SYS_page_alloc:
        return sys_page_alloc((envid_t)a1, (void * )a2, (int )a3);
        break;
    case SYS_page_map:
        return sys_page_map((envid_t) a1, (void *) a2, (envid_t) a3, (void *) a4, (int) a5);
        break;
    case SYS_page_unmap:
        return sys_page_unmap((envid_t) a1, (void *) a2);
        break;
    case SYS_exofork:
        return sys_exofork();
        break;

小结

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

子进程不像我理解的真正进入了sys_exofork, 而是 fake 的。 子进程只需要保存父进程的 env_tf , 我一直想要知道的 ret实际上发生在sched_yield()中的env_run()函数 的env_pop_tf(&(curenv->env_tf));语句。 其使得 eip 指向tf->eip, 从而能继续执行sys_exofork的下一条语句。

PART_A 疑问

  1. 在Question 1中:
    现在遇到链接与加载的问题,还是会觉得有点绕,概念还是有点模糊。比如链接脚本将内核链接到0xF0100000到底是个什么样的概念呢?实际上可以理解为:为所有位置相关代码加上一个偏移。

    /* Link the kernel at this address: "." means the current address */
     . = 0xF0100000;
    
  2. Exercise4: 运行结果为什么会出现3次重复的操作。讲道理只会出现一次啊!

发现经过Exercise 5 之后就没有重启多次的现象了,难道是因为没有Locking,导致在内核中发生了某种错误? 注释掉mp_main中的lock_kernel后输出明显增多。全注释之后,又出现了重启多次的现象!

  1. 突然产生了一个疑问,系统是在什么时候进入用户态的?
    运行第一个用户程序时,进入用户态,但是这其中怎么实现切换的呢?iret指令?可能与下面这段代码有关。
    env_create中加载了二进制文件,其eip等信息存放在e->env_tf中, 在env_run中调用了env_pop_tf(&(curenv->env_tf));, 即执行完这个函数之后,eip已经指向了需要执行的用户程序。
This exits the kernel and starts executing some environment's code.
void
env_pop_tf(struct Trapframe *tf)
{
    // Record the CPU we are running on for user-space debugging
    curenv->env_cpunum = cpunum();

    asm volatile(
        "\tmovl %0,%%esp\n"
        "\tpopal\n"
        "\tpopl %%es\n"
        "\tpopl %%ds\n"
        "\taddl $0x8,%%esp\n" /* skip tf_trapno and tf_errcode */
        "\tiret\n"
        : : "g" (tf) : "memory");
    panic("iret failed");  /* mostly to placate the compiler */
}
  1. COW,spawn, 方法级切换,内核用户态切换,线程级&进程级,VM级,不同层次的切换涉及的上下文不一样。很多工作都与这个相关,像上次说的checkpoint,快照,或是code offloading。

声明:一丁点儿|版权所有,违者必究|如未注明,均为原创|本网站采用BY-NC-SA协议进行授权

转载:转载请注明原文链接 - MIT6.828 LAB4_PartA Multiprocessor Support and Cooperative Multitasking


勿在浮沙筑高台,每天进步一丁点儿!