系统V IPC机制(System V IPC Mechanisms)
前面讨论的信号和管道虽然可以在进程之间通信,但还有许多应用程序的IPC需求它们不能满足。因此在System V UNIX(1983)中首次引入了另外三种进程间通信机制(IPC)机制:消息队列、信号灯和共享内存(message queues,semaphores and shared memory)。它们最初的设计目的是满足事务式处理的应用需求,但目前大多数的UNIX供应商(包括基于BSD的供应商)都实现了这些机制。 Linux完全支持Unix System V中的这三种IPC机制。
System V IPC机制共享通用的认证方式。进程在使用某种类型的IPC资源以前,必须首先通过系统调用创建或获得一个对该资源的引用标识符。进程只能通过系统调用,传递一个唯一的引用标识符到内核来访问这些资源。在每一种机制中,对象的引用标识符都作为它在资源表中的索引。但它不是直接的索引,需要一个简单的操作来从引用标识符产生索引。对于System V IPC对象的访问,使用访问许可权检查,这很象对文件访问时所做的检查。System V IPC对象的访问权限由对象的创建者通过系统调用设置。
系统中表示System V IPC对象的所有Linux数据结构中都包括一个ipc_perm数据结构,用它记录IPC资源的认证信息。其定义如下:
struct ipc_perm
{
__kernel_key_t key;
__kernel_uid_t uid;
__kernel_gid_t gid;
__kernel_uid_t cuid;
__kernel_gid_t cgid;
__kernel_mode_tmode;
unsigned short seq;
};
在ipc_perm数据结构中包括了创建者进程的用户和组标识、所有者进程的用户和组标识、对于这个对象的访问模式(属主、组和其它)以及IPC对象的键值(key)。Linux通过key 来定位System V IPC对象的引用标识符,每个IPC对象都有一个唯一的key。Linux支持两种key:公开的和私有的。如果key是公开的,那么系统中的任何进程,只要通过了权限检查,就可以找到它所对应的System V IPC对象的引用标识符。System V IPC对象不能直接使用key来引用,必须使用它们的引用标识符来引用。(参见include/linux/ipc.h)每种IPC机制都提供一种系统调用,用来将键值(key)转化为对象的引用标识符。
对所有的System V IPC,Linux提供了一个统一的系统调用:sys_ipc,通过该函数可以实现对System V IPC的所有操作。函数sys_ipc的定义如下:
int sys_ipc (uint call, int first, int second,
int third, void *ptr, long fifth)
这里call是一个32位的整数,其低16位指明了此次调用所要求的工作。对不同的call值,其余各参数的意义也不相同。以下将分别介绍各IPC机制及其所提供的操作。
Message Queues(消息队列)
消息队列就是消息的一个链表,它允许一个或多个进程向它写消息,一个或多个进程从中读消息。Linux维护了一个消息队列向量表:msgque,来表示系统中所有的消息队列。其定义如下:
struct msqid_ds *msgque[MSGMNI];
该向量表中的每一个元素都是一个指向msqid_ds数据结构的指针,而一个msqid_ds数据结构完整地描述了一个消息队列。
MSGMNI的值是128,就是说,系统中同时最多可以有128个消息队列。
msqid_ds数据结构的定义如下:
struct msqid_ds {
struct ipc_perm msg_perm;
struct msg *msg_first; /* first message on queue */
struct msg *msg_last; /* last message in queue */
__kernel_time_t msg_stime; /* last msgsnd time */
__kernel_time_t msg_rtime; /* last msgrcv time */
__kernel_time_t msg_ctime; /* last change time */
struct wait_queue *wwait;
struct wait_queue *rwait;
unsigned short msg_cbytes; /* current number of bytes on queue */
unsigned short msg_qnum; /* number of messages in queue */
unsigned short msg_qbytes; /* max number of bytes on queue */
__kernel_ipc_pid_t msg_lspid; /* pid of last msgsnd */
__kernel_ipc_pid_t msg_lrpid; /* last receive pid */
};
其中包括:
l 一个ipc_perm的数据结构(msg_perm域),描述该消息队列的通用认证方式。
l 一对消息指针(msg_first、msg_last),分别指向该消息队列的队头(第一个消息)和队尾(最后一个消息)(msg)。发送者将新消息加到队尾,接收者从队头读取消息。
l 三个时间域(msg_stime、msg_rtime、msg_ctime)用于记录队列最后一次发送时间、接收时间和改动时间。
l 两个进程等待队列(wwait、rwait)分别表示等待向消息队列中写的进程(wwait)和等待从消息队列中读的进程(rwait)。如果某进程向一个消息队列发送消息而发现该队列已满,则进程挂在wwait队列中等待。从该消息队列中读取消息的进程将从队列中删除消息,从而腾出空间,再唤醒wwait队列中等待的进程。如果某进程从一个消息队列中读消息而发现该队列已空,则进程挂在rwait队列中等待。向该消息队列中发送消息的进程将消息加入队列,再唤醒rwait队列中等待的进程。
l 三个记数域(msg_cbytes、msg_qnum、msg_qbytes)分别表示队列中的当前字节数、队列中的消息数和队列中最大字节数;
l 两个PID域(msg_lspid、msg_lrpid)分别表示最后一次向该消息队列中发送消息的进程和最后一次从该消息队列中接收消息的进程。
见右图(参见include/linux/msg.h)图 System V IPC 机制——消息队列
当创建消息队列时,一个新的msqid_ds数据结构被从系统内存中分配出来,并被插入到msgque 向量表中。
每当进程试图向消息队列写消息时,它的有效用户和组标识符就要和消息队列的ipc_perm数据结构中的模式域比较。如果进程可以向这个消息队列写(比较成功),则消息会从进程的地址空间拷贝到一个msg数据结构中,该msg数据结构被放到消息队列的队尾。每一个消息都带有进程间约定的、与应用程序相关的类型标记。但是,因为Linux限制了可以写的消息的数量和长度,所以可能会没有空间来容纳该消息。这时,进程会被放到消息队列的写等待队列中,然后调度程序会选择一个新的进程运行。当一个或多个消息从这个消息队列中读出去时,等待的进程会被唤醒。
从队列中读消息与向队列中写消息是一个相似的过程。进程对消息队列的访问权限一样要被检查。读进程可以选择从队列中读取第一条消息而不管消息的类型,也可以选择从队列中读取特殊类型的消息。如果没有符合条件的消息,读进程会被加到消息队列的读等待队列,然后运行调度程序。当一个新的消息写到消息队列时,这个进程会被唤醒,继续它的运行。
Linux提供了四个消息队列操作。
1. 创建或获得消息队列(MSGGET)
在系统调用sys_ipc中call值为MSGGET,调用的函数为sys_msgget。该函数的定义如下:
int sys_msgget (key_t key, int msgflg)
其中key是一个键值,而msgflg是一个标志。
该函数的作用是创建一个键值为key的消息队列,或获得一个键值为key的消息队列的引用标识符。这是使用消息队列的第一步,即获得消息队列的引用标识符,以后就通过该标识符使用这个消息队列。
工作过程如下:
1) 如果key == IPC_PRIVATE,则申请一块内存,创建一个新的消息队列(数据结构msqid_ds),将其初始化后加入到msgque向量表中的某个空位置处,返回标识符。
2) 在msgque向量表中找键值为key的消息队列,如果没有找到,结果有二:
l msgflg表示不创建新的队列,则错误返回。
l msgflg表示要创建新的队列,则创建新消息队列,创建过程如1)。
3) 如果在msgque向量表中找到了键值为key的消息队列,则有以下情况:
l 如果msgflg表示一定要创建新的消息队列而且不允许有相同键值的队列存在,则错误返回。
l 如果找到的队列是不能用的或已损坏的队列,则错误返回。
l 认证和存取权限检查,如果该队列不允许msgflg要求的存取,则错误返回。
l 正常,返回队列的标识符。
2. 发送消息
在系统调用sys_ipc中call值为MSGSND,调用的函数为sys_msgsnd。该函数的定义如下:
int sys_msgsnd (int msqid, struct msgbuf *msgp, size_t msgsz, int msgflg)
其中:msqid是消息队列的引用标识符;
msgp是消息内容所在的缓冲区;
msgsz是消息的大小;
msgflg是标志。
该函数做如下工作:
1) 该消息队列在向量msgque中的索引是id = (unsigned int) msqid % MSGMNI,认证检查(权限、模式),合法性检查(类型、大小等)。
2) 如果队列已满,以可中断等待状态(TASK_INTERRUPTIBLE)将当前进程挂起在wwait等待队列上。
3) 申请一块空间,大小为一个消息数据结构加上消息大小,在其上创建一个消息数据结构struct msg,将消息缓冲区中的消息内容拷贝到该内存块中消息头的后面(从用户空间拷贝到内核空间)。
4) 将消息数据结构加入到消息队列的队尾,修改队列的相应参数(大小等)。
5) 唤醒在该消息队列的rwait进程队列上等待读的进程。
6) 返回
3. 接收消息
在系统调用sys_ipc中call值为MSGRCV,调用的函数为sys_msgrcv。该函数的定义如下:
int sys_msgrcv (int msqid, struct msgbuf *msgp, size_t msgsz,
long msgtyp, int msgflg)
其中:msqid是消息队列的引用标识符;
msgp是接收到的消息将要存放的缓冲区;
msgsz是消息的大小;
msgtyp是期望接收的消息类型;
msgflg是标志。
该函数做如下工作:
1) 该消息队列在向量msgque中的索引是id = (unsigned int) msqid % MSGMNI,认证检查(权限、模式),合法性检查。
2) 根据msgtyp和msgflg搜索消息队列,情况有二:
l 如果找不到所要的消息,则以可中断等待状态(TASK_INTERRUPTIBLE)将当前进程挂起在rwait等待队列上。
l 如果找到所要的消息,则将消息从队列中摘下,调整队列参数,唤醒该消息队列的wwait进程队列上等待写的进程,将消息内容拷贝到用户空间的消息缓冲区msgp中,释放内核中该消息所占用的空间,返回。
4. 消息控制
在系统调用sys_ipc中call值为MSGCTL,调用的函数为sys_msgctl。该函数的定义如下:
int sys_msgctl (int msqid, int cmd, struct msqid_ds *buf)
其中:msqid是消息队列的引用标识符;
cmd是执行命令;
buf是一个缓冲区。
该函数做如下工作:
该函数对消息队列做一些控制动作,如:释放队列,获得队列的认证信息,设置队列的认证信息等。
消息队列和管道提供相似的服务,但消息队列要更加强大并解决了管道中所存在的一些问题。消息队列传递的消息是不连续的、有格式的信息,给对它们的处理带来了很大的灵活性。可以用不同的方式解释消息的类型域,如可以将消息的类型同消息的优先级联系起来,类型域也可以用来指定接收者。
小消息的传送效率很高,但大消息的传送性能则较差。因为消息传送的过程中要经过从用户空间到内核空间,再从内核空间到用户空间的拷贝,所以,大消息的传送其性能较差。另外,消息队列不支持广播,而且内核不知道消息的接收者。