【发布时间】: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:
如果在 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