消息队列

        消息队列是进程间通信的另一种方式。一个进程通过系统调用接口创建一个消息队列。操作系统会在内核中开辟一片内存用于进程间通信。而这片内存现在又以消息队列的形式来进行通信的。

        消息队列提供了一个进程向另一个进程发送数据块的方法来进行通信。每个数据块都有一个类型,接收进程根据类型来接受自己想要的数据块,且消息的读取不一定是先入先出的,它是由类型决定的。

        可以通过发送消息(数据块)来避免命名管道的同步与阻塞问题。消息队列与管道的不同是,消息队列是基于消息的,而管道是基于字节流的。但是消息队列也有与管道一样的不足,即没个消息的长度是有上限的,消息队列中总的字节数是有上限的,系统上消息队列的总数也是有上限的。

        下面通过一些系统调用接口来使用消息队列进行进程间通信:

1. 首先操作系统会在内核中开辟一片区域用于进程间通信:

key_t ftok(const char* pathname,int proj_id);//头文件:<sys/types.h>和<sys/ipc.h>

参数:这两个参数由用户自己设定,如参数一:"./jakqwjh",参数二:0x8888

返回值:成功返回这片区域的标识符key值。失败返回-1。

        只要这两个参数相同,得到的key值便相同。故两个进程可以通过相同的参数来获取统一key值,进而看到同一片区域。

        操作系统会给该区域(IPC对象)维护一个数据结构:

struct ipc_perm {
key_t __key; /* Key supplied to xxxget(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 */
};

        其中主要的就是key值和权限值mode。

2. 创建和访问消息队列

        两个进程通过上述的key值可以看到同一片区域。但该区域要被当做消息队列来使用,所以还需要创建消息队列。

int msgget(key_t key,int msgflg);//头文件:<sys/types.h>,<sys/ipc.h>,<sys/msg.h>

        该函数的功能是创建和访问一个消息队列。

参数:

        key:表示上述ftok函数返回的key值

        msgflg:如果是创建消息队列,该参数为:IPC_CREAT | IPC_EXCL | 权限值(如0666)

                      如果是访问消息队列,该参数是:IPC_CREAT

返回值:

    如果是创建消息队列,若消息队列已经存在,则出错返回-1;若不存在,则创建后成功返回该消息队列标识符(非负整数)。

    如果是访问消息队列,若消息队列存在,则成功返回该消息队列的标识符(非负整数),若不存在,则创建后返回。访问失败返回-1。

        消息队列创建好后,系统也会为该消息队列维护一个数据结构:

消息队列结构/usr/include/linux/msg.h
struct msqid_ds {
struct ipc_perm msg_perm;//即上面IPC对象的数据结构
struct msg *msg_first; /* first message on queue,unused */
struct msg *msg_last; /* last message in queue,unused */
__kernel_time_t msg_stime; /* last msgsnd time */
__kernel_time_t msg_rtime; /* last msgrcv time */
__kernel_time_t msg_ctime; /* last change time */
unsigned long msg_lcbytes; /* Reuse junk fields for 32 bit */
unsigned long msg_lqbytes; /* ditto */
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 */
};

        所以,通过msgget函数可以一个进程去创建一个消息队列,另一个进程去访问已创建的这个消息队列,这两个进程就可以通过该消息队列进行通信啦。

3. 消息队列的控制函数

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

参数:

        msqid:上述由msgget函数返回的消息队列标识符

        cmd:表示对该消息队列作何操作,它共有三个取值:

                    IPC_STAT:把参数3结构中的数据设置为消息队列的当前关联值

                    IPC_SET:在进城有足够权限的前提下,把消息队列的当前关联值设置为参数3结构中给出的值

                    IPC_RMD:删除消息队列,此时参数3为NULL

返回值:成功返回0,失败返回-1。

4. 向消息队列中添加消息

        上面有说过,消息是以数据块的形式添加到消息队列中的。该数据块不仅有大小还有类型,所以操作系统对消息维护了一个结构体:

struct msgbuf
{
    long mtype;//消息的类型
    char mtext[1];//消息的长度,实际是一柔性数组,在实际使用时有用户自主指定大小
}

注意:

        上述指定的消息大小必须小于系统规定的上限值。

        消息结构中必须包含一个long类型的数据,以确定消息的类型

        接下来就可以发送消息了:

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

参数:

        msgid:要添加消息的消息队列标识符

        msgp:指向要发送的消息,即上述的消息结构体

        msgsz:msgp指向的消息长度,不包含消息结构体中的long int长整型

        msgflg:控制着当前消息队列满或到达系统上显示要发生的事情

                      msgflg = IPC_NOWAIT表示队列满不等待,出错返回

                      msgflg = 0表示阻塞等待

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

5. 从消息队列中接收数据

ssize_t msgrcv(int msgid,void *msgp,size_t size,long msgtyp,int msgflg);

参数:

        msgid:要接收消息来自的消息队列标识符

        msgp:指向准备接收的消息,即指向消息结构体

        size:由msgp指向的消息长度,不包含long int长整型

        msgtyp:指定要接收消息的类型

                       msgtyp=0表示返回消息队列中的第一条消息

                       msgtyp>0表示返回队列中第一条类型等于msgtyp的消息

                       msgtyp<0表示返回队列中小于等于msgtyp绝对值的消息,且是满足条件的消息类型最小的消息

        msgflg:消息队列中没有要接收的消息时要发生的事情。

                       msgflg=IPC_NOWAIT表示没有可读时不等待,出错返回

                       msgflg=MSG_NOERROR表示消息大小超过msgsz时被截断

        msgtyp>0且msgflg=MSG_EXCEPT表示接收类型不等于msgtyp的第一条消息

返回值:成功返回实际接受到的消息字符个数,失败返回-1.

        下面,通过上述函数来通过消息队列实现进程间通信:

首先,将消息队列的相关操作函数进行封装:

头文件:

#pragma once                                                                                                                          

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

//异常退出宏函数
#define ERR_EXIT(m)\
    do\
    {\
        perror(m);\
        exit(EXIT_FAILURE);\
    }while(0)
//ftok函数中的两个参数
#define PATH_NAME "/"
#define PROJ_ID 0x6666
//两个进程发送的消息类型
#define SERVER_TYPE 1
#define CLIENT_TYPE 2
//消息的结构体
struct msgbuf
{
    long int mtype;
    char mtext[1024];
};
//消息队列的操作函数
int CreateMsgQueue();
int GetMsgQueue();
int SendMsgQueue(int msgid,int who,char* msg);int RcvMsgQueue(int msgid,int who,char out[]);
int DestoryMsgQueue(int msgid);

封装对消息队列操作的方法:

#include "comm.h"
//创建或获得消息队列
static int CommMsgQueue(int flags)                                                                                                    
{
    key_t key = ftok(PATH_NAME,PROJ_ID);//获得key值
    if(key < 0)//key值获取失败
    {
        ERR_EXIT("ftok error"); 
    }
    int msgid = msgget(key,flags);//创建或获取消息队列
    if(msgid < 0)//消息队列创建或获取失败
    {
        ERR_EXIT("msgget error");
    }
    return msgid;//返回创建或获取的消息队列标识符
}

//创建消息队列
int CreateMsgQueue()
{
    return CommMsgQueue(IPC_CREAT|IPC_EXCL|0666);//返回创建的消息队列的标识符
}   
    
//获取消息队列
int GetMsgQueue()
{
    return CommMsgQueue(IPC_CREAT);//返回获取的消息队列的标识符
}
//销毁消息队列
int DestoryMsgQueue(int msgid)
{
    if(msgctl(msgid,IPC_RMID,NULL) < 0)
    {
        perror("msgctl error");
        return -1;
    }
    return 0;
}

//向消息队列中添加数据
int SendMsgQueue(int msgid,int who,char* msg)
{
    struct msgbuf buf;//定义数据块结构体
    buf.mtype = who;//给结构体赋值                                                                                                    
    strcpy(buf.mtext,msg);
    if(msgsnd(msgid,(void*)&buf,sizeof(buf.mtext),0) < 0)//向消息队列中添加数据块
    {
        perror("msgsed error");//添加失败
        return -1;
    }
    return 0;
}
//在消息队列中接受数据
int RcvMsgQueue(int msgid,int who,char out[])
{
    struct msgbuf buf;
    if(msgrcv(msgid,(void*)&buf,sizeof(buf.mtext),who,0) < 0)
    {
        perror("msgrcv error");
        return -1;
    }
    strcpy(out,buf.mtext);
    return 0;
}

在下列程序中创建消息队列用于接收和发送消息:server.c

#include "comm.h"                                                                                                                     
int main()
{
    int msgid = CreateMsgQueue();//创建消息队列
    char buf[1024] = {0};
    while(1)
    {   
        RcvMsgQueue(msgid,CLIENT_TYPE,buf);//接受client发送的消息
        printf("client say#%s\n",buf);
    
        //发送消息
        printf("please enter#");
        fflush(stdout);
        ssize_t s = read(0,buf,sizeof(buf));//从键盘中读取信息保存到buf中
        if(s > 0)
        {
            buf[s - 1] = 0;
            SendMsgQueue(msgid,SERVER_TYPE,buf);//向消息队列中发送消息
            printf("send done\n");
        }
    }   
    DestoryMsgQueue(msgid);//销毁消息队列
    return 0;
}

下列程序中获取上述已创建的消息队列用于发送和接收消息:client.c

#include"comm.h"                                                                                                                      
int main()
{
    //获得消息队列
    int msgid = GetMsgQueue();
    char buf[1024] = {0};
    while(1)
    {   
        printf("please enter#");
        fflush(stdout);
        ssize_t s = read(0,buf,sizeof(buf));//先从键盘上获取消息
        if(s > 0)
        {
            buf[s-1] = 0;
            SendMsgQueue(msgid,CLIENT_TYPE,buf);//向消息队列中写入内容
            printf("client send done\n");
        }    
        RcvMsgQueue(msgid,SERVER_TYPE,buf);//从消息队列中获取server发送的消息
        printf("server say#%s\n",buf);
    }   
    return 0;
}

在一个终端运行进程server.c,另一终端运行进程client.c,结果显示如下:

[admin@localhost MsgQueue]$ ./server 
client say#ksjbcjajbbc
please enter#ascbassc 
send done
client say#scscsc
please enter#^C
[admin@localhost MsgQueue]$ ./client 
please enter#ksjbcjajbbc
client send done
server say#ascbassc 
please enter#scscsc
client send done
^C

再次运行server时,出现如下信息:

[admin@localhost MsgQueue]$ ./server 
msgget error: File exists

        这是因为消息队列的生命周期随内核,而不随进程,进程被异常终止时,并没有执行到上述的销毁消息队列函数:

DestoryMsgQueue(msgid);//销毁消息队列

所以,该消息队列还存在,因此在运行程序,执行到创建消息队列时,会出错返回。

        因此要进行认为删除消息队列,输入以下命令:

[admin@localhost MsgQueue]$ ipcs -q    //查看系统内已存在的消息队列

------ Message Queues --------
key        msqid      owner      perms      used-bytes   messages    
0x66020002 294912     admin      666        0            0           

[admin@localhost MsgQueue]$ ipcrm -q 294912  //删除指定的消息队列,294912为要删除消息队列的标识符msqid

        再次运行时,便可正常运行。