6.828 操作系統 lab4 實驗報告:Part A

簡介


在 lab4 中我們將實現多個同時運行的用戶進程之間的搶占式多任務處理。
在 part A 中,我們需要給 JOS 增加多處理器支持。實現輪詢( round-robin, RR )調度,并增加基本的用戶程序管理系統調用( 創建和銷毀進程,分配和映射內存 )。
在 part B 中,我們需要實現一個與 Unix 類似的 fork(),允許一個用戶進程創建自己的拷貝。
在 part C中,我們會添加對進程間通信 ( IPC ) 的支持,允許不同的用戶進程相互通信和同步。還要增加對硬件時鐘中斷和搶占的支持。

Part A: 多處理器支持及協同多任務處理


我們首先需要把 JOS 擴展到在多處理器系統中運行。然后實現一些新的 JOS 系統調用來允許用戶進程創建新的進程。我們還要實現協同輪詢調度,在當前進程不使用 CPU 時允許內核切換到另一個進程。

多處理器支持

我們即將使 JOS 能夠支持“對稱多處理” (Symmetric MultiProcessing, SMP)。這種模式使所有 CPU 能對等地訪問內存、I/O 總線等系統資源。雖然 CPU 在 SMP 下以同樣的方式工b作,在啟動過程中他們可以被分為兩個類型:引導處理器(BootStrap Processor, BSP) 負責初始化系統以及啟動操作系統;應用處理器( Application Processors, AP ) 在操作系統拉起并運行后由 BSP 激活。哪個 CPU 作為 BSP 由硬件和 BIOS 決定。也就是說目前我們所有的 JOS 代碼都運行在 BSP 上。
在 SMP 系統中,每個 CPU 都有一個附屬的 LAPIC 單元。LAPIC 單元用于傳遞中斷,并給它所屬的 CPU 一個唯一的 ID。在 lab4 中,我們將會用到 LAPIC 單元的以下基本功能 ( 見`kern/lapic.c1 ):

  • 讀取 APIC ID 來判斷我們的代碼運行在哪個 CPU 之上。
  • 從 BSP 發送STARTUP 跨處理器中斷 (InterProcessor Interrupt, IPI) 來啟動 AP。
  • 在 part C 中,我們為 LAPIC 的內置計時器編程來觸發時鐘中斷以支持搶占式多任務處理。

處理器通過映射在內存上的 I/O (Memory-Mapped I/O, MMIO) 來訪問它的 LAPIC。在 MMIO 中,物理內存的一部分被硬連接到一些 I/O 設備的寄存器,因此,訪問內存的 load/store 指令可以被用于訪問設備的寄存器。實際上,我們在 lab1 中已經接觸過這樣的 IO hole,如0xA0000被用來寫 VGA 顯示緩沖。LAPIC 開始于物理地址 0xFE000000 ( 4GB以下32MB處 )。如果用以前的映射算法(將0xF0000000 映射到 0x00000000,也就是說內核空間最高只能到物理地址0x0FFFFFFF)顯然太高了。因此,JOS 在 MMIOBASE (即 虛擬地址0xEF800000) 預留了 4MB 來映射這類設備。我們需要寫一個函數來分配這個空間并在其中映射設備內存。

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.

lapic_init()函數的一開始就調用了該函數,將從 lapicaddr 開始的 4kB 物理地址映射到虛擬地址,并返回其起始地址。注意到,它是以頁為單位對齊的,每次都 map 一個頁的大小。

    // lapicaddr is the physical address of the LAPIC's 4K MMIO
    // region.  Map it in to virtual memory so we can access it.
    lapic = mmio_map_region(lapicaddr, 4096);

因此實際就是調用 boot_map_region 來建立所需要的映射,需要注意的是,每次需要更改base的值,使得每次都是映射到一個新的頁面。

void *
mmio_map_region(physaddr_t pa, size_t size)
{
    static uintptr_t base = MMIOBASE;

    size_t rounded_size = ROUNDUP(size, PGSIZE);

    if (base + rounded_size > MMIOLIM) panic("overflow MMIOLIM");
    boot_map_region(kern_pgdir, base, rounded_size, pa, PTE_W|PTE_PCD|PTE_PWT);
    uintptr_t res_region_base = base;   
    base += rounded_size;       
    return (void *)res_region_base;
}

引導應用處理器

在啟動 APs 之前,BSP 需要先搜集多處理器系統的信息,例如 CPU 的總數,CPU 各自的 APIC ID,LAPIC 單元的 MMIO 地址。kern/mpconfig.c 中的 mp_init() 函數通過閱讀 BIOS 區域內存中的 MP 配置表來獲取這些信息。
boot_aps() 函數驅動了 AP 的引導。APs 從實模式開始,如同 boot/boot.S 中 bootloader 的啟動過程。因此 boot_aps() 將 AP 的入口代碼 (kern/mpentry.S) 拷貝到實模式可以尋址的內存區域 (0x7000, MPENTRY_PADDR)。
此后,boot_aps() 通過發送 STARTUP 這個跨處理器中斷到各 LAPIC 單元的方式,逐個激活 APs。激活方式為:初始化 AP 的 CS:IP 值使其從入口代碼執行。通過一些簡單的設置,AP 開啟分頁進入保護模式,然后調用 C 語言編寫的 mp_main()boot_aps() 等待 AP 發送 CPU_STARTED 信號,然后再喚醒下一個。

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. Your code should pass the updated check_page_free_list() test (but might fail the updated check_kern_pgdir() test, which we will fix soon).

實際上就是標記 MPENTRY_PADDR 開始的一個物理頁為已使用,只需要在 page_init() 中做一個特例處理即可。唯一需要注意的就是確定這個特殊頁在哪個區間內。

...
size_t mp_page = MPENTRY_PADDR/PGSIZE;
for (i = 1; i < npages_basemem; i++) {
    if (i == mp_page) {
        pages[i].pp_ref = 1;
        continue;
    }
    pages[i].pp_ref = 0;
    pages[i].pp_link = page_free_list;
    page_free_list = &pages[i];
}
...

現在執行 make qemu,可以通過 check_kern_pgdir() 測試了,Exercise 1, 2 完成。

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

注意 kern/mpentry.S 注釋中的一段話,說明了這兩者的區別。

# This code is similar to boot/boot.S except that
#    - it does not need to enable A20
#    - it uses MPBOOTPHYS to calculate absolute addresses of its
#      symbols, rather than relying on the linker to fill them

此外,還有個關鍵問題就是 MPBOOTPHYS 宏的作用。
kern/mpentry.S 是運行在 KERNBASE 之上的,與其他的內核代碼一樣。也就是說,類似于 mpentry_start, mpentry_end, start32 這類地址,都位于 0xf0000000 之上,顯然,實模式是無法尋址的。再仔細看 MPBOOTPHYS 的定義:

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

其意義可以表示為,從 mpentry_startMPENTRY_PADDR 建立映射,將 mpentry_start + offset 地址轉為 MPENTRY_PADDR + offset 地址。查看kern/init.c,發現已經完成了這部分地址的內容拷貝。

static void
boot_aps(void)
{
    extern unsigned char mpentry_start[], mpentry_end[];
    void *code;
    struct CpuInfo *c;

    // Write entry code to unused memory at MPENTRY_PADDR
    code = KADDR(MPENTRY_PADDR);
    memmove(code, mpentry_start, mpentry_end - mpentry_start);

    ...
}

因此,實模式下就可以通過 MPBOOTPHYS 宏的轉換,運行這部分代碼。boot.S 中不需要這個轉換是因為代碼的本來就被加載在實模式可以尋址的地方。

CPU 狀態和初始化

當寫一個多處理器操作系統時,分清 CPU 的私有狀態 ( per-CPU state) 及全局狀態 (global state) 非常關鍵。 kern/cpu.h 定義了大部分的 per-CPU 狀態。
我們需要注意的 per-CPU 狀態有:

  • Per-CPU 內核棧
    因為多 CPU 可能同時陷入內核態,我們需要給每個處理器一個獨立的內核棧。percpu_kstacks[NCPU][KSTKSIZE]
    在 Lab2 中,我們將 BSP 的內核棧映射到了 KSTACKTOP 下方。相似地,在 Lab4 中,我們需要把每個 CPU 的內核棧都映射到這個區域,每個棧之間留下一個空頁作為緩沖區避免 overflow。CPU 0 ,即 BSP 的棧還是從 KSTACKTOP 開始,間隔 KSTACKGAP 的距離就是 CPU 1 的棧,以此類推。

  • Per-CPU TSS 以及 TSS 描述符
    為了指明每個 CPU 的內核棧位置,需要任務狀態段 (Task State Segment, TSS),其功能在 Lab3 中已經詳細講過。

  • Per-CPU 當前環境指針
    因為每個 CPU 能夠同時運行各自的用戶進程,我們重新定義了基于cpus[cpunum()]curenv

  • Per-CPU 系統寄存器
    所有的寄存器,包括系統寄存器,都是 CPU 私有的。因此,初始化這些寄存器的指令,例如 lcr3(), ltr(), lgdt(), lidt() 等,必須在每個 CPU 都執行一次。

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().

比較簡單的一個練習,起初只 map 了BSP,這次是 map 所有的 cpu(包括實際不存在的)。 在 kern/cpu.h 中可以找到對 NCPU 以及全局變量percpu_kstacks的聲明。

// Maximum number of CPUs
#define NCPU  8
...
// Per-CPU kernel stacks
extern unsigned char percpu_kstacks[NCPU][KSTKSIZE];

percpu_kstacks的定義在 kern/mpconfig.c 中可以找到:

// Per-CPU kernel stacks
unsigned char percpu_kstacks[NCPU][KSTKSIZE]
__attribute__ ((aligned(PGSIZE)));

此后就是修改 kern/pmap.c 中的函數,代碼很簡單:

static void
mem_init_mp(void)
{
    uintptr_t start_addr = KSTACKTOP - KSTKSIZE;    
    for (size_t i=0; i<NCPU; i++) {
        boot_map_region(kern_pgdir, (uintptr_t) start_addr, KSTKSIZE, PADDR(percpu_kstacks[i]), PTE_W | PTE_P);
        start_addr -= KSTKSIZE + KSTKGAP;
    }
}

但是有個違和感很強的地方,之前已經把 BSP,也就是 cpu 0 的內核棧映射到了bootstack對應的物理地址:

boot_map_region(kern_pgdir, (uintptr_t) (KSTACKTOP-KSTKSIZE), KSTKSIZE, PADDR(bootstack), PTE_W | PTE_P);

然而這里又映射到了另一片物理地址,具體可以打印出來觀察:

BSP: map 0xefff8000 to physical address 0x115000
...
cpu 0: map 0xefff8000 to physical address 0x22c000

這樣做會不會有什么問題呢?
實際上,觀察函數 boot_map_region() 可以看出,其實新地址覆蓋了舊地址。 而頁面引用是對虛擬內存來講的,因此更換物理地址并不需要增加或減少頁面引用,這種寫法不會有任何問題。當然,我們也可以把之前對 BSP 棧的映射直接注釋掉,也能通過檢查。

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.)

先注釋掉 ts,再根據單個cpu的代碼做改動。在 inc/memlayout.h 中可以找到 GD_TSS0 的定義:

#define GD_TSS0   0x28     // Task segment selector for CPU 0

但是并沒有其他地方說明其他 CPU 的任務段選擇器在哪。因此最大的難點就是找到這個值。實際上,偏移就是 cpu_id << 3

// static struct Taskstate ts;
...
    struct Taskstate* this_ts = &thiscpu->cpu_ts;

    // Setup a TSS so that we get the right stack
    // when we trap to the kernel.
    this_ts->ts_esp0 = KSTACKTOP - thiscpu->cpu_id*(KSTKSIZE + KSTKGAP);
    this_ts->ts_ss0 = GD_KD;
    this_ts->ts_iomb = sizeof(struct Taskstate);

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

    // Load the IDT
    lidt(&idt_pd);

運行 make qemu CPUS=4 成功(雖然我只有2核,似乎初始化的 cpu 個數完全靠用戶指定)。

我們現在的代碼在初始化 AP 后就會開始自旋。在進一步操作 AP 之前,我們要先處理幾個 CPU 同時運行內核代碼的競爭情況。最簡單的方法是用一個大內核鎖 (big kernel lock)。它是一個全局鎖,在某個進程進入內核態時鎖定,返回用戶態時釋放。這種模式下,用戶進程可以并發地在 CPU 上運行,但是同一時間僅有一個進程可以在內核態,其他需要進入內核態的進程只能等待。
kern/spinlock.h 聲明了一個大內核鎖 kernel_lock。它提供了 lock_kernel()unlock_kernel() 方法用于獲得和釋放鎖。在以下 4 個地方需要使用到大內核鎖:

  • i386_init(),BSP 喚醒其他 CPU 之前獲得內核鎖
  • mp_main(),初始化 AP 之后獲得內核鎖,之后調用 sched_yield() 在 AP 上運行進程。
  • trap(),當從用戶態陷入內核態時獲得內核鎖,通過檢查 tf_Cs 的低 2bit 來確定該 trap 是由用戶進程還是內核觸發。
  • env_run(),在切換回用戶模式前釋放內核鎖。

Exercise 5.
Apply the big kernel lock as described above, by calling lock_kernel() and unlock_kernel() at the proper locations.

實現比較簡單,不用細講。
關鍵要理解兩點:

  • 大內核鎖的實現
void
spin_lock(struct spinlock *lk)
{
#ifdef DEBUG_SPINLOCK
    if (holding(lk))
        panic("CPU %d cannot acquire %s: already holding", cpunum(), lk->name);
#endif

    // The xchg is atomic.
    // It also serializes, so that reads after acquire are not
    // reordered before it. 
    // 關鍵代碼,體現了循環等待的思想
    while (xchg(&lk->locked, 1) != 0)
        asm volatile ("pause");

    // Record info about lock acquisition for debugging.
#ifdef DEBUG_SPINLOCK
    lk->cpu = thiscpu;
    get_caller_pcs(lk->pcs);
#endif
}

其中,在 inc/x86.h 中可以找到 xchg() 函數的實現,使用它而不是用簡單的 if + 賦值 是因為它是一個原子性的操作。

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

這是一段內聯匯編,語法在 Lab3 中已經講解過。lock 確保了操作的原子性,其意義是將 addr 存儲的值與 newval 交換,并返回 addr 中原本的值。于是,如果最初 locked = 0,即未加鎖,就能跳出這個 while循環。否則就會利用 pause 命令自旋等待。這就確保了當一個 CPU 獲得了 BKL,其他 CPU 如果也要獲得就只能自旋等待。

  • 為什么要在這幾處加大內核鎖
    為了避免多個 CPU 同時運行內核代碼,這基本是廢話。從根本上來講,其設計的初衷就是保證獨立性。由于分頁機制的存在,內核以及每個用戶進程都有自己的獨立空間。而多進程并發的時候,如果兩個進程同時陷入內核態,就無法保證獨立性了。例如內核中有某個全局變量 A,cpu1 讓 A=1, 而后 cpu2 卻讓 A=2,顯然會互相影響。最初 Linux 設計者為了使系統盡快支持 SMP,直接在內核入口放了一把大鎖,保證其獨立性。參見這篇非常好的文章 大內核鎖將何去何從
    其流程大致為:
    BPS 啟動 AP 前,獲取內核鎖,所以 AP 會在 mp_main 執行調度之前阻塞,在啟動完 AP 后,BPS 執行調度,運行第一個進程,env_run() 函數中會釋放內核鎖,這樣一來,其中一個 AP 就可以開始執行調度,運行其他進程。

Question 2.
It seems that using the big kernel lock guarantees that only one CPU can run the kernel code at a time. Why do we still need separate kernel stacks for each CPU? Describe a scenario in which using a shared kernel stack will go wrong, even with the protection of the big kernel lock

例如,在某進程即將陷入內核態的時候(尚未獲得鎖),其實在 trap() 函數之前已經在 trapentry.S 中對內核棧進行了操作,壓入了寄存器信息。如果共用一個內核棧,那顯然會導致信息錯誤。

輪詢調度

下一個任務是讓 JOS 內核能夠以輪詢方式在多個任務之間切換。其原理如下:

  • kern/sched.c 中的 sched_yield() 函數用來選擇一個新的進程運行。它將從上一個運行的進程開始,按順序循環搜索 envs[] 數組,選取第一個狀態為 ENV_RUNNABLE 的進程執行。

  • sched_yield()不能同時在兩個CPU上運行同一個進程。如果一個進程已經在某個 CPU 上運行,其狀態會變為 ENV_RUNNING

  • 程序中已經實現了一個新的系統調用 sys_yield(),進程可以用它來喚起內核的 sched_yield() 函數,從而將 CPU 資源移交給一個其他的進程。

Exercise 6.
Implement round-robin scheduling in sched_yield() as described above. Don't forget to modify syscall() to dispatch sys_yield().
Make sure to invoke sched_yield() in mp_main.
Modify kern/init.c to create three (or more!) environments that all run the program user/yield.c.

注意以下幾個問題:

  • 如何找到目前正在運行的進程在 envs[] 中的序號?
    kern/env.h 中,可以找到指向 struct Env的指針 curenv,表示當前正在運行的進程。但是需要注意,不能直接由 curenv->env_id得到其序號。在 inc/env.h 中有一個宏可以完成這個轉換。
// The environment index ENVX(eid) equals the environment's offset in the 'envs[]' array.
#define ENVX(envid)     ((envid) & (NENV - 1))
  • 查看 kern/env.c 可以發現 curenv 可能為 NULL。因此要注意特例。

kern/sched.c 中實現輪詢調度。

void
sched_yield(void)
{
    struct Env *idle;

    // LAB 4: Your code here.
    idle = curenv;
    size_t idx = idle!=NULL ? ENVX(idle->env_id):-1;
    for (size_t i=0; i<NENV; i++) {
        idx = (idx+1 == NENV) ? 0:idx+1;
        if (envs[idx].env_status == ENV_RUNNABLE) {
            env_run(&envs[idx]);
            return;
        }
    }
    if (idle && idle->env_status == ENV_RUNNING) {
        env_run(idle);
        return;
    }
    // sched_halt never returns
    sched_halt();
}

kern/syscall.c 中添加新的系統調用。

// syscall()
...
    case SYS_yield:
        sys_yield();
        break;
...

kern/init.c 中運行的用戶進程改為以下:

// i386_init()
...
#if defined(TEST)
    // Don't touch -- used by grading script!
    ENV_CREATE(TEST, ENV_TYPE_USER);
#else
    // Touch all you want.
    ENV_CREATE(user_primes, ENV_TYPE_USER);
#endif // TEST*
    ENV_CREATE(user_yield, ENV_TYPE_USER);
    ENV_CREATE(user_yield, ENV_TYPE_USER);
    ENV_CREATE(user_yield, ENV_TYPE_USER);
...

運行 make qemu CPUS=2 可以看到三個進程通過調用 sys_yield 切換了5次。

Hello, I am environment 00001000.
Hello, I am environment 00001001.
Back in environment 00001000, iteration 0.
Hello, I am environment 00001002.
Back in environment 00001001, iteration 0.
Back in environment 00001000, iteration 1.
Back in environment 00001002, iteration 0.
Back in environment 00001001, iteration 1.
Back in environment 00001000, iteration 2.
Back in environment 00001002, iteration 1.
Back in environment 00001001, iteration 2.
Back in environment 00001000, iteration 3.
Back in environment 00001002, iteration 2.
Back in environment 00001001, iteration 3.
Back in environment 00001000, iteration 4.
Back in environment 00001002, iteration 3.
All done in environment 00001000.
[00001000] exiting gracefully
[00001000] free env 00001000
Back in environment 00001001, iteration 4.
Back in environment 00001002, iteration 4.
All done in environment 00001001.
All done in environment 00001002.
[00001001] exiting gracefully
[00001001] free env 00001001
[00001002] exiting gracefully
[00001002] free env 00001002
No runnable environments in the system!
Welcome to the JOS kernel monitor!
Type 'help' for a list of commands.
K> 

記錄一下自己遇到的問題:
這個 exercise 出現了 triple fault 報錯,查了很久原因。由于是triple fault 肯定是 trap 過程中的錯誤,仔細檢查發現是自己的 exercise4 的做法出現了問題,一個非常二的錯誤。

// 錯誤版本,顯然沒有更改 thiscpu 中的值
    struct Taskstate this_ts = thiscpu->cpu_ts;
// 正確版本
    struct Taskstate* this_ts = &thiscpu->cpu_ts;

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

大意是問為什么通過 lcr3() 切換了頁目錄,還能照常對 e 解引用。回想在 lab3 中,曾經寫過的函數 env_setup_vm()。它直接以內核的頁目錄作為模版稍做修改。因此兩個頁目錄的 e 地址映射到同一物理地址。

static int
env_setup_vm(struct Env *e)
{
    int i;
    struct PageInfo *p = NULL;

    // Allocate a page for the page directory
    if (!(p = page_alloc(ALLOC_ZERO)))
        return -E_NO_MEM;

    // LAB 3: Your code here.
    e->env_pgdir = page2kva(p);
    memcpy(e->env_pgdir, kern_pgdir, PGSIZE); // use kern_pgdir as template 
    p->pp_ref++;
    // UVPT maps the env's own page table read-only.
    // Permissions: kernel R, user R
    e->env_pgdir[PDX(UVPT)] = PADDR(e->env_pgdir) | PTE_P | PTE_U;

    return 0;
}

Question 4.
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?

在進程陷入內核時,會保存當前的運行信息,這些信息都保存在內核棧上。而當從內核態回到用戶態時,會恢復之前保存的運行信息。
具體到 JOS 代碼中,保存發生在 kern/trapentry.S,恢復發生在 kern/env.c。可以對比兩者的代碼。
保存:

#define TRAPHANDLER_NOEC(name, num)
    .globl name;                            
    .type name, @function;                      
    .align 2;                           
    name:                               
    pushl $0;                           
    pushl $(num);                           
    jmp _alltraps
...

_alltraps:
pushl %ds    // 保存當前段寄存器
pushl %es
pushal    // 保存其他寄存器

movw $GD_KD, %ax
movw %ax, %ds
movw %ax, %es
pushl %esp    //  保存當前棧頂指針
call trap

恢復:

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

系統調用:創建進程

現在我們的內核已經可以運行多個進程,并在其中切換了。不過,現在它仍然只能運行內核最初設定好的程序 (kern/init.c) 。現在我們即將實現一個新的系統調用,它允許進程創建并開始新的進程。
Unix 提供了 fork() 這個原始的系統調用來創建進程。fork()將會拷貝父進程的整個地址空間來創建子進程。在用戶空間里,父子進程之間的唯一區別就是它們的進程 ID。fork()在父進程中返回其子進程的進程 ID,而在子進程中返回 0。父子進程之間是完全獨立的,任意一方修改內存,另一方都不會受到影響。
我們將為 JOS 實現一個更原始的系統調用來創建新的進程。涉及到的系統調用如下:

  • sys_exofork:
    這個系統調用將會創建一個空白進程:在其用戶空間中沒有映射任何物理內存,并且它是不可運行的。剛開始時,它擁有和父進程相同的寄存器狀態。sys_exofork 將會在父進程返回其子進程的envid_t,子進程返回 0(當然,由于子進程還無法運行,也無法返回值,直到運行:)
  • sys_env_set_status:
    設置指定進程的狀態。這個系統調用通常用于在新進程的地址空間和寄存器初始化完成后,將其標記為可運行。
  • sys_page_alloc:
    分配一個物理頁并將其映射到指定進程的指定虛擬地址上。
  • sys_page_map:
    從一個進程中拷貝一個頁面映射(而非物理頁的內容)到另一個。即共享內存。
  • sys_page_unmap:
    刪除到指定進程的指定虛擬地址的映射。

Exercise 7.
Implement the system calls described above in kern/syscall.c. You will need to use various functions in kern/pmap.c and kern/env.c, particularly envid2env(). For now, whenever you call envid2env(), pass 1 in the checkperm parameter. Be sure you check for any invalid system call arguments, returning -E_INVAL in that case. Test your JOS kernel with user/dumbfork and make sure it works before proceeding.

一個比較冗長的練習。重點應該放在閱讀 user/dumbfork.c 上,以便理解各個系統調用的作用。
user/dumbfork.c 中,核心是 duppage() 函數。它利用 sys_page_alloc() 為子進程分配空閑物理頁,再使用sys_page_map() 將該新物理頁映射到內核 (內核的 env_id = 0) 的交換區 UTEMP,方便在內核態進行 memmove 拷貝操作。在拷貝結束后,利用 sys_page_unmap() 將交換區的映射刪除。

void
duppage(envid_t dstenv, void *addr)
{
    int r;

    // This is NOT what you should do in your fork.
    if ((r = sys_page_alloc(dstenv, addr, PTE_P|PTE_U|PTE_W)) < 0)
        panic("sys_page_alloc: %e", r);
    if ((r = sys_page_map(dstenv, addr, 0, UTEMP, PTE_P|PTE_U|PTE_W)) < 0)
        panic("sys_page_map: %e", r);
    memmove(UTEMP, addr, PGSIZE);
    if ((r = sys_page_unmap(0, UTEMP)) < 0)
        panic("sys_page_unmap: %e", r);
}

sys_exofork() 函數

該函數主要是分配了一個新的進程,但是沒有做內存復制等處理。唯一值得注意的就是如何使子進程返回0。
sys_exofork()是一個非常特殊的系統調用,它的定義與實現在 inc/lib.h 中,而不是 lib/syscall.c 中。并且,它必須是 inline 的。

// This must be inlined.  Exercise for reader: why?
static inline envid_t __attribute__((always_inline))
sys_exofork(void)
{
    envid_t ret;
    asm volatile("int %2"
             : "=a" (ret)
             : "a" (SYS_exofork), "i" (T_SYSCALL));
    return ret;
}

可以看出,它的返回值是 %eax 寄存器的值。那么,它到底是什么時候返回?這就涉及到對整個 進程->內核->進程 的過程的理解。

static envid_t
sys_exofork(void)
{
    // LAB 4: Your code here.
    // panic("sys_exofork not implemented");
    struct Env *e;
    int r = env_alloc(&e, curenv->env_id);
    if (r < 0) return r;
    e->env_status = ENV_NOT_RUNNABLE;
    e->env_tf = curenv->env_tf;
    e->env_tf.tf_regs.reg_eax = 0;
    return e->env_id;
}

在該函數中,子進程復制了父進程的 trapframe,此后把 trapframe 中的 eax 的值設為了0。最后,返回了子進程的 id。注意,根據 kern/trap.c 中的 trap_dispatch() 函數,這個返回值僅僅是存放在了父進程的 trapframe 中,還沒有返回。而是在返回用戶態的時候,即在 env_run() 中調用 env_pop_tf() 時,才把 trapframe 中的值賦值給各個寄存器。這時候 lib/syscall.c 中的函數 syscall() 才獲得真正的返回值。因此,在這里對子進程 trapframe 的修改,可以使得子進程返回0。

sys_page_alloc() 函數
在進程 envid 的目標地址 va 分配一個權限為 perm 的頁面。

static int
sys_page_alloc(envid_t envid, void *va, int perm)
{
    // LAB 4: Your code here.
    // panic("sys_page_alloc not implemented");
    if ((~perm & (PTE_U|PTE_P)) != 0) return -E_INVAL;
    if ((perm & (~(PTE_U|PTE_P|PTE_AVAIL|PTE_W))) != 0) return -E_INVAL;
    if ((uintptr_t)va >= UTOP || PGOFF(va) != 0) return -E_INVAL; 
    
    struct PageInfo *pginfo = page_alloc(ALLOC_ZERO);
    if (!pginfo) return -E_NO_MEM;
    struct Env *e;
    int r = envid2env(envid, &e, 1);
    if (r < 0) return -E_BAD_ENV;
    r = page_insert(e->env_pgdir, pginfo, va, perm);
    if (r < 0) {
        page_free(pginfo);
        return -E_NO_MEM;
    }
    return 0;
}

sys_page_map() 函數
簡單來說,就是建立跨進程的映射。

static int
sys_page_map(envid_t srcenvid, void *srcva,
         envid_t dstenvid, void *dstva, int perm)
{
    // LAB 4: Your code here.
    // panic("sys_page_map not implemented");

    if ((uintptr_t)srcva >= UTOP || PGOFF(srcva) != 0) return -E_INVAL;
    if ((uintptr_t)dstva >= UTOP || PGOFF(dstva) != 0) return -E_INVAL;
    if ((perm & PTE_U) == 0 || (perm & PTE_P) == 0 || (perm & ~PTE_SYSCALL) != 0) return -E_INVAL;
    struct Env *src_e, *dst_e;
    if (envid2env(srcenvid, &src_e, 1)<0 || envid2env(dstenvid, &dst_e, 1)<0) return -E_BAD_ENV;
    pte_t *src_ptab;    
    struct PageInfo *pp = page_lookup(src_e->env_pgdir, srcva, &src_ptab);
    if ((*src_ptab & PTE_W) == 0 && (perm & PTE_W) == 1) return -E_INVAL;
    if (page_insert(dst_e->env_pgdir, pp, dstva, perm) < 0) return -E_NO_MEM;
    return 0;
}

sys_page_unmap() 函數
取消映射。

static int
sys_page_unmap(envid_t envid, void *va)
{
    // LAB 4: Your code here.
    // panic("sys_page_unmap not implemented");
    if ((uintptr_t)va >= UTOP || PGOFF(va) != 0) return -E_INVAL;
    struct Env *e;
    if (envid2env(envid, &e, 1) < 0) return -E_BAD_ENV;
    page_remove(e->env_pgdir, va);
    return 0;
}

sys_env_set_status() 函數
設置狀態,在子進程內存 map 結束后再使用。

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

最后,不要忘記在 kern/syscall.c 中添加新的系統調用類型,注意參數的處理。

...
    case SYS_exofork:
        retVal = (int32_t)sys_exofork();
        break;
    case SYS_env_set_status:
        retVal = sys_env_set_status(a1, a2);
        break;
    case SYS_page_alloc:
        retVal = sys_page_alloc(a1,(void *)a2, (int)a3);
        break;
    case SYS_page_map:
        retVal = sys_page_map(a1, (void *)a2, a3, (void*)a4, (int)a5);
        break;
    case SYS_page_unmap:
        retVal = sys_page_unmap(a1, (void *)a2);
        break;
...

make grade 成功。至此,part A 結束。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,837評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,196評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,688評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,654評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,456評論 6 406
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,955評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,044評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,195評論 0 287
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,725評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,608評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,802評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,318評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,048評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,422評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,673評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,424評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,762評論 2 372

推薦閱讀更多精彩內容