嵌入式 Linux环境进程间通信(三):消息队列

概念解释:

IPC随进程持续、随内核持续以及随文件系统持续的定义:

随进程持续:IPC一直存在到打开IPC对象的最后一个进程关闭该对象为止。如管道和有名管道;

随内核持续:IPC一直持续到内核重新自举或者显示删除该对象为止。如消息队列、信号灯以及共享内存等;

随文件系统持续:IPC一直持续到显示删除该对象为止。

消息队列(也叫做报文队列)能够克服早期unix通信机制的一些缺点。

作为早期unix通信机制之一的信号能够传送的信息量有限,后来虽然POSIX 1003.1b在信号的实时性方面作了拓广,使得信号在传递信息量方面有了相当程度的改进,但是信号这种通信方式更像"即时"的通信方式,它要求接受信号的进程在某个时间范围内对信号做出反应,因此该信号最多在接受信号进程的生命周期内才有意义,信号所传递的信息是接近于随进程持续的概念(process-persistent),见附录 1;

管道及有名管道则是典型的随进程持续IPC,并且,只能传送无格式的字节流,这无疑会给应用程序开发带来不便,另外,它的缓冲区大小也受到限制。

消息队列就是一个消息的链表。可以把消息看作一个记录,具有特定的格式以及特定的优先级。

对消息队列有写权限的进程可以向中按照一定的规则添加新消息;对消息队列有读权限的进程则可以从消息队列中读走消息。消息队列是随内核持续的

目前主要有两种类型的消息队列:POSIX消息队列以及系统V消息队列,系统V消息队列目前被大量使用。

(考虑到程序的可移植性,新开发的应用程序应尽量使用POSIX消息队列)

在本系列专题的序(深刻理解Linux进程间通信(IPC))中,提到对于消息队列、信号灯、以及共享内存区来说,有两个实现版本:POSIX的以及系统V的。

Linux内核(内核2.4.18)支持POSIX信号灯、POSIX共享内存区以及POSIX消息队列(POSIX:可移植操作系统接口,Unix和Microsoft Windows NT,都支持 POSIX 标准

但对于主流Linux发行版本之一redhad8.0(内核2.4.18),还没有提供对POSIX进程间通信API的支持,不过应该只是时间上的事。

因此,本文将主要介绍系统V消息队列及其相应API。在没有声明的情况下,以下讨论中指的都是系统V消息队列。

一、消息队列基本概念

系统V消息队列是随内核持续的,只有在内核重启,或者显示删除一个消息队列时,该消息队列才会真正被删除。

系统中记录消息队列的数据结构(struct ipc_ids msg_ids)位于内核中,系统中的所有消息队列都可以在结构msg_ids中找到访问入口。

消息队列就是一个消息的链表,用结构struct msg_queue来描述。每个消息队列都有一个队列头。队列头中包含了该消息队列的大量信息,包括消息队列键值、用户ID、组ID、消息队列中消息数目等等,甚至记录了最近对消息队列读写进程的ID。

其中:struct ipc_ids msg_ids是内核中记录消息队列的全局数据结构;

            struct msg_queue是每个消息队列的队列头。

结构msg_queue用来描述消息队列头,存在于系统空间:

struct msg_queue {
struct kern_ipc_perm q_perm;
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;
};

从上图可以看出,全局数据结构 struct ipc_ids msg_ids 可以访问到每个消息队列头的第一个成员:struct kern_ipc_perm;而每个struct kern_ipc_perm能够与具体的消息队列对应起来是因为在该结构中,有一个key_t类型成员key,而key则唯一确定一个消息队列。kern_ipc_perm结构如下:

struct kern_ipc_perm{ //内核中记录消息队列的全局数据结构msg_ids能够访问到该结构;

key_t key; //该键值则唯一对应一个消息队列
uid_t uid;
gid_t gid;
uid_t cuid;
gid_t cgid;
mode_t mode;
unsigned long seq;
}

二、操作消息队列

对消息队列的操作无非有下面三种类型:

1、 打开或创建消息队列

消息队列的内核持续性要求每个消息队列都在系统范围内对应唯一的键值,所以,要获得一个消息队列的描述字,只需提供该消息队列的键值即可;

注:消息队列描述字是由在系统范围内唯一的键值生成的,而键值可以看作对应系统内的一条路经。

2、 读写操作

消息读写操作非常简单,对开发人员来说,每个消息都类似如下的数据结构:

struct msgbuf{
long mtype;
char mtext[1];
};

mtype成员代表消息类型,从消息队列中读取消息的一个重要依据就是消息的类型;

mtext是消息内容,当然长度不一定为1。因此,对于发送消息来说,首先预置一个msgbuf缓冲区并写入消息类型和内容,调用相应的发送函数即可;对读取消息来说,首先分配这样一个msgbuf缓冲区,然后把消息读入该缓冲区即可。

3、 获得或设置消息队列属性:

消息队列的信息基本上都保存在消息队列头中,因此,可以分配一个类似于消息队列头的结构(struct msqid_ds,见附录 2),来返回消息队列的属性;同样可以设置该数据结构。

三、消息队列API

1、文件名到键值

#include <sys/types.h>
#include <sys/ipc.h>

函数原型:

key_t ftok(const char *pathname, int proj_id);

它用pathname标识的文件名和至少占8bit的非零proj_id生成一个系统V IPC键值;

该键值可以用于msgget()/ semget()/ shmget() 等函数;

返回值:

如果成功,则生成一个键值;

如果失败,则返回-1,并设置相应的errno

注意:

Under libc4 and libc5 (and under SunOS 4.x)  函数原型为:

key_t ftok(char *pathname, char proj_id);

如今,proj_id是一个int型的数值,但是仍然只占用8bits

典型的调用代码是:

key=ftok(path_ptr, 'a');
ipc_id=ipc(MSGGET, (int)key, flags,0,NULL,0);

2、linux为操作系统V进程间通信的三种方式(消息队列、信号灯、共享内存区)提供了一个统一的内核入口:ipc():

函数原型:

int ipc(unsigned int call, int first, int second, int third, void *ptr, long fifth);

参数:

1> call指明对IPC对象的操作方式,对消息队列而言共有四种操作:MSGSND、MSGRCV、MSGGET以及MSGCTL,分别代表向消息队列发送消息、从消息队列读取消息、打开或创建消息队列、控制消息队列;

2> first参数代表唯一的IPC对象;下面将介绍四种操作。

int ipc(MSGGET, int first, int second, int third, void *ptr, long fifth);

与该操作对应的系统V调用为:int msgget( (key_t)first,second)。

int ipc(MSGCTL, int first, int second, int third, void *ptr, long fifth)

与该操作对应的系统V调用为:int msgctl( first,second, (struct msqid_ds*) ptr)。

int ipc(MSGSND, int first, int second, int third, void *ptr, long fifth);

与该操作对应的系统V调用为:int msgsnd( first, (struct msgbuf*)ptr, second, third)。

int ipc(MSGRCV, int first, int second, int third, void *ptr, long fifth);

与该操作对应的系统V调用为:int msgrcv( first,(struct msgbuf*)ptr, second, fifth,third),

注:本人不主张采用系统调用ipc(),而更倾向于采用系统V或者POSIX进程间通信API。原因如下:

虽然该系统调用提供了统一的用户界面,但正是由于这个特性,它的参数几乎不能给出特定的实际意义(如以first、second来命名参数),在一定程度上造成开发不便。

正如ipc手册所说的:ipc()是linux所特有的,编写程序时应注意程序的移植性问题;

该系统调用的实现不过是把系统V IPC函数进行了封装,没有任何效率上的优势;

系统V在IPC方面的API数量不多,形式也较简洁。

3. 系统V消息队列API(重要)

系统V消息队列API共有四个,使用时需要包括几个头文件:

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>

1)打开或创建消息队列(返回队列号)

函数原型:

int msgget(key_t key, int msgflg)

该系统调用返回或创建与健值key相对应的消息队列描述字。

参数:

key是一个键值,由ftok()获得;

msgflg参数是一些标志位;可以为以下:IPC_CREAT、IPC_EXCL、IPC_NOWAIT或三者的或结果:

        1> IPC_CREAT:创建消息队列;

        2> IPC_EXCL:确保创建消息队列成功;

        3> IPC_NOWAIT:立刻返回,不阻塞;

当msglfg指定IPIC_CREAT | IPC_EXCL时,如果key对应的消息队列已经存在,则函数失败,并设置errno=EEXIST

在以下两种情况下,该调用将创建一个新的消息队列

1> 如果没有消息队列与健值key相对应,并且msgflg中包含了IPC_CREAT标志位;

2> key参数为IPC_PRIVATE(注意,该值不是flag);

返回值:

成功则返回消息队列描述字;

失败则返回-1,并设置相应的errno

注:参数key设置成常数IPC_PRIVATE并不意味着其他进程不能访问该消息队列,只意味着即将创建新的消息队列。

2)从消息队列中接收消息

函数原型:

ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);

功能:

该系统调用从msgid代表的消息队列中移除一个消息,并把消息存储在msgp指向的msgbuf结构中

参数:

msqid为消息队列描述字;

msgp指向一个存储消息的结构体,结构体格式如下:

 struct msgbuf {
               long mtype;       /* message type, must be > 0 */
               char mtext[1];    /* message data */
           };

msgsz指定msgbuf的mtext成员的长度(即消息内容的长度);

msgtyp为请求读取的消息类型,可以取值如下:

        1> msgtyp=0:读取队列的第一条消息;

        2> msgtyp>0:如果msgflg不是MSG_EXCEPT,则读取第一条msgtyp类型的消息;

                                    如果msgflg指定了MSG_EXCEPT,则读取第一条非msgtyp类型的消息;

        3> msgtyp<0:读取队列中第一条消息类型<=| msgtyp |的最小的消息;

msgflg为读消息标志,可以为以下几个常值的或:

        IPC_NOWAIT 如果没有满足条件的消息,调用立即返回,此时,errno=ENOMSG

        IPC_EXCEPT 与msgtyp>0配合使用,返回队列中第一个类型不为msgtyp的消息

        IPC_NOERROR 如果队列中满足条件的消息内容大于所请求的msgsz字节,则把该消息截断,截断部分将丢失。

注意:

如果msgflg没有指定IPC_NOWAIT,而且队列中没有相应msgtyp的消息时,会导致阻塞

msgrcv()解除阻塞的条件有三个:

        消息队列中有了满足条件的消息;

        msqid代表的消息队列被删除;

        调用msgrcv()的进程被信号中断;

返回值:

成功则返回读出消息的实际字节数;

失败则返回-1。

3)向消息队列发送消息

函数原型:

int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);

功能:

向msgid代表的消息队列发送一个消息,即将发送的消息存储在msgp指向的msgbuf结构中

结构体格式如下:

 struct msgbuf {
               long mtype;       /* message type, must be > 0 */
               char mtext[1];    /* message data */
           };

消息的大小(mtext的字符数)由msgze指定;

对发送消息来说,有意义的msgflg标志为IPC_NOWAIT,指明在消息队列没有足够空间容纳要发送的消息时,msgsnd是否等待。

注意:

如果指定的消息队列中有足够的空间,则立即返回;否则会阻塞,直到有足够的空间(除非msgflg指定了IPC_NOWAIT)

(队列的容量是由与队列相关的数据结构中的msg_qbytes字段来指定的,可以通过msgctl()来修改)

造成msgsnd()等待的条件有两种:

        1> 当前消息的大小与当前消息队列中的字节数之和超过了消息队列的总容量;

        2> 当前消息队列的消息数(单位"个")不小于消息队列的总容量(单位"字节数"),此时,虽然消息队列中的消息数目很多,但基本上都只有一个字节。

msgsnd()解除阻塞的条件有三个:

        1> 消息队列中有容纳该消息的空间;

        2> msqid代表的消息队列被删除;

        3> 调用msgsnd()的进程被信号中断;

返回值:

成功返回0,否则返回-1。

4)控制消息队列

函数原型:

int msgctl(int msqid, int cmd, struct msqid_ds *buf);

功能:

该系统调用对由msqid标识的消息队列执行cmd操作

参数:

cmd指定要执行的操作,共有三种cmd操作:IPC_STAT、IPC_SET 、IPC_RMID。

        1> IPC_STAT:该命令用来获取消息队列信息,返回的信息存贮在buf指向的msqid结构中;

        2> IPC_SET:该命令用来设置消息队列的属性,要设置的属性存储在buf指向的msqid结构中;可设置属性包括:msg_perm.uid、msg_perm.gid、msg_perm.mode以及

 msg_qbytes,同时,也影响msg_ctime成员。

        3> IPC_RMID:删除msqid标识的消息队列;

msqid_ds用来设置或返回消息队列的信息,存在于用户空间,其结构体的定义如下:

struct msqid_ds {
               struct ipc_perm msg_perm;     /* Ownership and permissions */
               time_t          msg_stime;    /* Time of last msgsnd(2) */
               time_t          msg_rtime;    /* Time of last msgrcv(2) */
               time_t          msg_ctime;    /* Time of last change */
               unsigned long   __msg_cbytes; /* Current number of bytes in queue (nonstandard) */
               msgqnum_t       msg_qnum;     /* Current number of messages  in queue */
               msglen_t        msg_qbytes;   /* Maximum number of bytes  allowed in queue */
               pid_t           msg_lspid;    /* PID of last msgsnd(2) */
               pid_t           msg_lrpid;    /* PID of last msgrcv(2) */
           };

ipc_perm结构体的定义如下:

struct ipc_perm {
               key_t          __key;       /* Key supplied to msgget(2) */
               uid_t          uid;         /* Effective UID of owner */
               gid_t          gid;         /* Effective GID of owner */
               uid_t          cuid;        /* Effective UID of creator */
               gid_t          cgid;        /* Effective GID of creator */
               unsigned short mode;        /* Permissions */
               unsigned short __seq;       /* Sequence number */
           };

返回值:

成功返回0,否则返回-1。

三、消息队列的限制

每个消息队列的容量(所能容纳的字节数)都有限制,该值因系统不同而不同。在后面的应用实例中,输出了redhat 8.0的限制,结果参见附录 3。

另一个限制是每个消息队列所能容纳的最大消息数:在redhad 8.0中,该限制是受消息队列容量制约的:消息个数要小于消息队列的容量(字节数)。

注:上述两个限制是针对每个消息队列而言的,系统对消息队列的限制还有系统范围内的最大消息队列个数,以及整个系统范围内的最大消息数。一般来说,实际开发过程中不会超过这个限制。

四、消息队列应用实例

消息队列应用相对较简单,下面实例基本上覆盖了对消息队列的所有操作,同时,程序输出结果有助于加深对前面所讲的某些规则及消息队列限制的理解。

  1. #include <sys/types.h>  
  2. #include <sys/msg.h>  
  3. #include <unistd.h>  
  4. #include <stdio.h>  
  5. #include <time.h>  
  6. #include <errno.h>  
  7.   
  8. struct msgsbuf  
  9. {  
  10.     int mtype;  
  11.     char mtext[10];  
  12. }msg_sbuf;  
  13.   
  14. struct msgmbuf  
  15. {  
  16.     int mtype;  
  17.     char mtext[10];  
  18. }msg_rbuf;  
  19.   
  20. //显示消息队列的状态  
  21. void msg_stat(int msq_fd, struct msqid_ds );  
  22. //查看错误原因  
  23. void printMsgGetErr(int err);  
  24. void printMsgSndErr(int err);  
  25. void printMsgRcvErr(int err);  
  26.   
  27. int main()  
  28. {  
  29.     int gflags,sflags, rflags;  
  30.     //与消息队列相关的键值  
  31.     key_t key;  
  32.     //消息队列号  
  33.     int msgid;  
  34.   
  35.     struct msqid_ds msg_ginfo,msg_sinfo;  
  36.     char * msgpath="mymsgqueue";  
  37.     printf("msgpath is %s\n", msgpath);  
  38.   
  39.     //生成一个系统V IPC键值  
  40.     key=ftok(msgpath, 100);  
  41.   
  42.     //通过键值来打开或创建一个消息队列(返回队列号)  
  43.     gflags=IPC_CREAT|IPC_EXCL;  
  44.     msgid=msgget(key, gflags);  
  45.   
  46.     if(msgid==-1)  
  47.     {  
  48.         if(errno==EEXIST)  
  49.          {  
  50.              printf("消息队列已经存在,获取消息队列\n");  
  51.   
  52.              gflags=IPC_NOWAIT;  
  53.              msgid=msgget(key, gflags);  
  54.   
  55.              if(msgid==-1)  
  56.              {  
  57.                  printf("获取消息队列失败\n");  
  58.                  printMsgGetErr(errno);  
  59.                  return -1;  
  60.              }  
  61.          }  
  62.         else  
  63.         {  
  64.             printf("创建消息队列失败\n");  
  65.             printMsgGetErr(errno);  
  66.             return -1;  
  67.         }  
  68.     }  
  69.   
  70.     printf("打开消息队列成功,消息队列标识为%d\n", msgid);  
  71.   
  72.     //创建一个消息队列后,输出消息队列缺省属性  
  73.     msg_stat(msgid, msg_ginfo);  
  74.   
  75.     /*发送消息*/  
  76.   
  77.     //设置发送标识,有空间则发送,否则返回错误,不等待  
  78.     sflags=IPC_NOWAIT;  
  79.     //填充消息  
  80.     msg_sbuf.mtype=10;  
  81.     msg_sbuf.mtext[0]='a';  
  82.     //成功返回0,否则返回-1  
  83.     int nRet=msgsnd(msgid,&msg_sbuf,sizeof(msg_sbuf.mtext),sflags);  
  84.     if(nRet==-1)  
  85.     {  
  86.         printf("向消息队列%d发送消息失败\n", msgid);  
  87.         printMsgSndErr(errno);  
  88.     }  
  89.     else  
  90.     {  
  91.         printf("向消息队列%d发送消息成功\n", msgid);  
  92.     }  
  93.   
  94.     //发送一个消息后,输出消息队列属性  
  95.     msg_stat(msgid,msg_ginfo);  
  96.   
  97.     /*接收消息*/  
  98.   
  99.     //设置读取标识为:不等待,并保证正确性  
  100.     rflags=IPC_NOWAIT|MSG_NOERROR;  
  101.     //读取队列中消息类型为10,消息长度为4的消息  
  102.     size_t sRecv=msgrcv(msgid,&msg_rbuf,4,10,rflags);  
  103.     if(sRecv==-1)  
  104.     {  
  105.         printf("读取消息失败\n");  
  106.         printMsgRcvErr(errno);  
  107.     }  
  108.     else  
  109.     {  
  110.         printf("从消息队列%d中读取%dBytes\n", msgid, sRecv);  
  111.     }  
  112.   
  113.     //从消息队列中读出消息后,输出消息队列属性  
  114.     msg_stat(msgid,msg_ginfo);  
  115.   
  116.     //设置消息队列的最大字节数  
  117.     msg_sinfo.msg_qbytes=16388;  
  118.     //进行设置  
  119.     nRet=msgctl(msgid, IPC_SET, &msg_sinfo);  
  120.     if(nRet==-1)  
  121.     {  
  122.         printf("消息队列设置失败\n");  
  123.         return -1;  
  124.     }  
  125.   
  126.     //输出队列消息  
  127.     msg_stat(msgid,msg_ginfo);  
  128.   
  129.     //删除消息队列  
  130.     nRet=msgctl(msgid,IPC_RMID,NULL);  
  131.     if(nRet==-1)  
  132.     {  
  133.         printf("删除消息队列失败\n");  
  134.         return -1;  
  135.     }  
  136. }  

输出信息队列状态函数如下:

  1. //输出消息队列状况  
  2. void msg_stat(int msgid,struct msqid_ds msg_info)  
  3. {  
  4.     int reval;  
  5.     sleep(1);//只是为了后面输出时间的方便  
  6.     reval=msgctl(msgid,IPC_STAT,&msg_info);  
  7.     if(reval==-1)  
  8.     {  
  9.         printf("获取消息队列信息失败\n");  
  10.         return;  
  11.     }  
  12.     printf("\n");  
  13.     printf("队列中现有的字节数为%d\n",msg_info.msg_cbytes);  
  14.     printf("队列中的消息数为%d\n",msg_info.msg_qnum);  
  15.     printf("队列中最大的字节数为%d\n",msg_info.msg_qbytes);  
  16.     //每个消息队列的容量(字节数)都有限制MSGMNB,值的大小因系统而异。在创建新的消息队列时,//msg_qbytes的缺省值就是MSGMNB  
  17.     printf("调用msgsnd的上一个进程是%d\n",msg_info.msg_lspid);  
  18.     printf("调用msgrcv的上一个进程是%d\n",msg_info.msg_lrpid);  
  19.     printf("上次msgsnd的时间是%s", ctime(&(msg_info.msg_stime)));  
  20.     printf("上次msgrcv的时间是%s", ctime(&(msg_info.msg_rtime)));  
  21. }  
输出结果:

msgpath is /linux/mymsgqueue
打开消息队列成功,消息队列标识为32768

队列中现有的字节数为0
队列中的消息数为0
队列中最大的字节数为16384
调用msgsnd的上一个进程是0
调用msgrcv的上一个进程是0
上次msgsnd的时间是Thu Jan  1 08:00:00 1970
上次msgrcv的时间是Thu Jan  1 08:00:00 1970
向消息队列32768发送消息成功


队列中现有的字节数为10
队列中的消息数为1
队列中最大的字节数为16384
调用msgsnd的上一个进程是3589
调用msgrcv的上一个进程是0
上次msgsnd的时间是Thu Mar 29 13:54:14 2012
上次msgrcv的时间是Thu Jan  1 08:00:00 1970
从消息队列32768中读取4Bytes


队列中现有的字节数为0
队列中的消息数为0
队列中最大的字节数为16384
调用msgsnd的上一个进程是3589
调用msgrcv的上一个进程是3589
上次msgsnd的时间是Thu Mar 29 13:54:14 2012
上次msgrcv的时间是Thu Mar 29 13:54:15 2012


队列中现有的字节数为0
队列中的消息数为0
队列中最大的字节数为16388
调用msgsnd的上一个进程是3589
调用msgrcv的上一个进程是3589
上次msgsnd的时间是Thu Mar 29 13:54:14 2012
上次msgrcv的时间是Thu Mar 29 13:54:15 2012

小结:

消息队列与管道以及有名管道相比,具有更大的灵活性

1> 首先,它提供有格式字节流,有利于减少开发人员的工作量;

2> 其次,消息具有类型,在实际应用中,可作为优先级使用。

这两点是管道以及有名管道所不能比的。

同样,消息队列可以在几个进程间复用,而不管这几个进程是否具有亲缘关系,这一点与有名管道很相似;但消息队列是随内核持续的,与有名管道(随进程持续)相比,生命力更强,应用空间更大。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值