POSIX 消息队列

定义

POSIX消息队列,它允许进程之间以消息的形式交换数据,即数据交换的基本单位是整个消息

消息队列就是存储消息的队列

特性

  • 引用计数

    POSIX 消息队列是引用计数的。只有当所有当前使用队列的进程都关闭了队列之后会对队列进行标记以便删除

  • 优先级排队

    POSIX消息有一个关联的优先级,并且消息之间是严格按照优先级顺序排队的(以及接收)

  • 异步通知

    POSIX 消息队列提供了一个特性允许在队列中的一条消息可用时异步地通知进程

消息队列的打开、关闭、删除

mq_open()

mq_open()函数创建一个新消息队列或打开一个既有队列

mqd_t mq_open(const char *name, int oflag, ...
               /* mode_t mode, struct mq_attr *attr */);

mq_open()通常用来打开一个既有消息队列,这种调用只需要两个参数,但如果在 flags中指定了O_CREAT,那么就还需要另外两个参数:mode和attr

  • 参数

    • name: 消息队列的名称,以斜杠开头

    • oflag: 打开选项,控制打开方式

      1

    • mode: 新创建消息队列的权限,受umask掩码的影响

    • attr: 用于设置队列的属性(如最大消息数、最大消息大小等),attr为NULL,那么将使用实现定义的默认属性创建队列

  • 返回值

    • 返回一个消息队列描述符(mqd_t),成功时大于或等于0;失败时返回(mqd_t)-1,并设置errno以指示错误类型

mq_close()

mq_close()函数关闭消息队列描述符mqdes。

int mq_close(mqd_t mqdes);

关闭消息队列会自动删除该进程注册在该队列上的消息通知,关闭并不意味着删除该消息队列,其他进程仍旧可以打开它

mq_unlink()函数删除通过 name 标识的消息队列,并将队列标记为在所有进程使用完该队列之后销毁该队列(这可能意味着会立即删除,前提是所有打开该队列的进程已经关闭了该队列)

int mq_unlink(const char *name);

fork()、exec()以及进程终止对消息队列描述符的影响

在fork()中子进程会接收其父进程的消息队列描述符的副本,并且这些描述符会引用同样的打开着的消息队列描述。子进程不会继承其父进程的任何消息通知注册。

当一个进程执行了一个exec()或终止时,所有其打开的消息队列描述符会被关闭。关闭消息队列描述符的结果是进程在相应队列上的消息通知注册会被注销。

描述符和消息队列之间的关系

2

与文件描述符类似

消息队列的属性

属性的数据结构

3

设置属性

  • 在创建时设置消息队列属性,mq_open()
  • 使用 mq_setattr() 修改属性
int mq_setattr(mqd_t mqdes, const struct mq_attr *newattr, 
              struct mq_attr *oldattr);

获取属性

int mq_getattr(mqd_t mqdes, struct mq_attr *attr);

交换消息

发送消息mq_send()

mq_send()函数将位于 msg_ptr 指向的缓冲区中的消息添加到描述符mqdes所引用的消息队列中。

int mq_send(mqd_t mqdes, const char *msg_ptr, size_t msg_len
                  unsigned int msg_prio);
  • 参数

    • mqdes: 消息队列描述符
    • msg_ptr: 指向消息的指针
    • msg_len: 消息的长度
    • msg_prio: 消息的优先级,非负整数表示,数值越小优先级越低
  • 返回值
    0表示成功,-1表示失败

如果消息队列已经满了(即已经达到了队列的mq_maxmsg限制),那么后续的mq_send()调用会阻塞直到队列中存在可用空间为止或者在O_NONBLOCK标记起作用时立即失败并返回EAGAIN错误。

接收消息mq_receive()

mq_receive()函数从 mqdes 引用的消息队列中删除一条优先级最高、存在时间最长的消息并将删除的消息放置在msg_ptr指向的缓冲区。

这里的删除其实就是接收消息,所以肯定是优先级最高,同级别里存在时间最长的消息最先被接收,也就是删除

ssize_t mq_receive(mqd_t mqdes, char *msg_ptr, size_t msg_len,
                        unsigned int msg_prio);
  • 参数

    • mqdes: 消息队列描述符
    • msg_ptr: 指向存放消息的缓冲区
    • msg_len: 缓冲区的长度
    • msg_prio: 存储接收消息的优先级,可以为NULL
  • 返回值
    返回读取到的字节数,-1表示失败

如果消息队列当前为空,那么 mq_receive()会阻塞直到存在可用的消息或在
O_NONBLOCK标记起作用时会立即失败并返回EAGAIN错误。

消息通知

POSIX消息队列允许一个进程能够请求消息到达通知,然后继续执行其他任务直到收到通知为止。进程可以选择通过信号的形式或通过在一个单独的线程中调用一个函数的形式来接收通知。

特别要注意以下:

  • 只有一个进程可以向消息队列注册通知,消息队列已注册的话,其他进程在注册会失败
  • 只有当消息队列为空时,新消息的到来才会通知进程,如果注册时不为空,则只能等消息队列清空后才能通知
  • 消息通知一次后,会自动删除注册信息,这样其他进程就能注册了
  • 在一个消息队列中,进程的接收行为是互相影响的。如果有进程在等待消息,新的注册进程不会被通知,直到这些阻塞的进程处理完消息

注册消息到达通知

int mq_notify(mqd_t mqdes, const struct sigevent *notification);
  • 参数
    • mqdes: 要监听的消息队列的描述符
    • notification: 指定进程接收通知的机制,为NULL表示取消之前的注册信息

以下是 sigevent 结构体的简化版本
5

需要比较注意的是 sigev_notify 字段,它指定了消息通知的机制,有以下三种
这段话描述了三种通知机制,供进程在消息队列中接收消息时使用:

  • SIGEV_NONE:进程注册接收通知,但当消息进入空队列时不会通知该进程。新消息到来后,注册信息会被删除

  • SIGEV_SIGNAL:通过发送指定信号(在sigev_signo字段中定义)来通知进程。如果是实时信号,sigev_value字段会包含信号携带的数据。信号处理时可以通过siginfo_t结构获得消息相关信息,如发送者的进程ID和用户ID

  • SIGEV_THREAD:通过调用sigev_notify_function指定的函数来通知进程,类似于在新线程中执行该函数。sigev_notify_attributes字段可为NULL或指向线程特性结构的指针,sigev_value中的值作为参数传入该函数

消息通知机制

  • SIGEV_NONE(默认):不会通知注册进程,由注册进程自己接收
int main() {
    mqd_t mq;
    struct mq_attr attr;
    char buffer[1024];

    // 创建消息队列
    attr.mq_flags = 0;
    attr.mq_maxmsg = 10;
    attr.mq_msgsize = sizeof(buffer);
    attr.mq_curmsgs = 0;

    mq = mq_open("/test_queue", O_CREAT | O_RDONLY, 0644, &attr);
    
    // 进程注册SIGEV_NONE(默认)
    // 只会在调用mq_receive时处理消息

    while (1) {
        ssize_t bytes_read = mq_receive(mq, buffer, sizeof(buffer), NULL);
        if (bytes_read >= 0) {
            buffer[bytes_read] = '\0';
            printf("Received: %s\n", buffer);
        }
    }

    mq_close(mq);
    mq_unlink("/test_queue");
    return 0;
}
  • SIGEV_SIGNAL:通过 信号机制 通知注册进程
void signal_handler(int signo, siginfo_t *info, void *context) {
    printf("Received signal %d\n", signo);
}

int main() {
    mqd_t mq;
    struct mq_attr attr;
    struct sigevent sev;
    struct sigaction sa;

    // 设置信号处理器
    sa.sa_flags = SA_SIGINFO;
    sa.sa_sigaction = signal_handler;
    sigaction(SIGUSR1, &sa, NULL);

    // 创建消息队列
    attr.mq_flags = 0;
    attr.mq_maxmsg = 10;
    attr.mq_msgsize = 256;
    attr.mq_curmsgs = 0;

    mq = mq_open("/test_queue", O_CREAT | O_RDONLY | O_NONBLOCK, 0644, &attr);

    // 注册mq_notify
    sev.sigev_notify = SIGEV_SIGNAL;
    sev.sigev_signo = SIGUSR1; // 指定信号
    sev.sigev_value.sival_ptr = NULL; // 这里可以设置消息内容

    mq_notify(mq, &sev); // 注册通知

    // 发送测试消息
    char *msg = "Hello, World!";
    mq_send(mq, msg, strlen(msg), 0);

    while (1) {
        pause(); // 等待信号
    }

    mq_close(mq);
    mq_unlink("/test_queue");
    return 0;
}
  • SIGEV_THREAD:通过 线程机制 通知注册进程
// 该线程由注册进程自动创建
void process_message(union sigval sv) {
    char *msg = (char *)sv.sival_ptr;
    printf("Processing message: %s\n", msg);
    free(msg); // 释放内存
}

int main() {
    mqd_t mq;
    struct mq_attr attr;
    struct sigevent sev;

    // 创建消息队列
    attr.mq_flags = 0;
    attr.mq_maxmsg = 10;
    attr.mq_msgsize = 256;
    attr.mq_curmsgs = 0;

    mq = mq_open("/test_queue", O_CREAT | O_RDONLY | O_NONBLOCK, 0644, &attr);

    // 设置SIGEV_THREAD
    sev.sigev_notify = SIGEV_THREAD;
    sev.sigev_notify_function = process_message;
    sev.sigev_value.sival_ptr = NULL; // 初始化为空

    mq_notify(mq, &sev); // 注册通知

    // 发送测试消息
    for (int i = 0; i < 5; i++) {
        char *msg = malloc(256);
        snprintf(msg, 256, "Message %d", i);
        mq_send(mq, msg, strlen(msg) + 1, 0); // 发送消息
        sleep(1);
    }

    // 等待处理
    sleep(2);

    mq_close(mq);
    mq_unlink("/test_queue");
    return 0;
}
 posted on 2024-10-27 11:43  Dylaris  阅读(63)  评论(0)    收藏  举报