摘要:对于进程间通信,我们往往并不陌生。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需要对这些文件进行相应的编译和处理