- 同步(synchronous)模型
- BIO:阻塞IO
- NIO:非阻塞IO
- IO多路复用
- 异步(asynchronous)模型
- AIO:异步IO
- I/O 多路复用
- select / poll / epoll
1. 先验知识
- Linux和Java
- 本文讨论的是Linux环境下的IO模型。Java中的NIO与这里的NIO不同,Java中的NIO属于IO多路复用模型
- IO的两个阶段
- 用户进行IO的读写,基本上都会用到read&write两大系统调用。以read为例,会涉及到两个系统对象,经历两个阶段。
- 1)等待数据准备,由操作系统内核kernal完成
- 2)将数据从内核缓冲区拷贝到进程缓冲区中
- 用户进行IO的读写,基本上都会用到read&write两大系统调用。以read为例,会涉及到两个系统对象,经历两个阶段。
- 缓冲区的作用
- 缓冲区的目的,是为了减少频繁的系统IO调用。由于系统调用需要保存和恢复进程状态信息,为了减少这种损耗时间、也损耗性能的系统调用,于是出现了缓冲区
- 在linux系统中,系统内核有个缓冲区叫做内核缓冲区
- 每个进程有自己独立的缓冲区,叫做进程缓冲区
2. 四种模型
(1)同步阻塞IO(blocking IO)
- 在linux中的Java进程中,默认情况下所有的socket都是blocking IO。实际上,除非特别指定,几乎所有的IO接口 ( 包括socket接口 ) 都是阻塞型的
- blocking IO的特点是在IO两个阶段,用户线程都被阻塞了
- 具体的说:从用户线程执行read()后,用户线程一直处于block状态,直到kernel返回结果后,用户线程才解除block的状态,重新运行起来
- 例子,以read()为例
- (IO一阶段)用户线程执行read()后,进入阻塞状态 ——> kernel开始准备数据 ——> kernel就要等待足够的数据到来(数据在一开始还没有到达,比如,还没有收到一个完整的Socket数据包)
- (IO二阶段)kernel数据准备完成 ——> 将数据从kernel缓冲区复制到用户进程缓冲区(用户内存)——> kernel返回结果 ——> 用户线程得到结果,结束阻塞状态
- 优缺点
- 优点
- 程序简单,用户线程挂起期间基本不占用CPU资源
- 缺点
- 一般情况下,改进后的BIO会用线程池给每个连接分配独立线程,根据并发量动态生成和销毁。但是当并发量大时,需要大量的线程来维护大量的网络连接,内存、线程切换开销巨大
(2)同步非阻塞NIO(non-blocking IO)
- 在linux系统下,可以通过设置socket使其变为non-blocking
- 阶段一不阻塞(轮询),阶段二用户线程阻塞
- 例子,以read()为例
- 用户线程调用read()
- 如果kernal缓冲区的数据没准备好,则立即返回error,用户线程得到error后无需阻塞。
- 用户线程可以过一段时间(或者立即)再执行read()。这个过程被称之为轮询
- 如果kernal缓冲区的数据已经准备好,进入IO阶段二:缓冲区间复制
- IO阶段二:内核缓冲区数据 ——> 用户缓冲区
- 如果用户线程没收到error,kernal开始复制数据,此时用户线程处于block状态
- kernal复制完成后,返回结果给用户线程,用户线程结束block状态
- 优缺点
- 优点
- 每次发起的 IO 系统调用,在内核的等待数据过程中可以立即返回。用户线程不会阻塞,实时性较好
- 缺点
- 如果不间断地调用read(),大幅度占用CPU资源
- 如果每隔n秒调用read(),即每过一段时间才去轮询,任务完成的响应延迟增大了,数据吞吐量降低
- 备注
- Java NIO与这个没有关系,Java NIO属于IO多路复用模型。java的实际开发中,也不会涉及这种IO模型。
(3)IO多路复用(IO multiplexing)
- 该模型也称 事件驱动IO(event driven IO)。目前支持IO多路复用的系统调用,有 select,epoll等等
- select系统调用,是目前几乎在所有的操作系统上都有支持,具有良好跨平台特性
- epoll是在linux 2.6内核中提出的,是select系统调用的linux增强版本
- IO多路复用模型,就是通过一种新的系统调用,一个进程可以监视多个文件描述符,一旦某个描述符就绪(一般是内核缓冲区可读/可写),内核kernel能够通知程序进行相应的IO系统调用
- 阶段一阻塞,阶段二也阻塞
- 例子,以select()为例
- 调用select()前需要将目标网络连接,提前注册到select/epoll的可查询socket列表中
- 用户线程执行select()系统调用,查询可以读的连接,kernel会查询所有select的可查询socket列表
- 当任何一个socket中的数据准备好了,select就会立即返回,IO阶段一结束,开始阶段二。
- 如果没准备好,用户线程会一直阻塞。
- IO阶段二开始,用户线程获得目标连接后,发起read()调用,用户线程阻塞
- kernal复制完成后,返回结果,用户线程blcok结束
- 补充
- 整个流程需要两个系统调用: 一个select/epoll查询调用,一个是IO的read/write调用
- 在多路复用模型中,对于每一个socket,一般都设置成为non-blocking,这样select/epoll查询调用会不断轮询
- 如果处理的连接数不是很高的话,使用select/epoll的web server不一定比使用(多线程 + BIO)的web server性能更好,可能延迟还更大。select/epoll的优势并不是对于单个连接能处理得更快,而是在于能处理更多的连接。
- 虽然整个用户线程是一直被block的,但它是被select()给block,而不是被read()给block
- 优缺点
- 优点
- 多路复用模型用单线程执行,不像(多线程+BIO),需要多个read()。占用资源少,不消耗太多 CPU。
- 缺点
- select()接口本身需要消耗大量时间去轮询所有句柄,因此linux提供了epoll作为增强,遗憾的是不同的操作系统特供的epoll接口有很大差异
(4)异步IO模型(asynchronous IO)
- 该模型也称为信号驱动 IO
- 用户线程两个阶段全程不阻塞
- 例子
- 用户线程执行前,一般需要注册回调函数
- 用户线程调用read系统调用,立刻就可以开始去做其它的事,用户线程不阻塞
- kernal将IO两个阶段(数据准备 + 数据复制)都执行好后,会返回给用户线程一个信号(signal),或者回调用户线程注册的回调接口
- 优缺点
- 优点
- 全程不阻塞
- Windows 系统下通过 IOCP 实现了真正的异步 I/O,但Windows很难作为高并发的服务器系统
- 缺点
- 需要完成事件的注册与传递,这里边需要底层操作系统提供大量的支持,去做大量的工作
- 在 Linux 系统下,异步IO模型在2.6版本才引入,目前并不完善
3. I/O模型总结
(1)blocking和non-blocking的区别
- 数据准备(IO阶段一)和数据复制(IO阶段二)两个阶段,只要有一个阶段被阻塞,都称为blocking。
- 所以哪怕non-blocking通过轮询,让数据准备阶段不阻塞。在从kernal拷贝到进程缓冲区时,也会被阻塞。NIO指的是第一个阶段(数据准备)不阻塞
- 除了AIO,剩下都会有block
(2)异步IO和同步IO
- 除了AIO是异步IO,剩下的都是同步IO
4. I/O多路复用的函数
(1)select
int select (int n, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
- 调用后select函数会阻塞,直到有描述副就绪(有数据 可读、可写、或者有except),或者超时(timeout指定等待时间,如果立即返回设为null即可),函数返回。当select函数返回后,可以 通过遍历fdset,来找到就绪的描述符
- 优点:良好跨平台支持:目前几乎在所有的平台上支持
- 缺点:单个进程能够监视的文件描述符的数量存在最大限制,在Linux上一般为1024
(2)poll
int poll (struct pollfd *fds, unsigned int nfds, int timeout);
struct pollfd {
int fd; /* file descriptor */
short events; /* requested events to watch */
short revents; /* returned events witnessed */
};
- 不同与select使用三个位图来表示三个fdset的方式,poll使用一个 pollfd的指针实现
- pollfd结构包含了要监视的event和发生的event,不再使用select“参数-值”传递的方式。同时,pollfd并没有最大数量限制(但是数量过大后性能也是会下降)
- 和select函数一样,poll返回后,需要轮询pollfd来获取就绪的描述符
- 缺点:
- select和poll都需要在返回后,通过遍历文件描述符来获取已经就绪的socket。事实上,同时连接的大量客户端在一时刻可能只有很少的处于就绪状态,因此随着监视的描述符数量的增长,其效率也会线性下降
(3)epoll
- epoll是在2.6内核中提出的,是之前的select和poll的增强版本
- 相对于select和poll来说,epoll更加灵活,没有描述符限制
- epoll使用一个文件描述符管理多个描述符,将用户关系的文件描述符的事件存放到内核的一个事件表中,这样在用户空间和内核空间的copy只需一次
- 与select/poll的区别:无需遍历文件描述符,通过Callback完成
- 在 select/poll 中,进程只有在调用一定的方法后,内核才对所有监视的文件描述符进行扫描
- epoll事先注册一个文件描述符,一旦基于某个文件描述符就绪时,内核会采用类似callback的回调机制,迅速激活这个文件描述符,当进程调用epoll_wait() 时便得到通知
- 优点:
- 监视的描述符数量不受限制,远大于2048,在1GB内存的机器上大约是10万左右
- 与select/poll不同,IO的效率不会随着监视fd的数量的增长而下降
Reference
强烈推荐:https://segmentfault.com/a/1190000003063859
https://www.cnblogs.com/cainingning/p/9556642.html
https://www.cnblogs.com/crazymakercircle/p/10225159.html