Linux应用进程间通信三(消息队列)

Linux应用进程间通信三(消息队列)

一、消息队列(Message Queue)概述

消息队列(Message Queue,MQ)是 Linux 系统中用于进程间通信的一种机制。它允许不同进程之间以消息的形式交换信息。消息队列提供了一种异步的通信方式,使得发送方和接收方可以在不同的时间和不同的速度进行数据传输,从而避免了直接的同步操作。

消息队列具有以下特点:

  • 异步通信:发送者不需要等待接收者处理完消息才能继续执行,消息会暂时存储在队列中,直到接收者取出。
  • 先进先出(FIFO):消息在队列中是按照发送的顺序排列的,先发送的消息先被接收。
  • 可扩展性:消息队列可以处理多个发送者和多个接收者,可以适应多种进程通信需求。
  • 内核管理:消息队列的管理由操作系统内核进行,因此能够提供高效的资源分配和访问控制。

1.1、消息队列的工作原理

消息队列的基本工作原理是通过内核提供的 API,允许进程将消息写入队列(发送),并允许其他进程从队列中读取消息(接收)。消息队列作为一个内核对象,负责存储和管理这些消息。

  1. 创建消息队列:进程可以通过 msgget() 函数创建一个新的消息队列或打开已存在的队列。

  2. 发送消息:通过 msgsnd() 函数将消息写入队列。写入操作是异步的,发送进程不需要等待接收进程读取消息即可继续执行。

  3. 接收消息:接收进程通过 msgrcv() 函数从消息队列中读取消息。如果队列为空,接收操作可以选择阻塞,直到有新的消息可供接收。

  4. 删除消息队列:通过 msgctl() 函数可以删除消息队列,释放与该队列相关的系统资源。

1.2、消息队列的组成

消息队列由一系列消息组成,每条消息由两部分构成:

  • 消息类型(Message Type):用于标识消息的类型,可以是整数值。接收方可以根据类型选择性地接收消息。
  • 消息内容(Message Data):消息体部分,包含实际的数据。

1.3、消息队列的优点

  1. 异步通信:进程之间不需要直接同步,能够避免因等待而导致的阻塞。
  2. 可存储大量数据:消息队列可以存储多个消息,当队列未满时,发送者可以继续发送消息。
  3. 灵活性高:可以通过消息类型进行过滤,进程可以根据类型选择接收特定的消息。
  4. 内核管理:由操作系统内核管理消息队列,提供了系统级的安全性和资源管理。

1.4、消息队列的缺点

  1. 较为复杂的管理:需要程序员显式地管理消息的发送和接收,容易出现竞态条件。
  2. 内存消耗:系统需要为每个消息队列分配内存,如果消息队列过大或过多,可能会消耗大量系统资源。
  3. 消息丢失风险:如果队列已满,发送方的消息可能会被丢弃。虽然也有优先级队列和阻塞接收等方式来缓解这种情况,但仍然存在一定风险。

1.5、Linux 中的消息队列 API

Linux 提供了几个与消息队列相关的系统调用,最常见的包括:

  1. msgget():创建一个消息队列或打开一个已存在的消息队列。

int msgget(key_t key, int msgflg);
  • msgsnd():将消息发送到指定的消息队列。

int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
  • msgrcv():从消息队列接收一条消息。

ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);
  • msgctl():控制消息队列的属性(如删除队列、获取队列状态等)。

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

二、实现过程

2.1、消息队列

消息队列:提供一种从一个进程向另一个进程发送一个数据块的方法。与FIFO相比,消息队列的优势在于,它独立于发送和接收进程而存在。 

  1.链表式结构组织,存放于内核。

  2.通过队列标识来引用。

  3.通过一个数据类型来索引指定的数据。

每个队列都有一个msqid_ds结构与其关联“

struct msqid_ds{
    struct ipc_perm    msg_perm;
    msgqnum_t          msg_qnum;
    msglen_t           msg_qbytes;
    pid_t              msg_lspid;
    pid_t              msg_lrpid;
    ...
}

2.2、msgget函数

 #include <sys/msg.h>

int msgget(key_t key, int msgflg);
第一个参数key:每一个IPC对象与一个key对应。也可以由函数ftok生成。
第二个参数msgflg:函数的行为(0666|IPC_CREAT表示用户具有读写权限)          IPC_CREAT值,若没有该队列,则创建一个并返回新标识符;若已存在,则返回原标识符。          IPC_EXCL值,若没有该队列,则返回-1;若已存在,则返回0。
返回值: 成功:非负队列ID; 失败:-1;

作用 :创建或打开一个现有队列。

可通过ipcs -q(只查看消息队列的状态):查看系统的IPC状态

2.3、msgsnd函数

 #include<sys/msg.h>

int msgsnd(int msqid, const void *msg_ptr, size_t msg_sz, int msgflg);

第一个参数msqid是由msgget函数返回的消息队列标识符;
第二个参数msg_ptr是一个指向准备发送消息的指针,消息必须像刚才说的那样以一个长整型成员变量开始;
第三个参数msg_sz是msg_ptr指向的消息的长度。这个长度不能包括长整型消息类型成员变量的长度;
第四个参数msgfig:  0:当消息队列满时,msgsnd将会阻塞,直到消息能写进消息队列。  IPC_NOWAIT:当消息队列已满的时候,msgsnd函数不等待立即返回。  IPC_NOERROR:若发送的消息大于size字节,则把该消息截断,截断部分将被丢弃,且不通知发送进程            
返回值: 成功:0; 失败:-1;

作用:把消息添加到消息队列中。

消息的结构受两方面的约束。首先,它的长度必须小于系统规定的上限;其次,它必须以一个长整型成员变量开始。接收函数将用这个成员变量来确定消息的类型。当使用消息时,最好把消息结构定义为下面这样:

  struct my_message{
    long int message_type;   /* 必须大于0,消息类型 */
    char mtext[512];    //假如我们要发送512个字节的数据
  }

  由于在消息的接收中要用到message_type,所以你不能忽略它。你必须在声明自己的数据结构时包含它,并且最好将它初始化为一个已知值。

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

①    不满足消息队列满或个数满两个条件,即消息队列中有容纳该消息的空间。

②    msqid代表的消息队列被删除。

③    调用msgsnd函数的进程被信号中断。

2.4、msgrcv函数

 #include<sys/msg.h>

int msgrcv(int msqid, void *msg_ptr, size_t msg_sz, long int msgtype, int msgflg);
第一个参数msqid是由msgget函数返回的消息队列标识符;
第二个参数msg_ptr是一个指向准备接收消息的指针,消息必须像前面msgsnd函数中介绍的那样以一个长整型成员变量开始。
第三个参数msg_sz是msg_ptr指向的消息的长度,它不包括长整型消息类型成员变量的长度。
第四个参数msgtype是一个长整型,它可以实现一种简单形式的接收优先级。如果msgtype的值为0,就获取队列中的第一个可用消息。如果它的值大于零,将获取具有相同消息类型的第一个消息。如果它的值小于零,将获取消息类型等于或小于msgtype的绝对值的第一个消息。
这个函数看起来好像很复杂,但实际应用很简单。如果只想按照消息发送的顺序来接收它们,就把msgtype设置为0。如果只想获取某一特定类型的消息,就把msgtype设置为相应的类型值。如果想接收类型等于或小于n的消息,就把msgtype设置为-n。
第五个参数msgflg:  0: 阻塞式接收消息,没有该类型的消息msgrcv函数一直阻塞等待。  IPC_NOWAIT:如果没有返回条件的消息调用立即返回,此时错误码为ENOMSG。  IPC_EXCEPT:与msgtype配合使用返回队列中第一个类型不为msgtype的消息。  IPC_NOERROR:如果队列中满足条件的消息内容大于所请求的size字节,则把该消息截断,截断部成功:实际读取到的消息数据长度。 出错:-1,错误原因存于error中。

作用:从队列中取用消息。

  其中对第四个参数举例说明:消息队列中要求通过一个数据类型来索引指定的数据。也就是在msgsng发送数据前会对long int message_type变量进行复制。通过这个变量来标识数据。而且这个变量必须要大于0.

  那么在msgrcv中,对第四个参数msgtype进行选值时。

  如果等于0,则按照队列的顺序去取,先到先取的原则。

  如果大于0,假如msgtype=1。就会取找队列中message_type等于1的队列的排在第一个的消息。

  如果小于0,假如msgtype=-2。就会-2取绝对值,也就是等于2。再取队列中message_type等于或者小于2的排在第一个的消息。

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

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

②    msqid代表的消息队列被删除。

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

2.5、msgctl函数

 #include<sys/msg.h>

int msgctl(int msqid, int command, struct msqid_ds *buf);
第一个参数msqid是由msgget返回的消息队列标识符;
第二个参数command是将要采取的动作,它可以取3个值。
IPC_STAT  取出队列的msqid_ds结构,并将它存放在buf指向的结构中。   
IPC_SET   如果进程有足够的权限,将buf指向的结构复制到与这个队列相关的msqid_ds结构中。
IPC_RMID  从系统中删除该消息队列以及仍在该队列中的所有数据。这种删除立即生效。仍在使用这一消息队列的其他进程在它们下一次试图对此队列操作,将得到EIDRM错误。

返回值: 成功:0; 失败:-1

2.6、实例

send.c

 /*send.c*/  
#include <stdio.h>   
#include <sys/types.h>   
#include <sys/ipc.h>   
#include <sys/msg.h>   
#include <errno.h>   

#define MSGKEY 1024   
  
struct msgstru  
{  
   long msgtype;  
   char msgtext[2048];   
};  
  
main()  
{  
  struct msgstru msgs;  
  int msg_type;  
  char str[256];  
  int ret_value;  
  int msqid;  
  
  msqid=msgget(MSGKEY,IPC_EXCL);  /*检查消息队列是否存在*/  
  if(msqid < 0){  
    msqid = msgget(MSGKEY,IPC_CREAT|0666);/*创建消息队列*/  
    if(msqid <0){  
    printf("failed to create msq | errno=%d [%s]\n",errno,strerror(errno));  
    exit(-1);  
    }  
  }   
   
  while (1){  
    printf("input message type(end:0):");  
    scanf("%d",&msg_type);  
    if (msg_type == 0)                   //为什么msg_type=0要直接break?因为规定msg_type必须要大于0
       break;  
    printf("input message to be sent:");  
    scanf ("%s",str);  
    msgs.msgtype = msg_type;  
    strcpy(msgs.msgtext, str);  
    /* 发送消息队列 */  
    ret_value = msgsnd(msqid,&msgs,sizeof(struct msgstru),IPC_NOWAIT);     //疑问:sizeof(struct msgsrtu)包括了长整型的长度,不是说不包括吗
    if ( ret_value < 0 ) {  
       printf("msgsnd() write msg failed,errno=%d[%s]\n",errno,strerror(errno));  
       exit(-1);  
    }  
  }  
  msgctl(msqid,IPC_RMID,0); //删除消息队列   
}
 /*receive.c */  
#include <stdio.h>   
#include <sys/types.h>   
#include <sys/ipc.h>   
#include <sys/msg.h>   
#include <errno.h>   
  
#define MSGKEY 1024   
  
struct msgstru  
{  
   long msgtype;  
   char msgtext[2048];  
};  
  
/*子进程,监听消息队列*/  
void childproc(){  
  struct msgstru msgs;  
  int msgid,ret_value;  
  char str[512];  
    
  while(1){  
     msgid = msgget(MSGKEY,IPC_EXCL );/*检查消息队列是否存在 */  
     if(msgid < 0){  
        printf("msq not existed! errno=%d [%s]\n",errno,strerror(errno));  
        sleep(2);  
        continue;  
     }  
     /*接收消息队列*/  
     ret_value = msgrcv(msgid,&msgs,sizeof(struct msgstru),0,0);  
     printf("text=[%s] pid=[%d]\n",msgs.msgtext,getpid());  
  }  
  return;  
}  
  
void main()  
{  
  int i,cpid;  
  
  /* create 5 child process */  
  for (i=0;i<5;i++){  
     cpid = fork();  
     if (cpid < 0)  
        printf("fork failed\n");  
     else if (cpid ==0) /*child process*/  
        childproc();  
  }  
}

 注意,消息队列是由发送端也就是写端创建的。(为什么要由写端创建?)

  其中msgrcv和msgsng中sizeof(struct msgstru)是错误的,应该是sizeof(struct msgstru) - sizeof(long)。

2.7、msgctl.c实例

msgctl.c源代码如下:

 #include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <error.h>

#define TEXT_SIZE  512
struct msgbuf
{
    long mtype;
    char mtext[TEXT_SIZE];
};

int main(int argc, char **argv)
{
    int msqid ;
    struct msqid_ds info ;
    struct msgbuf buf ;
    struct msgbuf buf1 ;
    int flag ;
    int sendlength, recvlength ;
 
    msqid = msgget( IPC_PRIVATE, 0666 ) ;
    if ( msqid < 0 )
    {
        perror("get ipc_id error") ;
        return -1 ;
    }
 
    buf.mtype = 1 ;
    strcpy(buf.mtext, "happy new year!") ;
    sendlength = sizeof(struct msgbuf) - sizeof(long) ;
    flag = msgsnd( msqid, &buf, sendlength , 0 ) ;
    if ( flag < 0 )
    {
        perror("send message error") ;
        return -1 ;
    }
    buf.mtype = 3 ;
    strcpy(buf.mtext, "good bye!") ;
    sendlength = sizeof(struct msgbuf) - sizeof(long) ;
    flag = msgsnd( msqid, &buf, sendlength , 0 ) ;
    if ( flag < 0 )
    {
        perror("send message error") ;
        return -1 ;
    }
 
    flag = msgctl( msqid, IPC_STAT, &info ) ;
    if ( flag < 0 )
    {
        perror("get message status error") ;
        return -1 ;
    }
    printf("uid:%d, gid = %d, cuid = %d, cgid= %d\n" ,
           info.msg_perm.uid,  info.msg_perm.gid,  info.msg_perm.cuid,  info.msg_perm.cgid  ) ;
    printf("read-write:%03o, cbytes = %lu, qnum = %lu, qbytes= %lu\n" ,
           info.msg_perm.mode&0777, info.msg_cbytes, info.msg_qnum, info.msg_qbytes ) ;
    system("ipcs -q") ;
    recvlength = sizeof(struct msgbuf) - sizeof(long) ;
    memset(&buf1, 0x00, sizeof(struct msgbuf)) ;
 
    flag = msgrcv( msqid, &buf1, recvlength ,3,0 ) ;
    if ( flag < 0 )
    {
        perror("recv message error") ;
        return -1 ;
    }
    printf("type=%ld, message=%s\n", buf1.mtype, buf1.mtext) ;
 
    flag = msgctl( msqid, IPC_RMID,NULL) ;
    if ( flag < 0 )
    {
        perror("rm message queue error") ;
        return -1 ;
    }
    system("ipcs -q") ;
 
   return 0 ;
}

执行 ./msg,执行结果如下:

uid:1008, gid = 1003, cuid = 1008, cgid= 1003

read-write:666, cbytes = 1024, qnum = 2, qbytes= 163840

------ Message Queues --------

key                 msqid      owner      perms      used-bytes   messages   

0x00000000   65536      zjkf          666          1024            2      

type=3, message=good bye!



------ Message Queues --------

key        msqid      owner      perms      used-bytes   messages

2.8、两进程通过消息队列收发消息

(1)发送消息队列程序

msgsnd.c源代码如下:

 #include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <time.h>

#define TEXT_SIZE  512
struct msgbuf
{
    long mtype ;
    int  status ; 
    char time[20] ;
    char mtext[TEXT_SIZE] ;
}  ;
char  *getxtsj()
{  
    time_t  tv ;
    struct  tm   *tmp ;
    static  char  buf[20] ;
    tv = time( 0 ) ;
    tmp = localtime(&tv) ;
    sprintf(buf,"%02d:%02d:%02d",tmp->tm_hour , tmp->tm_min,tmp->tm_sec);
    return   buf ;
}

int main(int argc, char **argv)
{
    int msqid ;
    struct msqid_ds info ;
    struct msgbuf buf ;
    struct msgbuf buf1 ;
    int flag ;
    int sendlength, recvlength ;
    int key ;
 
    key = ftok("msg.txt", 0x01 ) ;
    if ( key < 0 )
    {
        perror("ftok key error") ;
        return -1 ;
    }
 
    msqid = msgget( key, 0600|IPC_CREAT ) ;
    if ( msqid < 0 )
    {
        perror("create message queue error") ;
        return -1 ;
    }
 
    buf.mtype = 1 ;
    buf.status = 9 ; 
    strcpy(buf.time, getxtsj()) ;
    strcpy(buf.mtext, "happy new year!") ;
    sendlength = sizeof(struct msgbuf) - sizeof(long) ;
    flag = msgsnd( msqid, &buf, sendlength , 0 ) ;
    if ( flag < 0 )
    {
        perror("send message error") ;
        return -1 ;
    }
    buf.mtype = 3 ;
    buf.status = 9 ; 
    strcpy(buf.time, getxtsj()) ;
    strcpy(buf.mtext, "good bye!") ;
    sendlength = sizeof(struct msgbuf) - sizeof(long) ;
    flag = msgsnd( msqid, &buf, sendlength , 0 ) ;
    if ( flag < 0 )
    {
        perror("send message error") ;
        return -1 ;
    }
    system("ipcs -q") ;
   return 0 ;
}

(2)接收消息队列程序

msgrcv.c源代码如下:

 #include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>

#define TEXT_SIZE  512
struct msgbuf
{
    long mtype ;
    int  status ; 
    char time[20] ;
    char mtext[TEXT_SIZE] ;
};
int main(int argc, char **argv)
{
    int msqid ;
    struct msqid_ds info ;
    struct msgbuf buf1 ;
    int flag ;
    int  recvlength ;
    int key ;
    int mtype ;
 
    key = ftok("msg.txt", 0x01 ) ;
    if ( key < 0 )
    {
        perror("ftok key error") ;
        return -1 ;
    }
 
    msqid = msgget( key, 0 ) ;
    if ( msqid < 0 )
    {
        perror("get ipc_id error") ;
        return -1 ;
    }
 
    recvlength = sizeof(struct msgbuf) - sizeof(long) ;
    memset(&buf1, 0x00, sizeof(struct msgbuf)) ;
    mtype = 1 ;
    flag = msgrcv( msqid, &buf1, recvlength ,mtype,0 ) ;
    if ( flag < 0 )
    {
        perror("recv message error") ;
        return -1 ;
    }
    printf("type=%ld,time=%s, message=%s\n", buf1.mtype, buf1.time,  buf1.mtext) ;
    system("ipcs -q") ;
    return 0 ;
}

(3)编译与执行程序

①    在当前目录下利用>msg.tmp建立空文件msg.tmp。

②    编译发送消息队列程序 gcc msgsnd.c -o  msgsnd。

③    执行./msgsnd,执行结果如下:

  ----- Message Queues --------

key                  msqid        owner      perms      used-bytes   messages   

0x0101436d    294912      zjkf          600          1072            2          

④    编译接收消息程序 gcc msgrcv.c -o msgrcv

⑤    执行./msgrcv,执行结果如下:

 type=1,time=03:23:16, message=happy new year!



------ Message Queues --------

key                  msqid        owner      perms      used-bytes   messages   

0x0101436d    294912     zjkf           600          536              1         

⑥    利用ipcrm -q 294912删除该消息队列。因为消息队列是随内核持续存在的,在程序中若不利用msgctl函数或在命令行用ipcrm命令显式地删除,该消息队列就一直存在于系统中。另外信号量和共享内存也是随内核持续存在的。

三、测试:两个进程都创建消息队列会发生什么?

在 Linux 中,两个进程如果都调用 msgget() 来创建消息队列,会根据传递的键(key)和标志(msgflg)的不同情况,发生以下几种情况:

1. 相同的键(key)和 IPC_CREAT 标志

如果两个进程都使用相同的 key 值,并且都传递了 IPC_CREAT 标志,这时操作系统会创建一个消息队列并返回同一个队列的标识符(msqid)。即使两个进程在不同时间调用 msgget(),它们将得到相同的消息队列 ID,并可以在这个队列上进行通信。

  • 进程 1:通过 msgget(key, IPC_CREAT | 0666) 创建消息队列,获得队列 ID msqid1
  • 进程 2:通过 msgget(key, IPC_CREAT | 0666) 创建消息队列,获得相同的队列 ID msqid2

结果

  • 两个进程将操作同一个消息队列,它们可以共享该队列中的消息,进行进程间通信。
  • 消息队列的创建和管理由内核负责,多个进程可以对该队列进行读写操作。

2. 相同的键(key)和不同的标志

如果两个进程使用相同的键值(key),但在标志(msgflg)上有所不同,结果将取决于它们的具体标志设置。

- IPC_CREATIPC_EXCL 标志

假设进程 1 使用 msgget(key, IPC_CREAT | IPC_EXCL | 0666) 来创建消息队列,而进程 2 使用 msgget(key, IPC_CREAT | 0666) 来尝试创建队列:

  • 进程 1 会创建消息队列,并且如果队列已经存在(即有其他进程已经创建),则返回 -1,并且设置 errnoEEXIST(表示队列已存在)。
  • 进程 2 在进程 1 已经创建消息队列后,调用 msgget(key, IPC_CREAT | 0666) 时,会成功返回相同的队列 ID(即队列已经存在)。这时,进程 2 获取的是现有队列的 ID,而不是新创建的队列。

- IPC_CREATIPC_EXCL 标志

如果两个进程都使用 IPC_CREAT 标志但不使用 IPC_EXCL 标志,则这两个进程都会创建同一个消息队列,进程 2 会得到相同的队列 ID。

3. 不同的键(key

如果两个进程使用不同的 key 值,系统会创建两个不同的消息队列,分别对应不同的队列 ID。每个进程会获得它们各自的队列 ID,且它们操作的是完全独立的消息队列。

  • 进程 1:通过 msgget(key1, IPC_CREAT | 0666) 创建消息队列,获得队列 ID msqid1
  • 进程 2:通过 msgget(key2, IPC_CREAT | 0666) 创建消息队列,获得队列 ID msqid2

结果

  • 每个进程操作独立的消息队列,消息不共享。

总结

  1. 相同的 keyIPC_CREAT 标志:多个进程可以获得相同的消息队列 ID,并共享该队列。
  2. 不同的标志(如 IPC_EXCL):会导致创建行为不同,但如果队列已存在,进程 2 可能会获取现有队列的 ID。
  3. 不同的 key:不同的队列会被创建,进程间无法共享消息。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值