我们知道进程间通信(IPC)可以使用管道来完成,但使用管道有一个缺陷——只能单向通信,即想要双向通信就需要两个管道。下面介绍另一种进程间通信的方法——消息队列,它可以实现双向通信。
什么是消息队列
“消息队列”是在消息的传输过程中保存消息的容器。它提供了一种从一个进程向另一个进程发送一个数据块的方法,每个数据块都被认为是有一个类型,接收者进程接收的数据块可以有不同的类型值。
消息队列就是一个消息的链表。可以把消息看作一个记录,具有特定的格式以及特定的优先级。对消息队列有写权限的进程可以向消息队列中按照一定的规则添加新消息;对消息队列有读权限的进程则可以从消息队列中读走消息。消息队列是随内核持续的。
与管道有什么不同?
- 打开管命名道的进程可能会阻塞,而消息队列不会。
- 命名管道的需要遵循同步机制(按照某种特定顺序访问资源的特质,以互斥为前提),而消息队列则不需要。
- 消息队列是基于消息的,⽽而管道是基于字节流的,且消息队列的读取不一定是先入先出。
- 消息队列与命名管道有一样的不足,就是每个消息的最大长度是有上限的(MSGMAX),每个消息队列的总的字节数是有上限的(MSGMNB),系统上消息队列的总数也有一个上限(MSGMNI)。
消息队列的结构
IPC结构
内核为每个IPC对象维护一个数据结构(定义于/include/linux/ipc.h),消息队列、共享内存和信号量都有这样一个结构:
#ifndef _LINUX_IPC_H
#define _LINUX_IPC_H
#include <linux/spinlock.h>
#include <linux/uidgid.h>
#include <uapi/linux/ipc.h>
#define IPCMNI 32768 /* <= MAX_INT limit for ipc arrays (including sysctl changes) */
/* used by in-kernel data structures */
struct kern_ipc_perm{
spinlock_t lock;
bool deleted;
int id;
key_t key;//唯一的标识该IPC
kuid_t uid;
kgid_t gid;
kuid_t cuid;
kgid_t cgid;
umode_t mode;
unsigned long seq;
void *security;
} ____cacheline_aligned_in_smp;
#endif /* _LINUX_IPC_H */
消息队列结构
消息队列的结构如下:(定义于include/linux/msg.h)
#ifndef _LINUX_MSG_H
#define _LINUX_MSG_H
#include <linux/list.h>
#include <uapi/linux/msg.h>
/* one msg_msg structure for each message */
struct msg_msg {
struct list_head m_list;
long m_type;
size_t m_ts; /* message text size */
struct msg_msgseg *next;//链表实现
void *security;
/* the actual message follows immediately */
};
/* one msq_queue structure for each present queue on the system */
struct msg_queue {
struct kern_ipc_perm q_perm;////创建的一个IPC结构体
time_t q_stime; /* last msgsnd time */
time_t q_rtime; /* last msgrcv time */
time_t q_ctime; /* last change time */
unsigned long q_cbytes; /* current number of bytes on queue */
unsigned long q_qnum; /* number of messages in queue */
unsigned long q_qbytes; /* max number of bytes on queue */
pid_t q_lspid; /* pid of last msgsnd */
pid_t q_lrpid; /* last receive pid */
struct list_head q_messages;//
struct list_head q_receivers;
struct list_head q_senders;
};
/* Helper routines for sys_msgsnd and sys_msgrcv */
extern long do_msgsnd(int msqid, long mtype, void __user *mtext,
size_t msgsz, int msgflg);
extern long do_msgrcv(int msqid, void __user *buf, size_t bufsz, long msgtyp,
int msgflg,
long (*msg_fill)(void __user *, struct msg_msg *,
size_t));
#endif /* _LINUX_MSG_H */
msgget(key_t key, int msgflg);
注意在msg_queue
中struct kern_ipc_perm q_perm
,创建一个IPC结构体。此外消息队列是用链表实现的,在msg_msg
中我们可以看到struct list_head m_list;
和struct msg_msgseg *next;
分别是指向链表头和下一个节点的指针。
消息队列的创建与使用
创建函数
函数原型:int msgget(key_t key, int msgflg)
;
参数:
key
:可以认为是.一个端..口号,也可以由函数ftok().
生成。
msgflg:
IPC_CREAT
如果IPC不存在,则创建.一个IPC资源,否则打开操作。
IPC_EXCL
:只有在共享内存不存在的时候,新的共享内存才建.立,否则就产.生错误。
如果单独使.用IPC_CREAT,XXXget()
函数要么返回.一个已经存在的共享内存的操作符,要么返回一个新建的共享内存的标识符。
如果将IPC_CREAT和IPC_EXCL
标志.一起使.用,XXXget()
将返回.一个新建的IPC标识符;如果该IPC资源已存在,或者返回-1。
IPC_EXEL
标志本.身并没有太.大的意义,但是和IPC_CREAT
标志.一起使.用可以.用来保证所得的对象是新建的,.而不是打开已有的对象。
#include <sys/types.h>
#include <sys/ipc.h>
函数原型:
key_t ftok( const char * fname, int id )
fname
就是你指定的文件名(已经存在的文件名),一般使用当前目录,如:
key_t key;
key = ftok(".", 1);
这样就是将fname设为当前目录。
id是子序号。虽然是int类型,但是只使用8bits(1-255)。
在一般的UNIX实现中,是将文件的索引节点号取出,前面加上子序号得到key_t的返回值。
如指定文件的索引节点号为65538,换算成16进制为0x010002,而你指定的ID值为38,换算成16进制为0x26,则最后的key_t返回值为0x26010002
读写函数:
msgrcv
从队列中取⽤用消息:ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int
msgflg);
msgsnd
将数据放到消息队列中:int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
参数:
msqid
:消息队列的标识码
msgp
:指向消息缓冲区的指针,此位置⽤用来暂时存储发送和接收的消息,是一个⽤用户可
定义的通⽤用结构,形态如下:
struct msgstru{
long mtype; //⼤大于0
char mtext[用户指定大小];
};
msgsz
:消息的大小。
msgtyp
:从消息队列内读取的消息形态。如果值为零,则表⽰示消息队列中的所有消息都
会被读取。
msgflg
:⽤用来指明核心程序在队列没有数据的情况下所应采取的⾏行动。如果sgflg
和常
数IPC_NOWAIT
合用,则在msgsnd()
执⾏行时若是消息队列已满,则msgsnd()
将不会阻塞,⽽
会立即返回-1,如果执⾏行的是msgrcv(),则在消息队列呈空时,不做等待马上返回-1,并设定
错误码为ENOMSG。当msgflg为0时,msgsnd()及msgrcv()在队列呈满或呈空的情形时,采取
阻塞等待的处理模式。
设置消息队列属性
原型:int msgctl ( int msgqid, int cmd, struct msqid_ds *buf )
;
参数:msgctl
系统调⽤用对 msgqid
标识的消息队列执⾏行 cmd 操作,系统定义了 3 种 cmd 操作
: PC_STAT , IPC_SET , IPC_RMID
IPC_STAT
: 该命令⽤用来获取消息队列对应的 msqid_ds 数据结构,并将其保存到 buf
指定的地址空间。
IPC_SET
: 该命令⽤用来设置消息队列的属性,要设置的属性存储在buf
中。
IPC_RMID
: 从内核中删除msqid
标识的消息队列。
测试代码
如下代码通过消息队列实现两个进程间的通信。
comm.h
#ifndef __COMM__
#define __COMM__
#include <stdio.h>
#include <stdlib.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <sys/types.h>
#include <string.h>
#include <time.h>
#define FILENAME "."//ftok时所指定的路径名
#define PROJ_ID 0//ftok时所需的ID
#define SER_TYPE 1
#define CLI_TYPE 2
typedef struct _msgbuf{
long mtype;
char mtext[1024];
}msgbuf;
int creatMsgQueue();
int getMsgQueue();
int destroyMsgQueue(int msgid);
int sendMsg(int msgid, int type, const char *msg);
int recvMsg(int msgid, int type, char *out);
#endif
comm.c
#include "comm.h"
static int commMsgQueue(int flag)
{
key_t _key = ftok(FILENAME, PROJ_ID);//ftok一个key值
if(_key < 0)
{
perror("ftok");
return -1;
}
int msgid = msgget(_key, flag);
if(msgid < 0)
{
perror("msgget");
return -2;
}
return msgid;
}
int creatMsgQueue()
{
return commMsgQueue(IPC_CREAT|IPC_EXCL|0666);//创建一个默认权限为0666的消息队列
}
int getMsgQueue()
{
return commMsgQueue(IPC_CREAT);//获取一个以存在的消息队列
}
int destroyMsgQueue(int msgid)
{
if(msgctl(msgid, IPC_RMID, NULL) < 0)
{
perror("msgctl");
return -1;
}
return 0;
}
int sendMsg(int msgid, int type, const char *msg)
{
msgbuf _mb;
_mb.mtype = type;
strcpy(_mb.mtext, msg);
if(msgsnd(msgid, &_mb, sizeof(_mb.mtext), 0) < 0)
{
perror("msgend");
return -1;
}
return 0;
}
int recvMsg(int msgid, int type, char *out)
{
msgbuf _mb;
if(msgrcv(msgid, &_mb, sizeof(_mb.mtext), type, 0) < 0)
{
perror("msgend");
return -1;
}
return 0;
}
server.c
#include "comm.h"
int main()
{
int msgid = creatMsgQueue();
char buff[1024];
while(1)
{
buff[0] = 0;
recvMsg(msgid, CLI_TYPE, buff);
printf("client say: %s\n", buff);
printf("Please Enter:");
fflush(stdout);
ssize_t s = read(0, buff, sizeof(buff)-1);
if(s > 0)
{
buff[s-1] = 0;
sendMsg(msgid, SER_TYPE, buff);
}
}
destroyMsgQueue(msgid);
return 0;
}
client.c
#include "comm.h"
int main()
{
int msgid = getMsgQueue();
char buff[1024];
while(1)
{
buff[0] = 0;
printf("Please Enter:");
fflush(stdout);
ssize_t s = read(0, buff, sizeof(buff)-1);
if(s > 0)
{
buff[s-1] = 0;
sendMsg(msgid, CLI_TYPE, buff);
}
recvMsg(msgid, SER_TYPE, buff);
printf("server say: %s\n", buff);
}
return 0;
}
运行效果如下:
注意:
上面的代码必须先让
server
,运行起来,再去运行client
,原因是消息队列的创建时在server
中。
【作者:果冻 http://blog.youkuaiyun.com/jelly_9】
———谢谢!