• POSIX与System v消息队列


    消息队列可认为是个消息链表,有足够写权限的线程可往队列放置信息,有足够读权限的线程可从队列中取走消息。每个消息都是个记录。由发送者赋予一个优先级,在某个进程往一个队列写入消息之前,并不需要另外某个进程在该队列上等待消息的达到。

    一、System v消息队列

    1、概述

    1. 消息队列是内核地址空间中的内部链表,通过 Linux 内核在各个进程之间传递内容。

    2. 消息顺序地发送到消息队列中,并以几种不同的方式从队列中获取,每个消息队列可以用lPC标识符唯一 地进行标识。

    3. 内核中的消息队列是通过IPC 的标识符来区别的,不同的消息队列之间是相对独立的。每个消息队列中的消息,又构成一 个独立的链表。

    1.信息缓冲区结构

    常用的结构msgbuf结构。可以使用下面的结构为模板定义自己的信息结构。在头文件中,定义如下:

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

    在结构msgbuf中有以下两个成员:

    1. mtype: 消息类型,以正数来表示。用户可以给某个消息设定一 个类型,可以在消息队列中正确地发送和接收自己的消息。例如,在 socket编程过程中,一 个服务器可以接受多个客户端的连接,可以为每个客户端设定一个消息类型,服务器和客户端之间的通信可以通过此消息类型来发送和接收消息,并且多个客户端之间通过消息类型来区分。

    2. mtext: 消息数据

    消息数据的类型为char,长度为1。在构建自己的消息结构时,这个域并不一定要设为char或者长度为1。可以根据实际的情况进行设定,这个域能存放任意形式的任意数据,应用程序编程人员可以重新定义msgbuf结构:

    struct msgmbuf{
       
    		long mtype;
    		char mtext[10];
    		long length;
    	};
    

    消息总的大小不能超过8192个字节,这其中包括mtype成员,长度是4个字节(long类型)。

    2.结构msgid_ds

    内核msgid_ds结构IPC对象分为3类,每一类都有一个内部数据结构,该数据结构是由内核维护的。对于消息队列而言,它的内部数据结构是 msgid_ds结构。对于系统上创建的每个消息队列,内核均为其创建、存储和维护该结构的一 个实例。该结构在 Linux/msg.h中定义,如下所示。

    struct msgid_ds{
       
    			struct ipc_perm msg_perm;		 
    			time_t		msg_stime;	//发送到队列的最后 一个消息的时间戳		 
    			time_t		msg_rtime;		//从队列中获取的最后 一个消息的时间戳	
    			time_t		msg_ctime;		//对队列进行最后一次变动的时间戳	
    			unsigned long	__msg_cbytes; //在队列上所驻留的字节总数	
    			msggnum_t		msg_qnum;	//当前处于队列中的消息数目	
    			msglen_t		msg_qbytes;		//队列中能容纳的字节的最大数目
    			pid_t			msg_lspid;   //发送最后一个消息进程的PIO
    			pid_t			msg_lrpid; //接收最后一个消息进程的PIO
    };
    

    下面对每个成员介绍一下:

    1. msg_perm: 它是 ipc_perm 结构的一 个实例,ipc_perm 结构是在Linux/ipc.h 中定义的。用于存放消息队列的许可权限信息,其中包括访问许可信息,以及队列创建者的有关信息(如 uid 等)。
    2. msg_stime: 发送到队列的最后一个消息的时间戳 (time_t)
    3. msg_rtime: 从队列中获取最后一个消息的时间戳
    4. msg_ctime: 对队列进行最后一 次变动的时间戳
    5. msg_ cbytes: 在队列上所驻留的字节总数(即所有消息的大小的总和)
    6. msg_qnum: 当前处千队列中的消息数目
    7. msg_qbytes: 队列中能容纳的字节的最大数目
    8. msg_lspid: 发送最后一个消息进程的PID
    9. msg_lrpid: 接收最后一个消息进程的PID

    3.结构ipc_perm

    内核把IPC对象的许可权限信息存放在ipc_perm类型的结构中。例如在前面描述的某个消息队列的内部结构中,msg_perm 成员就是ipc_perm 类型的,它的定义是在文件 中,如下所示。

    struct ipc_perm{
       
    		key_t key;//函数msgget()使用的键值
    		uid_t uid;//用户UD
    		gid_t gid;//用户GID
    		uid_t cuid;//建立者的UID
    		gid_t cgid;//建立者的GID
    		unsigned short mode;//权限
    		unsigned short seq;//序列号
    };
    

    这个结构主要是一些底层的东西,:

    1. key:key用于区分消息队列。
    2. uid:uid消息队列用户的ID号。
    3. gid:消息队列用户组的ID。
    4. cuid:消息队列创建的ID号。
    5. cgid:消息队列创建的组ID号。
    6. mode:权限,用户控制读写,例如0666,可以对消息进行读写操作。
    7. seq:序列号。

    4.内核中的消息队列关系

    作为IPC的消息队列,其消息的传递是通过Linux内核来进行的。如下图所示的结构成员与用户的表述基本一致。在消息的发送和接收的时候,内核通过一个比较巧妙的设置来实现消息插入队列的动作和消息中查消息的算法。
    在这里插入图片描述
    结构list_head形成一个链表,而结构msg_msg之中的m_list成员是一个struct list_head类型的变量,通过此变量消息形成了一个链表,在查找和插入时,对m_list域进行偏移操作就可以找到对应的信息体位置。内核中的代码在头文件中,主要的实现是插入信息和取出信息的操作。

    2.ftok()函数

    ftok()函数将路径和项目的表示法转变为一个系统V地IPC键值。其原型如下:

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

    其中pathname必须是已经存在的目录,而proj_id则是一个8位的值。通常用a,b等表示。例如建立如下目录:

    mkdir -p /ipc/msg/
    

    使用如下生成一个键值:

    ...
    key_t key;
    char *msgpath = "/ipc/msg/";
    key = ftok(msgpath,'a');
    if(key != -1)
    {
       
    	printf("成功建立KEY\n");
    	}
    	else
    	{
       
    	printf("建立KEY失败\n");
    	}
    	...
    

    3、msgget()函数

    创建一个新的消息队列,或者访问一个现有的队列,可以使用函数msgget(),原型如下:

    #include
    #include
    #include
    int msgget(key_t key,int msgflg);
    

    msgget()函数的第一个参数是键值,可以用ftok()函数生成,这个关键字将和内核中其他消息队列的现有关键字比较。比较后,打开或者访问操作依赖于msgflg参数内容:

    1. IPC_CREAT:如果在内核中不存在该队列,则创建它。
    2. IPC_EXCL:当于IPC_CREAT一起使用,如果队列早已存在则将出错。

    只使用IPC_CREAT,msgget()函数或者返回新创建消息队列的消息队列标识符,或者返回现有的具有同一个关键字值的队列的标识符。
    同时使用了IPC_EXCL和IPC_CREAT,会出现两个结果:创建一个新的队列,如果该队列存在,则调用将出错,并返回-1。IPC_EXCL本身是没有声明用处的,但在IPC_CREAT组合使用时,可以保证没有一个现存的队列为了访问而被打开。例如:下面创建一个消息队列:

    ...
    key key;
    int msg_flags,msg_id;
    msg_flags = IPC_CREAT | IPC_EXCL;//消息的标志为了建立、可执行
    msg_id = msgget(key,msg_flags(0x0666);//建立消息
    if( -1 == msg_id)
    {
       
    	printf("消息队列建立失败\n");
    	}
    	...
    
    4.msgsnd()函数

    一旦获得队列标识符,用户就可以开始在该消息队列上执行相关操作,为了向队列传递消息,可以使用msgsnd()函数:

    #incldue<sys/types.h>
    #include
    #incldue<sys/msg.h>
    
    int msgsnf(int msqid,const void *msgp,size_t msgsz,int msgflg);
    

    第一个参数是队列标识符,是前面调用msgget()获得的返回值。第二个参数msgp,是一个void类型指针,指向一个消息缓冲区。msgsz参数则包含着消息的大小。以字节为单位,其中不包括消息类型的长度(4个字节长)。

    msgflg参数可以设置为0(表示忽略),也可以设置为IPC_NOWAIT。如果消息队列已满,则消息将不会被写入到队列中。如果没有指定IPC_NOWAIT,则调用进程将被中断(阻塞),直到可以写消息为止,例:下面的代码已经向打开的消息队列发送信息:

    ...
    struct msgmbuf{
       
    		int mtype;
    		char mtext[10];
    		};
    	int msg_sflags;
    	int msg_id;
    	struct msgmbuf msg_mbuf;
    	msg_sflags = IPC_NOWAIT;
    	msg_mbuf.mtype = 10;
    	memcpy(msg_mbuf.mtext,“测试消息”,sizeof("测试消息"));
    
    ret = msgsnd(msg_id,&msg_mbuf,sizeof(“测试消息”,msg_sflags);
    
    
    if(-1 == ret)
    {
       
    printf("发送消息失败\n");
    }
    ...
    

    将发送消息打包到msg_mbuf.ntext域中,然后调用msgsnd发送消息给内核。这里mtype设置了类型为10,当接受时必须设置此域为10,才能接收到这时发送的消息。msgsnd()函数的msg_id是之前msgget创建的。

    5.msggrcv()函数

    当获得队列标识符后,可以在该消息队列上执行消息队列的接受操作。msgrcv()函数用于接受队列标识符中的信息,函数原型如下:

    #include
    #include
    #include
    ssize_t msgrcv(int msqid,void *msgp,size_t msgsz,long msgtyp,int msgflg);
    
    1. 第1个参数msqid是用来指定,在消息获取过程中所使用的队列(该值是由前面调用msgget()得到的返回值)。

    2. 第2个参数msgp代表消息缓冲区变量的地址,获取的消息将存放在这里。

    3. 第3个参数msgsz代表消息缓冲区结构的大小,不包括mtype成员的长度。

    4. 第4个参数mtype指定要从队列中获取的消息类型。内核将查找队列中具有匹配类型的第一 个到达的消息,并把它复制返回到由msgp 参数所指定的地址中。如果mtype参数传送 一 个为0 的值,则将返回队列中最老的消息,不管该消息的类型是什么。

    5. 如果把IPC_NOWAIT作为 一 个标志传送给该函数,而队列中没有任何消息,则该次调用将会向调用进程返回ENOMSG。否则,调用进程将阻塞,直到满足msgrcv()参数的消息到达队列为止。

    6. 如果在客户等待消息的时候队列被删除了,则返回EIDRM。如果在进程阻塞并等待消息的到来时捕获到 一 个信号,则返回EINTR

    函数 msgrcv 的使用代码如下:

    msg_rflags = IPC_NOWAIT MSG_NOERROR;
    ret = msgrcv(msg_id,&msg_mbuf,10,msg_rflags);
    if( -1 == ret)
    {
       
    	printf("接受消息失败\n");
    }
    else
    {
       
    	printf("接收信息成功,长度:%d\n",ret);
    }
    

    上面的代码将mtype设置为10,可以获得之前发送的内核的消息获得(因为之前发送的mtypes值也设置为10),msgrcv返回值为接收到消息长度。

    6.msgctl()函数

    已经了解如何简单地创建和利用消息队列,下面介绍一种如何直接地对那些与特定的消息队列相联系的内部结构进行操作,可以使用**msgctl()**函数在消息队列上执行控制操作。

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

    msgclt()向内核发送cmd命令,内核根据此来判断进行何种操作,buf为应用层和内核空间进行数据交换的指针。其中的cmd可以为下值:

    1. IPC_STAT: 获取队列的msqid_ds结构,并把它存放在buf变量所指定的地址中,通过这种方式,应用层可以获得当前消息队列的设置情况,例如是否有消息到来、消息队列的缓冲区设置等。

    2. TPC_SET: 设置队列的msqid_ds结构的ipc_perm成员值,它是从buf中取得该值的。通过 IPC_SET 命令,应用层可以设置消息队列的状态,例如修改消息队列的权限,使其他用户可以访问或者不能访问当前的队列;甚至可以设置消息队列的某些当前值来伪装。

    3. IPC_RMID: 内核删除队列。使用此命令执行后,内核会把此消息队列从系统中删除。

    7.消息队列的一个例子

    本例建立消息队列后,打印其属性,并在每次发送和接收后均查看其属性,最后对消息队列进行修改。

    1.显示信息属性的函数msg_show_attr()

    msg_show_ attr()函数根据用户输入的消息 ID , 将消息队列中的字节数、消息数、最大字节数、最后发送消息的进程、最后接收消息的进程、最后发送消息的时间、最后接收消息的时间、最后消息变化的时间,以及消息的 UID 和 GID 等信息进行打印。

    #include
    #include
    #include
    #include
    #include
    #include
    #include
    #include
    
    void msg_show(int msg_id,struct msqid_ds msg_info)//打印消息属性的函数
    {
       
            int ret = -1;
            sleep(1);
            ret = msgctl(msg_id,IPC_STAT,&msg_info);//获取消息
            if(-1 == ret)
            {
       
                    printf("获得消息信息失败\n");
                    return;
            }
            printf("\n");
            printf("现在队列中的字节数:%ld\n",msg_info.msg_cbytes);
            printf("队列消息数:%d\n",(int)msg_info.msg_qnum);
            printf("队列中最大字节数:%d\n",(int)msg_info.msg_qbytes);
    
            printf("最后发送消息的进程pid:%d\n",msg_info.msg_lspid);
            printf("最后接收消息的进程pid:%d\n",msg_info.msg_lrpid);
    
            printf("最后发送消息的时间:%s",ctime(&(msg_info.msg_stime)));
            printf("最后接收消息的时间:%s"
  • 相关阅读:
    Krahets 笔面试精选 88 题——40. 组合总和 II
    多种隐藏滚动条但是依然可以滚动实现方式
    网络编程概述
    十六章:Java8的其它新特性
    信息学奥赛一本通 1262:【例9.6】挖地雷 动态规划基本型
    Linux项目自动化构建工具-make/Makefile
    氨基修饰海藻酸钠;Alginate-Amine;Amine-Alginate
    为什么不推荐在Spring Boot中使用@Value加载配置
    三层交换机(三层配置基础命令)
    顾樵 量子力学I 导读(1)
  • 原文地址:https://blog.csdn.net/weixin_50866517/article/details/127060533