Linux 进程间通信和同步—消息队列

39 篇文章 2 订阅
1 篇文章 0 订阅

消息队列

  消息队列是内核地址空间中的内部链表,通过 Linux 内核在各个进程之间传递内容。消息顺序地发送到消息队列中,并以几种不同的方式从队列中获取,每个消息队列可以用 IPC 标识符唯一地进行标识。内核中的消息队列是通过 IPC 的标识符来区别的,不同的消息队列之间是相对独立的。每个消息队列中的消息,又构成一个独立的链表

1. 消息缓冲区结构

  常用的结构是 msgbuf 结构。程序员可以以这个结构为模板定义自己的消息结构。在头文件 <linux/msg.h> 中,它的定义如下:

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

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

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

struct msgmbuf
{
	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_rime:从队列中获取最后一个消息的时间戳。
  █ 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;		/* 序列号 */
}

  这个结构描述的主要是一些底层的东西,简单介绍如下。
  █ keykey 参数用于区分消息队列。
  █ uid:消息队列用户的 ID 号,
  █ gid:消息队列用户组的 ID 号。
  █ cuid:消息队列创建者的 ID 号。
  █ cgid:消息队列创建者的组 ID 号。
  █ mode:权限,用户控制读写,例如 0666,可以对消息进行读写操作。
  █ seq:序列号。

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

  作为 IPC 的消息队列,其消息的传递是通过 Linux 内核来进行的。如下图(消息机制在内核中的实现)所示的结构成员与用户空间的表述基本一致。在消息的发送和接收的时候,内核通过一个比较巧妙的设置来实现消息插入队列的动作和从消息中査找消息的算法。

消息机制在内核中的实现

  结构 list_head 形成一个链表,而结构 msg_msg 之中的 m_list 成员是一个 struct list_head 类型的变景,通过此变量,消息形成了一个链表,在查找和插入时,对 m_ist 域进行偏移操作就可以找到对应的消息体位置。内核中的代码在头文件 <linux/msg.h><linux/msg.c> 中,主要的实现是插入消息和取出消息的操作。

5. 键值构建 ftok() 函数

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

#include <sys/types.h>
#include <sys/ipc.h>
key_t ftok(const char *pathname, int proj_id);

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

$ 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");
}
......
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 一起使用时,如果队列早已存在则将出错。

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

......
key_t key;
int msg_flags, msg_id; 
msg_flags = IPC_CREAT|IPC_EXCL; 	/* 消息的标志为建立、可执行 */
mag_id = msgget(key, msg_flags|0x0666);	/* 建立消息 */
if( -1 == msg_id)	/* 建立消总失败 */
{
	printf("消息建立失败\n"); /* 打印信息 */
	return 0;	/* 退出 */
}
......
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_NOWAIT,则调用进程将被中断(阻塞),直到可以写消息为止。例如,如下代码向已经打开的消息队列发送消息:

......
struct msgmbuf		/* 消息的结构 */
{ 
	int mtype; 		/* 消息中的字节数 */
	char mtext[10];	/* 消息数据 */
};
int msg_sflags; /* 消息的标记 */
int msg_id;		/* 消息 ID 识别号 */
struct msgmbuf msg_mbuf; /* 建立消息结构变量 */
msg_sflags = IPC_NOWAIT; /* 直接读取消息,不等待 */ 
msg_mbuf.mtype = 10;	/* 消息的大小为 10 字节 */
memcpy(msg_mbuf.mtext,"测试消息",sizeof("测试消息")); /* 将数据复制如消息数据缓冲区 */
ret = msgsnd(msg_id, &msg_mbuf, sizeof("测试消息"),msg_sflags); /* 向消息 ID 发送消息 */
if(-1 == ret)	/* 发送消息失败 */
{
	printf("发送消息失败\n");	/* 打印消息 */
}
......

  首先将要发送的消息打包到 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 指定要从队列中获取的消息类型。内核将查找队列中具有匹配类型的第一个到达的消息,并把它复制返回到由 msgp 参数所指定的地址中。如果 msgtyp 参数传送一个为 0 的值,则将返回队列中最老的消息,不管该消息的类型是什么。msgtyp=0:收到的第一条消息,任意类型。msgtyp>0:收到的第一条 msgtyp 类型的消息。msgtyp<0:收到的第一条最低类型(小于或等于 msgtyp 的绝对值)的消息。

  如果把 IPC_NOWAIT 作为一个标志传送给该函数,而队列中没有任何消息,则该次调用将会向调用进程返回 ENOMSG。否则,调用进程将阻塞,直到满足 msgrcv() 参数的消息到达队列为止。如果在客户等待消息的时候队列被删除了,则返回 EIDRM。如果在进程阻塞并等待消息的到来时捕获到一个信号,则返回 EINTR。函数 msgrcv 的使用代码如下:

msg_rflags = IPC_NOWAIT|MSG_NOERROR;	/* 消息接收标记 */
ret = msgrcv(msg _id, &msg_mbuf, 10,10,msg_rflags); /* 接收消息 */
if( -1 == ret)	/* 接收消息失败 */
{
	printf("接收消息失败\n");	/* 打印信息 */
}
else /* 接收消息成功 */
{
	printf("接收消息成功,长度:%d\n",ret); /* 打印信息 */
} 

  上面的代码中将 mtype 设置为 10,可以获得之前发送的内核的消息获得(因为之前发送的 mtype 值也设背为 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,将消息队列中的字节数、消息数、最大字节数、最后发送消息的进程、最后接收消息的进程、最后发送消息的时间、最后接收消息的时间、最后消息变化的时间,以及消息的 UIDGID 等信息进行打印。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/msg.h>
#include <unistd.h>
#include <time.h>
#include <sys/ipc.h>
/* 打印消息属性的函数 */
void msg_show_attr(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", ctime(&(msg_info.msg_rtime))); /* 最后接受消息的时间 */
	printf("最后变化时间:%s", ctime(&(msg_info.msg_ctime))); /* 消息的最后变化时间 */
	printf("消息 UID 是:%d\n",msg_info.msg_perm.uid); /* 消息的 UID */
	printf("消息 GID 是:%d\n",msg_info.msg_perm.gid); /* 消息的 GID */
}
2. 主函数 main()

  主函数先用函数 ftok() 使用路径 “/tmp/msg/b” 获得一个键值,之后进行相关的操作并打印消息的属性。

  █ 调用函数 msgget() 获得一个消息后,打印消息的属性;
  █ 调用函数 msgsnd() 发送一个消息后,打印消息的属性;
  █ 调用函数 msgrcv() 接收一个消息后,打印消息的属性;
  █ 最后,调用函数 msgctl() 并发送命令 IPC_RMID 销毁消息队列。

int main(void)
{
	int ret = -1; 
	int msg_flags, msg_id; 
	key_t key; 
	struct msgmbuf		/* 消息的缓冲区结构 */
	{
		int mtype; 
		char mtext[10];
	};
	struct msqid_ds msg_info; 
	struct msgmbuf msg_mbuf;

	int msg_sflags,msg_rflags; 
	char *msgpath = "/ipc/msg/"; 	/* 消息 key 产生所用的路径 */
	key = ftok(msgpath,'b');		/* 产生 key */
	if(key != -1)	/* 产生 key 成功 */
	{
		printf("成功建立 KEY\n");
	}
	else	/* 产生 key 失败 */
	{
		printf("建立 KEY 失败\n");
	}
	msg_flags = IPC_CREAT|IPC_EXCL;  /* 消息的类型 */
	msg_id = msgget(key, msg_flags|0x0666); /* 建立消息 */
	if( -1 == msg_id)
	{
		printf("消息建立失败\n");
		return 0;
	}
	msg_show_attr(msg_id, msg_info); /* 显示消息的属性 */
	
	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_show_attr(msg_id,msg_info);	/* 显示消息属性 */
	
	msg_rflags = IPC_NOWAIT|MSG_NOERROR;
	ret = msgrcv(msg_id, &msg_mbuf, 10,10,msg_rflags);	/* 接收消息 */
	if( -1 == ret)
	{
		printf("接收消息失败\n");
	}
	else
	{
		printf("接收消息成功,长度:%d\n",ret);
	}
	msg_show_attr(msg_id, msg_info); /* 显示消息属性 */

	msg_info.msg_perm.uid =8;
	msg_info.msg_perm.gid = 8;
	msg_info.msg_qbytes = 12345;
	ret = msgctl(msg_id, IPC_SET, &msg_info); /* 设置消息属性 */
	if( -1 == ret)
	{
		printf("设置消息属性失败\n");
		return 0;
	}
	msg_show__attr(msg_id, msg_info); /* 显示消息属性 */
	
	ret = msgctl(msg_id, IPC_RMID,NULL); /* 删除消息队列 */
	if(-1 == ret) 
	{
		printf("删除消息失败\n");
		return 0;
	}
	return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值