IPC是Inter-Process Communication的缩写,直译为进程间通信,说白了就是进程间发消息。我们在上一节中把这种消息传递比作邮政系统,但实际上这种比喻并不全对。有的消息机制是很像收发邮件的,这种叫做异步IPC,意思是说,发信者发完就去干别的了,收信者也一样,看看信箱里没信,也不坐在旁边傻等。而有另一种消息机制正好相反,被称为同步IPC,它不像邮寄,倒像接力赛,发送者一直等到接收者收到消息才肯放手,接收者也一样,接不到就一直等着,不干别的。
当然你可以把同步IPC也比作邮寄,只不过寄信的人从把信投到信箱里的那一刻开始,就住在邮局不走了,其他什么也不干了,就等着邮局说:“哥们儿,你的信对方已经收到了,放心回家吧!”这才恋恋不舍地离开。收信的也一样,一旦决定收信,就守在自家信箱前面不走了,一直等,连觉也不睡,望穿秋水,等信拿在手里了,这才回屋,每收一次信,就得瘦个十几斤。
我们都是性情中人,我们选择傻等,或曰同步IPC。
同步IPC有若干的好处,比如:
• 操作系统不需要另外维护缓冲区来存放正在传递的消息;
• 操作系统不需要保留一份消息副本;
• 操作系统不需要维护接收队列(发送队列还是需要的);
• 发送者和接收者都可在任何时刻清晰且容易地知道消息是否送达;
• 从实现系统调用的角度来看,同步IPC更加合理──当使用系统调用时,我们的确需要等待内核返回结果之后再继续。
这些特性读者可能无法一下子全部明白,不要紧,我们接下来写完代码,你就全都明白了。
实现IPC
Minix的IPC机制我们已经明白了,它的核心乃在于“int SYSVEC”这个软中断以及与之对应的sys_call()这个函数。增加一个系统调用对我们来讲已是信手拈来的事,按照表7.6一步一步来就好了。我们把这个新的系统调用起名为sendrec。sendrec和sys_sendrec的函数体分别见下面两段代码:
25 sendrec:
26 mov eax, _NR_sendrec
27 mov ebx, [esp + 4] ; function
28 mov ecx, [esp + 8] ; src_dest
29 mov edx, [esp + 12] ; p_msg
30 int INT_VECTOR_SYS_CALL
31 ret
53 /*****************************************************************************
54 * sys_sendrec
55 *****************************************************************************/
56 /**
57 * <Ring 0> The core routine of system call ‘sendrec()’.
58 *
59 * @param function SEND or RECEIVE
60 * @param src_dest To/From whom the message is transferred.
61 * @param m Ptr to the MESSAGE body.
62 * @param p The caller proc.
63 *
64 * @return Zero if success.
65 *****************************************************************************/
66 PUBLIC int sys_sendrec(int function, int src_dest, MESSAGE* m, struct proc* p)
67 {
68 assert(k_reenter == 0); /* make sure we are not in ring0 */
69 assert((src_dest >= 0 && src_dest < NR_TASKS + NR_PROCS) ||
70 src_dest == ANY ||
71 src_dest == INTERRUPT);
72
73 int ret = 0;
74 int caller = proc2pid(p);
75 MESSAGE* mla = (MESSAGE*)va2la(caller, m);
76 mla->source = caller;
77
78 assert(mla->source != src_dest);
79
80 /**
81 * Actually we have the third message type: BOTH. However, it is not
82 * allowed to be passed to the kernel directly. Kernel doesn’t know
83 * it at all. It is transformed into a SEND followed by a RECEIVE
84 * by ‘send_recv()’.
85 */
86 if (function == SEND) {
87 ret = msg_send(p, src_dest, m);
88 if (ret != 0)
89 return ret;
90 }
91 else if (function == RECEIVE) {
92 ret = msg_receive(p, src_dest, m);
93 if (ret != 0)
94 return ret;
95 }
96 else {
97 panic(”{sys_sendrec}␣invalid␣function:␣”
98 ”%d␣(SEND:%d,␣RECEIVE:%d).”, function, SEND, RECEIVE);
99 }
100
101 return 0;
102 }
表7.6的最后一步中提到,如果参数个数与以前的系统调用比有所增加,则需要修改kernel.asm中的sys_call。额外要注意,我们新加的参数是通过edx这个参数传递的,而save这个函数中也用到了寄存器dx,所以我们同时需要修改save,见如下代码:
311 ; =============================================================================
312 ; save
313 ; =============================================================================
314 save:
315 pushad ; ‘.
316 push ds ; |
317 push es ; | 保存原寄存器值
318 push fs ; |
319 push gs ; /
320
321 ;; 注意,从这里开始,一直到‘mov esp, StackTop’,中间坚决不能用push/pop 指令,
322 ;; 因为当前esp 指向proc_table 里的某个位置,push 会破坏掉进程表,导致灾难性后果!
323
=> 324 mov esi, edx ; 保存edx,因为edx 里保存了系统调用的参数
325 ;(没用栈,而是用了另一个寄存器esi)
326 mov dx, ss
327 mov ds, dx
328 mov es, dx
329 mov fs, dx
330
=> 331 mov edx, esi ; 恢复edx
332
333 mov esi, esp ;esi = 进程表起始地址
334
335 inc dword [k_reenter] ;k_reenter++;
336 cmp dword [k_reenter], 0 ;if(k_reenter ==0)
337 jne .1 ;{
338 mov esp, StackTop ; mov esp, StackTop <--切换到内核栈
339 push restart ; push restart
340 jmp [esi + RETADR - P_STACKBASE] ; return;
341 .1: ;} else { 已经在内核栈,不需要再切换
342 push restart_reenter ; push restart_reenter
343 jmp [esi + RETADR - P_STACKBASE] ; return;
344 ;}
345
346
347 ; =============================================================================
348 ; sys_call
349 ; =============================================================================
350 sys_call:
351 call save
352
353 sti
354 push esi
355
356 push dword [p_proc_ready]
=> 357 push edx
358 push ecx
359 push ebx
360 call [sys_call_table + eax * 4]
=> 361 add esp, 4 * 4
362
363 pop esi
364 mov [esi + EAXREG - P_STACKBASE], eax
365 cli
366
367 ret
sys_sendrec()这个函数被设计得相当简单,它可以描述为:把SEND消息交给msg_send()处理,把RECEIVE消息交给msg_receive()处理。
msg_send()和msg_receive()这两个函数我们过一会儿细细分解,先来看看之前没出现过的assert()和panic()。这两个函数虽然起的是辅助作用,但绝对不是可有可无,因为在我们接下来要处理的消息收发中,有一些编程细节还真容易让人迷糊,这时候assert()就大显神威了,它会在错误被放大之前通知你。panic()的作用也类似,用于通知你发生了严重的错误。