繁体   English   中英

即使有 EPOLLET 标志,epoll_wait 也返回 EPOLLOUT

[英]epoll_wait return EPOLLOUT even with EPOLLET flag

我在边缘触发模式下使用 linux epoll。 每次有新连接传入时,我都会使用 EPOLLIN|EPOLLOUT|EPOLLET 标志将文件描述符添加到 epoll。 我的第一个问题是:在 epoll_wait 返回后,检查每个就绪文件描述符发生哪种事件的正确方法是什么? 我的意思是,我看到一些示例代码,例如来自https://github.com/yedf/handy/blob/master/raw-examples/epoll-et.cc第 124 行这样做:

for (int i = 0; i < n; i++) {
    //...
    if (events & (EPOLLIN | EPOLLERR)) {
        if (fd == lfd) {
            handleAccept(efd, fd);
        } else {
            handleRead(efd, fd);
        }
    } else if (events & EPOLLOUT) {
        if (output_log)
            printf("handling epollout\n");
        handleWrite(efd, fd);
    } else {
        exit_if(1, "unknown event");
    }
}

引起我注意的是:它使用“if and else if and else”来检查哪个事件发生,这意味着如果它handleRead,那么它不能同时handleWrite。 并且我认为这可能会在以下情况下导致事件丢失:套接字读写操作都满足 EAGAIN,然后远程端都读取和发送了一些数据,因此 epoll 等待可能会同时设置 EPOLLIN 和 EPOLLOUT,但它只能handleRead,并且由于没有调用 handleWrite,因此无法发送输出缓冲区中剩余的数据。 那么上面的用法是错误的吗?

根据 man 7 epoll QA:

  1. 如果在 epoll_wait(2) 调用之间发生多个事件,它们是合并还是单独报告?

    他们将被合并。

如果我做对了,在 epoll_wait 调用之间的单个文件描述符上可能会发生多个事件。 所以我认为我应该使用多个“if if and if”来检查可读/可写/错误事件是否发生,而不是使用“if and else if”。 我去看看nginx epoll模块是怎么做的,从https://github.com/nginx/nginx/blob/953f53921505a884f3912f2d8db5217a71c0479a/src/event/modules/ngx_epoll_module.c#L867看到如下代码:

    if (revents & (EPOLLERR|EPOLLHUP)) {
        //...
    }
    if ((revents & EPOLLIN) && rev->active) {
        //....
        rev->handler(rev);
    }
    if ((revents & EPOLLOUT) && wev->active) {
        //....
        wev->handler(wev);
    }

它似乎符合我一个接一个检查所有 EPOLLERR..​​,EPOLLIN,EPOLLOUT 事件的想法。 然后我在我的应用程序中做与 nginx 相同的事情。 但是我经过实验发现:如果我将文件描述符添加到带有 EPOLLIN|EPOLLOUT|EPOLLET 标志的 epoll 中,并且我没有填满输出缓冲区,我总是会在 epoll_wait 返回后因为一些数据到达而设置 EPOLLOUT 标志并且这个 fd 变得可读,因此会调用冗余的 write_handler,这不是我所期望的。

我做了一些搜索,发现这种情况确实存在,并不是由我的应用程序中的任何错误引起的。 根据epoll上投票最高的答案, 边缘触发事件说:

在一些相关的说明中:如果您注册 EPOLLIN 和 EPOLLOUT 事件并假设您永远不会填满发送缓冲区,那么每次触发 EPOLLIN 时,您仍然会在 epoll_wait 返回的事件中设置 EPOLLOUT 标志 - 请参阅https://lkml。 org/lkml/2011/11/17/234以获得更详细的解释。

这个答案中的链接说:

这并不意味着有一个 EPOLLOUT“事件”,它只是意味着一条消息被触发(通过套接字变得可读)所以你得到一个状态更新。 理论上,程序不需要在这里被告知 EPOLLOUT(它应该假设套接字已经是可写的),但它不会造成任何伤害。

到目前为止,我对 epoll边沿触发模式的理解是:

  • 当被监控的任何 fd 的状态发生变化时,epoll_wait 返回,例如从无到读取 -> 可读或缓冲区已满 -> 缓冲区可以写入

  • epoll_wait 可能会为就绪列表中的每个 fd 返回一个或多个事件(标志)。

  • sturct epoll_event.events 字段中的标志指示此 fd 的当前状态。 即使我们不填写输出缓冲区,由于可读,epoll_wait 返回时也会设置 EPOLLOUT 标志,因为 fd 的当前状态只是可写。

如果我错了,请纠正我。 那么我的问题是:我是否应该在每个连接中维护一个标志以指示写入输出缓冲区时是否发生 EAGAIN,如果未设置,则不要在“if (events & EPOLLOUT)”分支中调用 write_handler/handleWrite,所以我的上层程序不会在这里被告知 EPOLLOUT 吗?

多么好的问题(因为我有几乎相同的问题)! 我将总结我认为我现在知道的内容,与您的信息性问题/描述和有用的链接有关,希望更聪明的人会纠正任何错误。

是的,事件标志的 if/else 处理绝对是假的。 可以肯定的是,至少有两个 can 事件可以同时有效地到达。 例如,自上次调用epoll_wait()以来,读取和写入端可能都已epoll_wait() 而且,当然,一旦您accept()连接,读取和写入突然变得可能,因此您会得到EPOLLIN|EPOLLOUT的“事件”。

我真的不明白epoll_wait()总是提供整个当前状态,而不仅仅是状态发生变化的部分 - 感谢您清除它。 为了更清楚, epoll_wait()不会返回 fd 除非该套接字上的某些内容发生变化,但是如果某些内容确实发生了变化,它会返回代表当前状态的所有标志。 所以,我发现自己盯着EPOLLIN|EPOLLOUT事件流,想知道为什么它声称有一个“输出”事件,即使我还没有写任何东西。 你的答案是正确的:它只是告诉我输出端仍然是可写的。

“我应该维护一个标志吗……”是的,但我想在所有情况下,除了最微不足道的情况外,无论如何,您可能最终都会为您的读者/作者维护至少一点“我目前是否被阻止”状态. 例如,如果您想按照与数据到达方式不同的顺序处理数据(例如,将响应优先于请求,使您的服务器更能抵抗过载),您必须立即放弃仅让 I/ 到达的简单性。 O 驱动一切。 在特定的写作情况下,epoll 根本没有足够的信息在“正确”的时间通知您。 一旦你接受一个连接,就会有一个事件说“你现在可以写了”——但如果你是一个不可能已经从客户端收到请求的服务器,你可能没有什么可写的。 epoll 只是不知道你是否有东西要写,所以你总是要么遭受本质上“无关”的事件,要么维护自己的状态。

除了最简单的情况外,套接字文件描述符最终没有足够的信息来处理 I/O 事件,因此您总是必须将某些数据结构与其相关联,或者如果您愿意,则为对象。 所以,我的 C++ 看起来像:

nAwake = epoll_wait(epollFd, events, 100, milliseconds);
if(nAwake < 0)
    {
    perror("epoll_wait failed");
    assert(false);
    }
for(int iSocket=0; iSocket < nAwake; ++iSocket)
    {
    auto This = static_cast<Eventable*>(events[iSocket].data.ptr);
    auto eventFlags = events[iSocket].events;
    fprintf(stderr, "%s event on socket [%d] -> %s\n",
        This->ClassName(), This->fd, DumpEvent(eventFlags));

    This->Event(eventFlags);
    }

其中Eventable是一个 C++ 类(或其派生类),它具有决定如何处理 epoll 提供的标志所需的所有状态。 (当然,这是让内核存储指向 C++ 对象的指针,需要一个非常清楚指针所有权/生命周期的设计。)

而且由于您是在 Linux 上编写低级代码,因此您可能还关心EPOLLRDHUP 这个不是高度可移植的标志让您可以节省一次对read()调用。 如果客户端(curl 似乎很擅长唤起这种行为)关闭其连接的写入端(发送 FIN),您通常会发现当 epoll 告诉您EPOLLINread()返回零字节。 但是,Linux 维护了一个额外的位来指示您客户端的写入端(您的读取端)已关闭。 所以,如果你告诉 epoll 你想要EPOLLRDHUP事件,你可以用它来避免执行read()它的唯一目的是告诉你作者关闭了他们的身边。

请注意,只要EPOLLRDHUP为 AFAIK, EPOLLIN仍将打开。 即使在您执行shutdown(fd, SHUT_RD) 另一个例子说明您通常会如何保持自己对连接状态的想法。 如果您正在实施 HTTP ,您会更关心那些能够进行半关闭的客户端。

当用作边缘触发接口时,出于性能原因,
可以通过指定 (EPOLLIN|EPOLLOUT) 在 epoll 接口 (EPOLL_CTL_ADD) 中添加一次文件描述符。
这允许您避免在 EPOLLIN 和 EPOLLOUT 之间连续切换,使用 EPOLL_CTL_MOD 调用 epoll_ctl(2)。 在此处输入图片说明

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM