epoll——常见面试问题
epoll——常见面试问题
常问面试问题
1、Linux epoll ET模式下 EPOLLOUT和EPOLLIN触发场景?
1、EPOLLOUT事件:
EPOLLOUT事件只有在连接时触发一次,表示可写,其他时候想要触发,那你要先准备好下面条件:
-
1.某次write,写满了发送缓冲区,返回错误码为EAGAIN。
-
2.对端读取了一些数据,又重新可写了,此时会触发EPOLLOUT。
简单地说:EPOLLOUT事件只有在不可写到可写的转变时刻,才会触发一次,所以叫边缘触发,这叫法没错的!
其实,如果你真的想强制触发一次,也是有办法的,直接调用epoll_ctl重新设置一下event就可以了,event跟原来的设置一模一样都行(但必须包含EPOLLOUT),关键是重新设置,就会马上触发一次EPOLLOUT事件。
2、EPOLLIN事件:
EPOLLIN事件则只有当对端有数据写入时才会触发,所以触发一次后需要不断读取所有数据直到读完EAGAIN为止。否则剩下的数据只有在下次对端有写入时才能一起取出来了。
现在明白为什么说epoll必须要求异步socket了吧?如果同步socket,而且要求读完所有数据,那么最终就会在堵死在阻塞里。
2、epoll的ET模式下,正确的读写方式
- 读: 只要可读, 就一直读,直到返回0,或者 errno = EAGAIN或者EWOULDBLOCK
- 写:只要可写, 就一直写,直到数据发送完,或者 errno = EAGAIN或者EWOULDBLOCK
从字面上看, 意思是:
- EAGAIN: 再试一次
- EWOULDBLOCK: 如果这是一个阻塞socket, 操作将被block
正确的读:
while ((byte_read = read(fd, buf + n, BUFSIZ-1)) > 0) {
n += byte_read;//计算读取的字节
}
if (nread == -1 && errno != EAGAIN) {
perror("read error");
}
正确的写:
int nwrite, data_size = strlen(buf);
n = data_size;
while (n > 0) {
nwrite = write(fd, buf + data_size - n, n);
if (nwrite < n) {
if (nwrite == -1 && errno != EAGAIN) {
perror("write error");
}
break;
}
n -= nwrite;
}
3、epoll的水平触发和边沿触发区别
(1)水平触发LT:缺省的工作方式(epoll默认的设置),并且同时支持block和no-block socket.
- 接受缓冲区不为空,对应fd一直处于“读就绪”状态;发送缓冲区不满,对应fd一直处于“写就绪”状态(关于这里的缓冲区:读多少就少多少,写多少就多多少)。所以水平触发,只要数据没读完,那么在epoll_wait中,就一直认为该fd处于就绪状态。
- 采用LT模式的文件描述符,当epoll_wait检测到其上有事件发生并将此事件通知应用程序后,应用程序可以不立即处理此事件,当下一次调用epoll_wait是,epoll_wait还会将此事件通告应用程序。
- 如果系统中有大量你不需要读写的就绪文件描述符,而它们每次都会返回,这样会大大降低处理程序检索自己关心的就绪文件描述符的效率
(2)边沿触发ET:只支持no-block socket
如果套接口被设置成阻塞模式,服务器就会一直阻塞在accept调用上,直到其他某个客户建立一个新的连接为止。但是在此期间,服务器单纯地阻塞在accept 调用上,就绪队列中的其他描述符都得不到处理。
- 当fd对应的读缓冲区从”有“变为了”无“时,对应fd才处于”读就绪“。当fd对应的写缓冲区从”无“变为了”有“时,对应fd才处于”写就绪“。
注意:一定是在”有“和”无“之间变化才能触发
。 - 当epoll_wait检测到其上有事件发生并将此通知应用程序后,应用程序必须立即处理该事件,因为后续的epoll_wait调用将不在向应用程序通知这一事件。
- ET模式降低了同意epoll事件被触发的次数,效率比LT模式高。
4、使用epoll时需要将socket设为非阻塞吗?
只有边沿触发才必须设置为非阻塞。
边沿触发的问题:
1、sockfd 的边缘触发
,高并发时,如果没有一次处理全部请求,则会出现客户端连接不上的问题。不需要讨论 sockfd 是否阻塞,因为epoll_wait() 返回的必定是已经就绪的连接,所以不管是阻塞还是非阻塞,accept() 都会立即返回。2、阻塞 connfd 的边缘触发
,如果不一次性读取一个事件上的数据,会干扰下一个事件,所以必须在读取数据的外部套一层循环,这样才能完整的处理数据。但是外层套循环之后会导致另外一个问题:处理完数据之后,程序会一直卡在 recv() 函数上,因为是阻塞 IO,如果没数据可读,它会一直等在那里,直到有数据可读。但是这个时候,如果用另一个客户端去连接服务器,服务器就不能受理这个新的客户端了。3、非阻塞 connfd 的边缘触发
,和阻塞版本一样,必须在读取数据的外部套一层循环,这样才能完整的处理数据。因为非阻塞 IO 如果没有数据可读时,会立即返回,并设置 errno。这里我们根据 EAGAIN 和 EWOULDBLOCK 来判断数据是否全部读取完毕了,如果读取完毕,就会正常退出循环了。
总结一下:
1、对于监听的 sockfd
,最好使用水平触发模式,边缘触发模式会导致高并发情况下,有的客户端会连接不上。如果非要使用边缘触发,可以用 while 来循环 accept()。2、对于读写的 connfd
,水平触发模式下,阻塞和非阻塞效果都一样,建议设置非阻塞。3、对于读写的 connfd
,边缘触发模式下,必须使用非阻塞 IO
,并要求一次性地完整读写全部数据。
所以在用EPOLL的时候,我们都用fcntl
将描述符置为非阻塞吧,皆大欢喜。
5、ET模式下的accept问题
请思考以下一种场景:在某一时刻,有多个连接同时到达,服务器的 TCP 就绪队列瞬间积累多个就绪连接,由于是边缘触发模式,epoll 只会通知一次,accept 只处理一个连接,导致 TCP 就绪队列中剩下的连接都得不到处理。在这种情形下,我们应该如何有效的处理呢?
解决的方法是:解决办法是用 while 循环抱住 accept 调用,处理完 TCP 就绪队列中的所有连接后再退出循环。如何知道是否处理完就绪队列中的所有连接呢? accept 返回 -1 并且 errno 设置为 EAGAIN 就表示所有连接都处理完。
6、epoll涉及的数据结构?epoll与select、poll的对比?
篇幅过长,参考上一篇博客,便于清楚理解