可以想象,如果两个进程都可以访问同一个队列:其中一个进程(sender)向其中写入结构化数据,另外一个进程(receiver)再从其中把结构化的数据读取出来。那么这两个进程就是在利用这个队列进行通信了,这个队列也就称为消息队列(message queue)。
消息队列有system V和POSIX两种,这里说system V的。
内核维护中很多个消息队列(因为有很多不同的进程要用它们来通信,不可能共享一个),我们使用一个key_t类型的key来让发送端和接收端进程都清楚了解自己使用的是哪个消息队列。
key_t类型实际上就是一个int类型
typedef int __kernel_key_t; (查看这里)
typedef __kernel_key_t key_t; (查看这里)
所以我们可以简单地想象成每个消息队列都有一个自己的唯一id。虽然直接为两个进程指定一个常数值(比如12345)作为你创建的消息队列的key一般是没有问题的,但为了保证唯一性和可读性,我们可以用ftok()函数来创建该key。
key_t ftok(const char *path, int id);
第一个参数是你指定的文件(或目录)路径,id是一个子序号,该函数将指定的文件路径对应的索引节点号和id结合起来生成一个key值,比如指定文件的索引节点号为56672,换算成16进制为0xDD60,而你指定的id值为52,换算成16进制为0x34,则最后的key_t返回值为0x3400DD60。
我们可以通过msgget函数来创建一个新的消息队列或者取得已经存在的消息队列。
int msgget(key_t key, int msgflg);
key参数:
就是要新建或获取的消息队列的唯一标识,其一般是由上面的ftok函数成长的,但有一个例外是其还可以是一个叫做IPC_PRIVATE的值(实际上是值为0的key):
#define IPC_PRIVATE ((__kernel_key_t) 0)
当key为IPC_PRIVATE时,则msgget始终创建一个新的消息队列。
msgflg参数:
其有两层含义,一是该参数可以取值IPC_CREATE,表示请求msget函数新建消息队列;也可以是 IPC_CREATE|IPC_EXCL的按位组合,表示请求新建消息队列但如果队列已经存在的话则报错。二是该参数表示权限控制,比如666表示全部可读写。那么如果该参数写成IPC_CREATE|IPC_EXCL|666则表示请求新建消息队列但如果队列已经存在的话则报错并且权限为666
与管道不一样,我们向消息队列中写入会从其中读取信息时采用的不是字节流而是按照某一约定的结构体,比如:
struct msg
{
long msg_type;
char msg_txt[SIZE];
};
其第一个字段是一个长整形数,表示该信息的类型,这个字段可以让我们将不同“类型”的消息写入同一个管道,比如可以用1表示普通信息,用2表示错误信息。当读取端从消息队列中读取消息时,如果不指定类型(类型值为0)则其读取队列中的第一个消息,而如果指定了类型的话,则读取该类型的第一个消息(所以其并不完全地按照先入先出的顺序)
后面的字段表示消息的内容部分,一般用一个字节数组,但其实是可以自定义的,比如:
struct msg
{
long msg_type;
int other_filed;
char msg_txt[SIZE];
};
用msgsnd函数来发送消息到消息队列中去:
int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg)
msgid参数:
消息队列的id,也就是上面所说的key.
msgp参数:
指向消息结构体的指针
msgsz参数:
表示消息结构体的大小,由于消息结构体是可以自定义的,所以必须指定大小类分割连续的消息
msgflg参数:
这个参数主要用于如下情况:消息队列中的消息数目达到了系统上限 或 消息队列没有足够的空间来容纳消息字节。此时,如果指定了msgflg为IPC_NOWAIT的话(msgflg & IPC_NOWAIT != ),那么消息将不被发送,而函数直接返回。相反的(msgflg & IPC_NOWAIT == ),调用该函数的进程将会被挂起,直到下面三种情况之一发生:
)上述两种情况不再满足,消息被发送
)消息队列被系统移除,消息不被发送。
)进程接收到信号转入进行信号处理,这种情况下,消息不会被发送。
struct message {
long msg_type;
char msg_text[SIZE];
} msg;
…
msg.msg_type = ;
strcpy(msg.msg_text, "This is message 1");
...
result = msgsnd(msqid, (void *) &msg, sizeof(msg.msg_text), IPC_NOWAIT);
相对应的,msgrcv函数用来从消息队列中读取消息:
ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);
msgtyp参数:
如果该值为0,则读取消息队列中的第一个消息
如果该值大于0,则读取消息队列中具有指定类型的第一个消息
如果该值小于0,则读取消息队列中类型值小于或等于指定值的绝对值的最小类型值的第一个消息(好晕,the first message of the lowest type that is less than or equal to the absolute value of msgtyp)
msgflg参数:
这个参数主要用于消息队列中当没有消息(或指定类型的消息)可供读取时,如果指定了msgflg为IPC_NOWAIT的话(msgflg & IPC_NOWAIT != ),函数将直接返回而放弃读取。相反的(msgflg & IPC_NOWAIT == ),调用该函数的进程将会被挂起,直到下面三种情况之一发生:
)有消息到来,可读取了
)消息队列被系统移除,放弃消息读取
)进程接收到信号转入进行信号处理,放弃消息读取
msgflg也可以指定如下操作:
/* msgrcv options */
#define MSG_NOERROR 010000 /* no error if message is too big */
#define MSG_EXCEPT 020000 /* recv any msg except of specified type.*/
另外还有一个msgctl函数可以对消息队列进行一些其他控制和读取消息队列的属性消息,不过在这之前需要先了解下消息队列本身的数据结构msqid_ds:
struct msqid_ds {
struct ipc_perm msg_perm;
struct msg *msg_first; /* first message in 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 */
};
其中msg_perm字段表示消息队列的所有者和权限信息:
struct ipc_perm
{
__kernel_key_t key;
__kernel_uid_t uid;
__kernel_gid_t gid;
__kernel_uid_t cuid;
__kernel_gid_t cgid;
__kernel_mode_t mode;
unsigned short seq;
};
msg_qbytes表示消息队列所能容纳的最大消息字节数。
再来看看msgctl函数:
int msgctl(int msqid, int cmd, struct msqid_ds *buf);
cmd参数:
表示要对消息队列执行的操作,可以有如下三种:
)IPC_STAT获取消息队列属性信息,并将这些信息放置到第3个参数所设置的buffer中
)IPC_SET 根据buffer中的数据设置消息队列的一些属性,并非所有属性都可设置,其仅仅限于:msg_perm.uid,msg_perm.gid,msg_perm.mode,msg_qbytes 这几种
)IPC_RMID 删除消息队列。
注:IPC_SET和IPC_RMID都会进行相应的权限检查,只有具备权限的进程才能执行相关操作