【问题标题】:Epoll zero recv() and negative(EAGAIN) send()epoll 零 recv() 和负数(EAGAIN) send()
【发布时间】:2020-01-08 20:23:02
【问题描述】:

前几天我在 epoll 上苦苦挣扎,现在我正处于茫茫人海中;)

互联网上有很多信息,显然是系统人,但我可能服用过量并有点困惑。

在我的服务器应用程序(nginx 的后端)中,我正在 ET 模式下等待来自客户端的数据:

event_template.events = EPOLLIN | EPOLLRDHUP | EPOLLET

当我注意到 nginx 以 502 响应时,一切都变得好奇了,尽管我可以看到我这边成功的 send()。我运行wireshark 嗅探并意识到我的服务器将(尝试并获取 RST)数据发送到网络上的另一台机器。所以,我决定套接字描述符是无效的,这是一种“未定义的行为”。最后,我发现在第二个 recv() 上,我得到零字节,这意味着必须关闭连接,并且不允许我再发回数据。尽管如此,我从 epoll 中获得的不仅仅是 EPOLLIN,而是 EPOLLRDHUP。

问题:在 EPOLLRDHUP 处理过程中,当 recv() 返回零和 shutdown(SHUT_WR) 时,我是否必须关闭套接字才能读取?

简而言之从套接字读取:

    std::array<char, BatchSize> batch;
    ssize_t total_count = 0, count = 0;
    do {
        count = recv(_handle, batch.begin(), batch.size(), MSG_DONTWAIT);

        if (0 == count && 0 == total_count) {
            /// @??? Do I need to wait zero just on first iteration?
            close();
            return total_count;
        } else if (count < 0) {
            if (errno == EAGAIN || errno == EWOULDBLOCK) {
                /// @??? Will be back with next EPOLLIN?!
                break ;
            }
            _last_error = errno;
            /// @brief just log the error               
            return 0;
        }

        if (count > 0) {
            total_count += count;
            /// DATA!
            if (count < batch.size()) {
                /// @??? Received less than requested - no sense to repeat recv, otherwise I need one more turn?! 
                return total_count;
            }
        }           
    } while (count > 0);

可能,我的一般错误是尝试在无效的套接字描述符上发送数据,而后来发生的一切都只是结果。但是,我继续挖掘 ;) 我的问题的 第二部分 也是关于在 MSG_DONTWAIT 模式下写入套接字。

据我所知,send() 也可能返回 -1 和 EAGAIN,这意味着我应该订阅 EPOLLOUT 并等待内核缓冲区足够空闲以接收我的一些数据。这是正确的吗?但是如果客户不会等那么久呢?或者,我可以调用阻塞发送(无论如何,我在不同的线程上发送)并保证我发送给内核的所有内容都会因为 setsockopt(SO_LINGER) 而真正发送给对等方?我要求确认的最终猜测:我被允许同时读取和写入,但 N>1 个并发写入是数据竞争,我必须处理的所有事情都是互斥体。

感谢所有至少读到最后的人:)

【问题讨论】:

  • 您需要关闭任何零,而不仅仅是第一个。
  • 毫无疑问“如果客户不等这么久怎么办?”如果您在send() 上收到 EAGAIN,则表示客户端根本没有读取数据,并且 TCP 已全部备份。

标签: c++ linux epoll epollet


【解决方案1】:

问题:recv() 时我是否必须关闭套接字才能阅读? 稍后在 EPOLLRDHUP 期间返回零并关闭(SHUT_WR) 处理?

不,没有特别的理由来执行这种有些复杂的动作序列。

收到来自recv()0 返回值后,您知道连接在网络层至少是半关闭的。您不会从它那里收到任何进一步的信息,我不希望 EPoll 在边缘触发模式下运行以进一步宣传它的阅读准备情况,但这本身并不需要任何特定的操作。如果写入端保持打开状态(从本地角度来看),那么您可以继续使用 write()send(),尽管您将没有确认收到您发送的内容的机制。

您实际上应该做什么取决于您假设的应用程序级协议或消息交换模式。如果您希望远程对等方在等待您的数据时关闭其端点的写入端(连接到本地端点的读取端),那么一定要发送它预期的数据。否则,当recv() 通过返回0 发出文件结束信号时,您可能应该关闭整个连接并停止使用它。请注意,close()ing 描述符会自动从它注册的任何 Epoll 兴趣集中删除它,但前提是没有其他打开文件描述符引用相同的打开文件描述。

不管怎样,在您对套接字执行close() 之前,它仍然有效,即使您无法通过它成功通信。在那之前,没有理由期望您尝试通过它发送的消息会到达任何地方,而不是可能到达原始远程端点。尝试发送可能会成功,或者即使数据从未到达远端也可能会成功,或者可能会因几种不同的错误之一而失败。

            /// @??? Do I need to wait zero just on first iteration?

无论是否已经收到任何数据,您都应该对返回值 0 执行操作。不一定是相同的操作,但无论哪种方式,您都应该安排一种或另一种方式将其从 EPoll 兴趣集中取出,很可能通过关闭它。

                /// @??? Will be back with next EPOLLIN?!

如果recv()EAGAINEWOULDBLOCK 失败,那么EPoll 很可能会在以后的调用中发出读取准备就绪的信号。不过,不一定是下一个。

                /// @??? Received less than requested - no sense to repeat recv, otherwise I need one more turn?! 

收到的金额低于您的要求是您应该始终做好准备的可能性。这并不一定意味着另一个recv() 不会返回任何数据,如果您在 EPoll 中使用边缘触发模式,那么假设相反是危险的。在这种情况下,您应该继续使用recv(),在非阻塞模式下或使用MSG_DONTWAIT,直到调用失败并使用EAGAINEWOULDBLOCK

据我所知,send() 也可能返回 -1 和 EAGAIN,这意味着我应该订阅 EPOLLOUT 并等待内核缓冲区足够空闲以接收我的一些数据。是这样吗?

send() 肯定会因EAGAINEWOULDBLOCK 而失败。它也可以成功,但发送的字节数少于您请求的字节数,您应该为此做好准备。无论哪种方式,通过订阅文件描述符上的 EPOLLOUT 事件来响应是合理的,以便稍后恢复发送。

但是如果客户不会等那么久呢?

这取决于客户在这种情况下会做什么。如果它关闭连接,那么将来尝试send() 将失败并出现不同的错误。如果您仅针对描述符上的 EPOLLOUT 事件进行了注册,那么我怀疑可能(尽管不太可能)陷入这种尝试永远不会发生的情况,因为没有发出进一步的事件信号。即使您的主要兴趣是写作,也可以通过注册和正确处理EPOLLRDHUP 事件来进一步降低这种可能性。

如果客户端在没有关闭连接的情况下放弃了,那么EPOLLRDHUP 可能就没有用了,而且您更有可能让过时的连接无限期地卡在您的 EPoll 中。使用每个 FD 超时来解决这种可能性可能是值得的。

或者,我可以调用阻塞发送(无论如何,我正在发送不同的 线程)并保证我发送给内核的所有内容都是 真的因为 setsockopt(SO_LINGER) 而发送到对等体?

如果您有一个单独的线程完全专用于发送该特定文件描述符,那么您当然可以考虑阻塞send()s。唯一的缺点是你不能在此之上实现超时,但除此之外,如果这样的线程在发送数据或接收更多要发送的数据时阻塞,它会做什么?

不过,至少在本地方面,我不太明白 SO_LINGER 与它有什么关系。内核将尽一切努力将您已经通过send() 调用发送到远程对等方的数据发送到远程对等方,即使您close() 套接字仍在缓冲数据,而不管SO_LINGER 的值如何。该选项的目的是在连接关闭后接收(并丢弃)与连接关联的散乱数据,这样它们就不会被意外传送到另一个套接字。

但是,这些都不能保证数据成功传送到远程对等方。没有什么可以保证。

我要求确认的最后一个猜测是:我可以阅读和 同时写入,但 N>1 并发写入是数据竞争,并且 我必须处理的一切都是互斥体。

套接字是全双工的,是的。此外,POSIX 要求大多数函数,包括send()recv(),都是线程安全的。然而,多个线程写入同一个套接字是自找麻烦,因为单个调用的线程安全并不能保证多个调用之间的一致性。

【讨论】:

  • 它不依赖。 “客户不会等那么久”是没有意义的。客户端没有等待。发送/EAGAIN 条件的发生只是因为对等方的读取落后,这导致其接收缓冲区,然后感知发送缓冲区被填满。一旦开始读取,它将开始耗尽所有未决数据,发送方的发送缓冲区将获得空间,并且将发生 EPOLLOUT。问题与现实背道而驰。
  • @user207421, EAGAIN on send 发生是因为本地机器在将发送的数据发送到网络时落后。其根本原因可能确实是接收方的备份,但也可能由于发送方或两者之间的条件而发生。对于 UNIX 域套接字或连接到本地主机的网络套接字来说,这有点混乱,但即便如此,在发送端备份时,接收端仍可能缺少数据。
猜你喜欢
  • 2014-05-05
  • 1970-01-01
  • 1970-01-01
  • 2019-01-13
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2016-07-27
相关资源
最近更新 更多