如何唤醒正在 recvmsg() 中休眠的 pthread?

How to wake up a pthread which is sleeping in recvmsg()?

我有一个使用 pthreads 的小 Unix 守护进程。一个线程循环运行,使用 recvmsg 读取网络数据包。当守护进程收到信号时,会设置一个标志,告诉所有线程跳出循环并退出。但是,"listening" 线程从不检查标志,直到下一个数据包到达 recvmsg returns,这可能需要一些时间。

使用 pthread_killSIGINT 传递给 "listening" 线程使其脱离 recvmsg,但它也会调用信号处理程序处理通过按下发送的中断Ctrl+C 在控制台。那是不需要的。另一种提前将 recvmsg 强制为 return 的方法是关闭它正在侦听的套接字,但我认为必须有更好的方法。你知道它是什么吗?

此守护程序仅在 Linux 上使用,如果它有所不同的话。

如果这是 UDP,我使用的一种常用方法是设置退出条件标志,然后只发送一个 1 字节的数据包以强制 recvfrom/recvmsg 调用 return。

或者,您可以在套接字上设置超时值(例如 1 秒)SO_RCVTIMEO。当套接字等待超时时,您可以检查退出条件。如果没有设置退出条件,则再次调用recvmsg。

pthread_cancel 可能就是您要找的。不过请仔细阅读手册页,不要过度使用它。

三个想法:

想法 #1: 为什么不是另一个信号?信号 SIGUSR1SIGUSR2 仅用于用户定义的信号。如果您已经设置了信号处理程序并且通常熟悉它们的使用,这可能是最简单的方法。

想法 #2: 按照其他人的建议发送一个虚拟数据包。

想法 #3: 如果你真的想避免发送数据包,请尝试使用非阻塞 I/O 这样 recvmsg 就不会阻塞(参见 MSG_DONTWAIT)。

相反,调用 epoll | poll | select 来阻止套接字接收事件和另一个文件描述符。

所以循环看起来像这样:

while(1){
   recvmsg();
   do_stuff();
}

变为:

while(1){
   wait_for_events();
   if( /* FD used for cancellation is readable */ )
      break;
   else
      recvmsg();
      do_stuff();
}

其中 wait_for_eventsselectpollepoll_wait。设置等待列表以包括您的套接字和额外的 fd,例如管道、unix 域套接字或 POSIX 消息队列(这是 Linux 中的文件)。

要取消套接字 reader 线程,请对取消文件对象执行 write。然后让 reader(s) 检查从 poll/epoll/select.

唤醒时是否能够读取该 fd

如果事物处理有多个实例,您应该能够使用一个这样的文件来唤醒所有线程 recvmsg,如果使用 [=22],请确保使用级别触发而不是边缘触发=].

如果您不确定使用这三个调用中的哪一个,我建议从 poll 开始,如果您已经非常了解它,则只使用 select 或如果你知道你需要它的可扩展性。

将空信号处理程序安装到未使用的 POSIX 实时信号,SIGRTMIN+0SIGRTMAX-0,并使用 pthread_kill() 将该信号发送到阻塞在 recvmsg()。实时信号是排队的(可靠的),而标准信号不是。这意味着如果您使用实时信号,同时中断两个不同的线程是可行的,但如果使用普通信号(例如 SIGUSR1)可能会失败。

将信号传递给信号处理程序会中断库函数或系统调用——即使信号处理函数的主体为空。 (当然,假设您使用 sigaction() without the SA_RESTART flag. See man 7 signal 安装信号处理程序,详情请参见 "Interruption of system calls and library functions by signal handlers"。)


您可能会遇到这种方法(使用信号中断阻塞 I/O 函数)的一个问题是,在目标线程实际阻塞 [=52= 之前提早发出信号时] 功能。 那window真的无法避免,你只能去面对它。

就个人而言,我喜欢使用专用线程来维护超时结构,例如

struct timeout {
    pthread_t        who;
    struct timespec  when;  /* Using CLOCK_MONOTONIC */
    volatile int     state;
};

每个线程都可以获取和释放——我发现一个线程可以同时保持多个超时特别有用——还有一个简单的原子函数来检查状态(例如在循环条件中使用) ).

诀窍是当超时最初触发时,由于上述时间问题 window,它不会被删除。相反,我喜欢标记超时 "elapsed",并在几毫秒后重新设置它。如此重复,直到目标线程释放超时。 (每次触发超时,我都会向目标线程发送一个 POSIX 实时信号,并安装一个空信号处理函数。)

这样,即使您的 recv()/send() 循环偶尔错过信号,它们也会很快收到通知。

如果您曾经取消线程,您还需要安装一个线程清理处理程序来释放线程拥有的所有超时。否则你可能 "leak" 超时结构。

如果您使用clock_gettime() and pthread_cond_timedwait(),等待新的超时条目(由其他线程添加)或现有超时结束,超时管理线程将非常轻量级并且不会消耗太多内存或CPU 时间。请注意,如果您使用通过 pthread_condattr_setclock() 选择时钟的属性集创建新添加的超时条件变量,则可以使用 CLOCK_MONOTONIC