MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Linux C语言消息队列使用的要点

2022-05-186.3k 阅读

消息队列概述

1. 消息队列的概念

在Linux系统中,消息队列是一种进程间通信(IPC,Inter - Process Communication)的机制。它允许不同的进程通过发送和接收消息来进行数据交换。消息队列就像是一个信箱,进程可以向这个信箱投递消息,也可以从信箱中取出消息。每个消息都有一个特定的类型,接收方可以根据消息类型有选择地接收消息,这使得消息队列在数据交互方面具有很强的灵活性。

2. 消息队列与其他IPC机制的比较

  • 管道(Pipe):管道是一种半双工的通信方式,数据只能单向流动,并且只能在具有亲缘关系(如父子进程)的进程间使用。而消息队列可以实现全双工通信,且不局限于亲缘关系的进程。
  • 共享内存(Shared Memory):共享内存是最快的IPC机制,它允许多个进程直接访问同一块内存区域。然而,共享内存需要额外的同步机制(如信号量)来保证数据的一致性,否则容易出现数据竞争问题。消息队列则自带同步机制,消息的发送和接收是顺序进行的,相对更简单易用。
  • 信号量(Semaphore):信号量主要用于进程间的同步,控制对共享资源的访问。它本身并不传输数据,而消息队列的主要目的是在进程间传递数据。

消息队列的相关系统调用

1. msgget函数

  • 函数原型int msgget(key_t key, int msgflg);
  • 参数说明
    • key:是一个键值,用于标识一个特定的消息队列。可以使用ftok函数生成一个唯一的键值。例如,key = ftok(".", 'a');,这里ftok函数通过当前目录(“.”)和一个字符(‘a’)生成一个键值。这个键值在系统中应该是唯一的,用于确保不同进程能够访问到同一个消息队列。
    • msgflg:是一组标志位,用于指定消息队列的创建和访问权限。例如,IPC_CREAT表示如果消息队列不存在则创建它,0666表示设置消息队列的权限为可读可写,其他用户也有相应的读写权限。
  • 返回值:成功时返回消息队列标识符,失败时返回 -1。例如:
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <stdio.h>

int main() {
    key_t key;
    int msgid;
    key = ftok(".", 'a');
    if (key == -1) {
        perror("ftok");
        return 1;
    }
    msgid = msgget(key, IPC_CREAT | 0666);
    if (msgid == -1) {
        perror("msgget");
        return 1;
    }
    printf("Message queue created with ID: %d\n", msgid);
    return 0;
}

2. msgsnd函数

  • 函数原型int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
  • 参数说明
    • msqid:是通过msgget函数获取的消息队列标识符。
    • msgp:是一个指向消息结构体的指针。消息结构体的定义通常如下:
struct msgbuf {
    long mtype; // 消息类型,必须大于0
    char mtext[100]; // 消息正文,这里定义为100字节,可以根据需要调整
};
- `msgsz`:是消息正文(不包括消息类型字段)的长度。例如,对于上述`msgbuf`结构体,`msgsz = sizeof(struct msgbuf) - sizeof(long)`。
- `msgflg`:标志位。例如,`IPC_NOWAIT`表示如果消息队列已满,不等待直接返回。通常设置为0,表示如果消息队列满则等待。
  • 返回值:成功时返回0,失败时返回 -1。以下是一个发送消息的示例:
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <stdio.h>
#include <string.h>

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

int main() {
    key_t key;
    int msgid;
    struct msgbuf message;
    key = ftok(".", 'a');
    msgid = msgget(key, IPC_CREAT | 0666);
    if (msgid == -1) {
        perror("msgget");
        return 1;
    }
    message.mtype = 1;
    strcpy(message.mtext, "Hello, message queue!");
    if (msgsnd(msgid, &message, sizeof(message.mtext), 0) == -1) {
        perror("msgsnd");
        return 1;
    }
    printf("Message sent successfully\n");
    return 0;
}

3. msgrcv函数

  • 函数原型ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);
  • 参数说明
    • msqid:消息队列标识符。
    • msgp:指向接收消息的结构体指针,与msgsnd中使用的结构体类型一致。
    • msgsz:接收消息正文的最大长度。
    • msgtyp:指定要接收的消息类型。如果msgtyp为0,则接收队列中的第一个消息;如果msgtyp大于0,则接收类型等于msgtyp的第一个消息;如果msgtyp小于0,则接收类型小于或等于msgtyp绝对值的消息中类型最小的第一个消息。
    • msgflg:标志位。例如,IPC_NOWAIT表示如果没有符合条件的消息,不等待直接返回;MSG_NOERROR表示如果消息正文长度大于msgsz,则截断消息正文而不报错。
  • 返回值:成功时返回接收到的消息正文长度,失败时返回 -1。以下是接收消息的示例:
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <stdio.h>
#include <string.h>

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

int main() {
    key_t key;
    int msgid;
    struct msgbuf receivedMessage;
    key = ftok(".", 'a');
    msgid = msgget(key, IPC_CREAT | 0666);
    if (msgid == -1) {
        perror("msgget");
        return 1;
    }
    if (msgrcv(msgid, &receivedMessage, sizeof(receivedMessage.mtext), 1, 0) == -1) {
        perror("msgrcv");
        return 1;
    }
    printf("Received message: %s\n", receivedMessage.mtext);
    return 0;
}

4. msgctl函数

  • 函数原型int msgctl(int msqid, int cmd, struct msqid_ds *buf);
  • 参数说明
    • msqid:消息队列标识符。
    • cmd:操作命令。常见的命令有IPC_STAT,用于获取消息队列的状态信息并存储到buf指向的结构体中;IPC_SET,用于设置消息队列的属性,属性值从buf指向的结构体中获取;IPC_RMID,用于删除消息队列。
    • buf:是一个指向msqid_ds结构体的指针,该结构体定义了消息队列的各种属性,如消息队列的权限、所有者信息、消息数量等。在使用IPC_STATIPC_SET命令时需要用到,使用IPC_RMID时可以设为NULL
  • 返回值:成功时返回0,失败时返回 -1。以下是删除消息队列的示例:
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <stdio.h>

int main() {
    key_t key;
    int msgid;
    key = ftok(".", 'a');
    msgid = msgget(key, IPC_CREAT | 0666);
    if (msgid == -1) {
        perror("msgget");
        return 1;
    }
    if (msgctl(msgid, IPC_RMID, NULL) == -1) {
        perror("msgctl");
        return 1;
    }
    printf("Message queue deleted successfully\n");
    return 0;
}

消息队列使用要点

1. 消息类型的合理使用

  • 类型匹配原则:在发送和接收消息时,消息类型的匹配至关重要。如前文所述,接收方可以根据不同的msgtyp值来选择接收特定类型的消息。这在实际应用中非常有用,例如,在一个多模块协作的系统中,不同模块可能发送不同类型的消息,接收方可以根据自身需求只接收特定类型的消息。假设一个监控系统,监控模块发送设备状态消息(类型1)和异常报警消息(类型2),显示模块可能只需要接收设备状态消息,而处理模块需要接收异常报警消息。通过合理设置消息类型,各模块可以高效地进行通信。
  • 避免类型冲突:在设计消息队列应用时,要确保不同模块发送的消息类型不会冲突。可以通过统一规划消息类型来解决这个问题。例如,为每个模块分配一定范围的消息类型,如模块1使用1 - 100的类型,模块2使用101 - 200的类型等。这样可以避免因消息类型重复导致的接收混乱。

2. 消息队列的同步与并发控制

  • 阻塞与非阻塞模式msgsndmsgrcv函数中的msgflg参数可以设置阻塞或非阻塞模式。在阻塞模式下(msgflg为0),如果消息队列满(对于msgsnd)或没有符合条件的消息(对于msgrcv),进程会被挂起,直到条件满足。这种模式适用于需要确保消息准确发送或接收的场景。例如,在一个数据处理流程中,下一个阶段必须等待上一个阶段发送的消息到达后才能继续处理,此时阻塞模式是合适的。
  • 非阻塞模式(IPC_NOWAIT:适用于进程不能长时间等待的情况。例如,在一个实时监控系统中,监控进程需要周期性地检查消息队列,如果没有新消息,它不能一直等待,而是继续执行其他监控任务。在这种情况下,使用IPC_NOWAIT标志可以使msgrcv函数立即返回,进程可以继续执行后续代码。
  • 并发访问控制:当多个进程同时访问消息队列时,可能会出现并发问题。虽然消息队列本身有一定的同步机制,但在复杂场景下,可能需要额外的同步手段。例如,可以使用信号量来控制对消息队列的访问。假设多个进程都要向消息队列发送消息,为了避免消息发送混乱,可以使用一个信号量来保证同一时间只有一个进程能够调用msgsnd函数。

3. 消息队列的大小限制

  • 系统限制:Linux系统对消息队列的大小有一定限制。这些限制包括单个消息的最大长度、消息队列中所有消息的总长度等。可以通过查看系统文档或使用sysctl命令来了解具体的限制值。例如,在一些系统中,单个消息的最大长度可能是8192字节,消息队列的总长度可能是65536字节。
  • 应用设计考虑:在设计应用时,要根据系统限制来合理规划消息的大小和数量。如果消息内容较大,可能需要考虑分拆消息或者采用其他IPC机制。例如,在传输大文件时,消息队列可能不是最佳选择,因为单个消息大小可能超过系统限制。可以先将文件分块,然后为每个块分配一个消息进行传输,但要注意在接收端正确地重组文件。

4. 错误处理

  • 系统调用错误处理:在使用msggetmsgsndmsgrcvmsgctl等系统调用时,要及时检查返回值并进行错误处理。常见的错误包括消息队列不存在(ENOENT)、权限不足(EACCES)、消息队列满(EAGAIN,在非阻塞模式下)等。例如,在调用msgget函数时,如果返回 -1,通过perror函数可以输出具体的错误信息,帮助开发者定位问题。
msgid = msgget(key, IPC_CREAT | 0666);
if (msgid == -1) {
    perror("msgget");
    return 1;
}
  • 应用层错误处理:除了系统调用的错误,在应用层也可能出现错误,如消息格式错误、消息类型不匹配等。在接收消息后,应该对消息内容进行合法性检查,确保消息符合预期的格式和类型。例如,在接收一个包含设备状态的消息时,要检查消息中的数据字段是否符合设备状态的定义,如温度值是否在合理范围内等。

5. 消息队列的持久化

  • 基本概念:默认情况下,消息队列在系统重启后会被删除。如果需要消息队列在系统重启后仍然存在,可以考虑消息队列的持久化。一种方法是将消息队列的相关信息(如键值、标识符等)存储在文件中,在系统重启后,应用程序可以读取文件中的信息并重新创建消息队列。
  • 实现示例:可以在程序启动时检查存储消息队列信息的文件是否存在。如果存在,读取文件中的键值,使用msgget函数重新获取消息队列标识符。在程序退出时,将消息队列的相关信息写入文件。以下是一个简单的示例框架:
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>

#define FILE_NAME "msgqueue_info.txt"

int main() {
    key_t key;
    int msgid;
    int fd;
    // 检查文件是否存在
    fd = open(FILE_NAME, O_RDONLY);
    if (fd != -1) {
        // 从文件中读取键值
        read(fd, &key, sizeof(key));
        close(fd);
        msgid = msgget(key, 0666);
        if (msgid == -1) {
            perror("msgget");
            return 1;
        }
    } else {
        // 文件不存在,创建新的消息队列
        key = ftok(".", 'a');
        msgid = msgget(key, IPC_CREAT | 0666);
        if (msgid == -1) {
            perror("msgget");
            return 1;
        }
        // 将键值写入文件
        fd = open(FILE_NAME, O_WRONLY | O_CREAT, 0666);
        if (fd == -1) {
            perror("open");
            return 1;
        }
        write(fd, &key, sizeof(key));
        close(fd);
    }
    // 后续消息队列操作
    //...
    return 0;
}

6. 性能优化

  • 批量操作:如果需要发送或接收大量消息,可以考虑批量操作。例如,一次性发送多个小消息可以减少系统调用的次数,提高性能。可以将多个小消息组合成一个大消息结构体进行发送,在接收端再进行拆分。假设要发送多个设备的状态消息,可以将这些设备状态信息合并到一个消息结构体中,通过一次msgsnd调用发送出去。
  • 减少内存拷贝:在消息的发送和接收过程中,尽量减少不必要的内存拷贝。例如,在定义消息结构体时,可以直接在结构体中定义数据缓冲区,避免在发送和接收时进行额外的内存分配和拷贝。另外,对于大消息,可以使用内存映射(mmap)技术来减少内存拷贝。

7. 安全性

  • 权限设置:在创建消息队列时,要合理设置权限(通过msgget函数的msgflg参数)。确保只有授权的进程能够访问消息队列,避免未授权的进程读取或修改消息。例如,只允许特定用户组的进程访问消息队列,可以通过设置msgflgIPC_CREAT | 0660,并将消息队列的所有者设置为特定用户组来实现。
  • 消息内容加密:如果消息内容包含敏感信息,如密码、机密数据等,应该对消息内容进行加密。可以使用常见的加密算法,如AES(高级加密标准),在发送消息前对消息正文进行加密,在接收消息后进行解密。这样可以防止消息在传输过程中被窃取或篡改。

消息队列在实际项目中的应用案例

1. 分布式日志系统

  • 系统架构:在一个分布式系统中,各个节点产生的日志需要集中收集和处理。可以使用消息队列来实现日志的传输。每个节点将日志信息封装成消息发送到消息队列,日志处理服务器从消息队列中接收日志消息并进行处理,如存储到数据库、进行分析等。
  • 消息队列的作用:消息队列在这里起到了缓冲和异步处理的作用。各个节点可以随时发送日志消息,而不需要等待日志处理服务器的响应。日志处理服务器可以按照自己的节奏从消息队列中读取消息进行处理,避免了因某个节点日志产生过快导致处理服务器过载的问题。
  • 实现要点:为每个节点分配不同的消息类型,以便日志处理服务器可以根据消息类型区分不同节点的日志。同时,要考虑消息队列的持久化,确保在日志处理服务器重启时不会丢失未处理的日志消息。

2. 任务调度系统

  • 系统架构:任务调度系统负责将不同的任务分配到合适的执行节点上。任务发起方将任务描述信息封装成消息发送到消息队列,任务调度器从消息队列中读取任务消息,并根据任务的优先级、节点负载等因素将任务分配到相应的执行节点。
  • 消息队列的作用:消息队列作为任务的暂存区,任务发起方不需要关心任务何时被处理,只需要将任务消息发送到队列即可。任务调度器可以根据系统状态灵活地从队列中选择任务进行调度,提高了系统的灵活性和可扩展性。
  • 实现要点:在消息结构体中定义任务的优先级字段,任务调度器根据优先级选择任务。同时,要处理好任务调度器和执行节点之间的通信,确保任务能够准确地分配到执行节点,并且执行节点能够及时反馈任务执行结果。可以通过在消息队列中设置不同类型的消息来实现任务分配和结果反馈的通信。

3. 实时数据处理系统

  • 系统架构:在实时数据处理系统中,如股票交易数据处理、物联网设备数据处理等,传感器或数据源不断产生实时数据。这些数据被封装成消息发送到消息队列,数据处理模块从消息队列中读取数据进行实时分析和处理,如计算统计指标、检测异常等。
  • 消息队列的作用:消息队列可以平滑数据流量,避免因数据源产生数据过快导致数据处理模块过载。同时,它为数据处理模块提供了一个统一的数据入口,使得数据处理模块可以专注于数据处理逻辑,而不需要关心数据的来源和速率。
  • 实现要点:根据数据的时效性要求,合理设置消息队列的接收策略。对于时效性要求高的数据,可以优先接收处理。另外,要考虑消息队列的性能优化,确保能够快速处理大量的实时数据。可以采用批量接收和处理数据的方式,减少系统调用次数,提高处理效率。