自己动手写操作系统 第八章 :进程间通信 IPC


摘要:对于进程间通信,我们往往并不陌生。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需要对这些文件进行相应的编译和处理 
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值