Linux/Unix系统IPC是各种进程间通信方式的统称,但是其中极少能在所有Linux/Unix系统实现中进行移植。随着POSIX和Open Group(X/Open)标准化的推进呵护影响的扩大,情况虽已得到改善,但差别仍然存在。一般来说,Linux/Unix常见的进程间通信方式有:管道、消息队列、信号、信号量、共享内存、套接字等。博主将在《进程间通信方式总结》系列博文中和大家一起探讨学习进程间通信的方式,并对其进行总结,让我们共同度过这段学习的美好时光。这里我们就以其中一种方式消息队列展开探讨,消息队列提供了一种从一个进程向另一个进程发送一个数据块的方法。每个数据块都被认为是一个管道,接收进程可以独立地接收含有不同管道的数据结构。我们可以通过发送消息来避免命名管道的同步和阻塞问题。消息队列与命名管道一样,每个数据块都有一个最大长度的限制。我们可以将每个数据块当作是一中消息类型(频道),发送和接收的内容就是这个类型(频道)对应的消息(节目),每个类型(频道)相当于一个独立的管道,相互之间互不影响。Linux提供了一系列消息队列的函数接口来让我们方便地使用它来实现进程间的通信。它的用法与其他两个System V IPC机制,即信号量和共享内存相似,下面就让我们一起来学习一下消息队列吧。
函数原型:int msgget(key_t, key, int msgflg);
与其他的IPC机制一样,程序必须提供一个键来命名某个特定的消息队列。当key=0(IPC_PRIVATE)创建一个只能在进程内部通信的消息队列。msgflg是一个权限标志,表示消息队列的访问权限,它与文件的,访问权限一样。msgflg可以与IPC_CREAT做或操作,表示当key所命名的消息队列不存在时创建一个消息队列,如果key所命名的消息队列存在时,IPC_CREAT标志会被忽略,而只返回一个标识符。IPC_CREAT|IPC_EXCL,如果内核中不存在键值与key相等的消息队列,则新建一个消息队列;如果存在这样的消息队列则报错。它返回一个以key命名的消息队列的标识符(非零整数),失败时返回-1,错误信息存于errno中。
错误码:
EACCES:指定的消息队列已存在,但调用进程没有权限访问它
EEXIST:key指定的消息队列已存在,而msgflg中同时指定IPC_CREAT和IPC_EXCL标志
ENOENT:key指定的消息队列不存在,同时msgflg中没有指定IPC_CREAT标志
ENOMEM:需要建立消息队列,但内存不足
ENOSPC:需要建立消息队列,但已达到系统的限制
函数原型:int msgsend(int msgid, const void *msg_ptr, size_t msg_sz, int msgflg);
msgid:msgget函数返回的消息队列标识符。
msg_ptr:指向准备发送消息的结构体指针,但是消息的数据结构却有一定的要求,指针msg_ptr所指向的消息结构一定要是以一个长整型成员变量开始的结构体,接收函数将用这个成员来确定接收的消息来自哪一个类型(频道)的消息(节目),其他字段不限。消息结构定义:
struct my_message{
long int message_type; //频道(你可以理解为消息通道号)
/* The data you wish to transfer*/ //消息主体
};
msg_sz:msg_ptr指向的消息的长度,注意是消息的长度,而不是整个结构体的长度,也就是说msg_sz是不包括长整型消息类型成员变量的长度。
msgflg:用于控制当前消息队列满或队列消息到达系统范围的限制时将要发生的事情。
返回值:如果调用成功,消息数据的一分副本将被放到消息队列中,并返回0,失败时返回-1。msgflg=0,当消息队列满时,msgsnd将会阻塞,直到消息能写进消息队列;msgflg=IPC_NOWAIT,当消息队列已满的时候,msgsnd函数不等待立即返回;msgflg=IPC_NOERROR,若发送的消息大于size字节,则把该消息截断,截断部分将被丢弃,且不通知发送进程。
错误代码:
EAGAIN:参数msgflg设为IPC_NOWAIT,而消息队列已满
EIDRM:标识符为msgid的消息队列已被删除
EACCESS:无权限写入消息队列
EFAULT:参数msgp指向无效的内存地址
EINTR:队列已满而处于等待情况下被信号中断
EINVAL:无效的参数msgid、msgsz或参数消息类型type小于0
msgsnd为阻塞函数,当消息队列容量满或消息个数满会阻塞。消息队列已被删除,则返回EIDRM错误;被信号中断返回E_INTR错误。如果设置IPC_NOWAIT消息队列满或个数满时会返回-1,并且置EAGAIN错误。msgsnd解除阻塞的条件有以下三个条件:
① 不满足消息队列满或个数满两个条件,即消息队列中有容纳该消息的空间。
② msgid代表的消息队列被删除。
③ 调用msgsnd函数的进程被信号中断(SIGINT)。
函数原型:int msgrcv(int msgid, void *msg_ptr, size_t msg_st, long int msgtype, int msgflg);
msgid:msgget函数返回的消息队列标识符。
msg_ptr:指向准备接收消息的结构体指针,但是消息的数据结构却有一定的要求,指针msg_ptr所指向的消息结构一定要是以一个长整型成员变量开始的结构体,接收函数将用这个成员来确定接收的消息来自哪一个类型(频道)的消息(节目)。消息结构定义:
struct my_message{
long int message_type; //频道(你可以理解为消息通道号)
/* The data you wish to transfer*/ //消息主体
};
msg_st:msg_ptr指向的消息的长度,注意是消息的长度,而不是整个结构体的长度,也就是说msg_sz是不包括长整型消息类型成员变量的长度。
msgtype:msgtype可以实现一种简单的接收优先级,消息是按照接收顺序存于消息队列中,每条消息对应一个消息类型号(频道号)。如果msgtype为0,就获取队列中的第一条消息。如果它的值大于零,将获取具有相同类型号(频道号)的第一条消息。如果它小于零,就获取频道号等于或小于msgtype的绝对值的第一个消息。
msgflg:用于控制当队列中没有相应类型的消息可以接收时将发生的事情。msgflg=0,阻塞式接收消息,没有该类型的消息msgrcv函数一直阻塞等待;msgflg=IPC_NOWAIT,如果没有返回条件的消息调用立即返回,此时错误码为ENOMSG;msgflg=IPC_EXCEPT,与msgtype配合使用返回队列中第一个类型不为msgtype的消息;msgflg=IPC_NOERROR,如果队列中满足条件的消息内容大于所请求的size字节,则把该消息截断,截断部分将被丢弃。
返回值:调用成功时,该函数返回放到接收缓存区中的字节数,消息被复制到由msg_ptr指向的用户分配的缓存区中,然后删除消息队列中的对应消息。失败时返回-1。
错误代码:
E2BIG:消息数据长度大于msgsz而msgflag没有设置IPC_NOERROR
EIDRM:标识符为msgid的消息队列已被删除
EACCESS:无权限读取该消息队列
EFAULT:参数msgp指向无效的内存地址
ENOMSG:参数msgflg设为IPC_NOWAIT,而消息队列中无消息可读
EINTR:等待读取队列内的消息情况下被信号中断
msgrcv解除阻塞的条件有以下三个:
①消息队列中有了满足条件的消息。
②msgid代表的消息队列被删除。
③调用msgrcv()的进程被信号中断(SIGINT)。
函数原型:int msgctl(int msgid, int command, struct msgid_ds *buf);
msgid:msgget函数返回的消息队列标识符。
command:是将要采取的动作,它可以取3个值:
IPC_STAT:把msgid_ds结构中的数据设置为消息队列的当前关联值,即用消息队列的当前关联值覆盖msgid_ds的值。
IPC_SET:如果进程有足够的权限,就把消息列队的当前关联值设置为msgid_ds结构中给出的值
IPC_RMID:删除消息队列
buf:指向msgid_ds结构的指针,它指向消息队列模式和访问权限的结构。
struct msqid_ds {//Linux系统中的定义
struct ipc_perm msg_perm; /* Ownership andpermissions*/
time_t msg_stime; /* Time of last msgsnd()*/
time_t msg_rtime; /* Time of last msgrcv()*/
time_t msg_ctime; /* Time of last change */
unsigned long __msg_cbytes; /* Currentnumber of bytes inqueue (non-standard) */
msgqnum_t msg_qnum; /* Current number of messagesinqueue */
msglen_t msg_qbytes; /* Maximum number ofbytesallowed in queue */
pid_t msg_lspid; /* PID of last msgsnd() */
pid_t msg_lrpid; /* PID of last msgrcv() */
};//不同的系统中此结构会有不同的新成员
返回值:成功时返回0,失败时返回-1。
错误代码:
EACCESS:参数cmd为IPC_STAT,无权限读取该消息队列
EFAULT:参数buf指向无效的内存地址
EIDRM:标识符为msgid的消息队列已被删除
EINVAL:无效的参数cmd或msgid
EPERM:参数cmd为IPC_SET或IPC_RMID,却无足够的权限执行
在程序实例一中,发送进程创建消息队列,接收进程连接消息队列。发送进程向指定的频道号(类别号)发送指定消息,接收进程接收指定频道中的消息。如果接收进程指定的频道号为0,表示接收消息队列中任意频道号对应的消息,按照消息的先后顺序接收。
#include
#include
#include
#include
#include
#include
//定义消息结构体
typedefstruct msgbuf
{
long channel;//消息结构体的第一个元素必须是long类型(消息类别号)
char buf[256];//保存消息(消息体)
}msg_t;
intmain(int argc, char **argv)
{
if (argc < 2) fprintf(stderr,"usage:%s msgid\n", argv[0]), exit(1);
//创建消息队列 int msgget(key_t key, int msgflg);
//参数1:消息队列的key值(标识一个消息队列)
//参数2:如果key对应的消息队列不存在则创建之,如果存在则打开
int msgid = msgget(atoi(argv[1]), IPC_CREAT| IPC_EXCL | 0666);
if (-1 == msgid)perror("msgget"), exit(1);
//定义并初始化消息结构体
msg_t msg;
memset(&msg, 0x00, sizeof(msg));
while (1)
{
//输入通道号(消息类别)
printf("channel:");
scanf("%d",&msg.channel);
scanf("%*c");//读取缓冲区中的换行符
//从标准输入获取数据,以回车结束
printf("msg:");
fgets(msg.buf, sizeof(msg.buf), stdin);
//scanf("%*c");//由于fgets会接收\n,不用再接收换行符
//int msgsnd(int msqid, const void*msgp, size_t msgsz, int msgflg);
//参数1:调用msgget函数返回的消息队列号(标识符)
//参数2:待发送的消息结构体地址
//参数3:待发送的消息长度(不包括消息结构体的第一个元素)
//参数4:附加功能(没有特殊需求填0即可)
msgsnd(msgid, &msg,sizeof(msg.buf), 0);
char ch = ' ';
fprintf(stdout, "是否继续发送?[yn]:");
scanf("%[yYnN]", &ch);
scanf("%*c");
if ('y' != ch && 'Y' != ch)
{
break;
}
}
//int msgctl(int msqid, int cmd, structmsqid_ds *buf);
//参数1:调用msgget函数返回的消息队列号
//参数2:执行的操作类型
//参数3:消息队列状态信息结构体指针
msgctl(msgid, IPC_RMID, 0);
return 0;
}
#include
#include
#include
#include
#include
#include
#include
//定义消息结构体
typedefstruct msgbuf
{
long channel;//消息结构体的第一个元素必须是long类型
char buf[256];
}msg_t;
intmsgid = -1;
//信号处理函数
voidhandler(int s)
{
//int msgctl(int msqid, int cmd, structmsqid_ds *buf);
//参数1:调用msgget函数返回的消息队列号
//参数2:执行的操作类型
//参数3:接收消息队列的信息
msgctl(msgid, IPC_RMID, 0);
fprintf(stdout, "exit\n");
exit(EXIT_SUCCESS);
}
intmain(int argc, char **argv)
{
if (argc < 2) fprintf(stderr,"usage:%s msgid\n", argv[0]), exit(EXIT_FAILURE);
//注册中断信号
signal(SIGINT, handler);
//创建消息队列 int msgget(key_t key, int msgflg);
//参数1:消息队列的key值(标识一个消息队列)
//参数2:如果key对应的消息队列不存在则创建之,如果存在则打开
msgid = msgget(atoi(argv[1]), 0);
if (-1 == msgid)perror("msgget"), exit(EXIT_FAILURE);
//定义并初始化消息结构体
msg_t msg;
memset(&msg, 0x00, sizeof(msg));
while (1)
{
//输入要接收的频道数(消息类别编号)
printf("channel:");
scanf("%d",&msg.channel);
//int msgsnd(int msqid, const void*msgp, size_t msgsz, int msgflg);
//参数1:调用msgget函数返回的消息队列号
//参数2:待接收的消息结构体地址
//参数3:待接收的消息长度(不包括消息结构体的第一个元素)
//参数4:接受的channel号
//参数5:附加功能(没有特殊需求填0即可)
msgrcv(msgid, &msg,sizeof(msg.buf), msg.channel, 0);
fprintf(stdout, "msg: %s\n",msg.buf);
}
return 0;
}
程序运行结果:
在实例程序二中,服务进程创建消息队列,客户进程连接消息队列。服务进程接收频道1中的消息,所有的客户进程下向消息队列中的频道1发送消息,客户进程将当前进程的PID(唯一区分客户进程)作为消息体的前4字节,服务进程接收到数据,首先读取消息体的前4字节,将其作为客户进程接收数据的频道号,向该频道发送接收到的消息,对应的客户端就可以接收到自己发送给服务进程的消息,接收的消息和自己发送的消息完全一致。
#include
#include
#include
#include
#include
#include
#include
intmsgid = 0;
voidhandler(int s)
{
fprintf(stdout, "exit\n");
msgctl(msgid, IPC_RMID, 0);
exit(1);
}
//定义消息结构体
typedefstruct msgbuf
{
long channel;//消息结构体的第一个元素必须是long类型
char buf[256];//保存消息
}msg_t;
intmain(int argc, char **argv)
{
if (argc < 2) fprintf(stderr,"usage:%s msgid\n", argv[0]), exit(1);
signal(SIGINT, handler);
//创建消息队列 int msgget(key_t key, int msgflg);
//参数1:消息队列的key值(标识一个消息队列)
//参数2:如果key对应的消息队列不存在则创建之,如果存在则打开之。(第二个参数可以直接填0,内核会替你完成相应的参作)
msgid = msgget(atoi(argv[1]), IPC_CREAT |IPC_EXCL | 0666);
if (-1 == msgid)perror("msgget"), exit(1);
msg_t sendmsg;
msg_t rcvemsg;
while (1)
{
memset(&sendmsg, 0x00,sizeof(msg_t));
memset(&rcvemsg, 0x00,sizeof(msg_t));
//接收客户端发送过来的消息
msgrcv(msgid, &rcvemsg,sizeof(rcvemsg.buf), 1, 0);
//从消息体中读取客户进程的频道号(消息体的前4字节)
sendmsg.channel = *(int*)rcvemsg.buf;
memcpy(sendmsg.buf,rcvemsg.buf+sizeof(int), strlen(rcvemsg.buf + sizeof(int)));
//将接收的消息原样发送回客户进程
//int msgsnd(int msqid, const void*msgp, size_t msgsz, int msgflg);
//参数1:调用msgget函数返回的消息队列号
//参数2:待发送的消息结构体地址
//参数3:待发送的消息长度(不包括消息结构体的第一个元素)
//参数4:附加功能(没有特殊需求填0即可)
msgsnd(msgid, &sendmsg,sizeof(sendmsg.buf), 0);
}
return 0;
}
#include
#include
#include
#include
#include
#include
#include
intmsgid = 0;
voidhandler(int s)
{
fprintf(stdout, "exit\n");
msgctl(msgid, IPC_RMID, 0);
exit(1);
}
//定义消息结构体
typedefstruct msgbuf
{
long channel;//消息结构体的第一个元素必须是long类型
char buf[256];//保存消息
}msg_t;
intmain(int argc, char **argv)
{
if (argc < 2) fprintf(stderr,"usage:%s msgid\n", argv[0]), exit(1);
signal(SIGINT, handler);
//创建消息队列 int msgget(key_t key, int msgflg);
//参数1:消息队列的key值(标识一个消息队列)
//参数2:如果key对应的消息队列不存在则创建之,如果存在则打开之。(第二个参数可以直接填0,内核会替你完成相应的参作)
msgid = msgget(atoi(argv[1]), 0);
if (-1 == msgid)perror("msgget"), exit(1);
msg_t sendmsg;
msg_t rcvemsg;
pid_t pid = getpid();
while (1)
{
memset(&sendmsg, 0x00, sizeof(msg_t));
memset(&rcvemsg, 0x00,sizeof(msg_t));
//服务进程接收的频道号默认为1
sendmsg.channel = 1;
//客户进程的频道号位当前进程的进程ID,并将消息体的前4字节作为频道号
*(int*)sendmsg.buf = pid;
fprintf(stdout, "msg:");
fgets(sendmsg.buf + sizeof(int),sizeof(sendmsg.buf) - sizeof(int), stdin);
//scanf("%s", sendmsg.buf +sizeof(int));
//scanf("%*c");
//int msgsnd(int msqid, const void*msgp, size_t msgsz, int msgflg);
//参数1:调用msgget函数返回的消息队列号
//参数2:待发送的消息结构体地址
//参数3:待发送的消息长度(不包括消息结构体的第一个元素)
//参数4:附加功能(没有特殊需求填0即可)
msgsnd(msgid, &sendmsg,sizeof(sendmsg.buf), 0);
msgrcv(msgid, &rcvemsg,sizeof(rcvemsg.buf), pid, 0);
fprintf(stdout, "msg:%s\n",rcvemsg.buf);
}
return 0;
}
程序运行结果:
消息队列跟命名管道有不少的相同之处,与命名管道一样,消息队列进行通信的进程可以是不相关的进程,同时它们都是通过发送和接收的方式来传递数据的。在命名管道中,发送数据用write,接收数据用read,而在消息队列中,发送数据用msgsnd,接收数据用msgrcv。而且它们对每个数据都有一个最大长度的限制。与命名管道相比,消息队列的优势在于,消息队列也可以独立以发送和接收进程而存在,从而消除了在同步命名管道的打开和关闭时可能产生的困难。同时通过发送消息还可以避免命名管道的同步和阻塞问题,不需要由进程自己来提供同步方法。接收程序可以通过消息类型号(频道号)有选择地接收数据,而不是像命名管道中那样,只能默认地接收。
关于消息队列的学习我们就到此结束了,相信大家都有所收获,希望小伙伴们都已经理解并掌握了消息队列的常用方法。如果你觉得对进程间通信的方式不胜了解,还有些许疑惑,请关注博主《进程间通信方式总结》系列博文,相信你在那里能找到答案。