IO多路复用
什么是IO多路复用
IO多路复用是一种同步IO模型,实现一个线程可以监视多个文件描述符;一旦某个文件描述符就绪,就能够通知应用程序做出相应的读写操作;当没有文件描述符就绪时,就会阻塞应用程序,让出cpu。
多路是指网络连接,复用是指同一个线程
同步阻塞IO
服务端采用单线程,当
accept
一个请求后,在recv
或send
调用阻塞时,将无法accept
其他请求(必须等上一个请求处理recv
或send
完 )(无法处理并发)服务端采用多线程,当
accept
一个请求后,开启线程进行recv
,可以完成并发处理,但随着请求数增加需要增加系统线程,大量的线程占用很大的内存空间,并且线程切换会带来很大的开销,10000个线程真正发生读写实际的线程数不会超过20%,每次accept
都开一个线程也是一种资源浪费。
同步非阻塞IO
- 服务器端当
accept
一个请求后,加入fds
集合,每次轮询一遍fds
集合recv
(非阻塞)数据,没有数据则立即返回错误,每次轮询所有fd
(包括没有发生读写实际的fd
)会很浪费CPU
。
IO多路复用
- 服务器端采用单线程通过
select/poll/epoll
等系统调用获取fd
列表,遍历有事件的fd
进行accept/recv/send
,使其能支持更多的并发连接请求。 整个过程只在调用select、poll、epoll
这些调用的时候才会阻塞,accept/recv
是不会阻塞。
select缺点
select本质上是通过设置或者检查存放fd标志位的数据结构来进行下一步处理。这样所带来的缺点是:
- 单个进程所打开的FD是有限制的,通过
FD_SETSIZE
设置,默认1024 ; - 每次调用 select,都需要把 fd 集合从用户态拷贝到内核态,这个开销在 fd 很多时会很大;
需要维护一个用来存放大量fd的数据结构,这样会使得用户空间和内核空间在传递该结构时复制开销大
- 对 socket 扫描时是线性扫描,采用轮询的方法,效率较低(高并发)
当套接字比较多的时候,每次
select()
都要通过遍历FD_SETSIZE
个Socket来完成调度,不管哪个Socket是活跃的,都遍历一遍。这会浪费很多CPU时间。如果能给套接字注册某个回调函数,当他们活跃时,自动完成相关操作,那就避免了轮询,这正是epoll
与kqueue
做的
poll缺点
它没有最大连接数的限制,原因是它是基于链表来存储的,但是同样有缺点:
- 每次调用
poll
,都需要把fd
集合从用户态拷贝到内核态,这个开销在fd
很多时会很大; - 对
socket
扫描是线性扫描,采用轮询的方法,效率较低(高并发时)
epoll的优点
- 没有最大并发连接的限制,能打开的FD的上限远大于1024(1G的内存上能监听约10万个端口);
- 效率提升,不是轮询的方式,不会随着FD数目的增加效率下降。只有活跃可用的FD才会调用callback函数;即
epoll
最大的优点就在于它只管你“活跃”的连接,而跟连接总数无关,因此在实际的网络环境中,epoll
的效率就会远远高于select
和poll
; - 内存拷贝,利用
mmap()
文件映射内存加速与内核空间的消息传递;即epoll
使用mmap
减少复制开销。
epoll LT 与 ET 模式的区别
epoll
有EPOLLLT
和 EPOLLET
两种触发模式,LT 是默认的模式,ET 是 “高速” 模式。
- LT 模式下,只要这个
fd
还有数据可读,每次epoll_wait
都会返回它的事件,提醒用户程序去操作; - ET 模式下,它只会提示一次,直到下次再有数据流入之前都不会再提示了,无论 fd 中是否还有数据可读。所以在 ET 模式下,
read
一个fd
的时候一定要把它的buffer
读完,或者遇到 EAGIN 错误。
epoll使用“事件”的就绪通知方式,通过epoll_ctl
注册fd
,一旦该fd
就绪,内核就会采用类似callback
的回调机制来激活该fd
,epoll_wait
便可以收到通知。
select/poll/epoll之间的区别
select,poll,epoll
都是IO多路复用的机制。I/O多路复用就通过一种机制,可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作。但select,poll,epoll
本质上都是同步I/O,因为他们都需要在读写事件就绪后自己负责进行读写,也就是说这个读写过程是阻塞的,而异步I/O则无需自己负责进行读写,异步I/O的实现会负责把数据从内核拷贝到用户空间。
epoll
跟select
都能提供多路I/O复用的解决方案。在现在的Linux内核里有都能够支持,其中epoll
是Linux所特有,而select则应该是POSIX所规定,一般操作系统均有实现
为什么 Redis 中要使用 I/O 多路复用这种技术呢?
首先,Redis
是跑在单线程中的,所有的操作都是按照顺序线性执行的,但是由于读写操作等待用户输入或输出都是阻塞的,所以I/O
操作在一般情况下往往不能直接返回,这会导致某一文件的 I/O
阻塞导致整个进程无法对其它客户提供服务,而 I/O 多路复用 就是为了解决这个问题而出现的。
epoll为什么会比select/poll快,快在哪些地方?
epoll
相比 select
和 poll
在处理大量文件描述符时更高效的原因主要有以下几点:
- 事件通知机制:
select
和poll
使用轮询方式来检查每个文件描述符是否就绪,这意味着即使只有少量的文件描述符就绪,也需要遍历整个文件描述符集合。这种方式的时间复杂度是O(n),n 是文件描述符的数量。epoll
利用事件通知机制,当文件描述符就绪时,内核会通过回调函数直接通知应用程序,而不需要应用程序自己去轮询。这样可以避免不必要的遍历,提高了效率。
- 文件描述符集合管理:
select
和poll
需要应用程序不断地维护文件描述符集合,包括添加、删除和修改。这样的操作会带来一定的开销。epoll
使用单独的系统调用epoll_ctl()
来添加、删除和修改文件描述符,这些操作都是在内核中进行,不需要应用程序去管理文件描述符集合,减少了额外的开销。
- 内存拷贝:
- 在
select
和poll
中,每次调用系统调用都需要将文件描述符集合从用户空间拷贝到内核空间,而epoll
使用内存映射(mmap)技术,在用户空间和内核空间共享一块内存,避免了不必要的内存拷贝。
- 在
epoll一定比 poll快吗?为什么还有人使用po模型?
- 规模小:
- 当文件描述符数量较少时,
poll
的开销可能更低。epoll
在维护内部数据结构上可能会有一些额外开销,这在规模小的情况下可能会影响性能。
- 当文件描述符数量较少时,
- 平台兼容性:
poll
是 POSIX 标准的一部分,因此在不同的平台上都可以使用,并且具有较好的兼容性。相比之下,epoll
是 Linux 特有的,不具备跨平台性,如果需要在其他操作系统上运行,可能会选择使用poll
。
- 简单性:
- 对于某些应用程序而言,
poll
的接口可能更简单易用,特别是对于初学者或者快速原型开发而言。epoll
的使用需要更多的了解和掌握,对于一些简单的场景,使用poll
可能更为方便。
- 对于某些应用程序而言,
- 稳定性:
- 虽然
epoll
在处理大量并发连接时通常更高效,但在某些情况下可能会出现性能问题或者不稳定的情况。在某些情况下,开发人员可能更倾向于使用poll
,因为它可能更为稳定和可靠。
- 虽然
epoll为什么采用红黑树,而不是hash、 b+树存储需要关注的事件?
- 快速查找:
- 红黑树是一种自平衡的二叉搜索树,其查找、插入和删除的时间复杂度都是 O(log n),其中 n 是树中节点的数量。这使得在红黑树中快速地查找需要关注的事件成为可能,尤其是当文件描述符数量很大时。红黑树处理大规模数据效率高
- 有序性:
- 红黑树是一种有序树,可以按照节点的键(比如文件描述符的值)进行排序。这种有序性在事件触发时非常有用,可以更容易地确定触发事件的顺序。
- 动态性:
epoll
的设计考虑了文件描述符的动态性,即文件描述符的增加和删除。红黑树的动态性比哈希表和 B+ 树更好,可以方便地进行节点的插入和删除操作,而不需要重新构建整个数据结构。红黑树容易缩容
- 内存开销:
- 相比哈希表和 B+ 树,红黑树的内存开销相对较小。哈希表需要额外的空间来存储哈希值和冲突解决机制,而 B+ 树需要额外的指针和索引节点。红黑树作为一种自平衡树,在保持相对较低的内存开销的同时,能够保持较好的性能。
epoll的底层原理
epoll
是 Linux 下的一种 I/O 多路复用机制,用于处理大量并发连接或操作。它通过操作系统内核提供的 epoll
系统调用来管理文件描述符并监视 I/O 事件。
- 事件驱动模型:
epoll
是基于事件驱动的模型。它利用操作系统内核的事件通知机制来通知应用程序发生了哪些 I/O 事件,例如套接字可读、套接字可写等。 - 注册事件: 应用程序可以通过
epoll_ctl
系统调用向内核注册需要监视的文件描述符以及对应的事件类型(读、写等)。一旦文件描述符上发生了指定的事件,内核就会通知应用程序。 - 等待事件: 应用程序使用
epoll_wait
系统调用来等待事件的发生。当有文件描述符上发生了注册的事件时,epoll_wait
将返回并通知应用程序。 - 高效处理事件:
epoll
的高效性体现在它在等待事件发生时不会阻塞整个进程,而是采用了非阻塞的方式。它通过内核态和用户态之间的数据结构共享,避免了频繁的上下文切换,从而提高了性能。 - 可扩展性:
epoll
在设计上考虑了可扩展性,能够有效地处理大量的并发连接或操作。它使用红黑树(Red-Black Tree)和双链表(Doubly Linked List)等数据结构来管理文件描述符,从而减少了对文件描述符数量的限制。
什么情况下需要I/O多路复用技术
传统的 I/O 模型中,每个连接或操作都需要一个独立的线程或进程来处理,这会消耗大量的系统资源。而使用 I/O 多路复用技术,可以通过一个线程或进程同时监听多个 I/O 事件,从而减少了资源的消耗。
IO多路复用是指使用一个线程来检查多个文件描述符(Socket)的就绪状态,比如调用select和poll函数,传入多个文件描述符,如果有一个文件描述符就绪,则返回,否则阻塞直到超时。
这样在处理1000个连接时,只需要1个线程监控就绪状态,对就绪的每个连接开一个线程处理就可以了,这样需要的线程数大大减少,减少了内存开销和上下文切换的CPU开销。