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

来源:互联网 发布:发货单打印软件 编辑:程序博客网 时间:2024/05/17 04:11

摘要:对于进程间通信,我们往往并不陌生。linux下的进程间通信主要有管道、信号量、消息队列等几种模式。在《自己动手写操作系统中》,我们将采用消息机制来实现进程间通信,原来和linux的消息队列有些类似。


1.IPC

同步与异步;很多领域里我们都用到了同步和异步的概念,这里再次区分一下。同步好比走路,走路毕竟需要同步嘛。当你的左脚迈出去之后,会等待你的右脚迈出去,不然你的左脚只能等待(一般人不会连续两次迈左脚)。异步正好相反,A不必总是等待着B。
同步IPC:在本节中,我们选用同步通信的方式,好处:1)操作系统不许要维护缓冲区来存放传递的消息 2)操作系统不需要保留消息副本 3)操作系统不许要维护接受队列(但是需要维护发送队列) 4)发送者和接收者能够快速知道状态信息 

2.实现IPC

要实现一个IPC,需要增加一个系统调用。用户态和内核态的对应是sendrec && sys_sendrec()


2.1code:sendrec(kernel/syscall.asm)

;=========================================================================================;sendrec(int function , int dest_src, MESSAGE *m)sendrec:moveax,_NR_sendrecmovebx,[esp+4];functionmovecx,[esp+8];sec_destmovedx,[esp+12];p_msgint INT_VECTOR_SYS_CALLret

2.2code: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 ring0assert((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;u64m3l1;u64m3l2;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 3enum msgtype{HARD_INT=1,GET_TIKES,};#define RETVALu.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#ifdefASSERT//ifvoidassertion_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 0
原创粉丝点击