Posix 消息队列内部维护了一个引用计数,当引用计数大于 0 的时候目标消息队列能够从系统中移除,但是队列的释放仅在最后一次 mq_close 发生时才会触发。
mq_notify 函数为 Posix 消息队列提供了一种异步通知机制,当消息被放到队列中时通知消费者进程, System V 消息队列就不具备这样的能力。
在调用 msgrcv 函数从 System V 消息队列中接收消息时进程可以挂起等待消息,但是在挂起期间不能执行任何其他任务。如果指定 NONBLOCK 标志调用 msgrcv 函数,进程不再阻塞但是却要持续调用此函数以确定队列中是否有数据到来,会浪费 cpu 时间。
Posix 消息队列支持通过如下两种方式来异步通知一个空的队列中有新的消息到来:
这两种机制通过指定不同的参数调用 mq_notify 函数来选择,mq_notify 函数的原型如下:
int mq_notify(mqd_t mqdes, const struct sigevent *sevp);
mq_notify 使用规则如下:
UNPV2 中提供了这两种不同方案的示例代码,我分别描述下关键的流程。
核心代码如下:
int main(int argc, char *argv[])
{
....................................
Signal(SIGUSR1, sig_usr1);
sigev.sigev_notify = SIGEV_SIGNAL;
sigev.sigev_signo = SIGUSR1;
Mq_notify(mqd, &sigev);
....................................
}
static void
sig_usr1(int signo)
{
ssize_t n;
Mq_notify(mqd, &sigev); /* reregister first */
n = Mq_receive(mqd, buff, attr.mq_msgsize, NULL);
printf("SIGUSR1 received, read %ld bytes\n", (long) n);
return;
}
上述代码实现了 SIGUSR1 的信号处理函数并配置 mq_notify 使用信号通知机制,通知信号为 SIGUSR。
main 函数中注册了 SIGUSR1 信号的处理函数 sig_usr1,此函数的逻辑如下:
此实现存在的问题为不应该在信号处理函数中调用 mq_notify、mq_receive、printf 函数,这些函数并不是异步信号安全的函数。
核心代码如下:
for ( ; ; ) {
Sigprocmask(SIG_BLOCK, &newmask, &oldmask); /* block SIGUSR1 */
while (mqflag == 0)
sigsuspend(&zeromask);
mqflag = 0; /* reset flag */
Mq_notify(mqd, &sigev); /* reregister first */
n = Mq_receive(mqd, buff, attr.mq_msgsize, NULL);
printf("read %ld bytes\n", (long) n);
Sigprocmask(SIG_UNBLOCK, &newmask, NULL); /* unblock SIGUSR1 */
}
static void
sig_usr1(int signo)
{
mqflag = 1;
return;
}
SIGUSR1 信号处理程序中仅仅设置一个全局变量 mqflag 的值,在程序主逻辑中调用 mq_notify 与 mq_receive 来接收消息。
上述代码首先修改当前线程的信号掩码,临时关闭 SIGUSR1,然后执行 sigsuspend 等待 SIGUSR1 信好到来。
sigsuspend 函数会使用 zeromask 表示的 signal mask 修改当前线程的 signal mask,然后挂起当前线程,直到有一个会执行 signal handler、终止进程的目标信号产生。当收到信号并终止进程时,sigsuspend 将不会返回。如果成功捕获到信号,sigsuspend 将会在信号处理函数执行后返回,signal 将会被恢复为调用 sigsuspend 函数之前的状态。
在 sigsuspend 返回后,程序重置 mqflag 标志并调用 Mq_notify 与 Mq_receive 接收消息并打印接收的字节数,最后调用 Sigprocmask unblock SIGUSR1 信号。
此实现存在如下问题:
由于通知消息仅在有一条新的消息被放到空的队列时产生,如果在我们能够读取第一个消息前有两个消息达到,那么只有一个通知事件产生,于是我们读取第一个消息并调用 sigsuspend 等待另一个消息,而后续可能没有新的消息产生,这样我们就会漏掉第二个消息。
为了解决这个问题,我们可以在 Mq_receive 的时候多次读取队列,这样就不会漏掉消息。示例代码如下:
for ( ; ; ) {
Sigprocmask(SIG_BLOCK, &newmask, &oldmask); /* block SIGUSR1 */
while (mqflag == 0)
sigsuspend(&zeromask);
mqflag = 0; /* reset flag */
Mq_notify(mqd, &sigev); /* reregister first */
while ( (n = mq_receive(mqd, buff, attr.mq_msgsize, NULL)) >= 0) {
printf("read %ld bytes\n", (long) n);
}
if (errno != EAGAIN)
err_sys("mq_receive error");
Sigprocmask(SIG_UNBLOCK, &newmask, NULL); /* unblock SIGUSR1 */
}
上文描述了在信号处理函数中设置标志的方式,一个更简单的方式是在一个函数中阻塞等待内核发送目标信号,可以通过 sigwait 函数来实现。
新的代码如下:
for ( ; ; ) {
Sigwait(&newmask, &signo);
if (signo == SIGUSR1) {
Mq_notify(mqd, &sigev); /* reregister first */
while ( (n = mq_receive(mqd, buff, attr.mq_msgsize, NULL)) >= 0) {
printf("read %ld bytes\n", (long) n);
}
if (errno != EAGAIN)
err_sys("mq_receive error");
}
}
sigwait 函数将会挂起当前线程直到 signal set 中指定的信号到来,此函数会接收这个信号(将信号从信号 pending list 中移除),然后通过第二个参数返回信号值。
上面的代码进一步简化,只调用 Sigwait,然后调用 Mq_notify、mq_receive,比使用 sigsuspend 更简单。
Posix 消息队列描述符并不是一个普通的描述符不能使用 select、epoll 函数监控此描述符。可以使用 mq_notify + pipe 的方式,mq_notify 注册监控消息队列事件,通知方式为信号,在程序初始化时创建一个 pipe,在信号处理函数中调用 write 向这个管道的 fd 中写入数据,在主程序循环中 select pipe 的 fd 来间接的监听 Posix 消息队列。 write 系统调用是异步信号安全的函数,在信号处理函数中调用不会产生问题。
示例代码如下:
Pipe(pipefd);
/* 4establish signal handler, enable notification */
Signal(SIGUSR1, sig_usr1);
sigev.sigev_notify = SIGEV_SIGNAL;
sigev.sigev_signo = SIGUSR1;
Mq_notify(mqd, &sigev);
FD_ZERO(&rset);
for ( ; ; ) {
FD_SET(pipefd[0], &rset);
nfds = Select(pipefd[0] + 1, &rset, NULL, NULL, NULL);
if (FD_ISSET(pipefd[0], &rset)) {
Read(pipefd[0], &c, 1);
Mq_notify(mqd, &sigev); /* reregister first */
while ( (n = mq_receive(mqd, buff, attr.mq_msgsize, NULL)) >= 0) {
printf("read %ld bytes\n", (long) n);
}
if (errno != EAGAIN)
err_sys("mq_receive error");
}
}
static void
sig_usr1(int signo)
{
Write(pipefd[1], "", 1); /* one byte of 0 */
return;
}
向 pipe 中写入的数据内容并不重要,重要的是写入这个动作触发 select 系统调用捕获事件,间接的绑定到 Posix 消息队列的通知事件。
示例代码如下:
int
main(int argc, char **argv)
{
if (argc != 2)
err_quit("usage: mqnotifythread1 " );
mqd = Mq_open(argv[1], O_RDONLY | O_NONBLOCK);
Mq_getattr(mqd, &attr);
sigev.sigev_notify = SIGEV_THREAD;
sigev.sigev_value.sival_ptr = NULL;
sigev.sigev_notify_function = notify_thread;
sigev.sigev_notify_attributes = NULL;
Mq_notify(mqd, &sigev);
for ( ; ; )
pause(); /* each new thread does everything */
exit(0);
}
static void
notify_thread(union sigval arg)
{
ssize_t n;
void *buff;
printf("notify_thread started\n");
buff = Malloc(attr.mq_msgsize);
Mq_notify(mqd, &sigev); /* reregister */
while ( (n = mq_receive(mqd, buff, attr.mq_msgsize, NULL)) >= 0) {
printf("read %ld bytes\n", (long) n);
}
if (errno != EAGAIN)
err_sys("mq_receive error");
free(buff);
pthread_exit(NULL);
}
sigev 中的 sigev_notify 设置为 SIGEV_THREAD 表示通过创建一个线程执行函数方式监听处理消息队列事件,sigev_notify_function 中设置了需要执行的函数指针为 notify_thread,此函数的主要逻辑如下:
在这种实现中,主线程可以做其它的任务,在示例程序中主线程啥也不干。这种创建线程执行函数的机制表面上看上去挺简单,可 mq_notify 注册的 notify_thread 是一个用户态虚拟内存空间的代码地址,它不能在内核态执行,意味着线程的创建与回调的执行都在用户态完成,那内核又是如何将事件投递到新创建的线程,让此线程执行回调来处理消息呢?
在进一步探讨前,先在我的本地 linux 环境上运行下示例程序,运行 log 如下:
[longyu@debian] pxmsg $ ./mqcreate /test1
[longyu@debian] pxmsg $ ./mqnotifythread1 /test1
notify_thread started
read 50 bytes
notify_thread started
read 50 bytes
notify_thread started
read 1024 bytes
mqnotifythread demo 能够正常接收消息。