【Linux应用编程】POSIX线程互斥与同步机制—消息队列

文章目录

  • 1 前言
  • 2 消息队列
    • 2.1 消息队列特点
    • 2.2 消息队列适用场景
    • 2.3 消息队列使用原则
  • 3 消息队列使用
    • 3.1 创建消息队列
    • 3.2 初始化消息队列
    • 3.3 接收消息
      • 3.3.1 阻塞方式
      • 3.3.2 指定超时时间阻塞方式
    • 3.4 发送消息
      • 3.4.1 阻塞方式
      • 3.4.2 指定超时时间阻塞方式
    • 3.5 消息队列销毁
    • 3.6 写个例子
  • 4 消息队列属性
    • 4.1 消息队列标识
    • 4.2 消息最大数目
    • 4.3 消息最大长度
    • 4.4 当前消息数目
    • 4.5 消息属性获取与设置
      • 4.5.1 获取属性
      • 4.5.2 设置属性
    • 4.6 消息队列持续性
  • 5 总结


1 前言

  在上一篇文章中,主要描述了信号量的含义、属性、使用场景和使用方法。信号量是用于进程或者线程同步的一种机制,信号量本质可以“传递”少量信息,达到进程或者线程同步的目的。如果进程或者线程间需要传递一些信息量比较大的或者长度不固定的消息,那么信号量就不是那么合适了,而本文将要描述的 “消息队列” 则是理想选择。消息队列与信号量一样,可以适用于进程和线程间同步(传递消息)。


2 消息队列

  POSIX消息队列(Message queue)是进程和线程间同步的一种机制。消息队列可以用于多个进程(线程)间传递一定量的不固定长度的信息,以此达到交换数据的目的。消息队列与信号量一样,即使接收消息的进程(线程)未就绪,消息也不会丢失,具有消息可靠性;当然,队列中存储的消息条目有限,未及时读取而队列满后会导致发送进程(线程)阻塞,消息发送延迟。消息队列可以引起调度进程(线程)睡眠,当队列中没有可用消息时,进程(线程)会被阻塞挂起,直至接收到有效消息。


2.1 消息队列特点

  • 用于线程同步
  • 传递不定长度消息,数据长度可以为0(数据为空)
  • 消息可靠性
  • 可引起线程睡眠
  • 具有访问权限

2.2 消息队列适用场景

   消息队列常用于需要传递一定量不定长度信息的进程(线程)之间,常用的场景有:

  • 异步处理
      两个进程(线程)异步执行,通过消息队列同步。

  • 应用解耦
      应用程序功能分层处理,每一层的界限通过消息队列传递信息,达到解耦的目的。


    具体实际场景有:

  • 广播通知

  • 日志记录

  • 串行(USB、UART)数据数据接收与处理过程

  • 数据异步输出(终端、文件)


2.3 消息队列使用原则

  • None

3 消息队列使用

  消息队列一般用于进程间通信,但也可以用于线程间通信,消息队列使用的基本步骤为:

【1】创建消息队列实例

【2】初始化消息队列

【3】接收消息

【4】发送消息

【5】销毁消息队列


3.1 创建消息队列

  posix消息队列以mqd_t数据结构表示。创建消息队列实例,首先定义一个消息队列数据描述符,然后调用mq_open创建消息队列。

mqd_t mq_open(const char *name, int oflag,  mode_t mode, struct mq_attr *attr);
  • name,消息队列名称

  • oflag,标识,根据传入标识来创建或者打开一个已创建的消息队列

标识 含义
O_CREAT 创建一个消息队列
O_EXCL 检查消息队列是否存在,一般与O_CREAT一起使用
O_CREAT|O_EXCL 消息队列不存在则创建,已存在返回NULL
O_NONBLOCK 非阻塞模式打开,消息队列不存在返回NULL
O_RDONLY 只读模式打开
O_WRONLY 只写模式打开
O_RDWR 读写模式打开
  • mode,访问权限
  • attr,消息队列属性地址, 用于创建队列(指定标识O_CREAT)设置属性;传入NULL表示使用默认属性
  • 返回,成功返回消息队列描述符,失败返回-1,错误码存于error

创建消息队列伪代码:

mqd_t mqd;
pmq = mq_open("mq", O_CREAT, 0777, NULL);

注:

创建消息队列成功后,会在"/dev/mqueue"目录下生成名称为name的消息队列文件。


3.2 初始化消息队列

  消息队列初始化一般调用sem_open函数创建或者打开已创建的消息队列即可,无需额外初始化,sem_open函数使用参考3.1.2节。

pmq = mq_open("mq", O_RDWR, 0777, NULL);	/* 读写模式打开一个消息队列 */

3.3 接收消息

  消息队列接收消息方式为阻塞模式,分为无限阻塞方式和指定超时时间阻塞方式。阻塞的前提是消息队列的阻塞属性标识设为阻塞模式,默认属性是阻塞模式。

3.3.1 阻塞方式

mqd_t mq_receive(mqd_t mqdes, 
                   char *msg_ptr,
                   size_t msg_len, 
                   unsigned *msg_prio);
  • mqdes,消息队列描述符
  • msg_ptr,消息体缓冲区地址,不能为NULL
  • msg_len,消息体长度,长度必须大于等于消息属性设定的最大值
  • msg_prio,消息优先级,数值越大(不超过MQ_PRIO_MAX值),优先级越高;接收到多个信息时,队列中优先级最高且最早的消息会最先返回;如果消息不需要设定优先级,在发送消息时,发送函数优先级参数设为0,msg_prio设为NULL
  • 返回,成功返回消息长度,失败返回-1,错误码存于error

   当有消息时,该函数会读取mqde 消息队列里面最早的一条消息,消息存放于msg_ptr指向的内存地址中。如果消息队列为空,调用线会被阻塞挂起,直至消息队列接收到可用消息,线程被唤醒继续执行。


3.3.2 指定超时时间阻塞方式

mqd_t mq_timedreceive(mqd_t mqdes,
                        char *msg_ptr,
                        size_t msg_len,
                        unsigned *msg_prio,
                        const struct timespec *abs_timeout);
  • abs_timeout,超时时间,单位为时钟节拍
  • 返回,成功返回消息长度,失败返回-1,错误码存于error
  • 其他,其他参数与mq_receive函数中的一致,参考上一小节

  该函数与mq_receive不同点是增加了一个超时时间参数,函数不会无限阻塞等待,超出指定的时钟节拍后,仍未接收到到有效的消息,函数会返回ETIMEDOUT,线程被唤醒继续执行。


3.4 发送消息

  消息队列发送消息方式为同样阻塞模式,分为无限阻塞方式和指定超时时间阻塞方式。阻塞的前提是消息队列的阻塞属性标识设为阻塞模式,默认属性是阻塞模式。

3.4.1 阻塞方式

mqd_t mq_send(mqd_t mqdes, 
               const char *msg_ptr,
               size_t msg_len, 
               unsigned msg_prio);
  • mqdes,消息队列描述符
  • msg_ptr,待发送消息体缓冲区地址,不能为NULL
  • msg_len,消息体长度,可以为0(消息内容为空时),不能超出消息队列属性中设定的最大消息长度
  • msg_prio,消息优先级,数值越大(不超过MQ_PRIO_MAX值),优先级越高;相同优先级消息按时间顺序发送到队列;如果消息不需要设定优先级,则该参数设为0,接收消息函数的优先级参数msg_prio设为NULL
  • 返回,成功返回0,失败返回-1,错误码存于error中,常见错误码如下
错误码 含义
EAGAIN 队列已满
EBADF 队列描述符无效
EMSGSIZE 消息长度超出范围
EINTR 信号被中断

   该函数用于将消息msg_pt发送到消息队列mqdes中。当队列中的消息未及时被接收,超出消息队列属性中设定的最大消息数目时,再调用该函数发送消息,信息将发送失败或者阻塞,与消息队列的阻塞属性有关。

【1】消息队列属性设为阻塞模式(0),该函数会一直阻塞,直至队列存在空间可以发送消息

【2】消息队列属性设为非阻塞模式(O_NONBLOCK),函数直接返回,返回队列已满的失败信息(EAGAIN)


3.4.2 指定超时时间阻塞方式

mqd_t mq_timedsend(mqd_t mqdes,
                const char *msg_ptr,
                size_t msg_len,
                unsigned msg_prio,
                const struct timespec *abs_timeout);
  • abs_timeout,超时时间,单位为时钟节拍
  • 返回,成功返回消息长度,失败返回-1,错误码存于error
  • 其他,其他参数与mq_send函数中的一致,参考上一小节

  该函数与mq_send不同点是增加了一个超时时间参数,队列消息已满时,即使消息队列设置为阻塞(0)属性,函数不会无限阻塞等待,超出指定的时钟节拍后,队列中仍未存在空闲空间,函数会返回ETIMEDOUT,线程被唤醒继续执行。


3.5 消息队列销毁

  消息队列销毁包括两个步骤,首先是关闭消息队列,接着是分离消息队列(删除)。调用分离函数销毁一个消息队列,只是把消息队列名称删除了,只有所有持有消息队列的进程(线程)都关闭消息队列后内核才会真正销毁消息队列。


【1】关闭消息队列

mqd_t mq_close(mqd_t mqdes);
  • mqdes,消息队列描述符
  • 返回,成功返回0,失败返回-1,错误码存于error

   当一个进程终止时,会对其占用的消息队列执行此关闭操作,不论进程是主动终止还是非主动终止都会调用这个函数执行关闭消息队列操作 。


【2】分离消息队列

mqd_t mq_unlink(const char *name);
  • name,消息队列名称
  • 返回,成功返回0,失败返回-1,错误码存于error

   mq_unlink函数会马上删除指定的消息队列名称, 此时分离消息队列本身并没有被删除,持有该分离消息队列的进程(线程)仍能正常使用消息队列,直到所有持有该消息队列的进程(线程)关闭该消息队列之后,内核才会真正删除消息队列。

   进程(线程)每调用一次mq_open函数打开一个消息队列,引用计数加一,调用mq_close关闭消息队列时,引用计数减一。调用mq_unlink函数分离一个消息队列时,先检查消息队列引用计数,引用计数为0时立即删除消息队列,否则等待引用计数为0内核再执行删除。


3.6 写个例子

  代码实现功能:

  • 创建一个“线程通信”模型
  • 创建两个线程,一个线程生成随机数,一个线程接收数据,并将数据输出到终端
  • 数据生成线程通过消息队列传递信息给接收线程
#include 
#include 
#include 
#include 
#include 
#include 			 
#include  	  
#include 
#include "pthread.h" 

#define	USE_RX_BLOC		0	/* 是否让接收消息线程挂起,0正常,1挂起 */
#define USE_MQ_NONBLOC	0	/* 是否使用非阻塞属性,0阻塞,1非阻塞 */
#define MQ_MSG_SIZE    1024	/* 最大消息长度 */
#define MQ_MSG_ITEM    10	/* 最大消息数目 */

/* 消息队列 */
mqd_t mq;

void *thread0_entry(void *data)  
{/* 接收消息线程 */
	uint8_t  i =0;
	uint8_t  buf[MQ_MSG_SIZE];
	int recv_size = 0;
	
	for (;;)
	{
		recv_size = mq_receive(mq, &buf[0], sizeof(buf), NULL);
		if (-1 != recv_size)
		{
			printf("thread0 recv %dByte message:", recv_size);
			for (i=0; i<recv_size; i++)
			{
				printf("0x%02x ", buf[i]);
			}
			printf("\r\n");
		}
		else
		{
			perror("mq_receive error");
			break;
		}
	#if	USE_RX_BLOC
		sleep(5);		/* 手动挂起线程,延迟读取消息 */
	#endif
	}
}

void *thread1_entry(void *data)  
{/* 发送消息线程 */
	uint8_t  i =0;
	uint8_t  buf[8];
	int	ret = 0;
	
	for (;;)
	{
		for (i = 0;i<8; i++)
		{
			buf[i] = rand();
		}
		printf("thread1 send %dByte message\r\n", i);
		ret = mq_send(mq, &buf[0], sizeof(buf), 0);
		if (ret < 0)
		{
        	perror("mq_send error");	/* 消息队列满 */
		}
		sleep(1);	/* 1秒产生数据 */	
	}
}

int main(int argc, char **argv)  
{
	pthread_t thread0,thread1,thread2;  
	void *retval;
    struct mq_attr attr,oldattr;

    memset(&attr, 0, sizeof(attr));
    attr.mq_maxmsg = MQ_MSG_ITEM;
    attr.mq_msgsize = MQ_MSG_SIZE;
    attr.mq_flags = 0;
    mq = mq_open("/mq0", O_CREAT|O_RDWR, 0777, &attr);
	if(-1 == mq)
    {
        perror("mq_open error");
        return -1;
    }
#if	USE_RX_BLOC
  #if USE_MQ_NONBLOC
	attr.mq_flags = O_NONBLOCK;		/* 设置非阻塞属性 */
	mq_setattr(mq, &attr, &oldattr);	
	printf("new mqflags=[%ld], old mqflags=[%ld]\r\n", attr.mq_flags, oldattr.mq_flags);
 #endif
#endif
    pthread_create(&thread0, NULL, thread0_entry, NULL);
	pthread_create(&thread1, NULL, thread1_entry, NULL);
    pthread_join(thread0, &retval);
    pthread_join(thread1, &retval);

	mq_close(mq);
	mq_unlink("/mq0");
	return 0;
 }

【1】正常输出结果

  将代码中宏USE_RX_BLOC设为0x00,宏USE_MQ_NONBLOC设为0x00。

acuity@ubuntu:/mnt/hgfs/LSW/STHB/pthreads/mq$ gcc mq.c -o mq -lpthread -lrt
acuity@ubuntu:/mnt/hgfs/LSW/STHB/pthreads/mq$ ./mq
thread1 send 8Byte message
thread0 recv 8Byte message:0x67 0xc6 0x69 0x73 0x51 0xff 0x4a 0xec 
thread1 send 8Byte message
thread0 recv 8Byte message:0x29 0xcd 0xba 0xab 0xf2 0xfb 0xe3 0x46 
thread1 send 8Byte message
thread0 recv 8Byte message:0x7c 0xc2 0x54 0xf8 0x1b 0xe8 0xe7 0x8d 

【2】使队列阻塞,消息队列属性为阻塞

  将代码中宏USE_RX_BLOC设为0x01,宏USE_MQ_NONBLOC设为0x00。消息队列属性为阻塞(0),手动挂起消息接收线程,延迟读取消息,一段时间后,队列被消息填满;观察消息发送线程,发送线程被阻塞,直至队列有可用消息空间才会唤醒继续发送消息。


【3】使队列阻塞,消息队列属性为非阻塞

  将代码中宏USE_RX_BLOC设为0x01,宏USE_MQ_NONBLOC设为0x01。消息队列属性为非阻塞(O_NONBLOCK)。手动挂起消息接收线程,延迟读取消息,一段时间后,队列被消息填满;观察消息发送线程,发送消息函数直接返回,提示队列资源暂时不可用,直至队列有可用消息空间消息才发送成功。

acuity@ubuntu:/mnt/hgfs/LSW/STHB/pthreads/mq$ gcc mq.c -o mq -lpthread -lrt
acuity@ubuntu:/mnt/hgfs/LSW/STHB/pthreads/mq$ ./mq
new mqflags=[2048], old mqflags=[0]
thread0 recv 8Byte message:0x89 0xd4 0xb6 0x63 0x35 0xc1 0xc7 0xe4 
thread1 send 8Byte message
thread1 send 8Byte message
mq_send error: Resource temporarily unavailable

注:

【1】消息队列名称格式为“/name”,以斜杠“/”开头,名字中间不能带有斜杠

【2】编译时需加上“-lrt”链接,否则链接失败。


4 消息队列属性

  使用默认的消息队列属性可以满足绝大部分的应用场景,特殊场景也可以调整消息队列属性。posix消息队列属性以struct mq_attr数据结构表示,数据结构内容如下。

struct mq_attr
{
    long mq_flags;      /* 消息队列标识,用来表示是否阻塞 */
    long mq_maxmsg;     /* 消息队列最大消息数目 */
    long mq_msgsize;    /* 消息队列每条消息的最大长度 */
    long mq_curmsgs;    /* 消息队列当前消息数 */
};

  根据消息队列属性数据结构,消息队列属性包括:

  • 消息队列标识
  • 消息最大数目
  • 每条消息最大长度
  • 队列当前消息数目

4.1 消息队列标识

  消息队列标识属性mq_flags决定消息的是否阻塞,默认属性是阻塞(0),(O_NONBLOCK)表示非阻塞。


4.2 消息最大数目

  消息队列的消息最大数目mq_maxmsg,表示队列中的消息数目缓存空间,当队列中消息为及时被接收,存储的消息数目等于该值时(消息满),再往队列中发消息时返回失败,失败信息为队列已满。该属性只能在创建消息队列时设置,创建后不能更改。


4.3 消息最大长度

  消息队列的消息长度属性mq_msgsize,表示每次发送的消息长度不能超过该值,否则发送失败。该属性只能在创建消息队列时设置,创建后不能更改。


4.4 当前消息数目

  当前消息数目是一个状态属性mq_curmsgs,记录队列中未被接收读出的消息数目。该属性为只读属性,不可设置。


4.5 消息属性获取与设置

4.5.1 获取属性

mqd_t mq_getattr(mqd_t mqdes, struct mq_attr *attr);
  • mqdes,消息队列描述符
  • attr,存放消息队列属性实例地址
  • 返回,成功返回0,失败返回-1,错误码存于error

4.5.2 设置属性

mqd_t mq_setattr(mqd_t mqdes, struct mq_attr *newattr, struct mq_attr *oldattr);
  • mqdes,消息队列描述符
  • newattr,待设置消息队列属性实例地址
  • oldattr,保存设置前的消息队列属性,不用时传入NULL
  • 返回,成功返回0,失败返回-1,错误码存于error

  使用该函数设置一个已经创建的消息队列时,可以设置的属性只有mq_flags,其他属性会被忽略。因为mq_maxmsgmq_msgsize属性只能在创建消息队列时设定,而mq_curmsgs属性是只读的,不可设置。


4.6 消息队列持续性

  消息队列持续性指的是消息队列生命周期,消息队列持续性是随内核持续。消息队列可以认为是内核维护一个链表,系统内所有进程(线程)都可以像文件一样访问消息队列(前提是具有访问权限),即使进程(线程)终止退出,也不影响消息队列文件。就像磁盘上的文件一样,与系统的进程任务状态无关;但是消息队列是基于内存的,系统重启后也就消失。因此,消息队列是随内核持续性,只要内核不发生自举,消息队列就一直存在。


5 总结

  消息队列是进程和线程同步的一种机制。消息队列可以使得在进程(线程)间互斥访问的同时,可以传递不定长度的消息,具有很大的灵活性。消息队列还具有权限设定、阻塞属性特点,对于进程(线程)间的访问更加健壮性;此外,消息队列随内核的持续性,不会因为进程(线程)的退出而影响。消息队列常用于进程(线程)异步处理、应用程序解耦等等。

你可能感兴趣的:(Linux应用编程,#,POSIX线程,POSIX线程,线程同步,消息队列,物联网)