1 IO的理解
I - Input O - output
这里的IO我们常常指网络的IO,也就是指套接字Socket通信。网络传输的本质也就是输入输出,所有才有了IO之称。
Socket 的中文翻译为插口。双方要进行网络通信之前,各自需要创建一个 Socket,这相当于客户端和服务器都打开一个插口,双方读取和发送数据的时候,都通过这个插口建立数据通道,进行网络通信。
2 常见的IO模型
- 同步阻塞IO(Blocking IO) 就是我们常说的BIO
- 同步非阻塞IO(Non-blocking IO):默认创建的socket都是阻塞的,非阻塞IO要求socket被设置为NONBLOCK。注意这里所说的NIO并非Java的NIO(New IO)库。
- IO多路复用(IO Multiplexing):即经典的Reactor设计模式,有时也称为异步阻塞IO,Java中的Selector和Linux中的epoll都是这种模型。这个就是我们Java常说的NIO,也是Netty的核心。
- 信号驱动IO(signal driven IO)
- 异步IO(Asynchronous IO):即经典的Proactor设计模式,也称为异步非阻塞IO。这就是我们Java常说的AIO。
同步: 前四种都属于同步IO,同步是指用户线程发起IO请求后,需要等待或者轮询内核IO操作完成后,才能继续执行。
异步: 异步是指用户线程发起IO请求后仍继续执行,当内核IO操作完成后会通知用户线程,或者调用用户线程注册的回调函数。
阻塞: 阻塞是指IO操作需要彻底完成后才返回到用户。
非阻塞: 非阻塞是指IO操作被调用后立即返回给用户一个状态值,无需等到IO操作彻底完成。
3 多路复用技术
多路复用技术是建立在内核提供的多路分离函数基础之上的,使用多路复用技术可以避免同步非阻塞IO模型中轮询等待等问题。
多路复用技术的最大优势是系统开销小,系统不必创建进程/线程,也不必维护这些进程/线程,从而大大减小了系统的开销。
谈及IO多路复用技术不得不介绍一下IO模型的发展:
最早期一个进程只能处理一个网络连接,后来演化成多进程,多线程处理,即每加入一个连接,操作系统就必须创建一个进程或者一个线程去处理IO,这样操作系统的压力无疑是巨大的。
IO多路复用技术就是为解决上述问题所产生的,使用IO多路复用技术可以使我们只需要一个进程就能处理多个网络连接。多个请求复用一个进程,这也是IO多路复用名称的由来。select, poll, epoll就是操作系统为我们提供的可以通过一个进程,获取多个事件的函数。
4 select, poll, epoll
select, poll, epoll 都是I/O多路复用的具体的实现
- select 跨平台 1983年
select 检测到的连接是有上限的,通常是1024,select 不是线程安全的。
- poll (linux支持) 1997年
poll修复了select的许多问题,去掉了连接上限,但是poll仍然不是线程安全的。
- epoll (linux支持,效率更高)2002年
epoll 可以说是I/O 多路复用最新的一个实现,epoll 修复了poll 和select绝大部分问题,并且epoll 是线程安全的,epoll 不仅告诉你sock组里面数据,还会告诉你具体哪个sock有数据。
select
select 实现多路复用的方式是,将已连接的 Socket 都放到一个文件描述符集合中,然后调用 select 函数将文件描述符集合拷贝到内核里,让内核来检查是否有网络事件产生,检查的方式很粗暴,就是通过遍历文件描述符集合的方式,当检查到有事件产生后,将此 Socket 标记为可读或可写, 接着再把整个文件描述符集合拷贝回用户态里,然后用户态还需要再通过遍历的方法找到可读或可写的 Socket,然后再对其处理。
对于 select 这种方式,需要进行 2 次「遍历」文件描述符集合,一次是在内核态里,一个次是在用户态里 ,而且还会发生 2 次「拷贝」文件描述符集合,先从用户空间传入内核空间,由内核修改后,再传出到用户空间中。
select 使用固定长度的 BitsMap,表示文件描述符集合,而且所支持的文件描述符的个数是有限制的,在 Linux 系统中,由内核中的 FD_SETSIZE 限制, 默认最大值为 1024
,只能监听 0~1023 的文件描述符,这也就是select方式有检测连接上限最底层的原因。
poll
poll 不再使用 BitsMap 来存储所关注的文件描述符,它改用动态数组,以链表形式来组织,突破了 select 的文件描述符个数限制,当然还会受到系统文件描述符限制。
poll 和 select 并没有太大的本质区别,都是使用线性结构存储进程关注的 Socket 集合,因此都需要遍历文件描述符集合来找到可读或可写的 Socket,时间复杂度为 O(n),而且也需要在用户态与内核态之间拷贝文件描述符集合。这样的方式,在大规模请求下,性能会急剧下降。
epoll
epoll 是为解决 select/poll 带来的问题而产生的,也是目前最主流的。
首先,epoll 在内核里使用红黑树来跟踪进程所有待检测的文件描述字,把需要监控的 socket 通过 epoll_ctl()
函数加入内核中的红黑树里,通过对黑红树进行操作,这样就不需要像 select/poll 每次操作时都传入整个 socket 集合,只需要传入一个待检测的 socket,减少了内核和用户空间大量的数据拷贝和内存分配。
其次, epoll 使用事件驱动的机制,内核里维护了一个链表来记录就绪事件,当某个 socket 有事件发生时,通过回调函数会将其加入到这个就绪事件列表中,当用户调用 epoll_wait()
函数时,只会返回有事件发生的文件描述符的个数,不需要像 select/poll 那样轮询扫描整个 socket 集合,大大提高了检测的效率。
5 总结
仔细思考一下,为什么各个大厂会对数据结构与算法情有独钟呢?就如同IO多路复用这样的底层技术,在它的升级演进过程中数据结构都发挥了巨大的作用,高效的红黑树,能够直接将时间复杂度降低到 O(logn)
,还有就是一些设计思想的融入,如事件驱动等,可以极大的提高检测速度,就是这些让epoll的性能,大大的超过了select/poll。我们去学习这些技术,不光要去知道它的实现原理,更重要的是去思考它演进的过程,进而去推动技术的创新与发展。