【问题标题】:Linux select() vs ppoll() vs pselect()Linux select() vs ppoll() vs pselect()
【发布时间】:2012-04-04 05:20:36
【问题描述】:

在我的应用程序中,有一个 io-thread,专用于

  1. 将从应用程序接收到的数据包装在自定义协议中
  2. 通过 tcp/ip 发送数据+自定义协议包
  3. 通过 tcp/ip 接收数据+自定义协议包
  4. 解开自定义协议并将数据交给应用程序。

应用程序通过不同的线程处理数据。此外,要求规定未确认的窗口大小应为 1,即任何时候都应该只有一条未确认的未确认消息。这意味着如果 io-thread 已经通过套接字发送了一条消息,它不会再发送任何消息,直到它从接收者那里听到一个 ack。 应用程序的处理线程通过管道与 io-thread 通信。如果有人从 linux CLI 键入 ctrl+C,应用程序需要正常关闭。 因此,鉴于这些要求,我有以下选择

  1. 在套接字和管道描述符上使用 PPoll()
  2. 使用 Select()
  3. 使用 PSelect()

我有以下问题

  1. select() 和 poll() 之间的决定。我的应用程序只处理少于 50 个文件描述符。可以假设我选择 select 还是 poll 没有区别吗?

    1. select() 和 pselect() 之间的决定。我阅读了 linux 文档,它说明了信号和 select() 之间的竞争条件。我没有信号经验,所以有人可以更清楚地解释比赛条件和 select() 吗?是否与有人在 CLI 上按 ctrl+C 并且应用程序未停止有关?

    2. pselect 和 ppoll() 之间的决定?关于一个与另一个的任何想法

【问题讨论】:

    标签: c linux network-programming


    【解决方案1】:

    我建议先比较 select()poll()。 Linux 还同时提供pselect()ppoll()const sigset_t *pselect()ppoll() (与 select()poll() 相比)的额外 const sigset_t * 参数对每个“p-variant”具有相同的效果。如果您不使用信号,则无需防范竞争,因此基本问题实际上是关于效率和编程的易用性。

    同时这里已经有一个 stackoverflow.com 的答案:what are the differences between poll and select

    至于比赛:一旦开始使用信号(无论出于何种原因),您将了解到,一般来说,信号处理程序应该只设置一个 volatile sig_atomic_t 类型的变量来指示已检测到信号。其根本原因是许多库调用不是re-entrant,并且当您“处于”此类例程的“中间”时可以传递信号。例如,简单地将消息打印到 stdout (C) 或 cout (C++) 等流式数据结构可能会导致重入问题。

    假设您的代码使用了volatile sig_atomic_t flag 变量,可能是为了捕获SIGINT,类似这样(另请参阅http://pubs.opengroup.org/onlinepubs/007904975/functions/sigaction.html):

    volatile sig_atomic_t got_interrupted = 0;
    void caught_signal(int unused) {
        got_interrupted = 1;
    }
    ...
        struct sigaction sa;
        sa.sa_handler = caught_signal;
        sigemptyset(&sa.sa_mask);
        sa.sa_flags = SA_RESTART;
        if (sigaction(SIGINT, &sa, NULL) == -1) ... handle error ...
        ...
    

    现在,在您的代码主体中,您可能希望“运行直到被中断”:

        while (!got_interrupted) {
             ... do some work ...
        }
    

    这很好,直到您开始需要进行等待某些输入/输出的调用,例如selectpoll。 “等待”操作需要等待该 I/O——但它需要等待 SIGINT 中断。如果你只是写:

        while (!got_interrupted) {
            ... do some work ...
            result = select(...); /* or result = poll(...) */
        }
    

    那么中断可能会发生在就在您调用select()poll() 之前,而不是之后。在这种情况下,你确实被打断了——变量got_interrupted被设置——但在那之后,你开始等待。您应该在开始等待之前检查got_interrupted 变量,而不是之后。

    你可以试试写:

        while (!got_interrupted) {
            ... do some work ...
            if (!got_interrupted)
                result = select(...); /* or result = poll(...) */
        }
    

    这会缩小“竞赛窗口”,因为现在您将检测到中断是否在您执行“做一些工作”代码时发生;但是仍然存在竞争,因为中断可能发生在就在您测试变量之后,但就在选择或轮询之前。

    解决方案是使用sigprocmask(或者,在POSIX线程代码中,pthread_sigmask)的信号阻塞属性,使“测试,然后等待”序列“原子化”:

    sigset_t mask, omask;
    ...
    while (!got_interrupted) {
        ... do some work ...
        /* begin critical section, test got_interrupted atomically */
        sigemptyset(&mask);
        sigaddset(&mask, SIGINT);
        if (sigprocmask(SIG_BLOCK, &mask, &omask))
            ... handle error ...
        if (got_interrupted) {
            sigprocmask(SIG_SETMASK, &omask, NULL); /* restore old signal mask */
            break;
        }
        result = pselect(..., &omask); /* or ppoll() etc */
        sigprocmask(SIG_SETMASK, &omask, NULL);
        /* end critical section */
    }
    

    (上面的代码实际上并没有那么好,它的结构是为了说明而不是效率——稍微不同地执行信号掩码操作,并以不同的方式放置“被中断”测试会更有效)。

    在您真正开始需要捕获 SIGINT 之前,您只需比较 select()poll()(如果您开始需要大量描述符,一些基于事件的东西,例如 epoll()比任何一个都更有效)。

    【讨论】:

    • “根本原因是许多库调用是不可重入的” 所以如果在我们说“读”的过程中传递了一个信号会发生什么。这是否意味着我们再也不能使用 read 了?
    • @kptlronyttcna:不,但也有一点是:你不能安全地调用,例如,fread 在信号处理程序的标准输入上。这并不意味着您可以永远调用fread,只是您不能在信号处理程序中的共享变量上执行此操作。您可以安全地做什么和不能做什么的确切细节因库、系统和其他细节而异。 (顺便说一句,当read 是系统调用时,就像在 Linux、BSD 和 Mac 上一样,它的精确行为取决于底层文件系统对象。“慢”设备可能会返回 EINTR 错误或短读取。)
    【解决方案2】:

    在 (p)select 和 (p)poll 之间是一个相当微妙的区别:

    对于 select,您必须在每次调用 select 之前初始化和填充丑陋的 fd_set 位图,因为 select 会以“破坏性”的方式就地修改它们。 (投票区分struct pollfd 中的.events.revents 成员)。

    选择后,即使大多数 fd 甚至没有被观看,也经常(通过人员/代码)扫描整个位图以查找事件。

    第三,位图只能处理数量小于某个限制的 fds(当代实现:介于 1024..4096 之间),这在可以轻松获得高 fds 的程序中排除了它(尽管这样的程序可能已经使用 epoll 代替)。

    【讨论】:

    • 我的感觉是 select 不应该再用于新编写的程序中,而只能用于 poll [或 ppoll] (至少在不太旧的 Linux 系统上,@987654328 @ 和 poll 是本机系统调用)。我相信select 仅适用于遗留程序...
    • kegel.com/c10k.html 或者仅仅是fd_set 的大小对最大文件描述符的限制比poll 允许的要严格得多。
    • 当然,使用ppoll 可能会更好,因为没有信号相关的种族(但它是特定于 Linux 的)。
    【解决方案3】:

    相对于 select 和 pselect 之间的差异,接受的答案是不正确的。它确实很好地描述了 sig-handler 和 select 之间的竞争条件是如何出现的,但是它在如何使用 pselect 来解决问题方面是不正确的。它错过了关于 pselect 的要点,即它等待文件描述符或信号准备就绪。 pselect 在其中任何一个准备就绪时返回。Select 仅在文件描述符上等待。选择忽略信号。有关一个很好的工作示例,请参阅此博客文章: https://www.linuxprogrammingblog.com/code-examples/using-pselect-to-avoid-a-signal-race

    【讨论】:

      【解决方案4】:

      为了使接受的答案所呈现的图片完整,应提及以下基本事实:select() 和 pselect() 都可能返回 EINTR,如其手册页中所述:

      EINTR 一个信号被捕获;见信号(7)。

      这个“被捕获”意味着信号应该被识别为“在系统调用执行期间发生”:
      1. 如果 select/pselect 执行过程中出现非屏蔽信号,则 select/pselect 将退出
      2. 如果在 调用 select/pselect 之前出现了非屏蔽信号,则此 不会产生任何影响,并且 select/pselect 将继续等待,可能会一直等待

      因此,如果在 select/pselect 执行期间出现信号,我们就可以了 - select/pselect 的执行将被中断,然后我们可以测试退出的原因并发现是 EINTR,然后我们可以退出循环。
      我们面临的真正威胁是在 select/pselect 执行之外出现信号的可能性,然后我们可能会永远挂在系统调用中。任何以天真的方式发现这个“局外人”信号的尝试:

      如果(was_a_signal){
      ...
      }

      将失败,因为无论此测试与调用 select/pselect 有多接近,信号总是有可能在测试之后和调用 select/pselect 之前出现。
      然后,如果捕获信号的唯一位置是在 select/pselect 执行期间,我们应该发明某种“酒漏斗”,以便所有“酒溅”(信号),甚至在“瓶颈”之外(select/pselect 执行期)最终会走到“瓶颈”。
      但是你怎么能欺骗系统调用,让它“认为”信号已经在这个系统调用执行期间发生了,而实际上它之前已经发生过呢?
      简单。这是我们的“葡萄酒漏斗”:您只需阻止感兴趣的信号,并因此(如果确实发生过)等待在过程之外“等待只有当您准备好“欢迎客人”(select/pselect 正在运行)时,您才可以“打开门”(取消屏蔽信号)。那么“到达”信号将被识别为“刚刚发生”,并会中断系统调用的执行。
      当然,“开门”是计划中最关键的部分——它不能通过通常的方式来完成(首先取消屏蔽,然后调用 select/pselect),唯一的可能是同时执行这两个操作(取消屏蔽和系统call) 一次(原子地) - 这是 pselect() 能够做到的,但 select() 不是

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2013-10-18
        • 2011-05-01
        • 1970-01-01
        • 2011-05-02
        • 2013-03-24
        • 1970-01-01
        • 2012-09-05
        • 2015-11-24
        相关资源
        最近更新 更多