IO多路复用
IO多路复用。poll、select、epoll,区别和异同。不同模式分别在什么情况下进行选择。比如遇到什么样的编程场景,会选择其中一个而不是另一个。
原理和实现:
IO多路复用是一种允许单个线程监视和管理多个IO流的技术。
poll
、select
和 epoll
都是实现IO多路复用的系统调用。以下是它们的区别和异同:
1. select
select
是最早的IO多路复用解决方案,支持的平台最广。
- 它的接口设计相对简单,使用起来较为方便。
select
使用一个位图来表示需要监视的文件描述符,因此它的最大连接数受限于FD_SETSIZE的大小,通常为1024。
- 每次调用
select
时,都需要在用户空间和内核空间之间复制文件描述符,当连接数非常大时,效率较低。
2. poll
poll
解决了select
的最大连接数限制问题,因为它不是使用位图,而是使用一个链表来存储文件描述符。
- 但
poll
仍然存在效率问题。每次调用poll
时,都需要遍历所有的文件描述符,即使大部分文件描述符都没有就绪。
3. epoll
epoll
是Linux特有的IO多路复用解决方案,它没有最大连接数的限制,也没有效率问题。
epoll
使用一个事件表来保存就绪的事件,因此无需遍历所有的文件描述符。
epoll
还支持“边缘触发”模式,这使得它能够更好地处理大量并发的连接。
水平触发和边缘触发。(from 小林)
epoll 支持两种事件触发模式,分别是边缘触发(edge-triggered,ET)和水平触发(level-triggered,LT)。
- 使用边缘触发模式时,当被监控的 Socket 描述符上有可读事件发生时,服务器端只会从 epoll_wait 中苏醒一次,即使进程没有调用 read 函数从内核读取数据,也依然只苏醒一次,因此我们程序要保证一次性将内核缓冲区的数据读取完;
- 使用水平触发模式时,当被监控的 Socket 上有可读事件发生时,服务器端不断地从 epoll_wait 中苏醒,直到内核缓冲区数据被 read 函数读完才结束,目的是告诉我们有数据需要读取;
边缘触发模式一般和非阻塞 I/O 搭配使用,程序会一直执行 I/O 操作,直到系统调用(如
read
和 write
)返回错误,错误类型为 EAGAIN
或 EWOULDBLOCK
。select/poll 只有水平触发模式,epoll 默认的触发模式是水平触发,但是可以根据应用场景设置为边缘触发模式。
另外,使用 I/O 多路复用时,最好搭配非阻塞 I/O 一起使用,多路复用 API 返回的事件并不一定可读写的,如果使用阻塞 I/O, 那么在调用 read/write 时则会发生程序阻塞,因此最好搭配非阻塞 I/O,以便应对极少数的特殊情况。
以下是在不同场景下选择不同解决方案的一些指导:
- 如果你的程序需要在各种平台上运行,包括非Linux平台,那么
select
或poll
可能是更好的选择,因为epoll
只在Linux上可用。
- 如果你的程序需要处理大量的并发连接,那么
epoll
是最好的选择,因为select
和poll
在处理大量连接时效率低。
- 如果你的程序只需要处理少量的并发连接,那么
select
和poll
都可以满足需求,你可以根据API的易用性来选择。
总的来说,
epoll
通常是最高效的解决方案,但是它的使用也最为复杂,而且只能在Linux上使用。select
和 poll
的性能较差,但是使用起来更简单,且跨平台。但是问题还是不知道什么场景会只使用其中一种技术而不是用另一种,上面回答面试官是不满意的。
select/poll/epoll适用场景
- select 应用场景
select 的 timeout 参数精度为微秒,而 poll 和 epoll 为毫秒,因此 select 更加适用于实时性要求比较高的场景,比如核反应堆的控制。
select 可移植性更好,几乎被所有主流平台所支持。
- poll 应用场景
poll 没有最大描述符数量的限制,如果平台支持并且对实时性要求不高,应该使用 poll 而不是 select。
- epoll 应用场景
只需要运行在 Linux 平台上,有大量的描述符需要同时轮询,并且这些连接最好是长连接。
需要同时监控小于 1000 个描述符,就没有必要使用 epoll,因为这个应用场景下并不能体现 epoll 的优势。
需要监控的描述符状态变化多,而且都是非常短暂的,也没有必要使用 epoll。因为 epoll 中的所有描述符都存储在内核中,造成每次需要对描述符的状态改变都需要通过 epoll_ctl() 进行系统调用,频繁系统调用降低效率。并且 epoll 的描述符存储在内核,不容易调试
socket编程
socket地址复用
在Linux中,为了使得一个socket地址(IP地址和端口号)可以被多个socket同时使用,我们通常需要设置socket的
SO_REUSEADDR
选项。这在服务器程序中尤其有用,因为它允许服务器在重启后立即重新绑定到相同的地址,而不是等待系统清理之前服务器使用的地址。你可以使用
setsockopt
函数来设置这个选项。下面是一个例子:在这个例子中,我们首先创建了一个socket,然后使用
setsockopt
函数来设置SO_REUSEADDR选项。这意味着这个socket现在可以被绑定到一个已经在使用的地址。注意,SO_REUSEADDR选项只允许在同一台机器上的多个socket同时绑定到同一个地址。如果你想在不同的机器上的socket能够绑定到同一个地址,你可能需要查看SO_REUSEPORT选项,这是一个在Linux 3.9及以后的版本中支持的特性。
阻塞和非阻塞编程
在Linux的socket编程中,阻塞和非阻塞模式是两种不同的IO处理方式。
- 阻塞模式(Blocking mode):在阻塞模式中,调用IO操作的线程会被操作系统挂起,直到IO操作完成为止。例如,当你调用
read()
函数时,如果没有数据可以读取,线程会被挂起,直到有数据可以读取为止。
- 非阻塞模式(Non-blocking mode):在非阻塞模式中,如果IO操作不能立即完成,函数会立即返回,并通常返回一个错误码表示“资源不可用”(在Linux中,这个错误码通常是
EAGAIN
或EWOULDBLOCK
)。调用线程可以在稍后再次尝试IO操作,或者进行其他的工作。
以下是两种模式的代码示例:
阻塞模式:
非阻塞模式:
使用非阻塞IO可以在单个线程中处理多个socket,这在开发高性能的网络服务器时非常有用。然而,非阻塞IO也会带来更复杂的编程模型,因为你需要处理"资源不可用"的情况,并且可能需要使用某种形式的事件驱动编程或异步IO。
非阻塞模式扩展
在非阻塞模式下,如果资源不可用(例如,socket的接收缓冲区没有数据可读,或发送缓冲区已满无法写入更多数据),
read()
或write()
函数会立即返回,并设置错误码为EAGAIN
或EWOULDBLOCK
。对于这种情况,一种常见的处理策略是:记录下这种状态,并在稍后再次尝试进行IO操作。
以下是一个处理非阻塞写的示例:
在这个例子中,如果
write()
操作因为资源不可用而不能立即完成,我们简单地退出循环,并在稍后再次尝试。在实际的应用中,你可能会使用
select()
,poll()
或epoll()
等函数来等待socket变为可写,这样可以避免无效的循环,并能在同一线程中处理多个socket。此外,如果写入的数据量大,你可能需要将未写完的数据保存到一个队列或缓冲区中,然后在socket可写时再从队列或缓冲区中取出数据进行写入。- 作者:Olimi
- 链接:https://olimi.icu/article/4587dc85-335b-4068-a483-a89d07cedff9
- 声明:本文采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。