使用EPOLLET边缘触发搭配EPOLLONESHOT时,应何时重新挂载epoll事件?
正确的重新挂载时机:非阻塞IO操作全部完成后执行
你代码中注释为or here?的位置是唯一正确的重新挂载(rearm)位置,不可以在非阻塞操作前执行rearm。
原因说明
EPOLLONESHOT的核心作用是:内核上报一次该文件描述符(fd)的事件后,会自动将该fd从epoll监听列表中屏蔽,直到你手动调用EPOLL_CTL_MOD重新挂载,目的就是保证同一时间只有一个执行流处理该fd的IO事件,避免竞态条件。
为什么不能在IO操作前rearm
如果先执行rearm再调用read:
- 刚rearm完成,内核如果检测到该fd有新的事件触发,会直接将事件上报给其他正在
epoll_wait的协程/线程,此时当前协程还在操作该fd,直接出现多个执行流同时读写同一个套接字的竞态问题,会导致数据错乱、重复处理等故障。
为什么必须在IO操作完成后rearm
当你把套接字缓冲区所有数据全部读空(读到返回EAGAIN/EWOULDBLOCK)后再执行rearm:
- 整个IO处理过程中,该fd的事件处于
EPOLLONESHOT的屏蔽状态,没有其他执行流会介入处理,完全避免竞态 - 边缘触发(EPOLLET)模式下,只有缓冲区状态发生新的变化时才会上报事件,处理完所有存量数据后再重新监听,既不会漏掉后续新到达的数据,也不会收到虚假事件。
注意事项
- 边缘触发模式下必须把缓冲区读空(读到
EAGAIN)才能结束IO操作,否则未读取的存量数据不会触发新的事件通知,会出现事件遗漏 - 调用
EPOLL_CTL_MOD重新挂载时,必须重新指定EPOLLET | EPOLLONESHOT标志,否则会覆盖掉原有监听参数 - 注意参数拼写错误:是
EPOLL_CTL_MOD而非EPOL_CTL_MOD
正确代码示例
// epoll_wait返回事件后进入fd处理逻辑 int ret; char buf[4096]; struct epoll_event ev; while (1) { ret = read(fd, buf, sizeof(buf)); if (ret < 0) { if (errno == EAGAIN || errno == EWOULDBLOCK) { // 缓冲区读空,此处执行重新挂载 ev.events = EPOLLIN | EPOLLET | EPOLLONESHOT; ev.data.fd = fd; epoll_ctl(epoll_, EPOLL_CTL_MOD, fd, &ev); break; } // 处理其他IO错误,关闭fd,无需重新挂载 close(fd); break; } else if (ret == 0) { // 对端关闭连接,清理资源,无需重新挂载 close(fd); break; } // 此处添加读取到的数据的处理逻辑 } // 处理完成回到epoll_wait等待新事件 ret = epoll_wait(epoll_, ...);
内容的提问来源于stack exchange,提问作者Šimon Tóth
相关产品推荐
相关产品推荐

