MIT6.828 Lab4 Preemptive Multitasking(下)

周小倫發表於2021-07-17

Lab4 Preemptive Multitasking(下)

lab4的第二部分要求我們實現fork的cow。在整個lab的第一部分我們實現了對多cpu的支援和再多系統環境中的切換,但是最後分析的時候沒有分析環境建立的系統呼叫,這裡先補一下對環境建立的系統呼叫的分析

recall A續

  1. 這裡的分析要從user/dumpfork.c開始
umain(int argc, char **argv)
{
	envid_t who;
	int i;

	// fork a child process
	who = dumbfork();

這裡的main函式呼叫了dumbfork函式。在dumbfork中先執行了sys_exofork這個系統呼叫來建立新的環境。

這裡為新的環境賦予和父環境一樣的暫存器資訊。並且將新環境的reg_eax暫存器置為0,這表示從子程式返回。而父程式會返回子程式的id。當然這裡說的都是環境id。

image-20210714215819124

  1. 從父程式中返回之後會copy父程式的內容到子程式

    // Eagerly copy our entire address space into the child.
    	// This is NOT what you should do in your fork implementation.
    	for (addr = (uint8_t*) UTEXT; addr < end; addr += PGSIZE)
    		duppage(envid, addr);
    
    

    也就是上面這一段。可以發現這段程式碼還是很簡單的。就是逐頁來copy父環境的內容---> 子環境中

    問題的關鍵就是duppage這個函式

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

    這裡的duppage用到了我們實驗中實現的三個系統呼叫,但是好像真正用到的就是第一個。。後面兩個只是在測試,我個人看起來是這樣的,如果有問題的話,歡迎大家評論指出

    這裡有兩個地址需要⚠️注意分別是UTEXTUTEMP

    image-20210714220719005

    分別表示使用者環境的程式碼段地址和一個用來測試的子環境段。。。

  2. 隨後執行sched_yield

    就可以實現類fork操作

Part B: Copy-on-Write Fork

上面的fork操作是會給子程式分配一個新的記憶體。並且copy父程式的地址空間過去,但我們知道真正的Linux作業系統並不是這樣做的而是利用了cow(寫時複製)的技術來實現的

1. User-level page fault handling

一個使用者級別的cow的fork函式需要知道哪些page fault是在防寫頁時觸發的,寫時複製只是使用者級缺頁中斷處理的一種。
通常建立地址空間以便page fault提示何時需要執行某些操作。例如大多數Unix核心初始只給新程式的棧對映一個頁,以後棧增長會導致page fault從而對映新的頁。典型的Unix系統會對程式地址空間的不同區域發生的page fault錯誤執行不同的處理。例如棧上缺頁,會分配和對映新的實體記憶體。.BSS區域缺頁會分配新的全0物理頁然後對映。在[csapp]書中有對應的內容。

image-20210714225332600

2. Setting the Page Fault Handler

為了處理自己的缺頁中斷,使用者環境需要在JOS核心中註冊缺頁中斷處理程式的入口。使用者環境通過sys_env_set_pgfault_upcall系統呼叫註冊它的缺頁中斷入口。我們在Env結構體中增加了一個新成員env_pgfault_upcall來記錄這一資訊。
練習8就是讓你實現缺頁中斷的入口,就是你用寫時複製,如果修改了該怎麼處理,呼叫哪個程式去處理。我們需要去實現這個sys_env_set_pgfault_upcall

static int
sys_env_set_pgfault_upcall(envid_t envid, void *func)
{
	// LAB 4: Your code here.
	struct Env * env;
	if(envid2env(envid,&env,1)<0)return -E_BAD_ENV;//先判斷程式可不可以用
	env->env_pgfault_upcall=func;//意思就是處理中斷的時候用func 這個函式。

	return 0;
	//panic("sys_env_set_pgfault_upcall not implemented");
}

3.Normal and Exception Stacks in User Environments

當缺頁中斷髮生時,核心會返回使用者模式來處理該中斷。我們需要一個使用者異常棧,來模擬核心異常棧。JOS的使用者異常棧被定義在虛擬地址UXSTACKTOP。

4. Invoking the User Page Fault Handler

您現在需要更改kern / trap.c中的頁面故障處理程式碼,以處理在使用者模式下發生的頁面故障。在故障處理程式中需要做下面的事情。

  1. 判斷curenv->env_pgfault_upcall是否設定。如果沒有的話則直接銷燬該程式
  2. 修改esp,切換到使用者異常棧
  3. 在棧中壓入UTrapframe結構
  4. eip設定為curenv->env_pgfault_upcall,然後回到使用者態執行curenv->env_pgfault_upcall處的程式碼

UTrapframe結構如下:

                    <-- UXSTACKTOP
trap-time esp
trap-time eflags
trap-time eip
trap-time eax       start of struct PushRegs
trap-time ecx
trap-time edx
trap-time ebx
trap-time esp
trap-time ebp
trap-time esi
trap-time edi       end of struct PushRegs
tf_err (error code)
fault_va            <-- %esp when handler is run

Exercise 9.

Implement the code in page_fault_handler in kern/trap.c required to dispatch page faults to the user-mode handler. Be sure to take appropriate precautions when writing into the exception stack. (What happens if the user environment runs out of space on the exception stack?)

// LAB 4: Your code here.
	// mustbe set upcall
	if (curenv->env_pgfault_upcall) {
		uintptr_t stack_top = UXSTACKTOP;
		if (UXSTACKTOP - PGSIZE < tf->tf_esp && tf->tf_esp < UXSTACKTOP) {
			stack_top = tf->tf_esp;
		}
		uint32_t size = sizeof(struct UTrapframe) + sizeof(uint32_t);
		user_mem_assert(curenv, (void *)stack_top - size, size, PTE_U | PTE_W);
		struct UTrapframe *utr = (struct UTrapframe *)(stack_top - size);
		utr->utf_fault_va = fault_va;
		utr->utf_err = tf->tf_err;
		utr->utf_regs = tf->tf_regs;
		utr->utf_eip = tf->tf_eip;
		utr->utf_eflags = tf->tf_eflags;
		utr->utf_esp = tf->tf_esp;				

		curenv->env_tf.tf_eip = (uintptr_t)curenv->env_pgfault_upcall;
		curenv->env_tf.tf_esp = (uintptr_t)utr;
		env_run(curenv);			//重新進入使用者態
	}

User-mode Page Fault Entrypoint

現在需要實現lib/pfentry.S中的_pgfault_upcall函式,該函式會作為系統呼叫sys_env_set_pgfault_upcall()的引數。

Exercise 10.

實現lib/pfentry.S中的_pgfault_upcall函式。

	// LAB 4: Your code here.
	addl $8, %esp // skip utf_fault_va and utf_err
	// Restore the trap-time registers.  After you do this, you
	// can no longer modify any general-purpose registers.
	// LAB 4: Your code here.
	movl 40(%esp), %eax
	movl 32(%esp), %edx
	movl %edx, -4(%eax)
	popal
	addl $4, %esp
	// Restore eflags from the stack.  After you do this, you can
	// no longer use arithmetic operations or anything else that
	// modifies eflags.
	// LAB 4: Your code here.
	popfl
	// Switch back to the adjusted trap-time stack.
	// LAB 4: Your code here.
	popl %esp
	// Return to re-execute the instruction that faulted.
	// LAB 4: Your code here.
	lea -4(%esp), %esp		
	ret

Exercise 11.

完成lib/pgfault.c中的set_pgfault_handler()

void
set_pgfault_handler(void (*handler)(struct UTrapframe *utf))
{
	int r;

	if (_pgfault_handler == 0) {
		// First time through!
		// LAB 4: Your code here.
		// panic("set_pgfault_handler not implemented");
		if (sys_page_alloc(0, (void *)(UXSTACKTOP-PGSIZE), PTE_W | PTE_U | PTE_P) < 0)
		{
			panic("set_pgfault_handler:sys_page_alloc failed!\n");
		}
		sys_env_set_pgfault_upcall(0, _pgfault_upcall);		
	}

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

5. 追蹤一次頁故障的發生

  1. 首先發生頁故障的時候就會先通過IDT表找到對應的handler

  2. 然後進入trap.ctrap_dispatch()對於頁故障會進入我們指定好的page_fault_handler函式

  3. 在這裡要做的事情就是把utr暫存器初始化好,隨後重新進入使用者態

  4. 追蹤了半天終於成功進入使用者狀態了。

    這裡有注意到地方如果你直接用在env_run(curenv)用n的話則會一直跳出去,我這裡用si追蹤終於進入了使用者狀態,看見一個正常的地址還是很開心的

    image-20210715215402273

  5. 實際上這裡就是在lib/pfentry.S中的_pgfault_upcall

    image-20210715215937994

    當然在進入這裡之前我們已經設定好了_pgfault_handler

    通過在我們user env中呼叫了位於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.
    		// panic("set_pgfault_handler not implemented");
    		if (sys_page_alloc(0, (void *)(UXSTACKTOP-PGSIZE), PTE_W | PTE_U | PTE_P) < 0)
    		{
    			panic("set_pgfault_handler:sys_page_alloc failed!\n");
    		}
    		sys_env_set_pgfault_upcall(0, _pgfault_upcall);		
    	}
    	// Save handler pointer for assembly to call.
    	_pgfault_handler = handler;
    }
    
  6. 這裡呼叫的call *%eax實際上就是呼叫了指定的handler函式。

    對於 user/faultalloc.這個使用者程式而言,就是下面這個函式

    image-20210715220833671

  7. 隨後我們完成了對於故障處理的全部過程,所以應該返回故障發生的地方繼續執行

    // LAB 4: Your code here.
    	addl $8, %esp // skip utf_fault_va and utf_err
    	// Restore the trap-time registers.  After you do this, you
    	// can no longer modify any general-purpose registers.
    	// LAB 4: Your code here.
    	movl 40(%esp), %eax
    	movl 32(%esp), %edx
    	movl %edx, -4(%eax)
    	popal
    	addl $4, %esp
    	// Restore eflags from the stack.  After you do this, you can
    	// no longer use arithmetic operations or anything else that
    	// modifies eflags.
    	// LAB 4: Your code here.
    	popfl
    	// Switch back to the adjusted trap-time stack.
    	// LAB 4: Your code here.
    	popl %esp
    	// Return to re-execute the instruction that faulted.
    	// LAB 4: Your code here.
    	lea -4(%esp), %esp		
    	ret
    

    上面這一堆彙編程式碼看起來非常麻煩。

    實際上他要做的就是把esp暫存器設定成原來故障發生前的esp。

    並且把eip暫存器設定成故障發生的地方,表示接下來要繼續執行這裡。

6. Implementing Copy-on-Write Fork

partB的最後就是要讓我們實現寫時複製的功能,在dumpfork函式中我們已經有過類似的fork函式,只不過這裡我們要實現cow的fork。整個fork函式的流程如下

  1. 父程式將pgfault函式作為c語言實現的頁處理錯誤,會用到之前lab中實現set_pgfault_handler的函式
  2. 父程式會呼叫sys_exofork建立一個子程式
  3. UTOP之下的在地址空間裡的每一個可寫或cow的頁,父程式就會呼叫duppage它會將cow頁對映到子程式的地址空間,然後重新對映cow頁到自己的地址的空間。duppage將COW的頁的PTEs設定為不能寫的,然後在PTE的avail域設定PTE_COW來區別 copy-on-write pages及真正的只讀頁。
  4. 父程式為子程式設定使用者頁錯誤入口
  5. 子程式現在可以執行,然後父程式將其標記為可執行

當父子程式試圖寫一個尚未寫過的copy-on-write頁寫時,就會產生一個頁錯誤。下面是使用者頁錯誤處理的控制流:

  1. 核心把頁錯誤傳遞到_pgfault_upcall,呼叫fork()的pgfault()處理頁錯誤。
  2. pgfault()檢查錯誤碼是否等於FEC_WR,這表示這是一個寫頁錯誤。同時檢測對應頁的PTE標記為PTE_COW。否則直接panic。
  3. pgfault()分配一個對映在一個臨時位置的新的頁,然後將錯誤頁中的內容複製進去。然後頁錯誤處理程式對映新的頁到引起page fault的虛擬地址,並設定PTE具有讀寫許可權。

使用者級的lib/fork.c必須訪問使用者環境的頁表完成以上的幾個操作(例如將一個頁對應的PTE標記為PTE_COW。核心對映使用者環境的頁表到虛擬地址UVPT的用意就在於此。它使用了一種聰明的手段讓使用者程式碼很方便的檢索PTE。lib/entry.S設定uvptuvpd使得lib/fork.c中的使用者程式碼能夠輕鬆地檢索頁表資訊。

Exercise 12.

Implement fork, duppage and pgfault in lib/fork.c.

關於這個fork的具體實現細節和深入思考,請看這裡

1. pgfault實現

  • pgfault的作用就是建立一個臨時頁用來寫入的。所以這裡的新頁必須設定PTE_W

  • 這裡必須要按照實驗給的要求判斷err & FEC_WR以及發生錯誤的頁是一個cow的頁

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.

	// 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.
	if ( !((err & FEC_WR) && (uvpd[PDX(addr)] & PTE_P) &&  (uvpt[PGNUM(addr)] & PTE_P) && (uvpt[PGNUM(addr)] & PTE_COW))) {
		panic("must be above reason");
	}
	// LAB 4: Your code here.
	if ((r = sys_page_alloc(0,PFTEMP,PTE_W | PTE_U | PTE_P) < 0)) {
		panic("alloc map error, error nubmer is %d\n",r);
	}
	// addr pgsize assign
	addr = ROUNDDOWN(addr, PGSIZE);
	memcpy((void *)PFTEMP,addr,PGSIZE);
	// 
	r = sys_page_map(0,(void *)PFTEMP,0,addr,PTE_W | PTE_U | PTE_P);
	if (r < 0) {
		panic("sysmap error");
	}
	r = sys_page_unmap(0,(void *) PFTEMP);
	if (r < 0) {
		panic("page_unmap error");
	}
	// panic("pgfault not implemented");
}

2. duppage的實現

  • 首先明確這個函式的功能就是把制定的pn頁從父程式對映到子程式中
  • 並且對映過去要把它標記成COW
  • 這裡一個主意點是這裡先將子程式(子環境)的頁標記成了cow具體原因見連結
static int
duppage(envid_t envid, unsigned pn)
{
	int r;

	// LAB 4: Your code here.
	// panic("duppage not implemented");
	void * va = (void *)(pn * PGSIZE);
	if ( (uvpt[pn] & PTE_W) || (uvpt[pn] & PTE_COW)) {
		r = sys_page_map(0, va, envid, va, PTE_COW | PTE_P | PTE_U);
		if (r < 0)  {
			return r;
		}
		r = sys_page_map(0,va, 0,va, PTE_U | PTE_COW | PTE_P);
		if (r < 0) {
			return r;
		}
	} else  {
		r = sys_page_map(0,va,envid,va, PTE_P | PTE_U);
		if (r < 0) {
			return r;
		}
	}
	return 0;
}

3. fork的實現

最後就是fork的實現

  • 首先通過系統呼叫sys_exofork建立一個子環境。
  • 如果返回的是0(也就是如果在子環境中)則直接執行
  • 否砸在父環境中要完成copy操作
  • 這裡的我們要從UTEXT開始到USTACKTOP為止依次check所有的頁,把屬於當前父程式的頁copy和子程式
  • 這裡要分配一個頁來表示使用者程式的異常棧。異常棧的作用在上面已經說過了。
envid_t
fork(void)
{
	// LAB 4: Your code here.
	// panic("fork not implemented");
	envid_t envid;
	int r;
	unsigned pn;
	set_pgfault_handler(pgfault);
	envid = sys_exofork();
	if (envid < 0) {
		panic("sys_exofork: %e", envid);
	}
	if (envid == 0) {
		// we are children
		thisenv = &envs[ENVX(sys_getenvid())];
		return 0;
	}
	// for parent
	for (pn=PGNUM(UTEXT); pn<PGNUM(USTACKTOP); pn++){ 
            if ((uvpd[pn >> 10] & PTE_P) && (uvpt[pn] & PTE_P))
                if ((r =  duppage(envid, pn)) < 0)
                    return r;
	}
	if ((r = sys_page_alloc(envid,(void *) (UXSTACKTOP - PGSIZE), (PTE_U | PTE_P | PTE_W))) < 0) {
		 panic("error");
	}
	extern void _pgfault_upcall(void); //缺頁處理
	if((r = sys_env_set_pgfault_upcall(envid, _pgfault_upcall)) < 0){
			panic("sys_set_pgfault_upcall:%e", r);
	}
	if ((r = sys_env_set_status(envid, ENV_RUNNABLE)) < 0)//設定成可執行
            return r;
	return envid; 
}

好了上面就是partB的所有內容,做完之後就可以通過partB的測試

image-20210716214918293

Recall PartB

當然在進入PartC之間,照例的分析一下partB。這裡發現一個清華小姐姐的部落格寫的很好

對於整個控制流的過程如下

image-20210716220547952

  1. 而在我們partb部分的寫時複製的話,我們修改了之前的duppage。在新的cow中我們不需要進行新的page_alloc只需要和父程式完全一樣的虛擬地址空間(注意是獨立的)
  2. 在上面的部落格裡大佬有講解了為什麼在page_fault的時候多分配了4位元組的空間同時配有圖進行分析。

PartC: IPC

在實驗4的最後一部分,我們將修改kernel以搶佔某些”不配合“的程式【搶佔式多工】,並允許程式之間顯式地進行傳遞訊息。

1.Clock Interrupts and Preemption

執行 user/spin 測試程式。這個測試程式fork出一個子程式,這個子程式一旦得到CPU的控制就會spin forever(while(1))。無論是父程式還是kernel都不會重新獲得CPU。This is obviously not an ideal situation in terms of protecting the system from bugs or malicious code in user-mode environments, 因為任何使用者模式環境只要進入一個無限迴圈並且永遠不退還 CPU,就可能使整個系統陷入停頓(halt)。為了允許核心搶佔(preempt )一個執行環境,強制重新控制 CPU,我們必須擴充套件 JOS 核心來支援來自時鐘硬體的外部硬體中斷(external hardware interrupts from the clock hardware)。

2. Interrupt discipline

外部中斷External Interrupts (即裝置中斷Device Interrupts)被稱為IRQs。有16種可能的IRQs,編號為[0,15]。IRQ 號到 IDT 條目的對映是不固定的。pic_init in picirq.c maps IRQs 0-15 to IDT entries IRQ_OFFSET through IRQ_OFFSET+15

inc/trap.h中,IRQ_OFFSET設定為32(十進位制)。因此IDT條目32-47對應於 IRQs 0-15。例如,時鐘中斷是 IRQ0,因此IDT[IRQ _ offset + 0],即IDT[32]儲存了the address of the clock’s interrupt handler routine in the kernel。之所以選擇這個IRQ_OFFSET值是為了使裝置中斷與處理器異常不重疊(事實上,在PC執行 MS-DOS的早期,IRQ_OFFSET實際上是零,這確實在處理硬體中斷和處理器異常之間造成了巨大的混亂!)。

在 JOS,與 xv6 Unix 相比,我們做了一個關鍵的簡化。外部裝置中斷在核心中總是禁用的,而和xv6一致,在使用者空間中是啟用。外部中斷由%eflags暫存器的FL_IF標誌位控制(見 inc/mmu.h)。當這個位被設定時,外部中斷被啟用。雖然這個位可以通過幾種方式修改,但是由於我們的簡化,在進入和離開使用者模式時,我們將只通過儲存和恢復%eflags暫存器的過程來處理它。

我們必須確保在使用者執行時在使用者環境中設定FL_IF標誌,以便當中斷到來時,它被傳遞到處理器並由中斷程式碼處理。否則,中斷將被遮蔽或忽略,直到中斷被重新啟用。此前,我們在bootloader的第一條指令中設定了中斷遮蔽(masked interrupts),到目前為止我們還沒有重新啟用過它們。

Exercise 13

Modify kern/trapentry.S and kern/trap.c to initialize the appropriate entries in the IDT and provide handlers for IRQs 0 through 15.

Then modify the code in env_alloc() in kern/env.c to ensure that user environments are always run with interrupts enabled.

Also uncomment the sti instruction in sched_halt() so that idle CPUs unmask interrupts(不遮蔽中斷請求).

The processor never pushes an error code when invoking a hardware interrupt handler.

修改kern/trapentry.S和kern/trap.c來初始化IDT中IRQs0-15的入口和處理函式。然後修改env_alloc函式來確保程式在使用者態執行時中斷是開啟的。

模仿原先設定預設中斷向量即可,注意在發生硬體中斷的時候,不會push error code,在kern/trapentry.S中定義IRQ0-15的處理例程時需要使用TRAPHANDLER_NOEC

  1. trap.c中加入IRQ0-15的處理例程
SETGATE(idt[IRQ_OFFSET + 0],  0, GD_KT, irq_0_handler,  0);
SETGATE(idt[IRQ_OFFSET + 1],  0, GD_KT, irq_1_handler,  0);
SETGATE(idt[IRQ_OFFSET + 2],  0, GD_KT, irq_2_handler,  0);
SETGATE(idt[IRQ_OFFSET + 3],  0, GD_KT, irq_3_handler,  0);
SETGATE(idt[IRQ_OFFSET + 4],  0, GD_KT, irq_4_handler,  0);
SETGATE(idt[IRQ_OFFSET + 5],  0, GD_KT, irq_5_handler,  0);
SETGATE(idt[IRQ_OFFSET + 6],  0, GD_KT, irq_6_handler,  0);
SETGATE(idt[IRQ_OFFSET + 7],  0, GD_KT, irq_7_handler,  0);
SETGATE(idt[IRQ_OFFSET + 8],  0, GD_KT, irq_8_handler,  0);
SETGATE(idt[IRQ_OFFSET + 9],  0, GD_KT, irq_9_handler,  0);
SETGATE(idt[IRQ_OFFSET + 10], 0, GD_KT, irq_10_handler, 0);
SETGATE(idt[IRQ_OFFSET + 11], 0, GD_KT, irq_11_handler, 0);
SETGATE(idt[IRQ_OFFSET + 12], 0, GD_KT, irq_12_handler, 0);
SETGATE(idt[IRQ_OFFSET + 13], 0, GD_KT, irq_13_handler, 0);
SETGATE(idt[IRQ_OFFSET + 14], 0, GD_KT, irq_14_handler, 0);
SETGATE(idt[IRQ_OFFSET + 15], 0, GD_KT, irq_15_handler, 0);
  1. 同時在trapentry.S中新增
TRAPHANDLER_NOEC(irq_0_handler,  IRQ_OFFSET + 0);
TRAPHANDLER_NOEC(irq_1_handler,  IRQ_OFFSET + 1);
TRAPHANDLER_NOEC(irq_2_handler,  IRQ_OFFSET + 2);
TRAPHANDLER_NOEC(irq_3_handler,  IRQ_OFFSET + 3);
TRAPHANDLER_NOEC(irq_4_handler,  IRQ_OFFSET + 4);
TRAPHANDLER_NOEC(irq_5_handler,  IRQ_OFFSET + 5);
TRAPHANDLER_NOEC(irq_6_handler,  IRQ_OFFSET + 6);
TRAPHANDLER_NOEC(irq_7_handler,  IRQ_OFFSET + 7);
TRAPHANDLER_NOEC(irq_8_handler,  IRQ_OFFSET + 8);
TRAPHANDLER_NOEC(irq_9_handler,  IRQ_OFFSET + 9);
TRAPHANDLER_NOEC(irq_10_handler, IRQ_OFFSET + 10);
TRAPHANDLER_NOEC(irq_11_handler, IRQ_OFFSET + 11);
TRAPHANDLER_NOEC(irq_12_handler, IRQ_OFFSET + 12);
TRAPHANDLER_NOEC(irq_13_handler, IRQ_OFFSET + 13);
TRAPHANDLER_NOEC(irq_14_handler, IRQ_OFFSET + 14);
TRAPHANDLER_NOEC(irq_15_handler, IRQ_OFFSET + 15);
  1. 在env_alloc函式中為建立的environment設定中斷開啟。

    	// Enable interrupts while in user mode.
    	// LAB 4: Your code here.
    	e->env_tf.tf_eflags |= FL_IF;
    
  2. sched_halt()中註釋sti指令,設定中斷開啟,允許接受時鐘硬體中斷

// Reset stack pointer, enable interrupts and then halt.	asm volatile (		"movl $0, %%ebp\n"		"movl %0, %%esp\n"		"pushl $0\n"		"pushl $0\n"		// Uncomment the following line after completing exercise 13		"sti\n"		"1:\n"		"hlt\n"		"jmp 1b\n"	: : "a" (thiscpu->cpu_ts.ts_esp0));

3. Handling Clock Interrupts

在 user/spin程式中,在子程式第一次執行之後,它只是在迴圈中spin,核心不再能得到控制權。因此我們需要對硬體進行程式設計來週期性地產生時鐘中斷,從而迫使CPU控制權回到核心中,故我們可以在不同的使用者環境中切換控制。

通過呼叫 lapic_init and pic_init(from i386_init in init.c)設定了時鐘和中斷控制器來生成中斷。現在需要編寫處理這些中斷的程式碼。

Exercise 14.

需要在核心程式碼中對時鐘中斷進行處理,呼叫之前實現的sched_yield,來排程別的environment使用CPU即可:

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

改完上面的程式碼之後就可以拿到65/80分了

4. Inter-Process communication (IPC)

有非常多程式之間的通訊方法,但在這裡我們只會實現一個非常簡單的方法。實驗指導書中有關於一些知識點講解,但是直接翻譯感覺好亂。後面我就直接梳理了

Exercise 15.

1. 實現在kern/syscall.c中的sys_ipc_try_send

  • 按照註釋提示中去實現各種判斷如必須對奇、perm是否合適
  • 第一個要注意考慮的是這個srcva必須是頁對奇的
  • 而且這個srcva要在UTOP之下表示要傳送page currently mapped at 'srcva'
  • you should set the checkperm flag to 0,
static int
sys_ipc_try_send(envid_t envid, uint32_t value, void *srcva, unsigned perm)
{
	// LAB 4: Your code here.
	struct Env *rec_env, *cur_env;
	int r;
	// get current env
	envid2env(0,&cur_env,0));
	assert(cur_env);
	if ((r = envid2env(envid, &rec_env,0)) < 0) {
		return r;
	}
	if (!rec_env->env_ipc_recving) {
		return -E_IPC_NOT_RECV;
	}
	if ((uintptr_t)srcva < UTOP) {
		struct PageInfo *pg;
		pte_t *pte;
		// if srcva is not page-aligned
		// 0x1000 - 1=  0x0111
		// if srcva any bit is 1 is not page-aligned
		if ((uintptr_t) srcva & (PGSIZE - 1)) {
			return -E_INVAL;
		}
		// perm is inappropriate is same as the sys_page_alloc
		if(!(perm & PTE_U) || !(perm & PTE_P) || (perm & (~PTE_SYSCALL))){
			return -E_INVAL;
		}
		// srcva is mapped in the caller's address spcae
		if (!(pg = page_lookup(cur_env->env_pgdir,srcva,&pte))) {
			return -E_INVAL;
		}
		// if (perm & PTE_W), but srcva is read-only 
		if ((perm & PTE_W) && !((*pte) & PTE_W)) {
			return -E_INVAL;
		}
		if ((uintptr_t)rec_env->env_ipc_dstva < UTOP) {
			if ((r = page_insert(rec_env->env_pgdir,pg,rec_env->env_ipc_dstva,perm)  < 0)) { 
				return -r;
			}
		}
	}
	rec_env->env_ipc_perm = perm;
	rec_env->env_ipc_value = value;
	rec_env->env_ipc_recving = 0;
	rec_env->env_ipc_from = cur_env->env_id;
	rec_env->env_status = ENV_RUNNABLE;
  rec_env->env_tf.tf_regs.reg_eax = 0;
	return 0;
}

2. 實現sys_ipc_recv函式

static int
sys_ipc_recv(void *dstva)
{
	// LAB 4: Your code here.
	if (((uintptr_t) dstva < UTOP) && ((uintptr_t)dstva & (PGSIZE  - 1))) {
		return -E_INVAL;
	}
	struct Env *cur_env;
	envid2env(0,&cur_env,0);
	assert(cur_env);
	cur_env->env_status = ENV_NOT_RUNNABLE;
	cur_env->env_ipc_recving = 1;
	cur_env->env_ipc_dstva = dstva;
	sys_yield();
	return 0;
}

3. 隨後實現在lib/ipc.cipc_recvipc_send函式

  • 如果pg = none就把pg設定成UTOP這樣在系統呼叫裡面就不會send page
  • 如果呼叫sys_ipc_recv成功的話則就會設定引數
int32_t
ipc_recv(envid_t *from_env_store, void *pg, int *perm_store)
{
	// LAB 4: Your code here.
	// panic("ipc_recv not implemented");
	int error;
	if(!pg)pg = (void *)UTOP; //
	if((error = sys_ipc_recv(pg)) < 0){
		if(from_env_store)*from_env_store = 0;
		if(perm_store)*perm_store = 0;
		return error;
	}

	if(from_env_store)*from_env_store = thisenv->env_ipc_from;
	if(perm_store)*perm_store = thisenv->env_ipc_perm;
	return thisenv->env_ipc_value;
}
  • 根據給定的引數目標環境、val、pg、perm呼叫系統呼叫傳送message
  • 如果得到的error code 不是E_IPC_NOT_RECV則直接panic
void
ipc_send(envid_t to_env, uint32_t val, void *pg, int perm)
{
	// LAB 4: Your code here.
	// panic("ipc_send not implemented");
	if (!pg) {
		pg = (void *)UTOP;
	} 
	int r;
	while((r = sys_ipc_try_send(to_env,val,pg,perm)) < 0) {
		if (r != -E_IPC_NOT_RECV) {
			panic("sys_ipc_try_send error %e\n",r);
		}
		sys_yield();
	}
}

相關文章