1

前几天我在 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 并发写入是数据竞争,我必须处理的所有事情都是互斥体。

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

4

1 回答 1

1

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

不,没有特别的理由来执行这一系列有点复杂的动作。

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

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

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

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

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

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

如果recv()失败,EAGAINEWOULDBLOCKEPoll 很可能会在未来的调用中发出读取准备就绪的信号。不过,不一定是下一个。

                /// @??? 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()当然可以失败EAGAINor EWOULDBLOCK。它也可以成功,但发送的字节数少于您请求的字节数,您应该为此做好准备。无论哪种方式,通过订阅文件描述符上的 EPOLLOUT 事件来响应是合理的,以便稍后恢复发送。

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

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

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

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

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

SO_LINGER不过,至少在本地方面,我看不出与它有什么关系。send()无论. _ close()_ SO_LINGER该选项的目的是在连接关闭后接收(和丢弃)与连接关联的散乱数据,以便它们不会意外传递到另一个套接字。

然而,这些都不能保证数据成功地传送到远程对等点。没有什么可以保证这一点。

最后一个我要求确认的猜测是:我被允许同时读写,但是 N>1 并发写入是数据竞争,我必须处理的所有事情都是互斥锁。

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

于 2019-09-06T05:01:12.613 回答