摘要:對于程序間通信,我們往往并不陌生。linux下的程序間通信主要有管道、信号量、消息隊列等幾種模式。在《自己動手寫作業系統中》,我們将采用消息機制來實作程序間通信,原來和linux的消息隊列有些類似。
1.IPC
同步與異步;很多領域裡我們都用到了同步和異步的概念,這裡再次區分一下。同步好比走路,走路畢竟需要同步嘛。當你的左腳邁出去之後,會等待你的右腳邁出去,不然你的左腳隻能等待(一般人不會連續兩次邁左腳)。異步正好相反,A不必總是等待着B。
同步IPC:在本節中,我們選用同步通信的方式,好處:1)作業系統不許要維護緩沖區來存放傳遞的消息 2)作業系統不需要保留消息副本 3)作業系統不許要維護接受隊列(但是需要維護發送隊列) 4)發送者和接收者能夠快速知道狀态資訊
2.實作IPC
要實作一個IPC,需要增加一個系統調用。使用者态和核心态的對應是sendrec && sys_sendrec()
2.1 code:sendrec(kernel/syscall.asm)
;=========================================================================================
;sendrec(int function , int dest_src, MESSAGE *m)
sendrec:
mov eax,_NR_sendrec
mov ebx,[esp+4];function
mov ecx,[esp+8];sec_dest
mov edx,[esp+12];p_msg
int INT_VECTOR_SYS_CALL
ret
2.2 code:sys_snedrec(kernel/proc.c)
int sys_sendrec(int function, int src_dest, MESSAGE *m, PROC *p)
{
assert(k_reenter==0);//make sure we are not in ring0
assert((src_dest>=0 && src_dest< NR_PROCS) ||
src_dest==ANY ||
src_dest == INTERRUPT);
int ret=0;
int caller=proc2pid(p);
MESSAGE *mla=(MESSAGE *)va2la(caller,m);
mla->source=caller;
assert(mla->source != src_dest);
if(function==SEND){
ret=msg_send(p,src_dest,m);
if(ret!=0)
return ret;
}
else if (function== RECEIVE){
ret=msg_receive(p,src_dest,m);
if(ret!0){
return ret;
}
}
else {
panic("{sys_sendrec} invalid function: %d (SEND:%d, RECEIVE:%d).",function,SEND, RECEIVE);
}
return 0;
}
函數解析:其中asser()和panic()是斷言函數,與邏輯關系不大,我們稍後分析。我們來看看其中的幾個常量定義:
code:include/msg.h:
struct mess1{
int m1i1;
int m1i2;
int m1i3;
int m1i4;
};
struct mess2{
void *m2p1;
void *m2p2;
void *m2p3;
void *m2p4;
};
struct mess3{
int m3i1;
int m3i2;
int m3i3;
int m3i4;
u64 m3l1;
u64 m3l2;
void *m3p1;
void *m3p2;
};
typedef struct{
int source;
int type;
union{
struct mess1 m1;
struct mess2 m2;
struct mess3 m3;
}u;
}MESSAGE;
#define SEND 1
#define RECEIVE 2
#define BOTH 3
enum msgtype{
HARD_INT=1,
GET_TIKES,
};
#define RETVAL u.ms3.m3i1
與程序相關的常量:include/proc.h
/*tasks */
#define INVALID_DRIVER -20
#define INTERRUPT -10
#define TASK_TTY 0
#define TASK_SYS 1
#define ANY (NR_PROCS+10)
#define NO_TASK (NR_PROCS + 20)
總結一下:function函數用三個宏表示SEND、RECEIVE、BOTH, src_dest也是整形常量,有6個相關的宏定義;msgtype有若幹定義。注意,程序通信和消息通信的相關變量分别存放在proc.h && msg.h. 兩個簡單的函數proc2pid()和va2la()比較簡單,不解釋,他們都定義在proc.c中。相應的,我們需要對sys_call和save進行改造,使得edx能夠被用作參數。
2.2.1 assert() && panic()
這兩個是出錯處理相關的函數,我們将它放在err.h && err.c之中
2.2.1.1 assert()
/* assert and panic*/
#define ASSERT
#ifdef ASSERT//if
void assertion_failure(char *exp, char *file, char * bas_file, int file);
#define assert(exp) if (exp);\
else assertion_failure(#exp,__FILE__, __BASE_FILE__, __LINE__)
#else//else
#define assert(exp)
#endif//end
//assert()是一個宏定義,如果exp表達式為假,那麼将列印這個表達式的相關變量,而且進入死loop。
這裡,三個宏定義是編譯器相關,不用使用者定義,"#exp"的意思是将exp參數加上雙引号。我們接下來看assertion_failure():
void assertion_failure(char *exp, char *file, char * base_file, int line)
{//
printl("%c assert(%s) failed: file: %s, base_file: %s,
ln%d",MAG_CH_ASSERT,exp, file, base_file, line);
spin("assertion_failure()");
__asm__ __volatile__("ud2");
}
void spin(char *funcname)
{
printl("\nspin in %s. . . \n",funcname);
while(1){
}
}
看到這裡,你需要索引到printl()了,printl就是printf的宏定義,這裡的printf将調用printx的系統調用,最終調用核心态的sys_pirntx(),具體過程省略,我們來看一下sys_printx():printl()>>printf()> > printx()> > sys_printx( )
int sys_printx(int _unused1, int _unused2, char * s, struct proc *proc_p)
實作過程我們在此處省略,這個函數的作用是将程序proc_p對應位址為s的字元串列印出來——如果是核心panic和系統任務,列印到顯存首位址開始的地方,停機;普通資訊,列印在該程序對應的console。
void panic(const char *fmt)
{
int i;
char buf[256];
va_list arg=(va_list)(fmt+4);
i=vsprintf(buf,fmt,arg);
printl("%c !!panic !! %s ",MAG_CH_PANIC,buf);
__asm__ __volatile__("ud2");
}
2.2.2msg_send() && msg_receive()
這裡,我們為了簡化邏輯,省略相關的小型功能函數和出錯處理資訊,來審查相關代碼功能:
msg_send(struct proc * sender, int dest , MESSAGE *m);
1)如果dest的狀态為等待接受,進入2);反之,進入步驟3
2)滿足接收條件,進入4)不滿足接收條件,直接丢棄
3)插入sending的等待隊列,挂起程序sender,從新排程
msg_receive(struct proc * receive, int src, MESSAGE *m):
1)如果有中斷消息,則封裝并取得中斷消息,傳回
2)scr==ANY?成立,從發送隊列中取出第一個
3)src!=ANY:按照scr号碼取得發送程序,更新receive的發送隊列
4)拷貝消息
2.3增加消息機制後的程序排程
核心思想:我們在進行程序排程的時候,需要增加一個限制條件——p_flags==0.也就是說,在原來程序按照時間片切換的基礎上,如果程序因為等待某種條件,需要挂起,此時即使時間片沒有用盡,也會進行程序排程。
3.使用IPC替換掉系統調用get_ticks
如何實作IPC呢,既然是收發消息,必然有兩方參與;而且,很顯然,我們需要一個系統程序來接收使用者的消息。
void task_sys()
{
MESSAGE msg;
while(1){
send_recv(RECEIVE,ANY,&msg);
int src=msg.source;
switch (msg.type){
case GET_TICKS:
msg.RETVAL=ticks;
send_recv(SEND,src,&msg);
break;
default:
panic("unknown msg type");
break;
}
}
}
int get_ticks()
{
MESSAGE msg;
reset_msg(&msg);
msg.type=GET_TICKS;
send_rec(BOTH,TASK_SYS,&msg);
return msg.RETVAL;
}
這個程序在等待着從任何程序發過來的消息,我們來追蹤一下函數的執行流程:
1) 等待接收消息:send_recv(RECEIVE,ANY,msg_p)> > sendrec(RECEIVE, ANY,msg_p) > > sys_sendrec(RECEIVE, ANY, msg_p)> > msg_receive(proc_p, ANY, msg_p) !!!注意,此時,如果沒有收到程序傳遞的消息,将task_sys程序将被block()。
2) 一個使用者調用get_ticks() > > send_recv(BOTH, TASK_SYS, msp_p),接下來将要走兩條路線,SEND && RECEIVE
2.1)sendrec(SEND,TASK_SYS ,msg) > >sys_sendrec(SEND,TASK_SYS, msg_p)> > msg_send(p_proc,TASK_SYS, msg_p)> > 拷貝消息,更新狀态,此時步驟1被喚醒flags==0
2.2)同樣,我們get_ticks()函數接下來調用msg_receive() ,和1)中的情況類似,最後get_ticks被block
注意:這裡的p_proc是如何而來呢?
3)我們在TASK_SYS中檢視msg.type,如果是GET_TICKS,那麼将消息的結果ticks放在msg.RETVAL,然後傳回消息,進入發送狀态,2.2)中被block的get_ticks進入就緒狀态。
總結:發送和接收資訊不是完全對等的,如果發送消息,即使對方不能及時接收,也會加入到q_sending之中;如果是等待接收消息,那麼就會産生阻塞。如果一個程序等待接收消息,自然進入挂起狀态;後來它等待的消息被發送給他,然後進入就緒狀态,等到下一個程序切換的時機,就可以作為就緒程序進行切換了。
4.Makefile 的更新
頭檔案:增加了include之下的err.h和msg.h,分别用于出錯處理與消息通信
C檔案:增加了一個C檔案,systask.c; lib/err.c需要對這些檔案進行相應的編譯和處理