【问题标题】:epoll_wait return EPOLLOUT even with EPOLLET flagepoll_wait 即使带有 EPOLLET 标志也返回 EPOLLOUT
【发布时间】:2020-04-04 21:49:16
【问题描述】:

我在边缘触发模式下使用 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。而且我认为这可能会导致以下情况下的事件丢失:socket读写操作都满足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 with edge triggered event 的最高投票回答说:

关于一些相关的说明:如果您注册 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?

【问题讨论】:

  • 听起来答案是肯定的。好吧,你可以让你的程序随心所欲地处理它。这是一种方式。您也可以考虑在不等待写入数据时使用 epoll_ctl 禁用 EPOLLOUT。
  • @demonatic 是否有充分的理由使用边缘触发语义?一种更简洁、更强大的方法是使用级别触发的轮询并根据您目前的需要修改事件集。
  • @Ctx 我使用边缘触发主要是为了减少系统调用和epoll_wait可能返回的fds数量

标签: c++ c linux networking epoll


【解决方案1】:

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

是的,事件标志的 if/else 处理绝对是假的。可以肯定的是,至少两个罐头事件可以同时有效地到达。例如,自从您上次调用 epoll_wait() 以来,读取端和写入端都可能已经畅通无阻。而且,当然,一旦你accept() 连接,读和写就突然成为可能,所以你得到一个EPOLLIN|EPOLLOUT 的“事件”。

我真的不明白epoll_wait() 总是传递整个当前状态,而不仅仅是状态发生变化的部分——感谢您澄清这一点。或许更清楚一点,epoll_wait() 不会返回 fd,除非 something 在该套接字上发生了变化,但如果确实发生了变化,它会返回代表当前状态的所有标志。所以,我发现自己盯着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 告诉您 EPOLLIN 时,read() 返回零字节。但是,Linux 会保留一个额外的位来指示客户端的写入端(您的读取端)已关闭。因此,如果您告诉 epoll 您想要 EPOLLRDHUP 事件,您可以使用它来避免执行 read() 其唯一目的是告诉您作者关闭了他们的身边。

请注意,只要 EPOLLRDHUP 是 AFAIK,EPOLLIN 仍将打开。即使你做了shutdown(fd, SHUT_RD)。另一个例子是你通常会被驱使保持你自己对连接状态的想法。你更关心那些好心做半关机的客户if you are implementing HTTP

【讨论】:

    【解决方案2】:

    当用作边缘触发接口时,出于性能原因,
    它 可以在 epoll 接口内添加文件描述符 (EPOLL_CTL_ADD) 一次,通过指定 (EPOLLIN|EPOLLOUT)。
    这让你 避免在 EPOLLIN 和 EPOLLOUT 调用之间不断切换 epoll_ctl(2) 和 EPOLL_CTL_MOD。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2012-11-14
      • 1970-01-01
      • 2011-01-08
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多