環(huán)境
deepin 20
實驗地址:mit6.828 2018 lab4 partC
很重要:不知道是不是我的代碼實現(xiàn)有些問題补箍,請把lib/syscall.c中的sys_ipc_recv()中的 return syscall()的第二個參數(shù)由1改為0。具體看文末
正文
在lab4的最后一部分衬横,你將會修改內(nèi)核的代碼來從uncooperative的進(jìn)程中搶回cpu终蒂,同時我們的內(nèi)核還將會支持讓進(jìn)程之間互相通信拇泣。
Clock Interrupts and Preemption
運(yùn)行user/spin程序。這個程序會創(chuàng)建一個子線程睁蕾,這個子線程在得到CPU后會運(yùn)行一個死循環(huán)惫霸。無論是父進(jìn)程還是子進(jìn)程都不能再次得到CPU葱弟。從保護(hù)系統(tǒng)遠(yuǎn)離bug和用戶程序中的惡意代碼來說芝加,這并不是一個好的情況射窒,因為任一一個用戶程序能夠獨占CPU脉顿。為了使得內(nèi)核能夠搶回CPU的執(zhí)行艾疟,強(qiáng)制終端當(dāng)前程序的執(zhí)行,所以我們必須讓JOS能夠支持外部中斷弟疆。
Interrupt discipline
外部中斷(也就是設(shè)備中斷盗冷,比如說鍵盤的中斷仪糖,時鐘的中斷等等) ,稱為IRQs. 一般來說呢有16個外部中斷(比如說級聯(lián)的8259A芯片)蟆湖,從0到15讼育。將IRQ映射到IDT中并不是固定的奶段。在picirq.c中的pic_init()
函數(shù)我們將IRQs的0-15映射到了IRQ_OFFSET到IRQ_OFFSET+15.
在inc/trap.h中,IRQ_OFFSET就是十進(jìn)制的32呢铆。因此IDT中的32-47對應(yīng)的就是IRQ的0-15棺克。比如說线定,時鐘中斷就是IRQ 0斤讥,于是呢 IDT[IFQ_OFFSET+0]
就是時鐘中斷的中斷處理程序地址。IRQ_OFFSET之所以這樣選擇是因為這樣一來和處理的異常(exceptions,比如說除0異常)之間不會存在overlap(這也就是說派草,比如說將IRQ_OFFSET設(shè)置為0近迁,這樣就會造成沖突0)鉴竭。原文說到早期的MS-DOS系統(tǒng)就是這樣做的勿侯,不知道到底咋實現(xiàn)的助琐。
在JOS中,和xv6比起來做了一點簡化蛆橡。外部中斷在內(nèi)核當(dāng)中的時候總是關(guān)閉了(也就是說在內(nèi)核中不響應(yīng)外部中斷)泰演,與xv6相似的是睦焕,在用戶程序中是開啟外部中斷的。外部中斷通過eflags中的FL_IF bit來控制猾普。當(dāng)這個bit被設(shè)置的時候初家,外部中斷開啟乌助。雖然設(shè)置這個bit有多種方法他托,簡單起見赏参,我們就通過保存以及恢復(fù)eflags的方法來設(shè)置。(eflags += FL_IF,通過或運(yùn)算可以設(shè)置某bit)
你需要確保在用戶程序當(dāng)中FL_IF的設(shè)置,這樣一來當(dāng)用戶程序運(yùn)行的時候可以通過外部中斷將CPU交給內(nèi)核纸俭。如果不這樣做的話揍很,那么中斷就被屏蔽了万伤。我們在bootloader當(dāng)中使用cli
指令屏蔽了外部中斷敌买,到目前為止都沒有開啟外部中斷。
Exercise 13:
修改kern/trapentry.S和kern/trap.c的代碼來初始化IDT中的成員膘融。修改kern/env.c中的env_alloc()函數(shù)祭玉,確保用戶程序執(zhí)行的時候可以響應(yīng)外部中斷脱货。
shced_halt()中的sti語句取消注釋振峻,這樣一來其他idle的CPU也可以響應(yīng)中斷。
在完成了這些練習(xí)后多矮,如果你運(yùn)行一些測試程序(比如說spin)塔逃,你應(yīng)該可以看到內(nèi)核輸出了trap frames for hardware interrupts. 雖然現(xiàn)在中斷已經(jīng)被interrupted了湾盗,但是JOS還沒有處理他們立轧,所以會出錯氛改。
代碼實現(xiàn):
這個不難胜卤。經(jīng)過我們前面的lab,修改IDT很容易實現(xiàn)澈段。代碼如下败富,我將幾個涉及到的地方放到一起兽叮,具體看注釋的內(nèi)容。
//trap.c中的trap_init()
SETGATE(idt[IRQ_OFFSET + IRQ_TIMER], 0, GD_KT, timer_handler, 0);
SETGATE(idt[IRQ_OFFSET + IRQ_KBD], 0, GD_KT, kbd_handler, 0);
SETGATE(idt[IRQ_OFFSET + IRQ_SERIAL], 0, GD_KT, serial_handler, 0);
SETGATE(idt[IRQ_OFFSET + IRQ_SPURIOUS], 0, GD_KT, spurious_handler, 0);
SETGATE(idt[IRQ_OFFSET + IRQ_IDE], 0, GD_KT, ide_handler, 0);
SETGATE(idt[IRQ_OFFSET + IRQ_ERROR], 0, GD_KT, error_handler, 0);
/*
external interrupt,trapentry.S
*/
TRAPHANDLER_NOEC(timer_handler, IRQ_OFFSET + IRQ_TIMER);
TRAPHANDLER_NOEC(kbd_handler, IRQ_OFFSET + IRQ_KBD);
TRAPHANDLER_NOEC(serial_handler, IRQ_OFFSET + IRQ_SERIAL);
TRAPHANDLER_NOEC(spurious_handler, IRQ_OFFSET + IRQ_SPURIOUS);
TRAPHANDLER_NOEC(ide_handler, IRQ_OFFSET + IRQ_IDE);
TRAPHANDLER_NOEC(error_handler, IRQ_OFFSET + IRQ_ERROR);
// LAB 4: Your code here.在env.c中的env_alloc()
e->env_tf.tf_eflags |= FL_IF;
sched.c,將sti注釋取消了,這樣就開啟了中斷
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));
Handling Clock Interrupts
在user/spin程序當(dāng)中,當(dāng)子程序輪到執(zhí)行的時候宰僧,陷入到了死循環(huán)當(dāng)中琴儿,于是內(nèi)核永遠(yuǎn)也得不到CPU嘁捷。我們需要program硬件讓它周期性的產(chǎn)生時鐘中斷雄嚣,使得將CPU控制權(quán)還給內(nèi)核缓升,經(jīng)調(diào)度程序之后可以在多個用戶程序之間交替港谊。
Exercise 14:
修改內(nèi)核的trap_dispatch()的代碼歧寺,當(dāng)時鐘中斷發(fā)生的時候可以讓內(nèi)核調(diào)用sched_yield()來尋找然后運(yùn)行不同的用戶程序。
現(xiàn)在你應(yīng)該可以讓user/spin正常運(yùn)行了龙致。
PS: 我們在每一次響應(yīng)時鐘中斷的時候净当,應(yīng)該主動告訴外設(shè),表示中斷結(jié)束了(End of Interrupt, e.g. EOI)潭苞。關(guān)于外設(shè)的一些IO port的意思我沒有詳細(xì)的去了解真朗,希望以后有時間補(bǔ)上這個坑吧。
關(guān)于EOI的一點資料:8259-PIC-EOI
思路:
響應(yīng)時鐘中斷十分簡單湖笨,只需要新加入一個case就可以慈省。然后在代碼實現(xiàn)中眠菇,調(diào)用sys_yield()捎废,別忘了發(fā)送EOI
case (IRQ_OFFSET + IRQ_TIMER):
// cprintf("hello world!\n");
lapic_eoi();
sched_yield();
// break;
Inter-Process communication(IPC)
(從技術(shù)來說登疗,在JOS中叫IEC更合適辐益。因為Inter-Environemnt commuications,感覺像冷笑話)荷腊。
我們過去一直專注于操作系統(tǒng)的隔離(isolation,各個進(jìn)程的執(zhí)行不會影響對方)女仰,這使得每一個程序都感覺他在獨享整個電腦疾忍。另外一個重要的東西操作系統(tǒng)需要提供的服務(wù)就是讓每一個進(jìn)程之間相互通信。Unix pipe就是一個經(jīng)典的例子杨幼。
現(xiàn)有非常多的模型用于進(jìn)程通信差购。甚至在今天還有爭論說到底哪個一個才是最好的欲逃。我們不討論這個稳析,我們只是實現(xiàn)一個最簡單的IPC。
IPC on JOS
你將會實現(xiàn)幾個JOS的系統(tǒng)調(diào)用诚纸,這幾個系統(tǒng)調(diào)用共同來實現(xiàn)進(jìn)程之間通訊(inter-process communiation).你將會實現(xiàn)兩個系統(tǒng)調(diào)用,sys_ipc_recv()
和sys_ipc_try_send()
.然后實現(xiàn)兩個wrapper,ipc_recv()
和ipc_send()
奴潘。
Sending and Receiving Messages
為了接受一個消息画髓,一個進(jìn)程調(diào)用sys_ipc_recv()
奈虾。這個系統(tǒng)調(diào)用de-schedules 當(dāng)前正在運(yùn)行的進(jìn)程并且不在運(yùn)行它直到數(shù)據(jù)已經(jīng)接收到了肉微。當(dāng)一個進(jìn)程等待接受數(shù)據(jù)的時候碉纳,任何其他的都可以向他發(fā)送數(shù)據(jù),并不是說只有某個進(jìn)程專屬的奴愉,也不需要發(fā)送者和接收者之間有某種聯(lián)系锭硼,比如說它倆是父子進(jìn)程檀头。換句話說岖沛,在part A中實現(xiàn)的權(quán)限校驗并不適用于IPC暑始,因為IPC system call 被精心設(shè)計過婴削,從而把是相當(dāng)安全的蒋荚,一個進(jìn)程不能通過發(fā)送數(shù)據(jù)使得另外一個進(jìn)程出問題。
為了發(fā)送一個數(shù)據(jù)馆蠕,一個進(jìn)程調(diào)用sys_ipc_try_send()
期升,參數(shù)是接收者的ID以及所有發(fā)送的內(nèi)容。如果指定的接收者正在準(zhǔn)備接收消息(也就是說調(diào)用了sys_ipc_recv()
且并未返回)互躬,那么發(fā)送數(shù)據(jù)給它并且返回0.否則的話播赁,返回-E_IPC_NOT_RECV來表明接收者現(xiàn)在并不想接收消息。這里可能比較難懂吼渡,反正我一開始的時候沒有理解其中的意思,待會到代碼實現(xiàn)的時候在解釋下寺酪。
Transferring Pages
當(dāng)一個進(jìn)程調(diào)用sys_ipc_recv()
并且?guī)в袇?shù)dstva的時候坎背,這就表明這個用戶進(jìn)程可以接受一個page mapping。如果發(fā)送者發(fā)送了一個pgae,那么這個page should be mapped at dstva in receiveris address space. 如果接收者已經(jīng)在dstva已經(jīng)有一個頁映射了寄雀,那么之前的就需要unmaped.
當(dāng)一個進(jìn)程調(diào)用sys_ipc_try_send()
并且?guī)в袇?shù)srcva,這就意味著發(fā)送者希望將當(dāng)前地址空間srcva映射的page發(fā)送到接收者得滤,權(quán)限為perm。在IPC成功以后盒犹,發(fā)送者繼續(xù)保持srcva在自己的地址空間內(nèi)的映射關(guān)系懂更,但是接收者也獲得了這個同樣的映射關(guān)系。(這也就是說在兩個不同的進(jìn)程中srcva映射到了相同的物理頁)急膀。這樣一來結(jié)果是sender和receiver共享了一個物理頁沮协。
Implementing IPC
Exercise 15:
實現(xiàn)sys_ipc_recv 和 sys_ipc_try_send在 kern/syscall.c中。在實現(xiàn)他們之前閱讀一下注釋卓嫂。當(dāng)你調(diào)用envid2env的時候checkperm設(shè)置為0.另外實現(xiàn)ipc_recv 和 ipc_sned在lib/ipc.c中慷暂。
運(yùn)行user/pingpong和user/primes程序來測試你的IPC實現(xiàn)。
這幾個函數(shù)的邏輯是:系統(tǒng)調(diào)用sys_ipc_recv和sys_ipc_try_send分別是接收數(shù)據(jù)和發(fā)送數(shù)據(jù)晨雳。sys_ipc_send
的幾個參數(shù)分別是目標(biāo)進(jìn)程ID(envid)行瑞,要發(fā)送的值(value), 要發(fā)送的頁如果需要的話(srcva),頁的權(quán)限(perm)。sys_ipc_recv
的參數(shù)dstva表示當(dāng)前進(jìn)程想要接收到的數(shù)據(jù)放到dstva處悍募。
在實現(xiàn)這些代碼之前蘑辑。先來看一下Env這個結(jié)構(gòu)當(dāng)中新加入的一些結(jié)構(gòu):
- env_ipc_recving
當(dāng)前進(jìn)程的狀態(tài),表明當(dāng)前進(jìn)程是否處于接受狀態(tài)坠宴。 - env_ipc_dstva
當(dāng)前進(jìn)程要把接收到的數(shù)據(jù)放到哪兒(如果需要用頁來傳遞數(shù)據(jù))洋魂。 - env_ipc_value
當(dāng)前進(jìn)程接收到的數(shù)據(jù)(如果用頁來傳遞數(shù)據(jù))。 - env_ipc_from
當(dāng)前進(jìn)程接收到的數(shù)據(jù)來自誰喜鼓。 - env_ipc_perm
當(dāng)前進(jìn)程接收到的數(shù)據(jù)的頁的權(quán)限副砍。
sys_ipc_recv():
某個進(jìn)程調(diào)用這個系統(tǒng)調(diào)用可以來接收數(shù)據(jù)。所以我們在這個函數(shù)內(nèi)部必然要做的是切換當(dāng)前的接受狀態(tài)庄岖,放棄CPU(如果不放棄CPU豁翎,那么發(fā)送者永遠(yuǎn)得不到CPU了)。在結(jié)合注釋里面的信息隅忿,我們還需要校驗一下地址是否是頁對齊的(page-aligned)下面是具體的代碼實現(xiàn):
static int
sys_ipc_recv(void *dstva)
{
// LAB 4: Your code here.
// panic("sys_ipc_recv not implemented");
uint32_t dst_addr = (uint32_t)dstva;
if(dst_addr < UTOP && PGOFF(dst_addr) > 0) {
// not page-aligned, return -E_INVAL;
return -E_INVAL;
}
curenv->env_ipc_recving = 1; // 表示當(dāng)前進(jìn)程正在接受信息
curenv->env_ipc_dstva = dstva; //表明想接收數(shù)據(jù)到dstva這個虛擬地址
curenv->env_status = ENV_NOT_RUNNABLE; // block until a message has been received
sched_yield();
return 0;
}
sys_ipc_try_send():
這個系統(tǒng)調(diào)用的實現(xiàn)心剥,需要進(jìn)行一大堆的條件判斷來剔除異常情況邦尊。除此之外,值得我們關(guān)注的就是注釋里面提到的
這里的意思是:如果目標(biāo)進(jìn)程沒有被掛起(放棄CPU)优烧,即env_ipc_recving == 0的時候蝉揍,因為env_ipc_recving == 1說明進(jìn)程等待接受數(shù)據(jù)。那么應(yīng)該返回-E_IPC_NOT_RECV畦娄。此外還需要關(guān)注的就是又沾,我們往目標(biāo)進(jìn)程插入頁,接收者和發(fā)送者共享一個頁熙卡。
代碼如下:
static int
sys_ipc_try_send(envid_t envid, uint32_t value, void *srcva, unsigned perm)
{
// LAB 4: Your code here.
int result;
struct Env* proc; //目標(biāo)進(jìn)程
uint32_t src_addr = (uint32_t)srcva; // the page address that will be sent to target process
struct PageInfo *page; //phyiscal page
pte_t *pg_table_entry; // page table entry
if ((result = envid2env(envid, &proc, 0)) < 0)
return result;
if(proc->env_ipc_recving == 0) {
// sys_ipc_recv()中設(shè)置了recving=1來表明這個進(jìn)程想接受數(shù)據(jù), 如果target process的recving == 0
// 說明target process并不想接收數(shù)據(jù)杖刷,所以return -E_IPC_NOT_RECV;
return -E_IPC_NOT_RECV;
}
if(src_addr < UTOP && PGOFF(src_addr) > 0) {
return -E_INVAL;
}
if (((uint32_t)srcva < UTOP) && ((perm | PTE_SYSCALL) != PTE_SYSCALL)) {
return -E_INVAL;
}
page = page_lookup(curenv->env_pgdir,srcva,&pg_table_entry);
if(src_addr < UTOP && page == NULL) {
return -E_INVAL;
}
if((perm & PTE_W) && !(*pg_table_entry & PTE_W)) {
//(*pg_table_entry & PTE_W)如果這個條件成立,說明是writetable的
//所以如果我們要判斷它是否是一個read-only的驳癌,只需要滑燃!即可
return -E_INVAL;
}
proc->env_ipc_perm = 0;
if ((src_addr< UTOP) && ((uint32_t) proc->env_ipc_dstva < UTOP)){
// 如果src_addr < UTOP,才可以使用頁來傳遞數(shù)據(jù)
result = page_insert(proc->env_pgdir,page,proc->env_ipc_dstva,perm);
if(result < 0) {
return -E_NO_MEM;
}
proc->env_ipc_perm = perm;
}
proc->env_ipc_recving = 0; //表示接受完畢
proc->env_ipc_value = value;
proc->env_ipc_from = curenv->env_id;
proc->env_status = ENV_RUNNABLE; //接收數(shù)據(jù)完畢后設(shè)置為RUNNABLE,接受調(diào)度
return 0;
// panic("sys_ipc_try_send not implemented");
}
ipc_send():
相對來說ipc_send()和系統(tǒng)調(diào)用比起來就沒有那么困難了喂柒。他只是一個對于系統(tǒng)調(diào)用sys_ipc_try_send()
的包裝不瓶。結(jié)合注釋,他說this function keeps trying until it succeeds 這就是意味著我們需要用一個循環(huán)來判斷目標(biāo)進(jìn)程是否準(zhǔn)備好接收消息灾杰。如果sys_ipc_try_send()
的返回值不是-E_IPC_NOT_RECV那么就panic蚊丐。另外我們要發(fā)送消息,肯定要讓當(dāng)前進(jìn)程放棄CPU的艳吠,因此在最后還需要調(diào)用sys_yield()來放棄CPU麦备。其他的一些信息就卸載注釋里面了,代碼如下:
void
ipc_send(envid_t to_env, uint32_t val, void *pg, int perm)
{
// LAB 4: Your code here.
int result;
if(pg == NULL) {
//如果不需要以頁來發(fā)送數(shù)據(jù),那么就將pg設(shè)置為一個合適的值
//讓sys_ipc_try_send理解這個不是一個合法的地址
//根據(jù)sys_ipc_try_send里面的注釋,很多條件都需要 < UTOP,那就意味著說如果我們傳入UTOP
//將會被視為不合法的地址
pg = (void*)UTOP;
}
while((result = sys_ipc_try_send(to_env,val,pg,perm)) == -E_IPC_NOT_RECV);
if(result != -E_IPC_NOT_RECV && result < 0) {
panic("ipc_send():send message to %d failed",to_env);
}
sys_yield();
}
ipc_recv():
這個函數(shù)是對系統(tǒng)調(diào)用sys_ipc_recv()
的封裝昭娩。根據(jù)注釋里面的解釋凛篙,主要是進(jìn)行返回值的判斷。不必多說栏渺,代碼如下呛梆。
int32_t
ipc_recv(envid_t *from_env_store, void *pg, int *perm_store)
{
// LAB 4: Your code here.
if(pg == NULL) {
// 系統(tǒng)調(diào)用中的條件都是要求pg < UTOP,所以傳入UTOP就會被視為
// no page
pg = (void*)UTOP;
}
int result;
result = sys_ipc_recv(pg);
if(result < 0) {
if(from_env_store != NULL) {
*from_env_store = 0;
}
if(perm_store != NULL) {
*perm_store = 0;
}
}
if(from_env_store != NULL) {
*from_env_store = thisenv->env_ipc_from;
}
if(perm_store != NULL) {
*perm_store = thisenv->env_ipc_perm;
}
// return the value sent by sender
return thisenv->env_ipc_value;
}
修改lib/syscall.c的代碼:
如果不修改,我一直無法通過pingpong和primes測試點磕诊。并且運(yùn)行的時候會報錯填物,將原來的代碼修改為:
int
sys_ipc_recv(void *dstva)
{
return syscall(SYS_ipc_recv, 0, (uint32_t)dstva, 0, 0, 0, 0);
//原來為return syscall(SYS_ipc_recv, 1, (uint32_t)dstva, 0, 0, 0,
}c
往kern/syscall.c中加入新的case:
雖然這個很簡單,但是我在這里找了很久的bug霎终。真是低級的錯誤
實驗結(jié)果
運(yùn)行make run-pingpong
滞磺,出現(xiàn)以下結(jié)果:
運(yùn)行make grade
,結(jié)果如下莱褒,我們通過了所有的測試點击困。