MIT6.828 Lab4 Preemptive Multitasking(上)


Lab4 Preemptive Multitasking(上)

PartA : 多處理器支持和協作多任務

在實驗的這部分中,我們首先拓展jos使其運行在多處理器系統上,然后實現jos內核一些系統功能調用以支持用戶級環境去創建新環境。我們還需要實現協同式輪詢調度(cooperative round-robin scheduling)算法,允許內核在舊的用戶環境資源放棄CPU或者退出的時候切換到一個新的用戶環境。

1. Multiprocessor Support

下面是一段對實驗指導書的翻譯。

我們將使JOS支持“對稱多處理”(SMP)的多處理器模型,其中所有CPU都有對系統資源(如內存和I / O總線)的等同權限的訪問。 雖然所有CPU在SMP中在功能上相同,但在引導過程中,它們可以分為兩種類型:

  • 引導處理器BSP(bootstrap processor)負責初始化系統並且引導操作系統。
  • 應用處理器AP(application processor)在操作系統啟動之后被BSP激活。

由哪個(些)處理器來擔任BSP的功能是由BIOS和硬件決定的,之前的所有代碼都是在BSP上實現的。

在一個SMP系統中,每一個CPU都有一個伴隨的本地APIC(LAPIC)單元。LAPIC單元負責整個系統的中斷傳遞。LAPIC為與其相關聯的CPU提供了一個唯一的標識符。在這個實驗中我們會使用LAPIC的一些基本功能(在kern/lapic.c中):

  • 讀取LAPIC標識符(APIC ID),告知我們的代碼正運行在哪個CPU上(參考cpunum()

    int
    cpunum(void)
    {
    	if (lapic)
    		return lapic[ID] >> 24;
    	return 0;
    }
    
  • 從BSP向AP發送STARTUP處理器間中斷信號IPI(interprocessor interrupt),喚醒其他CPU(參考lapic_startup()

  • 在Part C中,我們將通過LAPIC單元內置的定時器來觸發時鍾中斷,實現搶占式多任務(參考apic_init()

處理器通過內存映射I/O也稱為MMIO(memory-mapped I/O)來訪問LAPIC單元。在MMIO中,一部分的物理內存被硬鏈接到某些I/O設備的寄存器。因此load/store訪存指令也可以特別用於訪問設備寄存器。

在之前的實驗中我們已經知道了在物理地址0xA0000處有一個I/O hole(這個hole用於寫VGA顯示緩存)。LAPIC單元存在於第二個I/O hole上,物理地址0xFE000000(4064MB)。這個高地址無法用之前設置對KERNBASE的直接映射去訪問。jos的虛擬內存映射在MMMIOBASE處留了4MB的空隙,所以我們可以在這里映射硬件去訪問。之后的實驗中還會引入更多的MMIO區域,我們需要實現一個函數來分配這部分區域並映射到I/O設備對應的內存

Exercise 1

實現kern/pmap.c中的mmio_map_region()函數。我們可以看到它在kern/lapic.c中在lapic_init()的開頭被調用。

(為了讓這個函數的測試案例能夠正常運行,我還需要把下一個練習也做完。

本來這里想通過幾個斷點看一下運行流程的。。但是它的一堆assert測試會卡這個函數的實現。

好分析一下mmio_map_region()讓我們干的事

整體來講的功能就是把給定的[pa,pa+size]的物理地址和[MMIOBAZE, MMIOBASE + size]對應起來。

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;

根據代碼提示其實這里可以很容易的寫完

  1. size要和pagesize做上取整
  2. 如果size + base超過了MMIOLIM則發出panic
  3. 對應map則用我們之前實現過的boot_map_region即可
  4. 這里的base是一個靜態變量,所以我們要記得更新它,因為下一次分配的時候base就會變了
size = ROUNDUP(size,PGSIZE);
	if (size + base > MMIOLIM) {
		panic("wow overflow happen");
	}
	boot_map_region(kern_pgdir,base,size,pa,PTE_PCD|PTE_PWT|PTE_W);
	//panic("mmio_map_region not implemented");
	uintptr_t reserved_base = base;
	base += size;
	return (void*)reserved_base;

2. Application Processor Bootstrap

在引導AP之前,BSP首先需要收集有關多處理器系統的信息,比如總CPU數,每個CPU對應的APIC ID以及LAPIC的內存映射地址等。kern/mpconfig.c中的mp_init()函數通過讀取BIOS內存中的MP配置表來獲取相關信息。

kern/init.c中的boot_aps()函數驅動了AP的引導過程。AP從實模式開始啟動(類似於bootloader),因此boot_aps()kern/mpentry.S中拷貝了一份AP入口代碼(entry code)到一個實模式下可以訪問的內存位置。與bootloader不同的是,我們對於AP入口代碼存放的位置可以有一定控制權:在jos中使用MPENTRY_PADDR(0x7000)作為入口地址的存放位置,但是實際上640KB下任何未使用的地址都是可以使用的。

接下來,boot_aps()向對應AP的LAPIC單元發送STARTUP的IPI信號(處理器間中斷),使用AP的entry code初始化其CS:IP地址(在這里我們就使用MPENTRY_PADDR)依次激活APs。

在一些簡單的設置之后,AP將啟動分頁並進入保護模式,然后調用在kern/init.c中的啟動例程mp_mainboot_aps()在喚醒下一個AP之前會等待當前AP發出一個CPU_STARTED啟動標記,這個標記位在struct CpuInfo中的cpu_status域。

Exercise 2

首先閱讀kern/init.c中的boot_aps()以及mp_main()kern/mpentry.S匯編代碼。我們需要確保理解APs的bootstrap過程中的控制流切換。

控制流切換

  1. 首先在系統加載的過程中,boot_aps()被調用。這個時候是由BSP調用

  2. 然后使用memmove()函數從mpentry.S中拷貝文件中.global mpentry_start標簽處開始的入口代碼直到.global mpentry_end結束,代碼被拷貝到MPENTRY_PADDR(物理地址0x7000)對應的內核虛擬地址(別忘了必須拷貝到內核虛擬地址才可以被內核所操作)

    	code = KADDR(MPENTRY_PADDR);
    	memmove(code, mpentry_start, mpentry_end - mpentry_start);
    
  3. 然后boot_aps()根據每一個CPU的棧配置percpu_kstacks[]來為每一個AP設置棧地址mpentry_stack

  4. 再之后調用lapic_startup()函數來啟動AP,並等待AP的狀態變為CPU_STARTED以切換到下一個AP的配置。

    Lapis_startup后面看看在lab最后理解一下

  5. AP啟動后,執行從mpentry.S中復制的入口代碼:

然后修改我們之前在kern/pmap.cpage_init()的實現,避免將MPENTRY_PADDR的區域也添加到page_free_list中,這樣我們才可以安全地在該物理地址處拷貝以及運行AP的引導代碼。

其實只需要求出MPENTRY_PADDR對應在pages數組中的索引。當我們對page初始化的時候跳過MPENTRY_PADDR所在的頁就好。

因為只是幾行匯編代碼一頁足夠了。

Question

比較kern/mpentry.Sboot/boot.S,記住兩個代碼都是編譯連接后加載到KERNBASE之上運行的,為什么mpentry.S需要一個多余的宏定義MPBOOTPHYS?換句話說,如果在kern / mpentry省略它會出現問題

首先我們來看一下這個宏是在干嘛

這是用來計算對應匯編代碼的絕對地址。

因為我們會把mpentry_start移動到MPENTRY_PADDR上。因此下面的宏定義就相當於在算當前的代碼和起點的差值 + 真正的起始地址就會得到真正的地址

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

kern/mpentry.S 是運行在 KERNBASE 之上的,與其他的內核代碼一樣。也就是說,類似於 mpentry_start, mpentry_end, start32 這類地址,都位於 0xf0000000 之上,顯然,實模式是無法尋址的。因此,實模式下就可以通過 MPBOOTPHYS 宏的轉換,運行這部分代碼

3. Per-CPU State and Initialization

長長的翻譯。說實話每次看完這些翻譯,我還是一頭霧水,都還是通過慢慢看代碼看懂的。

在編寫多處理器OS時,重要的是區分每個CPU的私有狀態,以及整個系統共享的全局狀態。kern/cpu.h中定義了絕大部分CPU的狀態,包括用於儲存cpu變量的struct CpuInfo

cpunum()總是返回調用它的CPU ID,能用來索引例如cpus的數組。宏定義thiscpu是當前CPU的struct CpuInfo的簡寫。

下面是應該了解的每個CPU狀態:

CPU內核堆棧

由於多個CPU可以同時陷入內核,我們需要為每個CPU設置獨立的內核棧來避免相干擾。 percpu_kstacks [ncpu] [kstksize]保留NCPU的核堆棧的空間。

在Lab 2中,我們將BootStack指向的物理內存,映射到虛擬地址Kstacktop處作為BSP的內核堆棧。相似地我們在本次實驗中需要為每個CPU的內核棧映射到數組的對應區域。 CPU 0的堆棧仍將從Kstacktop開始向下增長;之后第n個CPU的內核棧從KSTACKTOP - n*KSTKGAP處開始向下增長。如inc/memlayout.h中所示。

CPU的TSS和TSS描述符

每CPU都需要任務狀態段(TSS),以便指定每個CPU的內核堆棧生命的位置。 CPU i的TSS存儲在CPU [i] .cpu_ts中,並且在GDT條目GDT [(GD_TSS0 >> 3)+ i]中定義相應的TSS描述符。 kern / trap.c中定義的全局TS變量將不再有用。

CPU指向當前環境的指針

由於每個CPU可以同時運行不同的用戶進程,將curenv定義為指向當前CPU(當前代碼正在執行的CPU)正在執行的環境的cpus[cpunum()].cpu_env(或是thiscpu->cpu_env)

CPU的系統寄存器。

包括系統寄存器在內的所有寄存器都屬於CPU私有。因此初始化這些寄存器的指令如lcr3, ltr, lgdt等,必須在每個CPU上都被執行。函數env_init_percpu()trap_init_percpu的功能就在於此。

Exercise 3

修改kern/pmap.c中的mem_init_mp(),使CPU內核棧映射到相應的虛擬內存。

這個函數根據代碼提示可以很快做完,基本上只有兩點需要注意的

  1. 就是權限記得寫成PTE_W
  2. 記得percpu_kstacks對應的是內核棧的虛擬地址要利用PADDR宏定義把它轉換成物理地址
// LAB 4: Your code here:
	int i = 0;
	for (; i < NCPU; i++) {
		boot_map_region(kern_pgdir,KSTACKTOP - i * (KSTKSIZE + KSTKGAP) - KSTKSIZE,KSTKSIZE, PADDR(percpu_kstacks[i]),PTE_W);
	}

Exercise 4

kern/trap.c中的trap_init_percpu()初始化了BSP的TSS和TSS描述符(它可以在lab3中給正常工作,但是本實驗運行在其他CPU時不能正常工作),我們需要修改代碼以使其支持所有CPU。

基本上也是根據代碼提示來

  1. 不要使用ts寄存器。轉而利用thiscpu->cpu_ts來為每個cpu初始化tss寄存器的值
  2. 對於tss描述符要用這樣的方式gdt[(GD_TSS0 >> 3) + i]來存儲
thiscpu->cpu_ts.ts_esp0 = (uintptr_t)percpu_kstacks[cpunum()];
	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) + cpunum()] = SEG16(STS_T32A, (uint32_t) (&(thiscpu->cpu_ts)),
					sizeof(struct Taskstate) - 1, 0);
	gdt[(GD_TSS0 >> 3) + cpunum()].sd_s = 0;

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

可以得到下面的結果,發現確實可以產生合理的結果

4. Locking

當前我們的代碼會在mp_main()初始化完成所有AP之后陷入自旋(spin)。在讓這些AP做出下一步操作之前,我們需要解決多個CPU同時執行內核代碼的競爭條件。

最簡單的方式就是使用一個大內核鎖(big kernel lock)。這個大內核鎖是一個單一的全局鎖,當一個環境進入內核模式的時候就可以被獲取,然后返回到用戶態的時候被釋放。在這種模型下,用戶模式的環境可以在任意多個CPU下並發運行(concurrently),但是只有一個環境能處於內核態,其余環境進入內核態需要強制等待。

kern/spinlock.h中聲明了這個大內核鎖的實現函數kernel_lock()。同時它提供了lock_kernel()unlock_kernel()兩個函數用於上鎖和解鎖,我們需要在以下四個場景使用大內核鎖:

  • i386_init():在BSP喚醒其它CPU之前進行上鎖
  • mp_main():初始化AP之后進行上鎖,然后調用sched_yield()在當前AP上運行環境
  • trap():從用戶模式陷入內核之前獲得大鎖進行上鎖。通過TF_CS寄存器的低位來判斷陷阱是否發生在用戶模式或內核模式下
  • env_run():在切換回用戶態之前進行解鎖。時機不對會導致競爭或死鎖

這個整體按照代碼提示,加一行減一行的非常容易

// In i386_init():
// Acquire the big kernel lock before waking up APs
// Your code here:
lock_kernel();

// In mp_main():
// Now that we have finished some basic setup, call sched_yield()
// to start running processes on this CPU.  But make sure that
// only one CPU can enter the scheduler at a time!
//
// Your code here:
// lock the kernel and start running enviroments
lock_kernel();
sched_yield();

// In trap():
// Trapped from user mode.
// Acquire the big kernel lock before doing any
// serious kernel work.
// LAB 4: Your code here.
lock_kernel();

// In env_run():
// address space switch
// reference from inc/x86.h
lcr3(PADDR(e->env_pgdir));
// release kernel lock here
unlock_kernel(); // newly added code
// drop into user mode
env_pop_tf(&(e->env_tf));

exercise 5

通過調用lock_kernel()unlock_kernel()函數來實現上面所描述的大內核鎖

首先在i386_init()中實現在bsp其他cpu之前進行上鎖

Question

似乎使用Big Kernel Lock保證了只能有一個CPU在內核態運行。 那為什么每個CPU還需要單獨內核堆棧? 描述一個場景,其中使用共享內核堆棧將出錯,即使是對大內核鎖定的保護。

當cpu0在內核態運行的時候,這個時候如果cpu1發生中斷想要陷入內核態,那么如果這兩個cpu是共享內核態的話就會發生錯誤。當發生中斷的時候,會進行棧的切換,cpu1再陷入之前要把一些參數保存到內核棧中。如果內核棧共享的話,則就出現問題

5. Round-Robin Scheduling

我們的下一個任務是改變jos內核,實現對用戶環境的輪詢調度:

  • kern/sched.c中的sched_yield()負責從用戶環境中選擇一個新環境執行。其按照順序遍歷envs[]數組,從上一次運行的環境開始,找到第一個ENV_RUNNABLE的環境然后調用env_run()
  • sched_yield()一定不能在兩個CPU上同時運行相同的環境。它可以通過環境的狀態是否為ENV_RUNNING來判斷這個環境是否正運行在某個CPU上。
  • 我們提供了一個新的系統調用sys_yield(),使得在用戶環境中可以通過該系統調用喚醒sched_yield(),主動放棄CPU。

exercise 6

sched_yield()中實現上述機制,注意我們要修改syscall()來支持對sys_yield()的調度。

// LAB 4: Your code here.
	size_t start = 0;
	if (curenv) {
		start = ENVX(curenv->env_id) + 1;
	}

	for (size_t i = 0; i < NENV; i++) {
		size_t index = (start + i) % NENV;
		if (envs[index].env_status == ENV_RUNNABLE) {
			env_run(&envs[index]);
		}
	}
	//
	// If no envs are runnable, but the environment previously
	// running on this CPU is still ENV_RUNNING, it's okay to
	// choose that environment.
	if(curenv && curenv->env_status == ENV_RUNNING) {
		env_run(curenv);
	}
	// sched_halt never returns
	sched_halt();

確保在mp_main()中調用sched_yield()

修改kern/init.c來創建三個或者更多的用戶環境,使其同時運行user/yield.c程序。

#else
	// Touch all you want.
	// ENV_CREATE(user_primes, ENV_TYPE_USER);
	ENV_CREATE(user_yield, ENV_TYPE_USER);
	ENV_CREATE(user_yield, ENV_TYPE_USER);
	ENV_CREATE(user_yield, ENV_TYPE_USER);

關於lab3的一個小bug

Lab3博客中已經修復

trap.c中的trap_init(void)函數中

(-) SETGATE(idt[T_SYSCALL],1,GD_KT,syscall_handler,3);
(+) SETGATE(idt[T_SYSCALL],0,GD_KT,syscall_handler,3);

關於系統調用是要關中斷的也就是說它不是一個trap類型。不然這里會過不了

Question

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

    這個是因為e位於UTOP以上,而在這上面的地址給予env_pgdir和kern_pgdir是一樣的

  2. 當內核進行用戶環境切換的時候,必須要保證舊的環境的寄存器值被保存起來以便之后恢復。這個過程是在哪里發生的?

    是在trapentry.S

    */
    .global _alltraps
    _alltraps:
    // make the stack look like a struct Trapframe
    	pushl %ds;
    	pushl %es;
    	pushal;
    // load GD_KD into %ds and %es
    	movl $GD_KD, %edx
    	movl %edx, %ds
    	movl %edx, %es
    // push %esp as an argument to trap()
    	pushl %esp;
    	call trap;
    
    

6. System Calls for Environment Creation

Unix系統提供了fork()系統調用作為進程創建原語(process creation primitive)。Unix的fork()拷貝調用進程(父進程)的整個進程空間以創建子進程,這種情況下父子進程之間唯一可觀察的區別就是他們的進程ID分別為pidppid(可以通過getpid()getppid()查看)。在父進程中,fork()函數返回子進程的ID,而在子進程中返回0.

默認情況下,每一個進程都有其私有的地址空間,而且任意一個進程對於內核的修改對於其他進程而言都是不可見的。

現在我們將實現一個jos系統調用原語以使用戶創建新的用戶模式環境。完成這些這些系統調用。我們將實現以下的系統調用函數:

  • sys_exofork():創建一個幾乎為空白狀態的新環境:這個地址空間沒有任何用戶部分映射,也無法運行。新環境將會有和父親環境完全一致的寄存器狀態,而在父親環境執行該系統調用后會返回新創建環境的envid_t(如果創建失敗則返回錯誤碼),子環境返回0。由於子環境最初被標記為不可執行,故在子環境中sys_exofork()會一直wait,直到父環境顯式標記子環境為可執行,其才會在子環境中返回。
  • sys_env_set_status():設置指定的環境的狀態為ENV_RUNNABLE或者RUN_NOT_RUNNABLE。這個系統調用通常在一個新環境的地址空間和寄存器狀態完全初始化完成之后將其標記為可執行。
  • sys_page_alloc():分配一頁的物理內存然后將其映射到特定環境的地址空間的給定虛擬地址。
  • sys_page_map():將一個頁映射關系(不是頁的具體內容)從一個環境的地址空間拷貝到另一個環境的地址空間。實現共享內存。
  • sys_page_unmap():將給定環境的虛擬地址頁面解除映射。

上述所有系統調用函數都需要接受一個環境ID,jos的內核支持了環境號0代表當前環境。在kern/env.c中的envid2env()實現了這種映射。

我們在user/dumpfork.c中提供了和原始Unix 系統中fork()函數類似的函數實現。測試程序用上述系統調用創建並運行一個當前地址空間拷貝的子進程,然后兩個環境使用sys_yield()來回切換。父進程在10次迭代后退出;子進程在20次迭代后退出。

exercise 7

實現上述在kern/syscall.c中的系統調用函數,確保syscall()可以調用它們。你可能需要用到kern/pmap.ckern/env.c中的一些函數,尤其是envid2env()

現在你使用envid2env()的時候,將checkperm參數設置為1,確保當你的一些系統調用參數無效的時候會返回-E_INVAL。使用user/dumpfork.c測試你實現的這些系統調用。

實現sys_exofork()

首先從dumpfork開始,可以找到sys_exofork的原始定義

  1. 通過int2中斷進入trapentry.s
  2. 根據syscall進入trap_dispatch()
// 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;
}
  1. 在trap_dispatch()中會保存當前寄存器信息,然后執行syscall

隨后我們根據代碼提示實現sys_exofork

static envid_t
sys_exofork(void)
{
	struct Env *child_env;
	int eno;
	// if alloc env error 
	// directly return
	if ((eno = env_alloc(&child_env,curenv->env_id) < 0)) {
		return eno;
	}
	// same register state as parent
	child_env->env_tf = thiscpu->cpu_env->env_tf;
	// status is not run
	child_env->env_status = ENV_NOT_RUNNABLE;
	// child_env return 0
	child_env->env_tf.tf_regs.reg_eax = 0;
	// father env return child env_id
	return child_env->env_id;
}

實現sys_env_set_status函數

首先找到這個函數的定義.

需要兩個參數分別為env_id和對應的狀態

int	sys_env_set_status(envid_t env, int status);

env_set就是把指定的env的狀態設置成傳入的status參數,只不過要注意一些條件判斷

static int
sys_env_set_status(envid_t envid, int status) {
	if((status != ENV_RUNNABLE) && (status != ENV_NOT_RUNNABLE)){
		return -E_INVAL;
	}
	struct Env *env;
	int eno = envid2env(envid,&env,1);
	if (eno < 0) {
		return -E_BAD_ENV;;
	}
	env->env_status = status;
	return 0;
}

實現sys_page_alloc()函數

基本按照提示來就可以了。但是有兩個要注意的點

  1. 就是如何判斷是否是頁對齊點

    PGOFF(va) != 0 // 來判斷是否是頁對奇的
    
  2. PTE_SYSCALL

// Flags in PTE_SYSCALL may be used in system calls.  (Others may not.)
#define PTE_SYSCALL	(PTE_AVAIL | PTE_P | PTE_W | PTE_U)

也就是說如果下面的式子成立的話,則出現了PTE_SYSCALL之外的位為1.

if (perm & (~PTE_SYSCALL))
// Allocate a page of memory and map it at 'va' with permission
// 'perm' in the address space of 'envid'.
// The page's contents are set to 0.
// If a page is already mapped at 'va', that page is unmapped as a
// side effect.
//
// perm -- PTE_U | PTE_P must be set, PTE_AVAIL | PTE_W may or may not be set,
//         but no other bits may be set.  See PTE_SYSCALL in inc/mmu.h.
//
// Return 0 on success, < 0 on error.  Errors are:
//	(1) -E_BAD_ENV if environment envid doesn't currently exist,
//		 or the caller doesn't have permission to change envid.
//	(2) -E_INVAL if va >= UTOP, or va is not page-aligned.
//	(3) -E_INVAL if perm is inappropriate (see above).
//	(4) -E_NO_MEM if there's no memory to allocate the new page,
//		 or to allocate any necessary page tables.
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.
	//(1)
	struct Env *env;
	int eno;
	if ((eno = envid2env(envid,&env,1) < 0)) {
		return -E_BAD_ENV;
	}
	// (2)
	if((uintptr_t)va >= UTOP || PGOFF(va) != 0){
		return -E_INVAL;
	}
	// (3)
	if(!(perm & PTE_U) || !(perm & PTE_P) || (perm & (~PTE_SYSCALL))){
		return -E_INVAL;
	}
	// (4)
	struct PageInfo *page;
	page = page_alloc(ALLOC_ZERO);
	if(page == NULL){
		return -E_NO_MEM;
	}
	eno = page_insert(env->env_pgdir,page,va,perm);
	if (eno < 0) {
		page_free(page);
		return -E_NO_MEM;
	}
	return 0;
	
}

實現sys_page_map函數

基本上按照提示也是比較好實現的

  1. 搞清楚page_map的功能就是把對應環境的虛擬地址和指定環境的虛擬地址相對應
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.
	// case 1 -E_BAD_ENV
	struct Env *srcv, *dstv;
	if (envid2env(srcenvid,&srcv,1) < 0 || envid2env(dstenvid,&dstv,1) < 0) {
		return -E_BAD_ENV;
	}
	// case 2 -E_INVAL
	if (((uintptr_t)srcva >= UTOP) || ((uintptr_t)dstva >= UTOP) ||
        (PGOFF(srcva) != 0) || (PGOFF(dstva) != 0)) {
        return -E_INVAL;
    }
	// case 3 -E_INVAL
	struct PageInfo *srcpage;
    pte_t *          scrpte_ptr;
    // use page look up to get source page and corresponding pte_t *
    if ((srcpage = page_lookup(srcv->env_pgdir, srcva, &scrpte_ptr)) ==
        NULL) {
        // srcva not mapped in srcenvid's address space
        return -E_INVAL;
    }

    if ((perm & (~PTE_SYSCALL)) || !(perm & PTE_U) || !(perm & PTE_P)) {
        return -E_INVAL;
    }
    if ((perm & PTE_W) && (!((*scrpte_ptr) & PTE_W))) {
        return -E_INVAL;
    }
	if (page_insert(dstv->env_pgdir, srcpage, dstva, perm) < 0) {
		return -E_NO_MEM;
	}
    return 0;
	
}

實現sys_page_unmap函數

static int sys_page_unmap(envid_t envid, void *va) {
	struct Env *curE;
	int eno;
	if ((eno = envid2env(envid,&curE,1) < 0)) {
		return -E_BAD_ENV;
	}
	if ((uintptr_t)va >= UTOP || PGOFF(va) != 0){
		return -E_INVAL;
	}
	page_remove(curE->env_pgdir,va);
	return 0;
}

PartA+: 回顧parA

emmmpartA寫了這么多代碼,居然才5分。但是在寫了幾個關於創建新環境的函數之后,相信大家都好奇之間的調用關系是怎么樣的。是在哪里執行了這些函數。以及之前的多cpu切換流程的梳理

1. 多cpu的初始化和啟動

關於BSP和AP的說明可以參考x86-64的多核初始化

關於Jos多cpu切換的流程分析多參考於Xv6學習小記(二)——多核啟動

感謝各位大佬們的無私分享。

1. 首先我們要從系統如何檢測CPU的個數開始說起

系統首先進行查找MP浮點結構:
1.如果BIOS的EBDA已經定義,則在其中的第一K字節中進行查找,否則到2;

2.若EBDA未被定義,則在系統基本內存的最后一K字節中尋找;

3.在BIOS ROM里的0xF0000到0xFFFFF的地址空間中尋找。

關於內存低1MB的詳細信息見下圖

對應於mpsearch函數

如果EBDA(Extended BIOS Data Area,擴展BIOS數據區)不存在,BDA[0x0E]和BDA[0x0F]的值為0;如果EBDA存在,其段地址被保存在BDA[0x0E]和BDA[0x0F]中,其中BDA[0x0E]保存EBDA段地址的低8位,BDA[0x0F]保存EDBA段地址的高8位,所以(BDA[0x0F]<<8) | BDA[0x0E]就表示了EDBA的段地址,將段地址左移4位即為EBDA的物理地址。如下面的代碼所示。

p <<= 4
static struct mp *
mpsearch(void)
{
	uint8_t *bda;
	uint32_t p;
	struct mp *mp;

	static_assert(sizeof(*mp) == 16);

	// The BIOS data area lives in 16-bit segment 0x40.
	bda = (uint8_t *) KADDR(0x40 << 4); 

	// [MP 4] The 16-bit segment of the EBDA is in the two bytes
	// starting at byte 0x0E of the BDA.  0 if not present.
	if ((p = *(uint16_t *) (bda + 0x0E))) {
		p <<= 4;	// Translate from segment to PA
		if ((mp = mpsearch1(p, 1024))) // 在EBDA的前1kb個字節中查找
			return mp;
	} else {
		// The size of base memory, in KB is in the two bytes
		// starting at 0x13 of the BDA.
		p = *(uint16_t *) (bda + 0x13) * 1024; // 得到系統內存的末尾邊界地址
		if ((mp = mpsearch1(p - 1024, 1024)))
			return mp;
	}
	return mpsearch1(0xF0000, 0x10000); // 在rom area中尋找
}

關於mpsearch1函數

該函數將_MP_這個長度為4的字符串作為了MP浮點結構的標識,匹配到此字符串即找到了MP浮點結構,然后返回指向該MP浮點結構的指針。

// Look for an MP structure in the len bytes at physical address addr.
static struct mp *
mpsearch1(physaddr_t a, int len)
{
	struct mp *mp = KADDR(a), *end = KADDR(a + len);

	for (; mp < end; mp++)
		if (memcmp(mp->signature, "_MP_", 4) == 0 &&
		    sum(mp, sizeof(*mp)) == 0)
			return mp;
	return NULL;
}

mp_init函數先執行了mpconfig方法返回了MP配置表頭的虛擬地址

mpconfig函數

  1. 通過mpsearch獲得指向mp浮點結構的指針m
  2. 隨后通過m指針訪問到mp配置表頭,並將其轉換成虛擬地址
static struct mpconf *
mpconfig(struct mp **pmp)
{
	struct mpconf *conf;
	struct mp *mp;

	if ((mp = mpsearch()) == 0)
		return NULL;
	if (mp->physaddr == 0 || mp->type != 0) {
		cprintf("SMP: Default configurations not implemented\n");
		return NULL;
	}
	conf = (struct mpconf *) KADDR(mp->physaddr); 
	if (memcmp(conf, "PCMP", 4) != 0) {
		cprintf("SMP: Incorrect MP configuration table signature\n");
		return NULL;
	}
	if (sum(conf, conf->length) != 0) {
		cprintf("SMP: Bad MP configuration checksum\n");
		return NULL;
	}
	if (conf->version != 1 && conf->version != 4) {
		cprintf("SMP: Unsupported MP version %d\n", conf->version);
		return NULL;
	}
	if ((sum((uint8_t *)conf + conf->length, conf->xlength) + conf->xchecksum) & 0xff) {
		cprintf("SMP: Bad MP configuration extended checksum\n");
		return NULL;
	}
	*pmp = mp;
	return conf;
}

MP配置表頭的結構體如下:

struct mpconf {         				// configuration table header
  uchar signature[4];           // 標志為"PCMP"
  ushort length;                // MP配置表的長度
  uchar version;                // [14]
  uchar checksum;               // all bytes must add up to 0
  uchar product[20];            // product id
  uint *oemtable;               // OEM table pointer
  ushort oemlength;             // OEM table length
  ushort entry;                 // 入口數
  uint *lapicaddr;              // local APIC的地址
  ushort xlength;               // extended table length
  uchar xchecksum;              // extended table checksum
  uchar reserved;
};

接下來來看mpinit方法

程序在mpinit()方法中遍歷MP擴展部分通過判斷入口類型來進行相應操作,如判斷入口類型為MPPROC時則將ncpu加1,部分代碼如下

	bootcpu = &cpus[0];
	if ((conf = mpconfig(&mp)) == 0) //獲得mp表頭的指針
		return;
	ismp = 1;
	lapicaddr = conf->lapicaddr;
	// 遍歷mp表的條目
	for (p = conf->entries, i = 0; i < conf->entry; i++) {
		switch (*p) {
    // 如果是處理器
		case MPPROC:
			proc = (struct mpproc *)p;
			if (proc->flags & MPPROC_BOOT)  //判斷此CPU是否為主引導CPU(BSP)
				bootcpu = &cpus[ncpu];    //若是BSP,將此CPU設為第0個CPU
			if (ncpu < NCPU) {
				cpus[ncpu].cpu_id = ncpu;  //給每個CPU設置ID並存入cpus數組中
				ncpu++; 		 //CPU個數+1
			} else {
				cprintf("SMP: too many CPUs, CPU %d disabled\n",
					proc->apicid);
			}
			p += sizeof(struct mpproc);
			continue;
		case MPBUS:
		case MPIOAPIC:
		case MPIOINTR:
		case MPLINTR:
			p += 8;
			continue;
		default:
			cprintf("mpinit: unknown config type %x\n", *p);
			ismp = 0;
			i = conf->entry;
		}
	}

	bootcpu->cpu_status = CPU_STARTED;
	if (!ismp) {
		// Didn't like what we found; fall back to no MP.
		ncpu = 1;
		lapicaddr = 0;
		cprintf("SMP: configuration not found, SMP disabled\n");
		return;
	}
	cprintf("SMP: CPU %d found %d CPU(s)\n", bootcpu->cpu_id,  ncpu);
	}

2. 隨后執行lapic_init函數

引用於

80486DX在1990年上市,其引入了SMP的概念,即多CPU(注意不是多核)。Intel為了適應SMP提出APIC(Advanced Programmable Interrupt Controller,高級中斷控制器)的新技術。APIC 由兩部分組成,一個稱為LAPIC(Local APIC,本地高級中斷控制器),一個稱為IOAPIC(I/O APIC,I/O 高級中斷控制器)。前者位於CPU中,在SMP 平台,每個CPU 都有一個自己的LAPIC(后期多核后,每個邏輯核都有個LAPIC)。后者通常位於外部設備芯片上,例如南橋上。像PIC 一樣,連接各個產生中斷的設備。而IOAPIC和LAPIC通過APIC Bus連接在一起。如圖:

img

因此這里我們要做的就是初始每個cpu的Local APIC。同時多cpu的中斷流程如下

  • 一個 CPU 給其他 CPU 發送中斷的時候, 就在自己的 ICR 中, 放中斷向量和目標LAPIC ID, 然后通過總線發送到對應 LAPIC,
  • 目標 LAPIC 根據自己的 LVT(Local Vector Table) 來對不同的中斷進行處理.
  1. 通過lab中實現的mmio_map_region函數將lapicaddr映射到虛擬地址.大小為4kb

    void
    llapic_init(void)
    {
    	if (!lapicaddr)
    		return;
    	
    	// 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);
    
  2. 下面的函數大量用到lapicw這里先看一下

    其實就是設置lvt表。具體關於apic的討論可以看這里 XV6 的中斷和系統調用

    static void
    lapicw(int index, int value)
    {
    	lapic[index] = value;
    	lapic[ID];  // wait for write to finish, by reading
    }
    
  3. 下面的代碼就是對於LVT表的初始化操作。深究可能仔細看看那這個xv6中文文檔


	// Enable local APIC; set spurious interrupt vector.
	lapicw(SVR, ENABLE | (IRQ_OFFSET + IRQ_SPURIOUS));

	// The timer repeatedly counts down at bus frequency
	// from lapic[TICR] and then issues an interrupt.  
	// If we cared more about precise timekeeping,
	// TICR would be calibrated using an external time source.
	lapicw(TDCR, X1);
	lapicw(TIMER, PERIODIC | (IRQ_OFFSET + IRQ_TIMER)); // 這會讓lapic周期性地在iRQ_TIMER產生中斷
	lapicw(TICR, 10000000); 

	// Leave LINT0 of the BSP enabled so that it can get
	// interrupts from the 8259A chip.
	//
	// According to Intel MP Specification, the BIOS should initialize
	// BSP's local APIC in Virtual Wire Mode, in which 8259A's
	// INTR is virtually connected to BSP's LINTIN0. In this mode,
	// we do not need to program the IOAPIC.
	if (thiscpu != bootcpu)
		lapicw(LINT0, MASKED);

	// Disable NMI (LINT1) on all CPUs
	lapicw(LINT1, MASKED);

	// Disable performance counter overflow interrupts
	// on machines that provide that interrupt entry.
	if (((lapic[VER]>>16) & 0xFF) >= 4)
		lapicw(PCINT, MASKED);

	// Map error interrupt to IRQ_ERROR.
	lapicw(ERROR, IRQ_OFFSET + IRQ_ERROR);

	// Clear error status register (requires back-to-back writes).
	lapicw(ESR, 0);
	lapicw(ESR, 0);

	// Ack any outstanding interrupts.
	lapicw(EOI, 0);

	// Send an Init Level De-Assert to synchronize arbitration ID's.
	lapicw(ICRHI, 0);
	lapicw(ICRLO, BCAST | INIT | LEVEL);
	while(lapic[ICRLO] & DELIVS)
		;

	// Enable interrupts on the APIC (but not on the processor).
	lapicw(TPR, 0);
}

3. boot_aps函數

隨后進入boot_aps函數

  1. 先把entryother.S的代碼拷貝到以0x7000起始的這塊內存。
  2. 然后逐步啟動所有的ap cpu
  3. 為每一個ap分配自己的內核棧
  4. 通過lapic_startap函數向這個CPU發中斷,讓此CPU執行boot程序
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);

	// Boot each AP one at a time
	for (c = cpus; c < cpus + ncpu; c++) {
		if (c == cpus + cpunum())  {// We've started already.
			cprintf("cpu has already startd(id): %08x\n", c->cpu_id);
			continue;
		}
		// Tell mpentry.S what stack to use 
		mpentry_kstack = percpu_kstacks[c - cpus] + KSTKSIZE;
		// Start the CPU at mpentry_start
		cprintf("cpu start(id): %08x\n", c->cpu_id);
		lapic_startap(c->cpu_id, PADDR(code));
		// Wait for the CPU to finish some basic setup in mp_main()
		while(c->cpu_status != CPU_STARTED)
			;
	}
}

lapic_startap函數

  1. outb指令

    用於向指定端口寫入1字節的數據

    static inline void
    outb(int port, uint8_t data)
    {
    	asm volatile("outb %0,%w1" : : "a" (data), "d" (port));
    }
    
  2. 看不懂下面這部分。。

void
lapic_startap(uint8_t apicid, uint32_t addr)
{
	int i;
	uint16_t *wrv;

	// "The BSP must initialize CMOS shutdown code to 0AH
	// and the warm reset vector (DWORD based at 40:67) to point at
	// the AP startup code prior to the [universal startup algorithm]."
	outb(IO_RTC, 0xF);  // offset 0xF is shutdown code
	outb(IO_RTC+1, 0x0A);
  wrv = (uint16_t *)KADDR((0x40 << 4 | 0x67));  // Warm reset vector
	wrv[0] = 0;
	wrv[1] = addr >> 4;
  1. BSP通過向AP逐個發送中斷來啟動AP,首先發送INIT中斷來初始化AP,然后發送SIPI中斷來啟動AP,發送中斷使用的是寫ICR寄存器的方式
// 發送INIT中斷以重置AP
lapicw(ICRHI, apicid<<24);             //將目標CPU的ID寫入ICR寄存器的目的地址域中
lapicw(ICRLO, INIT | LEVEL | ASSERT);  //在ASSERT的情況下將INIT中斷寫入ICR寄存器
microdelay(200);                       //等待200ms
lapicw(ICRLO, INIT | LEVEL);           //在非ASSERT的情況下將INIT中斷寫入ICR寄存器
microdelay(100); // 等待100ms (INTEL官方手冊規定的是10ms,但是由於Bochs運行較慢,此處改為100ms)

//INTEL官方規定發送兩次startup IPI中斷
for(i = 0; i < 2; i++){
    lapicw(ICRHI, apicid<<24);          //將目標CPU的ID寫入ICR寄存器的目的地址域中
    lapicw(ICRLO, STARTUP | (addr>>12));//將SIPI中斷寫入ICR寄存器的傳送模式域中,將啟動代碼寫入向量域中
    microdelay(200);                    //等待200ms
}

ICR寄存器說明

中斷命令寄存器(ICR)是一個 64 位本地 APIC寄存器,允許運行在處理器上的軟件指定和發送處理器間中斷(IPI)給系統中的其它處理器。發送IPI時,必須設置ICR 以指明將要發送的 IPI消息的類型和目的處理器或處理器組。一般情況下,ICR寄存器的物理地址為0xFEE00300

SIPI是一個特殊的IPI。典型情況下,在發送SIPI時,ICR的向量域中指向一個啟動例程,本例中即將entryother的代碼地址寫入了ICR的向量域,以啟動AP。
4. 運行boot函數

通過上面的分析我們可以知道是在lapicw(ICRLO, STARTUP | (addr>>12))之后執行了啟動代碼

在啟動匯編代碼的最后我們發現了對於mp_main函數的調用

  1. mp_main函數中我們初始化了每一個ap的lapicenv以及trap
  2. 然后通知bsp可以進行下一個ap的喚醒了
void
mp_main(void)
{
	// We are in high EIP now, safe to switch to kern_pgdir 
	lcr3(PADDR(kern_pgdir));
	cprintf("SMP: CPU %d starting\n", cpunum());

	lapic_init();
	env_init_percpu();
	trap_init_percpu();
	xchg(&thiscpu->cpu_status, CPU_STARTED); // tell boot_aps() we're up // 這里的BSP就可以被重新喚醒了

	// Now that we have finished some basic setup, call sched_yield()
	// to start running processes on this CPU.  But make sure that
	// only one CPU can enter the scheduler at a time!
	//
	// Your code here:
	lock_kernel();
	sched_yield();
}

2. 多cpu的切換

我們以CPUS = 4為參數,執行qemu

在我們完成了對一個bsp和3個ap的設置之后。我們在實驗中創建了三個user environment。來測試cpu切換

其中在user environment做了這樣的事情

是非常簡單的代碼,輸出當前環境之后切換環境。這里的sys_yied系統調用會執行我們上面實現的sched_yield()函數

#include <inc/lib.h>

void
umain(int argc, char **argv)
{
	int i;

	cprintf("Hello, I am environment %08x.\n", thisenv->env_id);
	for (i = 0; i < 5; i++) {
		sys_yield();
		cprintf("Back in environment %08x, iteration %d.\n",
			thisenv->env_id, i);
	}
	cprintf("All done in environment %08x.\n", thisenv->env_id);
}

1. Sched_yield函數

在進入這個函數之前我們先在shced_yield設一個斷點。看一下在run 第一個用戶環境之前的狀態

可以發現我們已經創建了三個env並且啟動了4個cpu。這里是在envs中找一個來在當前cpu執行

void
sched_yield(void)
{
	size_t start = 0;
	if (curenv) {
		start = ENVX(curenv->env_id) + 1;
	}

	for (size_t i = 0; i < NENV; i++) {
		size_t index = (start + i) % NENV;
		if (envs[index].env_status == ENV_RUNNABLE) {
			env_run(&envs[index]);
		}
	}

	if(curenv && curenv->env_status == ENV_RUNNING) {
		env_run(curenv);
	}
	// sched_halt never returns
	sched_halt();
}

2. 加鎖機制下的切換過程

  1. 在bsp中我們啟動aps的過程中會在執行boot_aps之前把內核鎖住
  2. 這樣當ap想要進入內核的時候就會pause住
  3. 當bsp內執行完第一個用戶環境后就會把它的鎖釋放
  4. 這樣pause在mp_main的ap就會獲得鎖。然后執行sched_yiedld去看一下是否有可以run的env
  5. 而在用戶環境我們執行sys_yied系統調用可以主動調用sched_yiedld




免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM