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::createListener
、TcpSocket::connect
和 TcpSocket::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
:
- 执行
shutdown(SHUT_WR)
发送 FIN
并用 SEND_SHUTDOWN
标记套接字。
- 执行
shutdown(SHUT_RD)
不发送任何内容并用 RCV_SHUTDOWN
标记套接字。
- 接收
FIN
标记套接字为 RCV_SHUTDOWN
。
关于epoll
:
- 如果插座标有
SEND_SHUTDOWN
和RCV_SHUTDOWN
,poll
将return EPOLLHUP
.
- 如果插座标有
RCV_SHUTDOWN
,poll
将return EPOLLRDHUP
。
所以 HUP
事件可以读作:
EPOLLRDHUP
:您已收到FIN
或已致电shutdown(SHUT_RD)
。无论如何你的读取半套接字挂了,也就是说,你将不再读取数据。
EPOLLHUP
:你的两个半套接字都挂了。读取半套接字就像上一点一样,对于发送半套接字,你做了类似 shutdown(SHUT_WR)
. 的事情
要完成正常关机,我会这样做:
- 做
shutdown(SHUT_WR)
发送一个FIN
并标记发送数据结束
- 等待对等方通过轮询执行相同操作,直到获得
EPOLLRDHUP
。
- 现在您可以优雅地关闭套接字了。
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。
另请参阅 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::createListener
、TcpSocket::connect
和 TcpSocket::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 receivedEOF
,poll
always returns immediately, making impossiblepoll()
onwrite()
in stateCLOSE_WAIT
. One solution is evident --- to setEPOLLHUP
if and only ifshutdown
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
:
- 执行
shutdown(SHUT_WR)
发送FIN
并用SEND_SHUTDOWN
标记套接字。 - 执行
shutdown(SHUT_RD)
不发送任何内容并用RCV_SHUTDOWN
标记套接字。 - 接收
FIN
标记套接字为RCV_SHUTDOWN
。
关于epoll
:
- 如果插座标有
SEND_SHUTDOWN
和RCV_SHUTDOWN
,poll
将returnEPOLLHUP
. - 如果插座标有
RCV_SHUTDOWN
,poll
将returnEPOLLRDHUP
。
所以 HUP
事件可以读作:
EPOLLRDHUP
:您已收到FIN
或已致电shutdown(SHUT_RD)
。无论如何你的读取半套接字挂了,也就是说,你将不再读取数据。EPOLLHUP
:你的两个半套接字都挂了。读取半套接字就像上一点一样,对于发送半套接字,你做了类似shutdown(SHUT_WR)
. 的事情
要完成正常关机,我会这样做:
- 做
shutdown(SHUT_WR)
发送一个FIN
并标记发送数据结束 - 等待对等方通过轮询执行相同操作,直到获得
EPOLLRDHUP
。 - 现在您可以优雅地关闭套接字了。
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。