目录
消息队列
消息队列是内核地址空间中的内部链表,通过Linux内核在各个进程之间传递内容。消息顺序地发送到消息队列中,并以几种不同的方式从队列中获取,每个消息队列可以用IPC标识符唯一地进行标识。内核中的消息队列是通过IPC的标识符来区别的,不同的消息队列之间是相对独立的。每个消息队列中的消息,又构成一个独立的链表。(进程间通信—IPC(Inter-Process Communication))
1. 消息缓冲区结构
常用的结构是msgbuf结构。程序员可以以这个结构为模板定义自己的消息结构。在头文件<linux/msg.h>中,它的定义如下:
struct msgbuf
{
long mtype;
char mtext[1];
};
在结构msgbuf中有以下两个成员。
□ mtype:消息类型,以正数来表示。用户可以给某个消息设定一个类型,可以在消息队列中正确地发送和接收自己的消息。
例如,在socket编程过程中,一个服务器可以接受多个客户端的连接,可以为每个客户端设定一个消息类型,服务器和客户端之间的通信可以通过此消息类型来发送和接收消息,并且多个客户端之间通过消息类型来区分。
□ mtext:消息数据。消息数据的类型为char,长度为1。在构建自己的消息结构时,这个域并不一定要设为char或者长度为1。可以根据实际的情况进行设定,这个域能存放任意形式的任意数据,应用程序编程人员可以重新定义msgbuf结构。
例如:
struct msgbuf
{
long mtype;
char mtext[10];
long length;
};
上面定义的消息结构与系统模板定义的不一致,但是mtype是一致的。
消息在通过内核在进程之间收发时,内核不对mtext域进行转换,任意的消息都可以发送。具体的转换工作是在应用程序之间进行的。
但是,消息的大小,存在一个内部的限制。在Linux中,它在Linux/msg.h屮的定义如下:
#define MSGMAX 8192
消息总的大小不能超过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; /*在队列上所驻留的字节总数*/
msgqnum_t msg_qnum; /*当前处丁队列中的消息数目*/
msglen_t msg_qbytes; /*队列中能容纳的字节的最大数目*/
pid_t msg_lspid; /*发送最后一个消息进程的PID */
pid_t msg_lrpid; /*接收最后一个消息进程的PID */
};
为了叙述的完整性,下面对每个成员都给出一个简短的介绍。
□ msg_perm:它是ipc_perm结构的一个实例,ipc_perm结构是在Linux/ipc.h中定义的。用于存放消息队列的许可权限信息,其中包括访问许可信息,以及队列创建者的有关信息(如uid等)。
□ msg_stime:发送到队列的最后一个消息的时间戳(time_t)。
□ msg_rtime:从队列中获取最后一个消息的时间戳。
□ msg_ctime:对队列进行最后一次变动的时间戳。
□ msg_cbytes:在队列上所驻留的字节总数(即所有消息的大小的总和)。
□ msg_qnum:当前处于队列中的消息数目。
□ msg_qbytes:队列中能容纳的字节的最大数目。
□ msg_lspid:发送最后一个消息进程的PID。
□ msg_lrpid:接收最后一个消息进程的PID。
3. 结构 ipc_perm
内核把IPC对象的许可权限信息存放在ipc_perm类型的结构中。例如某些消息队列的内部结构中,msg_perm成员就是ipc_perm类型的,它的定义是在文件 <linux/ipc.h>中,如下所示:
struct ipc_perm
{
key_t key; /*函数msgget ()使用的键值*/
uid_t uid; /*用户的UID*/
gid_t gid; /*用户的GID*/
uid_t cuid; /*建立者的UID*/
gid_t cgid; /*建立者的GID*/
unsigned short mode; /*权限*/
unsigned short seq; /*序列号*/
};
这个结构描述的主要是一些底层的东西,简单介绍如下:
□ key:key参数用于区分消息队列。
□ uid:消息队列用户的ID号。
□ gid:消息队列用户组的ID号。
□ cuid:消息队列创建者的ID号。
□ cgid:消息队列创建者的组ID号。
□ mode:权限,用户控制读写,例如0666,可以对消息进行读写操作。
□ seq:序列号。
4. 内核中的消息队列关系
作为IPC的消息队列,其消息的传递是通过Linux内核来进行的。如下图所示的结构成员与用户空间的表述基本一致。在消息的发送和接收的时候,内核通过一个比较巧妙的设置来实现消息插入队列的动作和从消息队列中查找消息的算法。
图 消息机制在内核中的实现
结构list_head形成一个链表,而结构msg_msg之中的m_list成员是一个struct list_head类型的变量,通过此变量,消息形成了一个链表,在查找和插入时,对m_list域进偏移操作就可以找到对应的消息体位置。内核中的代码在头文件 <linux/msg.h>和<linux/msg.c>中,主要的实现是插入消息和取出消息的操作。
5. 键值构建ftok()函数
ftok()函数将路径名和项目的表示符转变为一个系统V的IPC键值。其原型如下:
#include <sys/types.h>
#include <sys/ipc.h>
key_t ftok(const char *pathname, int proj_id);
其中pathname必须是已经存在的目录,而proj_id则是一个8位的值,通常用a、b等表示。例如建立如下目录:
$mkdir -p /ipc/msg/
然后用如下代码生成一个键值:
6. 获得消息msgget()函数
创建一个新的消息队列,或者访问一个现有的队列,可以使用函数msgget(),其原型如下:
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
int msgget(key_t key, int msgflg);
msgget()函数的第一个参数是键值,可以用ftok()函数生成,这个关键字的值将被拿来与内核中其他消息队列的现有关键字值相比较。比较之后,打开或者访问操作依赖于msgflg参数的内容。
□ IPC_CREAT:如果在内核中不存在该队列,则创建它。
□ IPC_EXCL:当与IPC_CREAT一起使用时,如果队列早已存在则将出错。
如果只使用了 IPC_CREAT,msgget()函数会返回新创建消息队列的消息队列标识符,或者会返回现有的具有同一个关键字值的队列的标识符。如果同时使用了IPC_EXCL和IPC_CREAT,那么将可能会有两个结果:创建一个新的队列,如果该队列存在,则调用将出错,并返回-1。IPC_EXCL本身是没有什么用处的,但在与IPC_CREAT组合使用时,它可以用于保证没有一个现存的队列为了访问而被打开。例如,下面的代码创建一个消息队列:
第一次执行时,会显示消息建立成功,再次执行时,会显示消息建立失败。
7. 发送消息msgsnd()函数
一旦获得了队列标识符,用户就可以开始在该消息队列上执行相关操作了。为了向队列传递消息,用户可以使用msgsnd()函数:
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
msgsnd()函数的第1个参数是队列标识符,它是前面调用msgget()获得的返回值。第二个参数是msgp,它是一个void类型的指针,指向一个消息缓冲区。msgsz参数则包含着消息的大小,它是以字节为单位的,其中不包括消息类型的长度(4个字节长)。
msgflg参数可以设置为0 (表示忽略),也可以设置为IPC_NOWAIT。如果消息队列已满,则消息将不会被写入到队列中。如果没有指定IPC_N0WA1T,则调用进程将被中断 (阻塞),直到可以写消息为止。例如,如下代码向己经打开的消息队列发送消息:
首先将要发送的消息打包到msg_mbuf.mtext域中,然后调用msgsnd发送消息给内核。这里的mtype设置了类型为10,当接收时必须设置此域为10,才能接收到这时发送的消息。 msgsnd()函数的msg_id是之前msgget创建的。
8. 接收消息msgrcv()函数
当获得队列标识符后,用户就可以开始在该消息队列上执行消息队列的接收操作。msgrcv()函数用于接收队列标识符中的消息,函数原型如下:
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);
msgrcv()函数的四个参数:
□第1个参数msqid是用来指定,在消息获取过程中所使用的队列(该值是由前面调用msgget()得到的返回值)。
□ 第2个参数msgp代表消息缓冲区变量的地址,获取的消息将存放在这里。
□ 第3个参数msgsz代表消息缓冲区结构的大小,不包括mtype成员的长度。
□ 第4个参数msgtyp指定要从队列中获取的消息类型。
决定从队列中返回哪条消息:
=0 返回消息队列中第一条消息
>0 返回消息队列中等于msgtyp类型的第一条消息。
<0 返回msgtyp<=type 绝对值最小值的第一条消息。
内核将查找队列中具有匹配类型的第一个到达的消息,并把它复制返回到由msgp参数所指定的地址中。如果msgtyp参数传送一个为0的值,则将返回队列中最老的消息,不管该消息的类型是什么。
如果把IPC_NOWAIT作为一个标志传送给该函数,而队列中没有任何消息,则该次调用将会向调用进程返回ENOMSG。否则,调用进程将阻塞,直到满足msgrcv()参数的消息到达队列为止。如果在客户等待消息的时候队列被删除了,则返回EIDRM。如果在进程阻塞并等待消息的到来时捕获到一个信号,则返回EINTR。函数msgrcv的使用代码如下:
上面的代码中将msgtyp设置为10,可以获得之前发送的内核的消息获得(因为之前发送的msgtyp值也设置为10),msgrcv返回值为接收到的消息长度。
9. 消息控制msgctl()函数
通过前面的介绍己经知道如何在应用程序中简单地创建和利用消息队列。下面介绍一下如何直接地对那些与特定的消息队列相联系的内部结构进行操作。为了在一个消息队列上执行控制操作,用户可以使用msgctl()函数。
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
int msgctl(int msqid, int cmd, struct msqid_ds *buf);
msgctl()向内核发送一个cmd命令,内核根据此来判断进行何种操作,buf为应用层和内核空间进行数据交换的指针。其中的cmd可以为如下值:
□ IPC_STAT:获取队列的msqid_ds结构,并把它存放在buf变量所指定的地址中,通过这种方式,应用层可以获得当前消息队列的设置情况,例如是否有消息到来、消息队列的缓冲区设置等。
□ IPC_SET:设置队列的msqid_ds结构的ipc_perm成员值,它是从buf中取得该值的。通过IPC_SET命令,应用层可以设置消息队列的状态,例如修改消息队列的权限,使其他用户可以访问或者不能访问当前的队列;甚至可以设置消息队列的某些当前值来伪装。
□ IPC_RMID:内核删除队列。使用此命令执行后,内核会把此消息队列从系统中删除。
4. 消息队列的例子
本例在建立消息队列后,打印其属性,并在每次发送和接收后均查看其属性,最后对消息队列进行修改。
1. 显示消息属性的函数msg_show_attr()
msg_show_attr()函数根据用户输入的消息ID,将消息队列中的字节数、消息数、最大字节数、最后发送消息的进程、最后接收消息的进程、最后发送消息的时间、最后接收消息的时间、最后消息变化的时间,以及消息的UID和GID等信息进行打印。
2. 主函数main()
主函数先用函数ftok()使用路径“/tmp/msg/b”获得一个键值,之后进行相关的操作并打印消息的属性。
□调用函数msgget()获得一个消息后,打印消息的属性;
□调用函数msgsnd()发送一个消息后,打印消息的属性;
□调用函数msgrcv()接收一个消息后,打印消息的属性;
□最后,调用函数msgctl()并发送命令IPC_RMID销毁消息队列。
来源:https://blog.csdn.net/Change_Improve/article/details/99432904