epoll_wait 时 POLLERR 与 POLLIN 同时返回的现象解析

本文讨论了在进行codereview时,对于poll()函数处理的不同意见。重点在于如何正确处理POLLIN、POLLOUT和POLLERR事件的同时出现,并通过内核源码解析了这一现象的原因。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

今天code review时,同事B对我代码中的poll()的处理做法提出了异议。于是做了些研究,还发现了一些好玩的故事。

异议的代码

我的代码是参考manpage写的,类似下面的做法。同事B说没有处理POLLERR、而且应当使用else if

OK。我赞同补充POLLERR的处理,但不赞同使用else if。原因:

  • fd的读事件、写事件可能会同时到达,因此我想同时处理这两个事件;
  • Linux Manpage里面的示例,就是三个if语句独立的。
ret = poll(fds, 2, timeout_msecs);
if (ret > 0) {
    /* An event on one of the fds has occurred. */
    for (i=0; i<2; i++) {
        if (fds[i].revents & POLLIN ) {
            /* Priority data may be written on device number i. */
            ...
        }
        if (fds[i].revents & POLLOUT ) {
            /* Data may be written on device number i. */
            ...
        }
    }
}

诡异的经历

但是同事B举出了他偶然体验到的诡异经历:

POLLIN, POLLOUT, POLLERR同时出现。

在这种异常下,我的代码处理逻辑就会坑爹了。

于是问题变成了,什么情况下会出现这种诡异场景、三个事件同时出现究竟是什么含义?

翻阅《UNIX环境高级编程》、《UNIX网络编程》里面对poll()的讲解,均没有提到信号是否会同时出现的问题(所以也没提到该不该用else if的事情了)。

在Github上查找POLLERR相关的代码,发现大多数人都是用3个if语句处理这三个事件。那真相究竟是啥?

牛人的解答

百般搜索,终于在StackOverflow.com上看到有人提到了一个相似的问题:

Sometimes epoll_wait returns with both POLLOUT & POLLERR events set for the same socket descriptor.

终于下面有大神做了解答

Here is some good information on non-blocking tcp connect().

When a socket error is detected (i.e. connection closed/refused/timedout), epoll will return the registered interest events POLLIN/POLLOUT with POLLERR. So epoll_wait() will return POLLOUT|POLLERR if you registered POLLOUT, or POLLIN|POLLOUT|POLLERR if POLLIN|POLLOUT was registered.

Just because epoll returns POLLIN doesn't mean there will be data available to read, since recv() may just return the error from the non-blocking connect() call. I think epoll returns all the registered events with POLLERR to make sure the program calls send()/recv()/etc.. and gets the socket error. Some programs never check for POLLERR/POLLHUP and only catch socket errors on the next send()/recv() call.

翻译一下:

这儿有些很赞的关于非阻塞TCP connect()的信息。

当一个socket出现错误时(例如 连接断开/拒绝/超时),epoll()会返回POLLERR加上注册时的POLLIN/POLLOUT事件。所以,如果监听的是POLLOUT,那epoll_wait()会返回POLLOUT|POLLERR;如果监听的是POLLIN,那epoll_wait()会返回POLLIN|POLLERR。

注意epoll()返回POLLIN并不表示会有数据可读,因为recv()会立刻返回前一个错误码(即非阻塞的connect()调用)。我个人认为epoll()返回所有的注册事件加POLLERR,是为了确保程序会调用send()/recv()等等,进而发现socket出错了。毕竟有些代码从来不检测POLLERR/POLLHUP,只折腾send()/recv()等函数的错误码。

呵呵,Github上翻看了这么多代码,的确是大神说的样子。

验证

所以同事B的经历是常见的场景。而且很容易就能够触发。只要在连接上闹些问题,就能达到目的了。例如下面这段代码演示了连接失败时,POLLERR/POLLIN/POLLOUT事件都同时触发了。

示例中使用了getsockopt()来获取错误码;也可以直接使用read()/write()也是能够获取相同的错误码。

深入探究

StackOverflow的大神只做了简要的解答。真正的原因只能自己去翻看代码了。

翻阅内核代码(我的系统版本是Linux-2.6.32.57-x86 ),可以看到在tcp_poll()里(net/ipv4/tcp.c的389行,我的场景是TCP),对于所有sock错误都置了POLLERR。而异常情况下,POLLIN/POLLOUT则分别与RCV_SHUTDOWN/SEND_SHUTDOWN有关。换个视角,和连接断开有关的代码在tcp_reset()中(net/ipv4/tcp_input.c的3957行)的处理,里面的tcp_done()代码)则明确设置了sk->sk_shutdown = SHUTDOWN_MASK——所以,对于关闭的连接,总是会有POLLIN/POLLOUT事件!

研究到此解决。真相大白。

所以啊,我还是听取同事B的建议,加个else if优化一下处理逻辑吧。

### Select、Poll、Epoll在网络编程中的I/O多路复用方法对比 #### 1. Select函数的工作原理及其局限性 `select()` 是最早被引入的操作系统调用来处理多个文件描述符的状态变化。它允许程序监视多个文件句柄,等待其中任意一个变为就绪状态(可读、可写或有异常条件待处理)。然而,`select()` 存在一个显著缺点——每次调用都需要遍历整个文件描述符集合来检查哪些已经准备好,这在监控大量连接效率低下[^1]。 ```c fd_set readfds; FD_ZERO(&readfds); FD_SET(sock, &readfds); struct timeval timeout; timeout.tv_sec = 5; // 设置超间为五秒 timeout.tv_usec = 0; int activity = select(max_fd + 1, &readfds, NULL, NULL, &timeout); if (activity < 0) { perror("select failed"); } else if (!activity) { printf("Timeout occurred! No data after five seconds.\n"); } ``` #### 2. Poll改进之处及适用场景 相比 `select()`, `poll()` 不再受限于固定大小的位图表示法,因此可以支持更多的文件描述符数量。此外,在某些平台上,`poll()` 的性能优于 `select()` ,因为前者不需要复制整组文件描述符到内核空间。不过,当监听成千上万个套接字,两者都会遇到性能瓶颈。 ```c struct pollfd fds[2]; fds[0].fd = sock1; fds[0].events = POLLIN; fds[1].fd = sock2; fds[1].events = POLLOUT | POLLERR; int ret = poll(fds, 2, TIMEOUT_MS); // 超间单位为毫秒 switch(ret){ case -1: perror("poll error"); break; default: for(int i=0;i<ret;i++){ if(fds[i].revents&POLLIN){ /* handle input */ } else if(fds[i].revents&POLLOUT){ /* handle output */ } else if(fds[i].revents&POLLERR){ /* handle errors */ } } } ``` #### 3. Epoll的优势特点 Linux特有的 `epoll` 接口提供了一种更为高效的解决方案,特别是对于高并发服务器而言。通过采用事件驱动模型以及内存映射技术,`epoll` 可以极大地减少不必要的上下文切换开销,并且能够轻松应对数万级别的活动连接。更重要的是,只有真正发生变动的那些文件描述符才会传递给应用程序层,从而进一步提高了系统的响应速度资源利用率。 ```c // 创建一个新的 epoll 实例 int epfd = epoll_create(EPOLL_SIZE_HINT); struct epoll_event ev, events[MAX_EVENTS]; ev.events = EPOLLIN; ev.data.fd = listen_sock; epoll_ctl(epfd, EPOLL_CTL_ADD, listen_sock, &ev); while(true){ int nfds = epoll_wait(epfd, events, MAX_EVENTS, -1); for(int n=0;n<nfds;++n){ if(events[n].data.fd==listen_sock){ struct sockaddr_in addr; socklen_t addrlen = sizeof(addr); int conn_sock = accept(listen_sock,(sockaddr*)&addr,&addrlen); ev.events = EPOLLIN|EPOLLET; ev.data.fd = conn_sock; epoll_ctl(epfd, EPOLL_CTL_ADD, conn_sock, &ev); }else{ handle_events(events[n]); } } } ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值