首页 > 代码库 > MINIX3 进程通信分析

MINIX3 进程通信分析

MINIX3 进程通信分析

6.1MINIX3 进程通信概要

MINIX3 的进程通信是 MINIX3 内核部分最重要的一个部件,我个人认为其实这 是内核中的“内核”,怎么来理解这个概念呢?其实 MINIX3 进程间通信部件的 实行不完全依赖任何一个部件,这个在后面会详细的看到。Minix3  实现进程通 信的方法是----消息机制。何为消息机制呢?

就是进程 A 有消息发送进程 B,希望进程 B 给进程 A 一个服务,进程 A 和 B 在 这里就发生了进程间的通信

注意这里的消息机制其实是不受任何机制管制,具有最高权限,这种消息机制
MINIX3 也没有单独设立进程,也不可能!实现下,如果设立一个进程,那么这
个所谓的“消息传递进程”怎么和其他进程发生联系呢?岂不是又要设立一个“消
息机制”,者就带来了很尴尬的局面:一直想调用另一个消息机制,永远找不到
尽头。

6.2 MINIX3 进程通信原语介绍

IPC 机制是多进程操作系统必须提供的,但是怎么能够让这个系统得到一个非常 高效安全的 IPC 机制是非常难考虑的。MINIX3 定义了一种消息传递机制。 主要通过以下 3 个函数来实现:send(),receive(),notify();理解这 3 个函数能够使得 消息机制有一个绝对宏观和微观的全局把握,在这里,我的讲解方式可能要有一 点改变,鉴于前面讲到内核其他部分,也涉及到消息原语,我们在讲解完消息机 制代码之后,会利用前面已有的消息原语来加以理解。

第一 我们来简要说明下那 3 个函数的大体含义: Send(dest,&message)

87

很容易理解,这个就是向目的进程 dest 发送一个消息。关于这个消息,在 MINIX3
是定义了一种内核能够识别的“呆板的”消息。关于这种消息在后面会有所介绍。
现在继续分析这个函数:内核会把消息传到给 dest 进程。当然就不是这种简单
的复制工作。事实上要比这个复杂的。我们设想下,如果进程 A 和进程 B 合作,
进程 A 要求发送给一个阻塞的进程 B 但是如果进程 B 没有阻塞,它就要阻塞自
己。这点就是一种协同合作的典型例子。事实上,有很多比这个要复杂的协同合
作的例子。但是如果仅仅就是上面那一步无法满足 IPC 机制------同步执行。

所以 MINIX3 提出这样一种内容:就是如果目的进程没有接受自己,自己就会阻 塞自己。

Receive(source,&message)

同样的道理。用于接受一个消息,如果源进程没有发送,它就阻塞自己。

上面 2 个函数就可以实现基本的进程通信机制。但是 MINIX3 还设置了一个通信 原语:

notify(dest),设置就要是出于效率考虑:有时候不需要阻塞自己来发送消息。希望
有的进程发送之后能够继续投入使用。事实上,在 MINIX3 中,使用 notfiy(dest)
都是系统进程间发送消息来使用的,当然不是全部的系统进程发送都是这样的。
在后面的讲解中会看到。这个函数就是个目的进程发送一个通知。而且这个通知
的格式是绝对一样的。是内核自己来生成的。这些点后面都会有详细的说明。

6.3 MINIX3 消息机制的消息格式

现在我们分析另外一个问题:消息格式

MINIX3 定义一种非常让人迷惑并且比较复杂的格式:消息

消息主体是一个含有共用体的结构体。主要是在 include/minix/ipc.h 里。我们拿 出来简要分析下:

#ifndef  _IPC_H
#define  _IPC_H

/*=================================================================== =======*

* Types relating to messages. *

*====================================================================

======*/

#define M1 1

#define M3 3

#define M4 4

#define M3_STRING 14

//定义专门的消息结构---一共有7种消息结构在MINIX3中

typedef struct {int m1i1, m1i2, m1i3; char *m1p1, *m1p2, *m1p3;} mess_1;
typedef struct  {int m2i1, m2i2, m2i3; long m2l1, m2l2; char  *m2p1;}

88

mess_2;

typedef struct  {int m3i1, m3i2; char  *m3p1; char m3ca1[M3_STRING];} mess_3;

typedef struct  {long m4l1, m4l2, m4l3, m4l4, m4l5;} mess_4;

typedef struct  {short m5c1, m5c2; int m5i1, m5i2; long m5l1, m5l2, m5l3;}mess_5;

typedef struct {int m7i1, m7i2, m7i3, m7i4; char *m7p1, *m7p2;} mess_7; typedef struct {int m8i1, m8i2; char *m8p1, *m8p2, *m8p3, *m8p4;} mess_8; //消息机制的主体结构,把他看成一个结构的含有公用体的结构比较好
typedef struct  {

int m_source; /* who sent the message  */

int m_type; /* what kind of message is it  */

union  {

mess_1 m_m1;
mess_2 m_m2;
mess_3 m_m3;

mess_4 m_m4;
mess_5 m_m5;
mess_7 m_m7;

mess_8 m_m8;
} m_u;

} message;

/* The following defines provide names for useful members.  */ //这些是一些宏定义

#define m1_i1    m_u.m_m1.m1i1
#define m1_i2    m_u.m_m1.m1i2
#define m1_i3    m_u.m_m1.m1i3
#define m1_p1    m_u.m_m1.m1p1
#define m1_p2    m_u.m_m1.m1p2
#define m1_p3    m_u.m_m1.m1p3

#define m2_i1    m_u.m_m2.m2i1
#define m2_i2    m_u.m_m2.m2i2
#define m2_i3    m_u.m_m2.m2i3
#define m2_l1    m_u.m_m2.m2l1
#define m2_l2    m_u.m_m2.m2l2
#define m2_p1    m_u.m_m2.m2p1

#define m3_i1    m_u.m_m3.m3i1
#define m3_i2    m_u.m_m3.m3i2
#define m3_p1    m_u.m_m3.m3p1
#define m3_ca1 m_u.m_m3.m3ca1

89

#define m4_l1 m_u.m_m4.m4l1

#define m4_l2 m_u.m_m4.m4l2

#define m4_l3 m_u.m_m4.m4l3

#define m4_l4 m_u.m_m4.m4l4

#define m4_l5 m_u.m_m4.m4l5

#define m5_c1 m_u.m_m5.m5c1

#define m5_c2 m_u.m_m5.m5c2

#define m5_i1 m_u.m_m5.m5i1

#define m5_i2 m_u.m_m5.m5i2

#define m5_l1 m_u.m_m5.m5l1

#define m5_l2 m_u.m_m5.m5l2

#define m5_l3 m_u.m_m5.m5l3

#define m7_i1 m_u.m_m7.m7i1

#define m7_i2 m_u.m_m7.m7i2

#define m7_i3 m_u.m_m7.m7i3

#define m7_i4 m_u.m_m7.m7i4

#define m7_p1 m_u.m_m7.m7p1

#define m7_p2 m_u.m_m7.m7p2

#define m8_i1 m_u.m_m8.m8i1

#define m8_i2 m_u.m_m8.m8i2

#define m8_p1 m_u.m_m8.m8p1

#define m8_p2 m_u.m_m8.m8p2

#define m8_p3 m_u.m_m8.m8p3

#define m8_p4 m_u.m_m8.m8p4

在 MINIX3 中,我们认为一个非常重要的问题:就是某个进程或者某种机制机制 发送某种消息类型。这点可能比较呆板。但是这样处理的效率和安全性都比较高。 这就是整个 MINIX3 的消息结构。当然在此不去追加具体的含义。事实上在讲解 system 系统函数时,每一个函数我们都认为是接受了某种类型的消息然后进行处 理相关内容。那个消息的各个结构在那里可以看的比较清楚。

6.4 MINIX3 消息机制源码导读

我们继续往下面走,现在我们就深入具体代码一个一个函数的分析: 进程间通信 IPC 机制与外层接口的代码是在 proc.c 中。
展开开头的一小部分代码来分析:

sys_call 是消息机制同外部世界的唯一接口。主要是由于系统陷入而将产生!

/*===================================================================

90

========*

* sys_call *

*==================================================================== =======*/

//这个是消息机制的传递入口,需要好好的理解一翻 PUBLIC int sys_call(call_nr, src_dst, m_ptr)

int call_nr; /* system call number and flags  */

int src_dst; /* src to receive from or dst to send to  */

message  *m_ptr; /* pointer to message in the caller‘s space */

{

/* System calls are done by trapping to the kernel with an INT instruction. * The trap is caught and sys_call() is called to send or receive a message *  (or both). The caller is always given by ‘proc_ptr‘.

*/

//系统调用通过INT指令陷入到内核中。这个陷阱被捕获并且sys_call被调用来 发送或者接受消息。

//调用者总是被proc_ptr所指的进程

register struct proc  *caller_ptr  = proc_ptr; /* get pointer to

caller  */

int function  = call_nr & SYSCALL_FUNC; /* get system call function

*/

//计算系统调用号和系统调用标志

unsigned flags  = call_nr & SYSCALL_FLAGS; /* get flags  */

int mask_entry;

int group_size;

int result;

vir_clicks vlo, vhi;

*/

/* bit to check in send mask  */

/* used for deadlock check  */
/* the system call‘s result  */

/* virtual clicks containing message to send

/* Check if the process has privileges for the requested call. Calls to the

* kernel may only be SENDREC, because tasks always reply and may not
block

* if the caller doesn‘t do receive().
*/

//防止违法操作调用

if  (!  (priv(caller_ptr)->s_trap_mask &  (1  << function))  ||
(iskerneln(src_dst) && function  != SENDREC

&& function  != RECEIVE))  {

#if DEBUG_ENABLE_IPC_WARNINGS

kprintf("sys_call: trap %d not allowed, caller %d, src_dst %d\n",
function, proc_nr(caller_ptr), src_dst);

#endif

91

return(ETRAPDENIED); /* trap denied by mask or kernel  */

}

/* Require a valid source and/ or destination process, unless echoing.
*/

//请求一个空操作

if  (src_dst  != ANY && function  != ECHO)  {
if  (! isokprocn(src_dst))  {

#if DEBUG_ENABLE_IPC_WARNINGS

kprintf("sys_call: invalid src_dst, src_dst %d, caller %d\n",
src_dst, proc_nr(caller_ptr));

#endif

return(EBADSRCDST); /* invalid process number  */

}

if  (isemptyn(src_dst))  {
#if DEBUG_ENABLE_IPC_WARNINGS

kprintf("sys_call: dead src_dst; trap  %d, from %d, to %d\n",
function, proc_nr(caller_ptr), src_dst);

#endif

return(EDEADSRCDST);
}

}

/* If the call involves a message buffer, i.e., for SEND, RECEIVE, SENDREC,

* or ECHO, check the message pointer. This check allows a message to

be

* anywhere in data or stack or gap. It will have to be made more elaborate

* for machines which don‘t have the gap mapped.
*/

if  (function & CHECK_PTR)  {

vlo  =  (vir_bytes) m_ptr  >> CLICK_SHIFT;

vhi  =  ((vir_bytes) m_ptr  + MESS_SIZE  -  1)  >> CLICK_SHIFT; if  (vlo  < caller_ptr->p_memmap[D].mem_vir  || vlo  > vhi  ||
vhi  >= caller_ptr->p_memmap[S].mem_vir  +

caller_ptr->p_memmap[S].mem_len)  {

#if DEBUG_ENABLE_IPC_WARNINGS

kprintf("sys_call: invalid message pointer, trap  %d, caller  %d\n",

function, proc_nr(caller_ptr));

#endif

return(EFAULT); /* invalid message pointer  */

}

92

}

/* If the call is to send to a process, i.e., for SEND, SENDREC or NOTIFY,
* verify that the caller is allowed to send to the given destination.
*/

if  (function & CHECK_DST)  {

if (! get_sys_bit(priv(caller_ptr)->s_ipc_to, nr_to_id(src_dst)))

{

#if DEBUG_ENABLE_IPC_WARNINGS

kprintf("sys_call: ipc mask denied trap  %d from  %d to  %d\n", function, proc_nr(caller_ptr), src_dst);

#endif

return(ECALLDENIED); /* call denied by ipc mask  */

}

}

/* Check for a possible deadlock for blocking SEND(REC) and RECEIVE.
*/

if  (function & CHECK_DEADLOCK)  {

if  (group_size  = deadlock(function, caller_ptr, src_dst))  { #if DEBUG_ENABLE_IPC_WARNINGS

kprintf("sys_call: trap  %d from  %d to  %d deadlocked, group size  %d\n",

function, proc_nr(caller_ptr), src_dst, group_size);

#endif

return(ELOCKED);

}

}

/* Now check if the call is known and try to perform the request. The
only

* system calls that exist in MINIX are sending and receiving messages. 现在检查调用是否被知道并且尝试一个请求。这是MINIX唯一的系统调用来发送 或者接受一个消息

* - SENDREC: combines SEND and RECEIVE in a single system call

* - SEND: sender blocks until its message has been delivered

* - RECEIVE: receiver blocks until an acceptable message has arrived

* - NOTIFY:    nonblocking call; deliver notification or mark pending

* - ECHO: nonblocking call; directly echo back the message

*/

//这里是最核心的操作,分为SENDREC SEND RECEIVE NOTIFY ECHO这几个操作 这里的几个函数我们将在下面会有详细的介绍

switch(function)  {

case SENDREC:

93

/* A flag is set so that notifications cannot interrupt SENDREC.

*/

priv(caller_ptr)->s_flags  |= SENDREC_BUSY;

/* fall through  */

case SEND: //执行SEND函数

result  = mini_send(caller_ptr, src_dst, m_ptr, flags);

if  (function  == SEND  || result  != OK)  {

break; /* done, or SEND failed  */

} /* fall through for SENDREC  */

case RECEIVE: //执行RECEIVE函数

if  (function  == RECEIVE)

priv(caller_ptr)->s_flags &=  ~SENDREC_BUSY;

result  = mini_receive(caller_ptr, src_dst, m_ptr, flags); break;

case NOTIFY:  //执行NOTIFY函数

result  = mini_notify(caller_ptr, src_dst); break;

case ECHO:  //这是回显操作

CopyMess(caller_ptr->p_nr, caller_ptr, m_ptr, caller_ptr, m_ptr); result  = OK;

break;

default:

result  = EBADCALL; /* illegal system call  */

}

/* Now, return the result of the system call to the caller.  */ //接受或者发送消息完成后,就进程返回操作

return(result);

}

在介绍最为核心的几个操作之前先来看下几个宏,同样也是在 proc.c 文件中 我们将整个 proc.c 前部分的代码放进来,形成一个整体感:

/* This file contains essentially all of the process and message handling. * Together with "mpx.s" it forms the lowest layer of the MINIX kernel. * There is one entry point from the outside:

* 本文件包括了所有进程消息机制处理的本质操作,这个文件也是MINIX最底层 内核部分

* 从外到内只有一个入口:sys_call

*      sys_call: a system call, i.e., the kernel is trapped with

an INT
*

* As well as several entry points used from the interrupt and task level: *当然也有几个其它的入口:(主要用于中断和系统任务)

*      lock_notify: notify a process of a system event

//通知给一个进程:一个系统任务

94

*      lock_send: send a message to a process

//发送一个消息给一个进程

*      lock_enqueue: put a process on one of the scheduling queues

//将一个进程放在调度队列中

*      lock_dequeue: remove a process from the scheduling queues

*从一个调度队列中移掉一个进程

* Changes:

*      Aug  19,  2005

*      Jul 25, 2005
*      May  26,  2005
Herder)

*      May 24, 2005
*      Oct  28,  2004
Herder)

*

rewrote scheduling code (Jorrit N. Herder)

rewrote system call handling (Jorrit N. Herder)

rewrote message passing functions (Jorrit N.

new notification system call (Jorrit N. Herder)

nonblocking send and receive calls (Jorrit N.

* The code here is critical to make everything work and is important for
the

* overall performance of the system. A large fraction of the code deals
with

* list manipulation. To make this both easy to understand and fast to execute

* pointer pointers are used throughout the code. Pointer pointers prevent * exceptions for the head or tail of a linked list.

*这个文件的代码可以说对整个系统的执行效率都有着非常重要的影响(在后面 可以看到,minix采用消息传递机制来实现通信)

*    node_t  *queue,  *new_node; // assume these as global variables

*    node_t  **xpp  = &queue; // get pointer pointer to head of queue

*    while  (*xpp  != NULL) // find last pointer of the linked list

* xpp  = &(*xpp)->next; // get pointer to next pointer

* *xpp  = new_node; // now replace the end  (the NULL pointer)

*    new_node->next  = NULL; // and mark the new end of the list

*

* For example, when adding a new node to the end of the list, one normally
* makes an exception for an empty list and looks up the end of the list
for

* nonempty lists. As shown above, this is not required with pointer pointers.

*/

#include  <minix/com.h>

#include  <minix/callnr.h>
#include "kernel.h"
#include "proc.h"

95

/* Scheduling and message passing functions. The functions are available
to

* other parts of the kernel through lock_...(). The lock temporarily disables

* interrupts to prevent race conditions.

调度器和消息传递函数。这些函数被提供给内核的其他部分,并且在操作前加上
Lock…()Lock主要是临时关闭中断来阻止其它进程来破坏状态量
*/

FORWARD  _PROTOTYPE( int mini_send,  (struct proc  *caller_ptr, int dst,
message  *m_ptr, unsigned flags));

FORWARD _PROTOTYPE( int mini_receive, (struct proc *caller_ptr, int src,
message  *m_ptr, unsigned flags));

FORWARD _PROTOTYPE( int mini_notify, (struct proc *caller_ptr, int dst)); FORWARD  _PROTOTYPE( int deadlock,  (int function,

register struct proc  *caller, int src_dst));

FORWARD  _PROTOTYPE( void enqueue,  (struct proc  *rp));

FORWARD  _PROTOTYPE( void dequeue,  (struct proc  *rp));

FORWARD  _PROTOTYPE( void sched,  (struct proc  *rp, int  *queue, int *front));

FORWARD  _PROTOTYPE( void pick_proc,  (void));

//宏定义,主要是用于构造消息,m_ptr:消息,src:源进程,dst_ptr:目标进程 //这个宏定义在notify中和receive中会用到,从字面看就是构造一个消息。我 们在后面会看到,这种构造消息的方法主要是用于notify的发送,发送一个小消 息,能够使得速度加快。

//

#define BuildMess(m_ptr, src, dst_ptr)  \

(m_ptr)->m_source  =  (src); \

(m_ptr)->m_type  = NOTIFY_FROM(src); \

(m_ptr)->NOTIFY_TIMESTAMP  = get_uptime(); \

switch  (src)  { \

case HARDWARE: \

(m_ptr)->NOTIFY_ARG  = priv(dst_ptr)->s_int_pending; \

priv(dst_ptr)->s_int_pending  =  0; \

break; \

case SYSTEM: \

(m_ptr)->NOTIFY_ARG  = priv(dst_ptr)->s_sig_pending; \

priv(dst_ptr)->s_sig_pending  =  0; \

break; \

}

//上段代码需要参考整个MINIX消息机制的结构, 对于源而言;主要分为

HARDWRAE和SYSTEM分别进行处理,如果是中断导致消息的发生,这要挂起中断位 图,如果是SYSTEM导致消息的产生,则要挂起sig_pending位图。挂起这2个位图 其实主要是用于内核间进程的一种快速通信。

#if  (CHIP  == INTEL)

96

#define CopyMess(s,sp,sm,dp,dm)  \

cp_mess(s,  (sp)->p_memmap[D].mem_phys, \

(vir_bytes)sm,  (dp)->p_memmap[D].mem_phys,  (vir_bytes)dm) #endif  /*  (CHIP  == INTEL)  */

//如果是是INTEL,则复制消息时就用cp_mess函数,函数的主体其实用汇编实现 的,在后面会有介绍

//如果是M68000芯片,则不做处理
#if  (CHIP  == M68000)

/* M68000 does not have cp_mess() in assembly like INTEL. Declare prototype

* for cp_mess() here and define the function below. Also define CopyMess.
*/

#endif  /*  (CHIP  == M68000)  */

现在我们接着上面的那个复制函数  ,我们找到源码,也一并进行分析:

*

!*=================================================================== ========*

!* cp_mess 把这个函数放到这里,以便更好的理解后面的函数

过程
*

!*=================================================================== ========*

! PUBLIC void cp_mess(int src, phys_clicks src_clicks, vir_bytes src_offset,

! phys_clicks dst_clicks, vir_bytes dst_offset);

! This routine makes a fast copy of a message from anywhere in the address

! space to anywhere else.    It also copies the source address provided as a parameter to the call into the first word of the destination message. !Note that the message size, "Msize" is in DWORDS  (not bytes) and must be set correctly.    Changing the definition of message in the type file and not changing it here will lead to total disaster.
!这个例程实际是非常重要的,这是一个被封装的C函数的底层实现
!主要是用于信息从一个地址快速的复制到另一个地址空间.

CM_ARGS = 4  +  4  +  4  +  4  +  4 !  4  +  4  +  4  +  4  +  4

! es    ds edi esi eip    proc scl sof dcl dof

.align  16
_cp_mess:
cld

push    esi
push    edi
push    ds
push    es

97

!将esi edi ds es压入栈,这4个寄存器留给下面的程序使用。 !设置数据段选择器,设置为平展模式,准备进行复制工作
mov eax, FLAT_DS_SELECTOR

mov ds, ax

mov es, ax

!将各个寄存器设置成相应的参数,准备进行复制工作

mov esi, CM_ARGS+4(esp) ! src clicks

shl esi, CLICK_SHIFT

add esi, CM_ARGS+4+4(esp) ! src offset

mov edi, CM_ARGS+4+4+4(esp) ! dst clicks

shl edi, CLICK_SHIFT

add edi, CM_ARGS+4+4+4+4(esp) ! dst offset

mov eax, CM_ARGS(esp) ! process number of sender

stos ! copy number of sender to dest message

add esi,  4 ! do not copy first word

mov ecx, Msize  -  1 ! remember, first word does not count

rep

movs ! copy the message

!复制完成之后,就将之前压栈的寄存器值弹回到相应的寄存器里
pop es

pop ds
pop edi
pop esi

ret ! that is all folks!

现在我们进行最核心的操作:也就是前面sys_call函数所调用的

mini_send,mini_recieve,mini_notify.这3个函数我们要仔细的来分析,因为3 个函数其实是MINIX3最精髓的地方:

我们先来看mini_send:

98

wps1A4E.tmp

这是大致执行流程图

我们仔细看这个执行流程图:首先判断是否满足条件:

一个进程想发送一个消息给另外一个进程,应该怎么做呢?

首先会检查最基本的:1发往的目的进程时候处于阻塞队列中且标志RECEIEVING 状态。2目的进程p_getfrom是不是ANY或者本进程。

对于第一点:每一个进程描述符项有一个p_rts_flags,这一位其实就是标志着 进程是否阻塞,并且是以接受态还是发送态阻塞。

对于第2点:只要检查目的进程的p_getfrom是不是应该指向本进程或者是任意进
程。

如果满足条件:发送进程就发消息复制给目的进程,之后将阻塞的目的进程放在 调度队列中运行。

99

wps1A6E.tmp

在发送时,调用copmess()函数,这个函数前面有分析过,是一个汇编形式的快
速复制消息函数。注意这里复制的真正含义:这里是在目的进程重新开辟一个内
存空间,由p_messbuf指向。之后把源进程的内容确确时时的复制到源进程中。
这一点我们必须明确。不是简单的指针指向。我们想下为什么不用指针简单的指
向呢?如果源进程在发送完消息之后,又准备给另外一个进程发送消息,但是之
前我们发送到得消息的进程还没有来得急使用这个消息,被这个进程发送的另外
一个消息给覆盖了。这样就会带来严重的问题。索性在每一个进程里都安装一个
消息结构的内存,这样就不需要用别人的消息地址内存。就相当于每家每一户都
有一个邮箱,别人发来的消息只装在我自己的邮箱中一样。

如果不满足上面2个条件,就需要做更加进一步的处理了。

如果是因为目的进程不在阻塞队列中,为了保持进程间的同步性,必须要阻塞这
个发送进程。现在我们来看看接下来是怎么做到的!先来看下面这个图:
这个图揭示了没有发送成功时,我们源进程怎么储存自己的消息。因为如果不满
足上述条件时,我们不能复制到对方的“邮箱”中,既然不能复制到对方的邮箱
中,我们只能自己储存起来,等待对方自己主动到我们这里来取邮件,这个过程
看起来非常的简单,其实要处理好非常的困难,首先,怎么让对方到我们这里来
取邮件呢?它有地址吗?没有,没有我们就要给对方设立一个地址,那个地址其
实用数据结构的语言来抽象,就是一个链表,我们可以想象一个问题,现在有一
家人去国外旅游了,有很多人想给这家人发信件,但是对方的邮箱打不开,只能
存回到自己的邮箱中,等待对方自己过来取,但是发送邮件的人就会在他家门口
留下自己的地址,让那家出国旅游的人自己回来取。在数据的世界里,我们就是
使用这个链表结构。这个链表结构就是用p_caller_q和p_q_link域构成。现在我
们先来接受下这2个域的含义:首先看p_caller_q:这个域代表的含义是我们指

100

wps1A8E.tmp

向想要发送消息进程的进程结构地址,而p_q_link是指向下一个进程的进程地 址。其实而言也很复杂,就是一个带链表的链式结构。

消息储存好之后还要做到的事情就是把本调度进程移除调度队列。让它处于阻塞 状态。

现在我们就来看代码。

/*=================================================================== ========*

* mini_send *

*====================================================================

=======*/

PRIVATE int mini_send(caller_ptr, dst, m_ptr, flags)

register struct proc  *caller_ptr; /* who is trying to send a message?

*/

int dst; /* to whom is message being sent?  */

message  *m_ptr; /* pointer to message buffer  */

//这里理解m_ptr 这里是message,指向其中的缓冲

unsigned flags; /* system call flags  */

{

/* Send a message from ‘caller_ptr‘ to ‘dst‘. If ‘dst‘ is blocked waiting

101

* for this message, copy the message to it and unblock ‘dst‘. If ‘dst‘
is

* not waiting at all, or is waiting for another source, queue ‘caller_ptr‘.

*/

register struct proc  *dst_ptr  = proc_addr(dst); register struct proc  **xpp;

/* Check if ‘dst‘ is blocked waiting for this message. The destination‘s
* SENDING flag may be set when its SENDREC call blocked while sending.
*/

// 检查目的进程是否满足如下条件:第一:目的进程是否在接受消息状态
// 第二:目的进程期望得到的消息来源是ANY或者是caller_ptr
if  (  (dst_ptr->p_rts_flags &  (RECEIVING  | SENDING))  == RECEIVING &&
(dst_ptr->p_getfrom  == ANY  || dst_ptr->p_getfrom  ==
caller_ptr->p_nr))  {

/* Destination is indeed waiting for this message.  */

//通过上述检查,果真就是满足上述条件的话就直接进行copyMess操作

CopyMess(caller_ptr->p_nr, caller_ptr, m_ptr, dst_ptr,
dst_ptr->p_messbuf);

if  ((dst_ptr->p_rts_flags &=  ~RECEIVING)  ==  0) enqueue(dst_ptr); //enqueue是让dst_ptr进入调度队列中

//如果不是满足上面的种情况,那么就进入下面的执行流,操作的目的主要 是阻塞调用者,

//记住在执行这个程序流的时候,调用进程还在调度队列中 } else if  (  !  (flags & NON_BLOCKING))  {

/* Destination is not waiting.    Block and dequeue caller.  */

caller_ptr->p_messbuf  = m_ptr;

if  (caller_ptr->p_rts_flags  ==  0) dequeue(caller_ptr);//看看 p_rts_flags是否为,如果是为标明其还在

//调度队列中,就应该将其移除调度队列

caller_ptr->p_rts_flags  |= SENDING;

//将p_rts_flags标志为发送阻塞状态

caller_ptr->p_sendto  = dst;

//将调用进程的p_sendto设置成为dst

/* Process is now blocked.    Put in on the destination‘s queue.  */
//通过上面的操作,调用进程已经处于阻塞状态,并且相关的属性值已经设
置好了

//现在要设置的是该进程在目的进程的等待队列中,这个信息供以后目的进 程在接受消息时来处理

xpp  = &dst_ptr->p_caller_q; /* find end of list  */

while  (*xpp  != NIL_PROC) xpp  = &(*xpp)->p_q_link;

*xpp  = caller_ptr; /* add caller to end  */

caller_ptr->p_q_link  = NIL_PROC; /* mark new end of list  */

102

wps1AAE.tmp

} else  {

return(ENOTREADY);

}

return(OK);

}

我们先不讲receive,我们先讲下notfiy这个函数。这个函数虽然短,但是不是 很好理解。我们要仔细的分析这个函数:

前面简单的接受这个notify这个函数,这个函数的本意就是快速的发送消息,而
不阻塞本发送进程,这样必定导致了消息是“小”的。注意理解这里的“小”, “小”代表的含义其实就是一个消息结构体非常明确的消息。也就是一个死板的
消息,不是前面的那7种。这种消息主要用于系统进程间的快速通信。
同样首先检查条件:第一 目的进程是否是处于接受的阻塞状态 第二:目的进程
是否是接受ANY或者本进程发送的消息。第3:目的进程的s_flag是否标志为这个
SENDREC_BUSY标记位。

如果这3个条件满足,本调用就会构造一个消息,这个消息构造的宏在前面讲到, 主要是一些位图和消息源以及时间戳的定义。构造完成就复制到目的进程中。之
后就返回

如果3个有一个不满足,就不能构造消息。不能构造消息 难道我们就储存消息 吗?当然这是不太可能的,为啥这样讲呢?如果我们储存了这个消息,源进程就 必定要阻塞,也就违背了MINIX3设计初衷:快速发送消息

MINIX3是怎么解决这个问题的呢?MINIX3是这样解决的:

每一个系统进程都有一些位图,这些位图能够标志有限个进程,一般往往就标记
着系统进程。Mini_notiy就会将这些位图置位,当然也就是置的目的进程的位,

103

在后面我们可以看到,mini_recieve函数首先就会检查这些位图,检查完这些位 图之后发送有之前有进程想发送通知给它,它就会自己构造消息和并且传递消息 给自己。这点就在后面一个函数会非常的明确。我们现在只要只要本函数
mini_notify是动用了哪些位图:

事实上,它就动用了一个位图:s_notify_pending,将本调用进程的s_id号传递
给目的进程的s_notify_pending位图上。主要是为了让mini_receive发现。

/*=================================================================== ========*

* mini_notify *

*====================================================================

=======*/

PRIVATE int mini_notify(caller_ptr, dst)

register struct proc  *caller_ptr; /* sender of the notification  */

int dst; /* which process to notify  */

{

register struct proc  *dst_ptr  = proc_addr(dst);

int src_id; /* source id for late delivery  */

message m; /* the notification message  */

/* Check to see if target is blocked waiting for this message. A process
* can be both sending and receiving during a SENDREC system call.
检查看这个目标是否是在阻塞等待这个消息。在执行一个SENDREC系统调用
时,

一个进程既能做出送也能做出接受
*/

if  ((dst_ptr->p_rts_flags &  (RECEIVING|SENDING))  == RECEIVING &&

!  (priv(dst_ptr)->s_flags & SENDREC_BUSY) &&

(dst_ptr->p_getfrom  == ANY  || dst_ptr->p_getfrom  == caller_ptr->p_nr))  {

//满足条件就进入内部构造消息和发送消息

/* Destination is indeed waiting for a message. Assemble a notification

* message and deliver it. Copy from pseudo-source HARDWARE, since

the

* message is in the kernel‘s address space.
*/

BuildMess(&m, proc_nr(caller_ptr), dst_ptr);

CopyMess(proc_nr(caller_ptr), proc_addr(HARDWARE), &m,
dst_ptr, dst_ptr->p_messbuf);

dst_ptr->p_rts_flags &=  ~RECEIVING;  /* deblock destination  */ if  (dst_ptr->p_rts_flags  ==  0) enqueue(dst_ptr);
return(OK);

}

104

wps1ACF.tmp

/* Destination is not ready to receive the notification. Add it to the
* bit map with pending notifications. Note the indirectness: the system
id

* instead of the process number is used in the pending bit map.
目的进程不在准备接受一个通知。将这个加到挂起通知位图上。
*/

//这是这个通知最显著的差别。就在于设置目的进程的通知位图.之后返回。 这里并没有来阻塞整个进程的运行

src_id  = priv(caller_ptr)->s_id;

set_sys_bit(priv(dst_ptr)->s_notify_pending, src_id); return(OK);

}

好了,我们现在来介绍最后一个函数调用函函数————mini_receive ,这个函数比较大,我们首先看下执行流程图:

105

继续回到前面分析的那个例子来说明接受进程是怎么来执行这个过程:

还是看那家出国的家庭,那个出国的家庭正好回家了,看到自己家楼下的邮箱外 有很多地址,这家的主人自然就一一跑到上面所写的各个地址上一一找到发送的 主人,取出相关的信件,并且告诉对方“你们现在可以继续发邮件给别人了!!” 还有另外一种情况:就是这个家庭发现自己没有收到自己想要的一家的邮件,所 以他被迫只能一直等到,一直等到想要的那家的邮件发送邮件过来。这个过程可 以比较形象的描述接受过程。

现在来具体的分析过程:

第一:检查调用者自身p_rts_flags标志位是否为SENDING,也就是说调用进程是
否是处在发送等待状态的阻塞队列上。如果是这种情况就比较危险,必须直接返
回。

第二:检查相关的位图信息,也就是s_notify_pending标志位。如果之前有想要 通过notify发送消息给本进程,则应该相应的标志位就应该会应该被置位。被置 位的话,mini_receive()函数这时就会自己构造一个信息,之后发送给自己进程 的消息缓冲区。

第3:如果想要接受mini_send()函数发来的消息的话,则就是接下来的操作了。 这个就对应了那家出国旅游的人回来查看邮箱事件,那家出国旅游回来的一家人 发现了邮箱上有好多地址,他们就一个一个去找,这里也是一样, 通过调用
p_call_q,把链表从头扫到尾,将消息接收过来,之后将那些发送消息的进程放 回到调度队列中。同时也就是标志返回成功。

第四:如果没有想要得到的进程消息,则应该阻塞自己,设置自己的各种标志位, 表明自己是在等待对方。但是有一点要声明,那就是有的进程不希望自己阻塞, 这时就会传递一个flags,如果不想标志自己阻塞就会完成本次mini_receive() 函数的调度而不阻塞自己。

/*=================================================================== ========*

* mini_receive *

*==================================================================== =======*/

//这个函数体是receive的主体主要实现接受信息的功能

PRIVATE int mini_receive(caller_ptr, src, m_ptr, flags)

register struct proc  *caller_ptr;  /* process trying to get message */

int src; /* which message source is wanted  */

message  *m_ptr; /* pointer to message buffer  */

unsigned flags; /* system call flags  */

{

/* A process or task wants to get a message.    If a message is already queued,

* acquire it and deblock the sender.    If no message from the desired source

* is available block the caller, unless the flags don‘t allow blocking.
*/

106

//如果信息已经在队列中,那么就取信息和将发送者解除阻塞状态

//如果没有想要得到的信息,那么就阻塞这个调用者当然如果调用进程的 flag标志不允许阻塞,

//阻塞操作就不能完成

register struct proc  **xpp;

register struct notification  **ntf_q_pp; message m;

int bit_nr;

sys_map_t  *map;

bitchunk_t  *chunk;

int i, src_id, src_proc_nr;

/* Check to see if a message from desired source is already available.
* The caller‘s SENDING flag may be set if SENDREC couldn‘t send. If
it is

* set, the process should be blocked.检查看看一个渴望的消息源是否已 经提供了,这个调用者的SENDING标志位也许被设定,如果SENDREC不能发送时。 如果真的被设定,进程应该是阻塞状态

*/

// 检查调用者的p_rts_flags是否是SENDING状态,调用者的SENDING标志设置 if  (!(caller_ptr->p_rts_flags & SENDING))  {

/* Check if there are pending notifications, except for SENDREC. */ if  (!  (priv(caller_ptr)->s_flags & SENDREC_BUSY))  {

map  = &priv(caller_ptr)->s_notify_pending;

//s_notify_pending表示的通知消息的位图
//这个位图在后面会有介绍

for  (chunk=&map->chunk[0]; chunk<&map->chunk[NR_SYS_CHUNKS]; chunk++)  {

/* Find a pending notification from the requested source. */

if  (!  *chunk) continue; /* no bits in chunk  */

for  (i=0;  !  (*chunk &  (1<<i));  ++i)  {} /* look up the bit

*/

src_id  =  (chunk  - &map->chunk[0])  * BITCHUNK_BITS  + i;

if  (src_id  >= NR_SYS_PROCS) break; /* out of range */

src_proc_nr  = id_to_nr(src_id); /* get source proc */

#if DEBUG_ENABLE_IPC_WARNINGS

if(src_proc_nr  == NONE)  {

kprintf("mini_receive: sending notify from NONE\n");
}

#endif

if  (src!=ANY && src!=src_proc_nr) continue;  /* source not

107

ok  */

*chunk &=  ~(1  << i); /* no longer pending  */

/* Found a suitable source, deliver the notification message.

*/

//找到了一个合适源,发送个通知消息

BuildMess(&m, src_proc_nr, caller_ptr); /* assemble message

*/

CopyMess(src_proc_nr, proc_addr(HARDWARE), &m, caller_ptr,

m_ptr);

return(OK); /* report success  */

}

}

/* Check caller queue. Use pointer pointers to keep code simple. */

xpp  = &caller_ptr->p_caller_q;

// xpp存放调用进程希望发送的进程的首地址。这个时候应该含有相应的信

//这里是将想要接受的消息给予接受,接受完之后就将xpp所指向的进程放 回到调度队列中

while  (*xpp  != NIL_PROC)  {

if  (src  == ANY  || src  == proc_nr(*xpp))  {

/* Found acceptable message. Copy it and update status.  */

CopyMess((*xpp)->p_nr,  *xpp,  (*xpp)->p_messbuf, caller_ptr,

m_ptr);

if (((*xpp)->p_rts_flags &= ~SENDING) == 0) enqueue(*xpp);

*xpp  =  (*xpp)->p_q_link; /* remove from queue  */

return(OK);

}

xpp  = &(*xpp)->p_q_link;
}

}

/* report success  */

/* proceed to next  */

/* No suitable message is available or the caller couldn‘t send in SENDREC.

* Block the process trying to receive, unless the flags tell otherwise.
*/

//如果调用进程没有想要接受的信息时,就应该阻塞正在尝试接受的进程,当然 如果flags标志不能阻塞,

if  (  !  (flags & NON_BLOCKING))  {
caller_ptr->p_getfrom  = src;
caller_ptr->p_messbuf  = m_ptr;

if  (caller_ptr->p_rts_flags  ==  0) dequeue(caller_ptr); caller_ptr->p_rts_flags  |= RECEIVING;

108

return(OK);

//上面的几个操作主要是用于设定call_ptr的相应的操作值,之后用于阻塞 } else  {

return(ENOTREADY);

}

}

下面几个函数就比较的简单,简单介绍下:

带有Lock_....()函数主要是防止其他进程或者中断干扰这里,所以提出了一种 加锁的概念。也就是说在发送时,会关闭中断,发送完毕,关闭中断。或者接受 之前关闭中断,接受完成,开启中断。

/*=================================================================== ========*

* lock_notify *

*====================================================================

=======*/

PUBLIC int lock_notify(src, dst)

int src; /* sender of the notification  */

int dst; /* who is to be notified  */

{

/* Safe gateway to mini_notify() for tasks and interrupt handlers. The
sender is explicitely given to prevent confusion where the call comes from.
MINIX    kernel is not reentrant, which means to interrupts are disabled
after      the first kernel entry (hardware interrupt, trap, or exception).
Locking    is done by temporarily disabling interrupts.
给为中断处理和系统任务提供一个Mini_notify()安全的方法。发送者被明确得
给出来阻止这种疑惑:调用者来自哪里。MINIX内核不是可以嵌套的,这就意味
着在第一次进入内核之后必须关闭中断。锁就是通过临时的关闭中断。
*/

int result;

/* Exception or interrupt occurred, thus already locked.  */

//k_reenter代表的就是嵌套中断,也就是如果大于0,代表嵌入了,则已经加上 锁了,就没有必要在此加锁。

if  (k_reenter  >=  0)  {

result  = mini_notify(proc_addr(src), dst);

}

/* Call from task level, locking is required.  */
//如果不是,则就在处理前加锁,处理完解开锁。
else  {

lock(0, "notify");

result  = mini_notify(proc_addr(src), dst);

109

unlock(0);

}

return(result);

}

/*=================================================================== ========*

* lock_send 同上函数一样,处理前加锁,处理后解开锁

*

*==================================================================== =======*/

PUBLIC int lock_send(dst, m_ptr)

int dst; /* to whom is message being sent?  */

message  *m_ptr; /* pointer to message buffer  */

{

/* Safe gateway to mini_send() for tasks.  */
int result;

lock(2, "send");

result  = mini_send(proc_ptr, dst, m_ptr, NON_BLOCKING); unlock(2);

return(result);

}

6.5 MINIX3内核部件使用消息机制举例

消息传递机制的主体实现基本上是分析完了,我们现在结合内核其他部分调用消 息传递机制来进一步分析之前的操作过程。

首先看CLOCK进程的消息传递机制
????

????
????

Receive(ANY,&m);
????

????

在 clock_handler 中
????

????
????

lock_notify(HARDWARE, CLOCK)

????

????
????

110

wps1AEF.tmp

首先从 RECEIVE(ANY,&m)入手,提出这样一个假设,还有一个能 ANY 消息
机制发送,现在 CLOCK 进程阻塞,之后再某次 clock_handler 中,我们会调用
lock_notify(HARDWARE,CLOCK)主要就是发送 CLOCK 时钟,让 CLOCK 时钟
马上从阻塞队列中移除出来,放到调度队列中准备接受 CPU 调度。我看下示意
图:

这就是 2 者相互同步示意图,当然这里是用 notify 同步。现在我们假设一种相反 的情况:

就是  CLOCK  进程是在就绪队列中 ,但是没有别调度 ,这时先发送了一个
mini_notify(HARDWARE,CLOCK),接下来该怎么处理呢?同样,当 CLOCK 时
钟任务调用 recive(ANY,&m),这时它就会检查位图,发现有消息,就会立即构
造消息并且将其复制自己的消息缓冲队列中。这种模式可以看如下的示意图:

111

wps1B0F.tmp

6.6 MINIX3 IPC 总结

整个过程就是这样,MINIX3 的消息机制其实就相当于送邮件人和发送邮件人的 关系:

1 发件人如果发现接受者正在家里等他来信,则发件人就会把信亲自给他,之后 告诉发件人你的信收到了,可以不必呆在家里,可以出去了。
2 如果送件人发现收件人不在家里,但是送件人又不在送,只有把自己的地址贴 在发件人的门上,并且自己回到家中等他来拿信。

3 收件人刚刚从外面回来,看到门口的信息,马上按照这个地址找到收件人,拿 了信件,并且告诉收件人我现在拿了信件,你可以自己去干其他事情了。
4 这时收件人回到家中,发现自己希望得到另外一个非常重要的来信,但是对方 没有按照规定的时间来,但是他又太想要这封重要的信件,所以他必须等待对方 把信件送来,在没有送来之前,他不得不一直呆在家里,防止错过了。
整个消息传递机制就是上面 4 种可能一直循环往复。这就是消息机制的最核心部 分。MINIX3 的消息传递到此全部结束了!

MINIX3 进程通信分析