当我们谈及到 IO 多路复用的时候通常会提及到的一个概念是非杜塞IO,网上的教程中在使用IO多路复用的时候通常会先将对于的socket设置为非杜塞的,但是这样子做的原因却很少被提及。在谈论为什么使用非杜塞IO之前可以先复习一下非杜塞 IO 和杜塞 IO 有什么区别[1]
- 接收消息:如果是采用杜塞IO,在调用
read
函数之后程序将会杜塞直到有消息发送过来。如果是采用非杜塞IO,在调用read
之后如果没有数据会立即返回一个EWOULDBLOCK
错误,而不会杜塞。 - 发送消息:如果是采用杜塞IO,在调用
write
函数,但是如果没有足够的发送缓冲区就会杜塞,直到数据全部发送出去才返回。如果是使用非杜塞 IO且没有足够的发送缓冲区,会直接将可以发送的全部发送出去,剩下的就不发送了,程序返回,也就是此时write
返回的 n 要小于调用write
函数需要发送的数据。
在了解非杜塞 IO 和杜塞 IO 的区别之后,再来看看为什么在使用 IO 多路复用的时候通常要使用非杜塞 IO。
- 对于接收场景,采用 IO 多路复用的时候通常是收到 readable 通知之后才会调用
read
函数读取数据的,所以说这里无论是使用非杜塞还是杜塞 IO,实际上效果都是一样的。 - 对于发送场景,如果发送缓冲区充足那当然没什么问题,但是如果发送缓冲区已经快满了,无法直接将这次需要发送的数据一下子发送出去,如果是采用杜塞 IO 程序就会堵住,直到消息全部发送出去,但是使用非杜塞 IO 的话会将可以发送的数据发送出去,剩下的数据就需要应用自身缓存起来,等待下次收到
writeable
信号的时候再次尝试发送。
总的来说对于接收场景使用杜塞或者非杜塞IO实际上是没有区别的,但是对于发送场景在高并发的情况下,如果使用杜塞 IO 发送可能会发送杜塞,如果使用非杜塞 IO 可以避免这个杜塞但是需要应用层自己也实现一个发送缓冲区来管理那些当时没有发送出去的数据,增加了软件设计的复杂度。在实际的应用中如果存在高并发的场景建议还是使用非杜塞 IO,如果不是使用杜塞式 IO 也未尝不可。
2024.11.9 补充
最近看了一篇博客:Blocking I/O, Nonblocking I/O, And Epoll,发现之前的认识有还是有些不足的,主要是下面这两点
-
如果是使用水平触发且应用层的 Buffer 要小于 TCP Read Buffer,采用非堵塞 IO 可以减少
select
或者epoll_wait
系统调用ssize_t nbytes; for (;;) { /* select call happens here */ if (select(FD_SETSIZE, &read_fds, NULL, NULL, NULL) < 0) { perror("select"); exit(EXIT_FAILURE); } for (int i = 0; i < FD_SETSIZE; i++) { if (FD_ISSET(i, &read_fds)) { /* NEW: loop until EWOULDBLOCK is encountered */ for (;;) { /* read call happens here */ nbytes = read(i, buf, sizeof(buf)); if (nbytes >= 0) { handle_read(nbytes, buf); } else { if (errno != EWOULDBLOCK) { /* real version needs to handle EINTR correctly */ perror("read"); exit(EXIT_FAILURE); } break; } } } } }
-
EPOLL 在使用边缘触发的时候,必须要使用非堵塞 IO,将 Socket Read Buffer 里面的数据全部读取出来,否则再来数据的时候系统不会通知应用,导致丢包。
详细内容可以参考 《Unix Network Programming》的第十六章 non-blocking I/O ↩︎