TCP:EPOLLHUP 是什么时候产生的?

TCP: When is EPOLLHUP generated?

另请参阅 this question,目前尚未答复。

即使在 man 和内核文档中,也存在很多关于 EPOLLHUP 的混淆。人们似乎相信它在轮询描述​​符时返回 本地关闭写入 ,即 shutdown(SHUT_WR),即导致 EPOLLRDHUP [=52= 的相同调用]同行。但这不是真的,在我的实验中我得到 EPOLLOUT,在 shutdown(SHUT_WR) 之后没有 EPOLLHUP(是的,得到 writable 是违反直觉的,因为写的一半是封闭的,但这不是问题的重点)。

man is poor, because it says EPOLLHUP comes when Hang up happened on the associated file descriptor, without saying what "hang up" means - what did the peer do? what packets were sent? This other article 只会让事情更加混乱,而且对我来说似乎是完全错误的。

我的实验表明,一旦 EOF(FIN 数据包)双向交换,即一旦双方发出 shutdown(SHUT_WR)EPOLLHUP 就会到达。它与我从不调用的 SHUT_RD 无关。也与 close 无关。在数据包方面,我怀疑 EPOLLHUP 是在主机发送的 FIN 的 ack 上引发的,即终止发起者在 4 次关闭握手的第 3 步引发此事件,而对等方在第 4 步(参见 here)。如果得到证实,那就太好了,因为它填补了我一直在寻找的空白,即如何在没有 LINGER 的情况下为最终确认轮询非阻塞套接字。 这是正确的吗?

(注意:我正在使用 ET,但我认为它与此无关)

示例代码和输出。

代码在一个框架中,我提取了它的内容,除了 TcpSocket::createListenerTcpSocket::connectTcpSocket::accept,它们做你期望的(不是显示在这里)。

void registerFd(int pollFd, int fd, const char* description)
{
    epoll_event ev = {
        EPOLLIN | EPOLLOUT | EPOLLRDHUP | EPOLLET,
        const_cast<char*>(description) // union aggregate initialisation, initialises first member (void* ptr)
    };
    epoll_ctl(pollFd, EPOLL_CTL_ADD, fd, &ev);
}

struct EventPrinter
{
    friend std::ostream& operator<<(std::ostream& stream, const EventPrinter& obj)
    {
        return stream << "0x" << std::hex << obj.events_ << " = "
            << ((obj.events_& EPOLLIN) ? "EPOLLIN " : " ")
            << ((obj.events_& EPOLLOUT) ? "EPOLLOUT " : " ")
            << ((obj.events_& EPOLLERR) ? "EPOLLERR " : " ")
            << ((obj.events_& EPOLLRDHUP) ? "EPOLLRDHUP " : " ")
            << ((obj.events_& EPOLLHUP) ? "EPOLLHUP " : " ");
    }

    const uint32_t events_;
};

void processEvents(int pollFd)
{
    static int iterationCount = 0;
    ++iterationCount;

    std::array<epoll_event, 25> events;
    int eventCount;
    if (-1 ==
        (eventCount = epoll_wait(pollFd, events.data(), events.size(), 1)))
    {
        throw Exception("fatal: epoll_wait failed");
    }

    for (int i = 0; i < eventCount; ++i)
    {
        std::cout << "iteration #" << iterationCount << ": events on [" << static_cast<const char*>(events[i].data.ptr) << "]: [" << EventPrinter{events[i].events} << "]" << std::endl;
    }
}

TEST(EpollhupExample, SmokeTest)
{
    int pollFd_;
    if (-1 ==
        (pollFd_ = epoll_create1(0)))
    {
        throw Exception("fatal: could not create epoll socket");
    }

    const TcpSocket listener_ = TcpSocket::createListener(13500);
    if (!listener_.setFileStatusFlag(O_NONBLOCK, true))
        throw Exception("could not make listener socket non-blocking");
    registerFd(pollFd_, listener_.fd(), "listenerFD");

    const TcpSocket client = TcpSocket::connect("127.0.0.1", AF_INET, 13500);
    if (!client.valid()) throw;
    registerFd(pollFd_, client.fd(), "clientFD");





    //////////////////////////////////////////////
    /// start event processing ///////////////////
    //////////////////////////////////////////////

    processEvents(pollFd_); // iteration 1

    const TcpSocket conn = listener_.accept();
    if (!conn.valid()) throw;
    registerFd(pollFd_, conn.fd(), "serverFD");

    processEvents(pollFd_); // iteration 2

    conn.shutdown(SHUT_WR);

    processEvents(pollFd_); // iteration 3

    client.shutdown(SHUT_WR);

    processEvents(pollFd_); // iteration 4
}

输出:

    Info| TCP connection established to [127.0.0.1:13500]
iteration #1: events on [listenerFD]: [1 = EPOLLIN     ]
iteration #1: events on [clientFD]: [4 =  EPOLLOUT    ]
    Info| TCP connection accepted from [127.0.0.1:35160]

iteration #2: events on [serverFD]: [4 =  EPOLLOUT    ]
    // calling serverFD.shutdown(SHUT_WR) here

iteration #3: events on [clientFD]: [2005 = EPOLLIN EPOLLOUT  EPOLLRDHUP  ]           // EPOLLRDHUP arrives, nice.
iteration #3: events on [serverFD]: [4 =  EPOLLOUT    ]                               // serverFD (on which I called SHUT_WR) just reported as writable, not cool... but not the main point of the question
    // calling clientFD.shutdown(SHUT_WR) here

iteration #4: events on [serverFD]: [2015 = EPOLLIN EPOLLOUT  EPOLLRDHUP EPOLLHUP ]   // EPOLLRDHUP arrives, nice. EPOLLHUP too!
iteration #4: events on [clientFD]: [2015 = EPOLLIN EPOLLOUT  EPOLLRDHUP EPOLLHUP ]   // EPOLLHUP on the other side as well. Why? What does EPOLLHUP mean actually?

除了 EPOLLHUP 是什么意思 之外,没有更好的方法来重新表述这个问题?我认为 documentation is poor, and information in other places (e.g. here and here) 是错误的或无用的。

注意:为了考虑问题的回答,我想确认在两个方向的最终 FIN-ACK 上都引发了 EPOLLHUP。

这类问题,use the source!在其他有趣的评论中,有这段文字:

EPOLLHUP is UNMASKABLE event (...). It means that after we received EOF, poll always returns immediately, making impossible poll() on write() in state CLOSE_WAIT. One solution is evident --- to set EPOLLHUP if and only if shutdown has been made in both directions.

然后唯一设置EPOLLHUP的代码:

if (sk->sk_shutdown == SHUTDOWN_MASK || state == TCP_CLOSE)
    mask |= EPOLLHUP;

SHUTDOWN_MASK 等于 RCV_SHUTDOWN |SEND_SHUTDOWN

TL;博士;没错,这个标志只有在读和写都关闭时才会发送(我认为对等关闭写等于我关闭读)。或者当连接关闭时,当然。

更新:通过更详细地阅读源代码,这些是我的结论。

关于shutdown

  1. 执行 shutdown(SHUT_WR) 发送 FIN 并用 SEND_SHUTDOWN 标记套接字。
  2. 执行 shutdown(SHUT_RD) 不发送任何内容并用 RCV_SHUTDOWN 标记套接字。
  3. 接收 FIN 标记套接字为 RCV_SHUTDOWN

关于epoll

  1. 如果插座标有SEND_SHUTDOWNRCV_SHUTDOWNpoll将return EPOLLHUP.
  2. 如果插座标有RCV_SHUTDOWNpoll将return EPOLLRDHUP

所以 HUP 事件可以读作:

  1. EPOLLRDHUP:您已收到FIN或已致电shutdown(SHUT_RD)。无论如何你的读取半套接字挂了,也就是说,你将不再读取数据。
  2. EPOLLHUP:你的两个半套接字都挂了。读取半套接字就像上一点一样,对于发送半套接字,你做了类似 shutdown(SHUT_WR).
  3. 的事情

要完成正常关机,我会这样做:

  1. shutdown(SHUT_WR)发送一个FIN并标记发送数据结束
  2. 等待对等方通过轮询执行相同操作,直到获得 EPOLLRDHUP
  3. 现在您可以优雅地关闭套接字了。

PS:关于您的评论:

it's counterintuitive to get writable, as the writing half is closed

如果您理解 epoll 的输出不是 ready,而是 will not block,这实际上是预期的。也就是说,如果你得到 EPOLLOUT 你就可以保证调用 write() 不会阻塞。当然,在 shutdown(SHUT_WR) 之后,write() 将立即 return。