【问题标题】:How to wake up a pthread which is sleeping in recvmsg()?如何唤醒在 recvmsg() 中休眠的 pthread?
【发布时间】:2015-05-28 01:17:37
【问题描述】:

我有一个使用 pthreads 的小 Unix 守护进程。一个线程循环运行,使用recvmsg读取网络数据包。当守护进程收到信号时,会设置一个标志,告诉所有线程跳出它们的循环并退出。但是,在下一个数据包到达并且recvmsg 返回之前,“监听”线程永远不会检查标志,这可能需要一些时间。

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

这个守护进程只在 Linux 上使用,如果它有所作为的话。

【问题讨论】:

  • 只是一个想法:你不能发送一个“假”数据包来强制recvmsg醒来吗?

标签: c linux sockets pthreads


【解决方案1】:

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

将信号传递给信号处理程序会中断库函数或系统调用——即使信号处理程序函数的主体是空的。 (当然,假设您使用不带SA_RESTART 标志的sigaction() 安装信号处理程序。请参阅man 7 signal,在“信号处理程序中断系统调用和库函数” 下了解详细信息.)


使用这种方法(使用信号中断阻塞 I/O 函数)可能会遇到的一个问题是在目标线程实际阻塞 I/O 函数之前提早发出信号。 那个窗口是无法避免的,你只需要处理它。

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

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

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

诀窍在于,当超时最初触发时,它并​​没有被移除,因为前面提到了有问题的时间窗口。相反,我喜欢将超时标记为“已过”,然后在几毫秒后重新设置它。如此重复,直到目标线程释放超时。 (每次触发超时,我都会向目标线程发送一个 POSIX 实时信号,并安装一个空的信号处理函数。)

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

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

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

【讨论】:

    【解决方案2】:

    三个想法:

    想法 #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 中是一个文件)。

    要取消套接字读取器线程,请在取消文件对象上执行write。然后让 reader(s) 检查从 poll/epoll/select 唤醒时是否能够读取该 fd。

    如果有多个处理 recvmsg 的事物的实例,您应该能够使用一个这样的文件来唤醒所有线程,只要确保在使用 epoll 时使用级别触发而不是边缘触发。

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

    【讨论】:

    • 好建议!我想我会尝试使用select;我以前用过。我从来没有用过poll,我不知道你为什么觉得它比select更好。
    • 使用poll,您不必像在select 循环中那样每次都重建/复制fd_set(或3)和timeval。它可以处理多个FDSET_SIZE 文件。当只使用少量文件时,它的内存占用更小。它处理更多的 evt 类型。如果你不知道这三个中的任何一个,我认为它最容易学习。但如果您知道select,它也是一个完全可以接受的工具。此外,select 的可移植性稍高一些,并且可以说在现有代码库中更常见,因此不太可能让维护者感到困惑。
    • 也感谢poll 的提示。
    【解决方案3】:

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

    【讨论】:

    • 我可以在手册页中看到 recvmsg 确实是 pthread 的“取消点”。我不认为这次我会使用这个选项,但很高兴知道这一点。
    【解决方案4】:

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

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

    https://stackoverflow.com/a/4182564/104458

    【讨论】:

    • 很好的建议,谢谢!套接字实际上是一个原始的IPPROTO_ICMP 套接字。
    猜你喜欢
    • 2012-08-15
    • 2011-08-05
    • 1970-01-01
    • 2021-09-11
    • 2014-08-02
    • 2014-07-15
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多