天天看點

自己動手寫作業系統 第八章 :程序間通信 IPC1.IPC2.實作IPC3.使用IPC替換掉系統調用get_ticks4.Makefile 的更新

摘要:對于程序間通信,我們往往并不陌生。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需要對這些檔案進行相應的編譯和處理