Posix消息队列

  它通常用来在不同进程间发送特定格式的消息数据。POSIX消息队列随内核持续性,即使当没有任何进程打开这某个消息队列,该队列上的消息也一直存在,直到调用mq_unlink并让他的引用计数达到0以上才删除队列。

  POSIX消息队列和system V消息队列都不能向接受者准确的表示每条信息的发送者。可以通过管道传递消息描述符

  编译时要链接lrt库即:-lrt(real time 实时库)
消息队列和管道和FIFO区别

  1. 一个进程向消息队列写入消息之前,并不需要某个进程在该队列上等待该消息的到达。管道和FIFO是相反的,进程向其中写消息时,管道和FIFO必需已经打开来读,那么内核会产生SIGPIPE信号
  2. 一个进程在往某消息队列写入消息后, 终止进程. 另一个进程某时刻读出该消息;消息队列是随内核的持续性,即一个进程向消息队列写入消息后,然后终止,另外一个进程可以在以后某个时刻打开该队列读取消息。只要内核没有重新自举,消息队列没有被删除。然而对于管带或FIFO而言, 当管道或FIFO的最后一次关闭发生时,仍在管道或FIFO中的数据将被抛弃(管道和FIFO是随进程的持续性)

Posix消息队列和System V 消息队列区别

  1. 对Posix消息队列的读取总是返回最高优先级的最早消息; 对System V消息队列的读则可以返回任意指定优先级的消息
  2. 当往一个空队列放置一个消息时, Posix消息队列允许产生一个信号或者启动一个线程; System V则不提供类似机制
  3. posix消息队列的mqd_t类型的句柄可以被select/poll/epoll监听。

消息队列中的每条消息通常具有以下属性:

  1. 一个表示优先级的整数;
  2. 消息的数据部分的长度(可以为0);
  3. 消息数据本身(如果长度大于0);

Posix消息队列_POSIX消息队列  

消息队列可认为是一个链表,有足够写权限的线程可往队列中放置消息,有足够读权限的线程可从队列中读走消息,每个消息都是一个记录。在链表头记录最大消息数及每个消息的最大大小

  mqd_t类型不能是一个数组类型,不必像文件描述那样为一个整数。它是指向一个mq_info结构的指针。使用内存映射I/O实现该消息队列。

#include <bits/mqueue.h>
typedef int mqd_t;

#include <mqueue.h> 
mqd_t mq_open(const char *name, int oflag, /* mode_t mode, struct mq_attr *attr */); //成功返回消息队列描述符,失败返回-1,创建消息队列时指定attr属性——最大消息数目和每个消息的最大大小
mqd_t mq_close(mqd_t mqdes); 
mqd_t mq_unlink(const char *name); //成功返回0,失败返回-1
  1. name:表示消息队列的名字,它符合POSIX IPC的名字规则。的名字只能以一个’/’开头,名字中不能包含其他的’/’;所创建的POSIXunpipc.h" #include <mqueue.h> int main() { printf("请输入消息队列的名字:"); char name[MAXLINE]; fgets(name,MAXLINE,stdin); int len=strlen(name); if(name[len-1]=='\n') --len; int flags=O_RDWR|O_CREAT|O_EXCL; mqd_t mqd=mq_open(name,flags,FILE_MODE,NULL); if(mqd<0) if(errno==EEXIST) { mq_unlink(name); mqd=mq_open(name,O_RDWR|O_CREAT,FILE_MODE,NULL); } else { printf("open message queue error...\n"); return -1; } struct mq_attr attr; mq_getattr(mqd,&attr); char msg[MAXLINE]; int i; for(i=0;i<6;++i) { printf("请输入第 %d 个消息:",i+1); scanf("%s",msg); printf("请输入第 %d 个消息的优先级:",i+1); int prio; scanf("%d",&prio); if(mq_send(mqd,msg,sizeof(msg),prio)<0) printf("send message: %d faild.error info: %s\n",i,strerror(errno)); printf("send message: %d success.\n",i+1); } exit(0); }
//mqreceive.c
#include "unpipc.h"
#include <mqueue.h>

int main()
{
    char name[MAXLINE];
    printf("请输入接受消息的消息队列的名字:");
    fgets(name,MAXLINE,stdin);
    int len=strlen(name);
    if(name[len-1]=='\n')
        --len;

    mqd_t mqd=mq_open(name,O_RDONLY);

    struct mq_attr attr;
    mq_getattr(mqd,&attr);
    char *buffer=(char *)malloc(sizeof(char)*attr.mq_msgsize);

    int i;
    for(int i=0;i<6;++i)
    {
        int prio;
        if(mq_receive(mqd,buffer,attr.mq_msgsize,&prio)<0)
        {
            printf("receive message failed. error info: %s\n",strerror(errno));
            continue;
        }
        printf("receive message %d: %s, prio is: %d\n",i+1,buffer,prio);
    }
    mq_close(mqd);
    exit(0);
}

消息队列的限制

  POSIX消息队列本身的限制就是mq_attr中的mq_maxmsg和mq_msgsize,分别用于限定消息队列中的最大消息数和每个消息的最大字节数。在前面已经说过了,这两个参数可以在调用mq_open创建一个消息队列的时候设定。当这个设定是受到系统内核限制的。
  ulimit -a |grep message

  该大小是整个消息队列的大小,不仅仅是最大消息数*消息的最大大小;还包括消息队列的额外开销。前面我们知道Linux 2.6.18下POSIX消息队列默认的最大消息数和消息的最大大小分别为:

//在<unistd.h>中定义

mq_maxmsg = 10
mq_msgsize = 8192 
MQ_OPEN_MAX //一个进程同时能够拥有的打开的消息队列的最大数目 posix要求至少位为8
MQ_PRIO_MAX// 一个进程的最大优先值加1 posix要求他至少为32

mq_notify函数

  当队列为空时,该函数才会响应

  Posix消息队列允许异步事件通知, 以告知何时有一个消息放置到了某个空消息队列中, 以下两种方式可选:

  1. 产生一个信号
  2. 创建一个线程来执行一个指定函数
mqd_t mq_notify(mqd_t mqdes, const struct sigevent *notification);

struct sigevent
{
    int sigev_notify; //notification type SIGEV_{NONE,SIGNAL,THREAD}
    int sigev_signo; //signal number if SIGEV_SIGNAL
    union sigval   sigev_value; //signal value
    void (*sigev_notify_function)(union sigval);//passed to signal handeler or thread following two if SIGEV_THREAD
    pthread_attr_t *sigev_notify_attributes;
}

union sigval
{
    int sival_int; //integer value
    void *sival_ptr; //pointer value
}
  1. SIGEV_SIGNAL:发送由evp->sigev_sino指定的信号到调用进程,evp->sigev_value的值将被作为siginfo_t结构体中si_value的值。
  2. SIGEV_NONE:什么都不做,只提供通过timer_gettime和timer_getoverrun查询超时信息。
  3. SIGEV_THREAD:以evp->sigev_notification_attributes为线程属性创建一个线程,在新建的线程内部以evp->sigev_value为参数调用evp->sigev_notification_function。
  4. 把sigev_notify设置成SIGEV_THREAD,这会创建一个新的线程,该线程调用由sigev_notify_function指定的函数,调用的参数由sigev_value指定,新线程属性由sigev_notifyattributes指定,要是默认属性合适的话,他可以是一个空指针。
  5. SIGEV_THREAD_ID:和SIGEV_SIGNAL类似,不过它只将信号发送到线程号为evp->sigev_notify_thread_id的线程,注意:这里的线程号不一定是POSIX线程号,而是线程调用gettid返回的实际线程号,并且这个线程必须实际存在且属于当前的调用进程。
  6. 成功返回0,出错返回-1

注意:

  1. 如果mq_notify函数的notification非空, 那么当前进程希望在有一个消息到达所指定的先前为空的队列时得到通知. 即"该进程被注册为接收该队列的通知"
  2. 如果notification为空, 而且当前进程目前被注册为接收所指定队列的通知, 那么已存在的注册将被撤销
  3. 任意时刻只能有一个进程可以被注册为接收某个给定队列的通知
  4. 当有一个消息到达先前为空的消息队列,而且已有一个进程被注册为接收该队列的通知, 只有在没有任何线程阻塞在该队列的mq_receive调用中的前提下, 通知才会发出. 即在mq_receive中的阻塞比任何通知的注册都要优先
  5. 当通知被发送给注册进程时, 其注册就被撤销, 如果想的话, 需要重新注册(所以一般情况下, 都是在信号处理函数中的一开始就再次调用mq_notify进行重新注册)
  6. 每当一个信号产生时,其行为就恢复为某认行为,信号处理程序调用的第一个函数通常时signal,用于重建处理程序,那么这就提供了一个短时间的窗口,他处于该信号产生的当前进程重建信号处理程序之间,这段事件再次产生同一个信号可能终止当前进程,所以每次当前进程发通知后还需重新注册,然而消息队列不同于信号,因为在队列变空之前通知不会再发生,所以应该在从队列中读出消息之前而不是之后重新注册。

  版本1:错误,因为在信号处理函数中不能调用mq_notify.mq_receive,printf等。启动第一个mqnotify程序后再启动另外一个会报错:Device or resource busy

#include "unpipc.h"
#include "my_err.h"
#include <mqueue.h>
#include <unistd.h>
#include <fcntl.h>

mqd_t mqd;
struct sigevent sigev;
struct mq_attr attr;
void *buff;

static void sig_usr1(int signo)
{
    ssize_t n;
    mq_notify(mqd,&sigev);
    n=mq_receive(mqd,buff,attr.mq_msgsize,NULL);
    printf("ISGUSR1 receievd read:%ld\n",(long)n);
    return ;
}

int main()
{
    printf("请输入要创建消息队列名:");

    char name[MAXLINE];
    fgets(name,MAXLINE,stdin);
    int len=strlen(name);
    if(name[len-1]=='\n')
        name[len-1]='\0';

    mqd=mq_open(name,O_RDONLY);
    if(mqd<0)
        {
            perror("open message queue error...\n");
            return -1;
        }
    
    /*sigset_t newmask;
    sigemptyset(&newmask);//清空当前信号集
    sigaddset(&newmask,SIGUSR1);//将SIGUSR1加入当前信号集
    sigprocmask(SIG_BLOCK,&newmask,NULL);//将当前信号集的状态设为阻塞
*/
    signal(SIGUSR1,sig_usr1);
    sigev.sigev_notify=SIGEV_SIGNAL;
    sigev.sigev_signo=SIGUSR1;
    mqd_t t=mq_notify(mqd,&sigev);//当前进程被注册为接收队列的通知
    if(t<0)
    {
        printf("%s\n",strerror(errno));
        exit(t);
    }

    mq_getattr(mqd,&attr);
    buff=(char *)malloc(sizeof(char)*attr.mq_msgsize);
    while(1)
    {
        pause();
    }
    exit(0);
}

   版本二:让信号吹程序只设置一个全局标志,让某个线程检查该标志以确定何时收到一个消息

  注意:如果在下一个消息被读出之前有两个消息到达(可在mq_notify前加一个sleep模拟),通知只是有一个消息被放置控队列才发出,如果读出第一个消息之前有两个消息到达,那么只有一个通知发出,于是读出第一个消息并调用sigsuspend等待另个消息,它对应的通知永远不会发出,在此期间,另一个消息可能已经防止该队列中继续等待而我们一直忽略它。

#include "unpipc.h"
#include "my_err.h"
#include <mqueue.h>
#include <unistd.h>
#include <fcntl.h>

volatile sig_atomic_t flag;
static void sig_usr1(int signo)
{
    flag=1;
    return;
}

int main()
{
    printf("请输入要创建消息队列名:");

    char name[MAXLINE];
    fgets(name,MAXLINE,stdin);
    int len=strlen(name);
    if(name[len-1]=='\n')
        name[len-1]='\0';

    mqd_t mqd=mq_open(name,O_RDONLY);
    if(mqd<0)
    {
        perror(strerror(errno));
        return -1;
    }
    
    sigset_t newmask,zeromask,oldmask;
    sigemptyset(&newmask);//清空当前信号集
    sigemptyset(&zeromask);
    sigemptyset(&oldmask);
    sigaddset(&newmask,SIGUSR1);//将SIGUSR1加入当前信号集

    signal(SIGUSR1,sig_usr1);
    struct sigevent sigev;
    sigev.sigev_notify=SIGEV_SIGNAL;
    sigev.sigev_signo=SIGUSR1;
    mqd_t t=mq_notify(mqd,&sigev);//当前进程被注册为接收队列的通知
    if(t<0)
    {
        perror(strerror(errno));
        return t;
    }
    struct mq_attr attr;
    mq_getattr(mqd,&attr);
    char *buffer=(char *)malloc(sizeof(char)*attr.mq_msgsize);
    while(1)
    {
        //no signal block,
        sigprocmask(SIG_BLOCK,&newmask,&oldmask);
        while(flag==0)
            sigsuspend(&zeromask);
        flag=0;

        mq_notify(mqd,&sigev);
        ssize_t n=mq_receive(mqd,buffer,attr.mq_msgsize,NULL);
        printf("read %ld",(long)n);
        //ublock usr1
        sigprocmask(SIG_UNBLOCK,&newmask,NULL);
    }
    exit(0);
}

  版本三:未解决版本二的问题使用非阻塞模式读消息队列

#include "unpipc.h"
#include "my_err.h"
#include <mqueue.h>
#include <unistd.h>
#include <fcntl.h>

int main()
{
    printf("请输入要创建消息队列名:");

    char name[MAXLINE];
    fgets(name,MAXLINE,stdin);
    int len=strlen(name);
    if(name[len-1]=='\n')
        name[len-1]='\0';

    mqd_t mqd=mq_open(name,O_RDONLY|O_NONBLOCK);
    if(mqd<0)
        {
            perror("open message queue error...\n");
            return -1;
        }
    
    sigset_t newmask;
    sigemptyset(&newmask);//清空当前信号集
    sigaddset(&newmask,SIGUSR1);//将SIGUSR1加入当前信号集
    sigprocmask(SIG_BLOCK,&newmask,NULL);//将当前信号集的状态设为阻塞

    struct sigevent sigev;
    sigev.sigev_notify=SIGEV_SIGNAL;
    sigev.sigev_signo=SIGUSR1;
    mq_notify(mqd,&sigev);//当前进程被注册为接收队列的通知

    int signo;
    struct mq_attr attr;
    mq_getattr(mqd,&attr);
    char *buffer=(char *)malloc(sizeof(char)*attr.mq_msgsize);
    while(1)
    {
        sigwait(&newmask,&signo);//把阻塞信号从挂起信号集中删除,解除阻塞,所检测的进程必须是阻塞处理的
        if(signo==SIGUSR1)
        {
            mq_notify(mqd,&sigev);//通知被发送给注册进程时,注册即被撤销,需再次注册

            size_t n=mq_receive(mqd,buffer,attr.mq_msgsize,NULL);
            if(n>0)
                printf("receive mesage is: %s\n",buffer);
            
            if(n<0)
            {
                if(errno==EAGAIN)
                    continue;
            }
        }
    }
    exit(0);
}

  版本四: 也可创建一个管道,当接收到通知时会触发信号处理函数,在信号处理函数中向管道写一个字节数据,当管道中有数据可读时即pipe[0]可读,从管道中读出一个字节数据,然后再读取通知线程的数据。

   代码github:https://github.com/tianzengBlog/test/tree/master/ipc2/mqueue