Lab4 Preemptive Multitasking(下)
lab4的第二部分要求我們實現fork的cow。在整個lab的第一部分我們實現了對多cpu的支援和再多系統環境中的切換,但是最後分析的時候沒有分析環境建立的系統呼叫,這裡先補一下對環境建立的系統呼叫的分析
recall A續
- 這裡的分析要從
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。
-
從父程式中返回之後會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
用到了我們實驗中實現的三個系統呼叫,但是好像真正用到的就是第一個。。後面兩個只是在測試,我個人看起來是這樣的,如果有問題的話,歡迎大家評論指出這裡有兩個地址需要⚠️注意分別是
UTEXT
和UTEMP
分別表示使用者環境的程式碼段地址和一個用來測試的子環境段。。。
-
隨後執行
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]書中有對應的內容。
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中的頁面故障處理程式碼,以處理在使用者模式下發生的頁面故障。在故障處理程式中需要做下面的事情。
- 判斷
curenv->env_pgfault_upcall
是否設定。如果沒有的話則直接銷燬該程式 - 修改
esp
,切換到使用者異常棧 - 在棧中壓入
UTrapframe
結構 - 將
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. 追蹤一次頁故障的發生
-
首先發生頁故障的時候就會先通過IDT表找到對應的
handler
-
然後進入
trap.c
和trap_dispatch()
對於頁故障會進入我們指定好的page_fault_handler
函式 -
在這裡要做的事情就是把utr暫存器初始化好,隨後重新進入使用者態
-
追蹤了半天終於成功進入使用者狀態了。
這裡有注意到地方如果你直接用在
env_run(curenv)
用n的話則會一直跳出去,我這裡用si追蹤終於進入了使用者狀態,看見一個正常的地址還是很開心的 -
實際上這裡就是在
lib/pfentry.S
中的_pgfault_upcall當然在進入這裡之前我們已經設定好了
_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; }
-
這裡呼叫的
call *%eax
實際上就是呼叫了指定的handler函式。對於
user/faultalloc
.這個使用者程式而言,就是下面這個函式 -
隨後我們完成了對於故障處理的全部過程,所以應該返回故障發生的地方繼續執行
// 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
函式的流程如下
- 父程式將
pgfault
函式作為c語言實現的頁處理錯誤,會用到之前lab中實現set_pgfault_handler
的函式 - 父程式會呼叫
sys_exofork
建立一個子程式 - 在
UTOP
之下的在地址空間裡的每一個可寫或cow的頁,父程式就會呼叫duppage
它會將cow頁對映到子程式的地址空間,然後重新對映cow頁到自己的地址的空間。duppage將COW的頁的PTEs設定為不能寫的,然後在PTE的avail域設定PTE_COW來區別 copy-on-write pages及真正的只讀頁。 - 父程式為子程式設定使用者頁錯誤入口
- 子程式現在可以執行,然後父程式將其標記為可執行
當父子程式試圖寫一個尚未寫過的copy-on-write頁寫時,就會產生一個頁錯誤。下面是使用者頁錯誤處理的控制流:
- 核心把頁錯誤傳遞到
_pgfault_upcall
,呼叫fork()的pgfault()處理頁錯誤。 - pgfault()檢查錯誤碼是否等於FEC_WR,這表示這是一個寫頁錯誤。同時檢測對應頁的PTE標記為PTE_COW。否則直接panic。
- pgfault()分配一個對映在一個臨時位置的新的頁,然後將錯誤頁中的內容複製進去。然後頁錯誤處理程式對映新的頁到引起page fault的虛擬地址,並設定PTE具有讀寫許可權。
使用者級的lib/fork.c
必須訪問使用者環境的頁表完成以上的幾個操作(例如將一個頁對應的PTE
標記為PTE_COW
。核心對映使用者環境的頁表到虛擬地址UVPT的用意就在於此。它使用了一種聰明的手段讓使用者程式碼很方便的檢索PTE。lib/entry.S
設定uvpt
和uvpd
使得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的測試
Recall PartB
當然在進入PartC之間,照例的分析一下partB。這裡發現一個清華小姐姐的部落格寫的很好
對於整個控制流的過程如下
- 而在我們partb部分的寫時複製的話,我們修改了之前的
duppage
。在新的cow中我們不需要進行新的page_alloc
只需要和父程式完全一樣的虛擬地址空間(注意是獨立的) - 在上面的部落格裡大佬有講解了為什麼在
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
andkern/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()
inkern/env.c
to ensure that user environments are always run with interrupts enabled.Also uncomment the
sti
instruction insched_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
。
- 在
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);
- 同時在
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);
-
在env_alloc函式中為建立的environment設定中斷開啟。
// Enable interrupts while in user mode. // LAB 4: Your code here. e->env_tf.tf_eflags |= FL_IF;
-
在
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.c
的ipc_recv
和ipc_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();
}
}