【Linux】小白也能懂的嵌入式 Linux 消息队列函数:基础概念与用法

目录

1.  概述

2.  特点

3.  System V

3.1  msgget()函数——创建或获取消息队列

3.2  msgsnd()函数——发送消息

3.3  msgrcv()函数——接收消息

3.4  msgctl函数()——控制操作(删除、获取信息等)

4.  POSIX

4.1  mq_open()函数——打开/创建消息队列

4.2  mq_timedsend()函数——发送消息

4.3  mq_timedreceive()——接收消息

4.4  mq_unlink()——删除队列


1.  概述

        早期通信机制之一的信号能够传送的信息量有限,管道则只能传送无格式的字节流,这无疑会给应用程序开发带来不便。消息队列则克服了这些缺点。消息队列的实质就是一个存放消息的链表,该链表由内核维护。可以把消息看作一个记录,具有特定的将式。一些进程可以向其中按照一定的规则添加新消息;另一些进程则可以从消息队列中读取消息。

        目前主要有两种类型的消息队列: POSIX消息队列以及System V消息队列,System V消息队列目前被大量使用,该消息队列是随内核持续的,只有在内核重启或者人工删除时该消息队列才会被删除。消息队列的内核持续性要求每个消息队列都在系统范围内对应一的键值,所以,要获得一个消息队列的描述字,必须提供该消息队列的键值。

        消息队列就是消息的一个链表,它允许一个或者多个进程向它写消息,或一个或多个进程向它读消息。在内核中以队列的方式管理,队列先进先出,是线性表。消息信息写在结构体中,并送到内核中,由内核管理。

        消息的发送不是同步机制,而是先发送到内核,只要消息没有被清除,则另一个程序无论何时打开都可以读取消息。消息可以用在同一程序之间(多个文件之间的信息传递)也可以用在不同进程之间。消息结构体必须自己定义,并按系统的要求定义。

struct msgbuf//结构体的名称自己定义
{
    long mtype;//必须是long,变量名必须是mtype
    char mdata[256];//必须是char,数组名和数组长度自己定义
};

        常见使用生产者-消费者模型,我们可以把消息队列想象成一个邮局快递柜:

  • 发送者(进程/线程)把数据打包成一个“消息”,然后放入队列中,无需等待接收者立即处理,就可以继续去做其他事情。

  • 接收者(进程/线程)在方便的时候,从队列中取出消息进行处理。如果队列为空,接收者可以选择等待(阻塞)或立即返回。

2.  特点

异步通信:发送和接收不需要同步进行,发送者发送完即可返回,降低了进程间的耦合度。

消息有边界:读取操作总是以一条完整的消息为单位,不会出现半条消息的情况。这区别于管道和套接字中的字节流模型。

有优先级:Linux 消息队列支持给消息赋予优先级,允许高优先级的消息被优先处理。

内核持久性:消息队列存在于内核中,其生命周期与创建它的进程无关。只有当系统重启或显式地删除队列时,队列才会被销毁。

多对多通信:多个进程可以向同一个队列写入消息,多个进程也可以从同一个队列读取消息。

        其实现方式有两种标准:System V IPC 和 POSIX IPC。

3.  System V

        这是 Linux 中传统的消息队列实现,使用一套标准的 System V IPC(进程间通信)函数。

3.1  msgget()函数——创建或获取消息队列

        头文件:

#include<sys/types.h>
#include<sys/ipc.h>
#include<sys/msg.h>

        函数原型:

/* Get messages queue.  */
extern int msgget (key_t __key, int __msgflg) __THROW;
  • key:键值,有 ftok() 函数获得,通常为一个整数,若键值为IPC_PRIVATE,则会创建一个只能被创建消息列表的进程读写的消息队列。
  • msgflg:标志位,用于设置消息队列的创建方式或权限,通常有一个9为的权限与以下值进行位操作后获得:
    ①IPC_CREAT:若内核中不存在指定消息队列,该函数会创建一个消息队列;若内核中已存在执行消息队列,则获取该消息队列。
    ②IPC_EXCL:与IPC_CREAT一起使用,表示如果创建的消息队列已存在,则返回错误。
    ③IPC_NOWAIT:读写消息队列要求无法得到满足时,不阻塞。

        返回值,如果成功则返回消息队列标识符(非负整数),否则为-1。

        创建一个消息队列并打印其ID:

#include <stdio.h>
#include <stdlib.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <errno.h>

int main() 
{
    key_t key;
    int msqid;

    // 1. 生成一个键值
    key = ftok("/tmp", 'A');
    if (key == -1) 
    {
        perror("ftok");
        exit(1);
    }

    // 2. 尝试创建消息队列,如果已存在则失败
    msqid = msgget(key, IPC_CREAT | IPC_EXCL | 0666);
    if (msqid == -1) 
    {
        // 如果失败是因为队列已存在,那我们尝试获取它
        if (errno == EEXIST) 
        {
            printf("消息队列已存在,正在获取它...\n");
            msqid = msgget(key, 0666); // 只获取,不创建
            if (msqid == -1) 
            {
                perror("msgget (get existing)");
                exit(1);
            }
        }
        else 
        {
            // 其他错误
            perror("msgget (create)");
            exit(1);
        }
    } 
    else 
    {
        printf("消息队列创建成功\n");
    }

    printf("消息队列ID: %d\n", msqid);

    // ... 这里可以使用 msqid 进行 msgsnd, msgrcv 等操作 ...

    return 0;
}

        Makefile函数如下:

cc := gcc

system_test : system_test.c
	-$(cc) -o $@ $^
	-./$@
	-rm ./$@

        运行两次,可以看到一次是创建,一次是获取:

3.2  msgsnd()函数——发送消息

        头文件:

#include<sys/types.h>
#include<sys/ipc.h>
#include<sys/msg.h>

        函数原型:

/* Send message to message queue.

   This function is a cancellation point and therefore not marked with
   __THROW.  */
extern int msgsnd (int __msqid, const void *__msgp, size_t __msgsz,
		   int __msgflg);
  • msqid:消息队列标识符,指定要向哪个消息队列发送消息,由 msgget 函数返回;
  • msgp:指向要发送消息的指针,第一个字段必须是 long mtype(消息类型),后面跟着实际的数据内容,消息类型必须是一个大于 0 的整数,如:
struct msgbuf {
    long mtype;       // 消息类型,必须 > 0
    char mtext[1];    // 消息数据,可以是任意类型
};

// 或者更实用的定义:
struct my_message {
    long mtype;
    char text[100];
    int  number;
    // 可以添加更多字段...
};
  • msgsz:消息数据部分的大小(字节数),需要注意的是这个大小不包括 mtype 字段的大小,只计算 mtype 之后的数据部分;
  • msgflg:控制发送行为的标志位,常用标志:
    ①0:阻塞模式。如果队列已满,调用进程会阻塞(睡眠),直到有空间可用。
    ②IPC_NOWAIT:非阻塞模式。如果队列已满,函数立即返回 -1,并设置 errno 为 EAGAIN。

        返回值,成功返回0,失败返回-1。

        我们重新创建一个用于发送的.c文件,当做生产者:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <errno.h>

// 定义消息结构
struct message 
{
    long mtype;          // 消息类型,必须 > 0
    char mtext[100];     // 消息内容
};

int main() 
{
    key_t key;
    int msqid;
    struct message msg;

    // 1. 生成一个键值
    key = ftok("/tmp", 'A');
    if (key == -1) 
    {
        perror("ftok");
        exit(1);
    }

    // 2. 尝试创建消息队列,如果已存在则失败
    msqid = msgget(key, IPC_CREAT | IPC_EXCL | 0666);
    if (msqid == -1) 
    {
        // 如果失败是因为队列已存在,那我们尝试获取它
        if (errno == EEXIST) 
        {
            printf("消息队列已存在,正在获取它...\n");
            msqid = msgget(key, 0666); // 只获取,不创建
            if (msqid == -1) 
            {
                perror("msgget (get existing)");
                exit(1);
            }
        }
        else 
        {
            // 其他错误
            perror("msgget (create)");
            exit(1);
        }
    } 
    else 
    {
        printf("消息队列创建成功\n");
    }

    printf("消息队列ID: %d\n", msqid);

    // 3. 准备并发送消息
    msg.mtype = 1;  // 设置消息类型为1
    
    printf("请输入要发送的消息: ");
    fgets(msg.mtext, sizeof(msg.mtext), stdin);
    
    // 移除换行符
    msg.mtext[strcspn(msg.mtext, "\n")] = '\0';

    // 发送消息(不阻塞方式)
    if (msgsnd(msqid, &msg, strlen(msg.mtext) + 1, IPC_NOWAIT) == -1) 
    {
        perror("msgsnd");
        
        // 如果是因为队列满,可以尝试阻塞方式
        if (errno == EAGAIN) 
        {
            printf("消息队列已满,正在以阻塞方式重新发送...\n");
            if (msgsnd(msqid, &msg, strlen(msg.mtext) + 1, 0) == -1) 
            {
                perror("msgsnd (blocking)");
                exit(1);
            }
        }
        else 
        {
            exit(1);
        }
    }

    printf("消息发送成功: %s\n", msg.mtext);
    printf("消息大小: %zu 字节\n", strlen(msg.mtext) + 1);    

    return 0;
}

        我们现在将数据发送出来了,那么怎么查看呢?我们可以通过接收函数查看。

3.3  msgrcv()函数——接收消息

        头文件:

#include<sys/types.h>
#include<sys/ipc.h>
#include<sys/msg.h>

        函数原型:

/* Receive message from message queue.

   This function is a cancellation point and therefore not marked with
   __THROW.  */
extern ssize_t msgrcv (int __msqid, void *__msgp, size_t __msgsz,
		       long int __msgtyp, int __msgflg);
  • msqid:消息队列标识符,指定要从哪个消息队列接收消息,由 msgget 函数返回;
  • msgp:指向接收消息缓冲区的指针,缓冲区必须足够大以容纳消息,第一个字段必须是 long mtype;
  • msgsz:接收缓冲区中数据部分的最大容量(字节数),这个大小不包括 mtype 字段的大小,只计算可用于存储数据部分的空间;
  • msgtyp:指定要接收的消息类型,这是 msgrcv 最强大的特性之一:
msgtyp 值行为描述
0读取队列中的第一条消息(不管什么类型)
> 0读取队列中第一条类型等于 msgtyp 的消息
< 0读取队列中类型值小于等于 |msgtyp| 的消息中类型值最小的第一条
  • msgflg:控制接收行为的标志位,常用标志:
    ①0:阻塞模式。如果队列中没有符合条件的消息,调用进程会阻塞;
    ②IPC_NOWAIT:非阻塞模式。如果没有消息,立即返回 -1,设置 errno 为 ENOMSG;
    ③MSG_NOERROR:如果消息数据实际长度大于 msgsz,则截断消息而不报错。如果没有这个标志,会返回 E2BIG 错误;
    ④MSG_EXCEPT(Linux特有):当 msgtyp > 0 时,接收第一条类型不等于 msgtyp 的消息。

        返回值,成功返回实际拷贝到 mtext 字段的字节数,失败返回-1。

        编写消费者,进行接收生产者发送的数据:

#include <stdio.h>
#include <stdlib.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <errno.h>

// 定义消息结构(必须与发送端一致)
struct message {
    long mtype;
    char mtext[100];
};

int main() 
{
    key_t key;
    int msqid;
    struct message msg;

    // 1. 生成相同的键值
    key = ftok("/tmp", 'A');
    if (key == -1) 
    {
        perror("ftok");
        exit(1);
    }

    // 2. 获取消息队列
    msqid = msgget(key, 0666);
    if (msqid == -1) 
    {
        perror("msgget");
        exit(1);
    }

    printf("等待接收消息...\n");

    // 3. 接收消息(阻塞方式)
    // 参数说明:消息队列ID, 消息缓冲区, 消息数据大小, 消息类型, 标志位
    if (msgrcv(msqid, &msg, sizeof(msg.mtext), 1, 0) == -1) 
    {
        perror("msgrcv");
        exit(1);
    }

    printf("收到消息: %s\n", msg.mtext);
    printf("消息类型: %ld\n", msg.mtype);

    return 0;
}

        运行一下看一下:

3.4  msgctl函数()——控制操作(删除、获取信息等)

        头文件:

#include<sys/types.h>
#include<sys/ipc.h>
#include<sys/msg.h>

        函数原型:

extern int msgctl (int __msqid, int __cmd, struct msqid_ds *__buf) __THROW;
  • msqid:消息队列标识符,指定要操作的消息队列,由 msgget 函数返回;
  • cmd:控制命令,指定要执行的操作;
命令描述
IPC_RMID0立即删除消息队列
IPC_STAT1获取消息队列的状态信息
IPC_SET2设置消息队列的参数
  • buf:指向 msqid_ds 结构体的指针。
    ①IPC_STAT:用于存储获取到的状态信息;
    ②IPC_SET:提供要设置的新参数;
    ③IPC_RMID:忽略(通常为 NULL)。

4.  POSIX

4.1  mq_open()函数——打开/创建消息队列

#include <fcntl.h>    /* For O_* constants */
#include <sys/stat.h> /* For mode constants */
#include <mqueue.h>

/**
 * @brief 创建或打开一个已存在的POSIX消息队列,消息队列是通过名称唯一标识的。
 *
 * @param name 消息队列的名称
 * 命名规则:必须是以正斜杠/开头,以\0结尾的字符串,中间可以包含若干字符,但不能有正斜杠
 * @param oflag 指定消息队列的控制权限,必须也只能包含以下三者之一
 * O_RDONLY 打开的消息队列只用于接收消息
 * O_WRONLY 打开的消息队列只用于发送消息
 * O_RDWR 打开的消息队列可以用于收发消息
 * 可以与以下选项中的0至多个或操作之后作为oflag
 * O_CLOEXEC 设置close-on-exec标记,这个标记表示执行exec时关闭文件描述符
 * O_CREAT 当文件描述符不存在时创建它,如果指定了这一标记,需要额外提供mode和attr参数
 * O_EXCL 创建一个当前进程独占的消息队列,要同时指定O_CREAT,要求创建的消息队列不存在,否则将会失败,并提示错误EEXIST
 * O_NONBLOCK 以非阻塞模式打开消息队列,如果设置了这个选项,在默认情况下收发消息发生阻塞时,会转而失败,并提示错误EAGAIN
 * @param mode 每个消息队列在mqueue文件系统对应一个文件,mode是用来指定消息队列对应文件的权限的
 * @param attr 属性信息,如果为NULL,则队列以默认属性创建

* @return mqd_t 成功则返回消息队列描述符,失败则返回(mqd_t)-1,同时设置errno以指明错误原因
*/
mqd_t mq_open(const char *name, int oflag, mode_t mode, struct mq_attr *attr);

/**
 * @brief 当oflag没有包含O_CREAT时方可调用
 *
 * @param name 同上
 * @param oflag 同上
 * @return mqd_t 同上
 */
mqd_t mq_open(const char *name, int oflag);

4.2  mq_timedsend()函数——发送消息

#include <time.h>
#include <mqueue.h>

/**
 * @brief 将msg_ptr指向的消息追加到消息队列描述符mqdes指向的消息队列的尾部。如果消息队列已满,默认情况下,调用阻塞直至有充足的空间允许新的消息入队,或者达到abs_timeout指定的等待时间节点,或者调用被信号处理函数打断。需要注意的是,正如上文提到的,如果在mq_open时指定了O_NONBLOCK标记,则转而失败,并返回错误EAGAIN。
 * 
 * @param mqdes 消息队列描述符
 * @param msg_ptr 指向消息的指针
 * @param msg_len msg_ptr指向的消息长度,不能超过队列的mq_msgsize属性指定的队列最大容量,长度为0的消息是被允许的
 * @param msg_prio 一个非负整数,指定了消息的优先级,消息队列中的数据是按照优先级降序排列的,如果新旧消息的优先级相同,则新的消息排在后面。
 * @param abs_timeout 指向struct timespec类型的对象,指定了阻塞等待的最晚时间。如果消息队列已满,且abs_timeout指定的时间节点已过期,则调用立即返回。
 * @return int 成功返回0,失败返回-1,同时设置errno以指明错误原因。
 */
int mq_timedsend(mqd_t mqdes, const char *msg_ptr, size_t msg_len, unsigned int msg_prio, const struct timespec *abs_timeout);

4.3  mq_timedreceive()——接收消息

#include <time.h>
#include <mqueue.h>

/**
 * @brief 从消息队列中取走最早入队且权限最高的消息,将其放入msg_ptr指向的缓存中。如果消息队列为空,默认情况下调用阻塞,此时的行为与mq_timedsend同理。
 * 
 * @param mqdes 消息队列描述符
 * @param msg_ptr 接收消息的缓存
 * @param msg_len msg_ptr指向的缓存区的大小,必须大于等于mq_msgsize属性指定的队列单条消息最大字节数
 * @param msg_prio 如果不为NULL,则用于接收接收到的消息的优先级 
 * @param abs_timeout 阻塞时等待的最晚时间节点,同mq_timedsend
 * @return ssize_t 成功则返回接收到的消息的字节数,失败返回-1,并设置errno指明错误原因
 */
ssize_t mq_timedreceive(mqd_t mqdes, char *msg_ptr, size_t msg_len, unsigned int *msg_prio, const struct timespec *abs_timeout);

4.4  mq_unlink()——删除队列

#include <mqueue.h>

/**
 * @brief 清除name对应的消息队列,mqueue文件系统中的对应文件被立即清除。消息队列本身的清除必须等待所有指向该消息队列的描述符全部关闭之后才会发生。
 * 
 * @param name 消息队列名称
 * @return int 成功返回0,失败返回-1,并设置errno指明错误原因
 */
int mq_unlink(const char *name);

嵌入式Linux_时光の尘的博客-优快云博客

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

时光の尘

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值