IO模型为什么需要单独拉出来来说?因为这里是我们理解linux底层一些契机,也是我们网络编程的基础。尤其现在我们主流的框架都支持了EPOLL IO模型,比如tomcat、redis、nginx等。
0. IO
IO (Input/Output,输入/输出)即数据的读取(接收)或写入(发送)操作,通常用户进程中的一个完整IO分为两阶段:用户进程空间<–>内核空间、内核空间<–>设备空间(磁盘、网络等)**。IO有内存IO、网络IO和磁盘IO三种,通常我们说的IO指的是后两者。
LINUX中进程无法直接操作I/O设备,其必须通过系统调用请求kernel来协助完成I/O动作;内核会为每个I/O设备维护一个缓冲区。
对于一个输入操作来说,进程IO系统调用后,内核会先看缓冲区中有没有相应的缓存数据,没有的话再到设备中读取,因为设备IO一般速度较慢,需要等待;内核缓冲区有数据则直接复制到进程空间。
所以,对于一个网络输入操作通常包括两个不同阶段:
- 等待网络数据到达网卡→读取到内核缓冲区,数据准备好;
- 从内核缓冲区复制数据到进程空间(用户空间)。
1. 阻塞和非阻塞
根据应用程序是否阻塞自身的运行,可以把 I/O 分为阻塞 I/O 和非阻塞 I/O:
- 所谓阻塞 I/O,是指应用程序在执行 I/O 操作后,如果没有获得响应,就会阻塞当前线程,不能执行其他任务。
- 所谓非阻塞 I/O,是指应用程序在执行 I/O 操作后,不会阻塞当前的线程,可以继续执行其他的任务。
阻塞 / 非阻塞针对的是 I/O 调用者(即应用程序)。
阻塞IO

进程发起IO系统调用后,进程被阻塞,转到内核空间处理,整个IO处理完毕后返回进程。操作成功则进程获取到数据。
典型应用
- 阻塞Socket
- Java BIO
特点
进程阻塞挂起不消耗CPU资源,及时响应每个操作;
实现难度低、开发应用较容易;
适用并发量小的网络应用开发;
不适用并发量大的应用:因为一个请求IO会阻塞进程,所以,得为每请求分配一个处理进程(线程)以及时响应,系统开销大。
非阻塞 I/O

进程发起IO系统调用后,如果内核缓冲区没有数据,需要到IO设备中读取,进程返回一个错误而不会被阻塞;进程发起IO系统调用后,如果内核缓冲区有数据,内核就会把数据返回进程。 对于上面的阻塞IO模型来说,内核数据没准备好需要进程阻塞的时候,就返回一个错误,以使得进程不被阻塞。
典型应用:
socket是非阻塞的方式(设置为NONBLOCK)
特点
- 进程轮询(重复)调用,消耗CPU的资源;
- 实现难度低、开发应用相对阻塞IO模式较难;
- 适用并发量较小、且不需要及时响应的网络应用开发;
多路复用

多个的进程的IO可以注册到一个复用器(select)上,然后用一个进程调用该select, select会监听所有注册进来的IO;
如果select没有监听的IO在内核缓冲区都没有可读数据,select调用进程会被阻塞;而当任一IO在内核缓冲区中有可数据时,select调用就会返回;
而后select调用进程可以自己或通知另外的进程(注册进程)来再次发起读取IO,读取内核中准备好的数据。
可以看到,多个进程注册IO后,只有另一个select调用进程被阻塞。
典型应用
select、poll、epoll三种方案,nginx都可以选择使用这三个方案;
Java NIO;
特点
专一进程解决多个进程IO的阻塞问题,性能好;Reactor模式;
实现、开发应用难度较大;
适用高并发服务应用开发:一个进程(线程)响应多个请求;
select、poll、epoll
Linux中IO复用的实现方式主要有select、poll和epoll:
- Select:注册IO、阻塞扫描,监听的IO最大连接数不能多于FD_SIZE;
- Poll:原理和Select相似,没有数量限制,但IO数量大扫描线性性能下降;
- Epoll :事件驱动不阻塞,mmap实现内核与用户空间的消息传递,数量很大,Linux2.6后内核支持;
select

select是第一版IO复用,提出后暴漏了很多问题。
- select 会修改传入的参数数组,这个对于一个需要调用很多次的函数,是非常不友好的。
- select 如果任何一个sock(I/O stream)出现了数据,select 仅仅会返回,但不会告诉是那个sock上有数据,只能自己遍历查找。
- select 只能监视1024个链接。
- select 不是线程安全的,如果你把一个sock加入到select, 然后突然另外一个线程发现这个sock不用,要收回,这个select 不支持的。
- select模型只解决accept()傻等的问题,不解决recv(),send()执行阻塞问题
poll
poll的实现和select非常相似,只是描述fd集合的方式不同,poll使用pollfd结构而不是select的fd_set结构,其他的都差不多,管理多个描述符也是进行轮询,根据描述符的状态进行处理,但是poll没有最大文件描述符数量的限制。poll和select同样存在一个缺点就是,包含大量文件描述符的数组被整体复制于用户态和内核的地址空间之间,而不论这些文件描述符是否就绪,它的开销随着文件描述符数量的增加而线性增大。
epoll
Epoll在Linux2.6内核正式提出,是基于事件驱动的I/O方式。相对于select来说,epoll没有描述符个数限制;使用一个文件描述符管理多个描述符,将用户关心的文件描述符的事件存放到内核的一个事件表中,通过内存映射,使其在用户空间也可直接访问,省去了拷贝带来的资源消耗。
相较于Select和Poll,Epoll内部还分为两种工作模式: LT水平触发(level trigger)和ET边缘触发(edge trigger)。
- LT模式: 默认的工作模式,即当epoll_wait检测到某描述符事件就绪并通知应用程序时,应用程序可以不立即处理该事件;事件会被放回到就绪链表中,下次调用epoll_wait时,会再次通知此事件。
- ET模式: 当epoll_wait检测到某描述符事件就绪并通知应用程序时,应用程序必须立即处理该事件。如果不处理,下次调用epoll_wait时,不会再次响应并通知此事件。
由于上述两种工作模式的区别,LT模式同时支持block和no-block socket两种,而ET模式下仅支持no-block socket。即epoll工作在ET模式的时候,必须使用非阻塞套接口,以避免由于一个fd的阻塞I/O操作把多个处理其他文件描述符的任务饿死。ET模式在很大程度上减少了epoll事件被重复触发的次数,因此效率要比LT模式高。
三者总结
|
| select | poll | epoll |
|---|---|---|
| 事件集合 | 通过writefds、readfds、和exceptfds三个参数传入感兴趣的可读、可写以及异常事件。 | |
| 内核通过对这三个参数的在线修改来反馈其中的就绪事件,这使得用户每次调用都要重置这三个参数 | 统一处理所有事件,因此只需要一个事件集参数。 | |
| 用户通过pollfd.events传入感兴趣的事件,内核通过修改pollfd。revents参数反馈其中的就绪事件 | 内核通过一个事件表直接管理用户感兴趣的所有事件。 | |
| 因此每次调用epollwait时,无需传入用户感兴趣的事件。epollwait系统调用的参数events仅用来反馈就绪事件 | ||
| 内核实现和工作效率 | 采用轮询方式检测就绪事件,时间复杂度:O(n) | 采用轮序方式检测就绪事件,时间复杂度:O(n) |
| 最大连接数 | 1024 | 无上限 |
| 工作模式 | LT | LT |
| fd拷贝 | 每次调用,每次拷贝 | 每次调用,每次拷贝 |
综上,在选择select,poll,epoll时要根据具体的使用场合以及这三种方式的自身特点。
1、表面上看epoll的性能最好,但是在连接数少并且连接都十分活跃的情况下,select和poll的性能可能比epoll好,毕竟epoll的通知机制需要很多函数回调。
2、select低效是因为每次它都需要轮询。但低效也是相对的,视情况而定,也可通过良好的设计改善
同步、异步
根据 I/O 响应的通知方式的不同,可以把文件 I/O 分为同步 I/O 和异步 I/O。
- 所谓同步 I/O,是指收到 I/O 请求后,系统不会立刻响应应用程序;等到处理完成,系统才会通过系统调用的方式,告诉应用程序 I/O 结果。
- 所谓异步 I/O,是指收到 I/O 请求后,系统会先告诉应用程序 I/O 请求已经收到,随后再去异步处理;等处理完成后,系统再通过事件通知的方式,告诉应用程序结果。
同步 / 异步针对的是 I/O 执行者(即系统),当请求被阻塞,就是同步IO,否则就是异步IO。
真正意义上的 异步IO 是说内核直接将数据拷贝至用户态的内存单元,再通知程序直接去读取数据。select / poll / epoll 都是同步IO的多路复用模式。
比如在 Linux I/O 调用中:
- 系统调用 read 是同步读,所以,在没有得到磁盘数据前,read 不会响应应用程序。
- 而 aio_read 是异步读,系统收到 AIO 读请求后不等处理就返回了,而具体的 read 结果,再通过回调异步通知应用程序。
Java BIO、NIO、AIO
- BIO:同步并阻塞,服务器的实现模式是一个连接一个线程,这样的模式很明显的一个缺陷是:由于客户端连接数与服务器线程数成正比关系,可能造成不必要的线程开销,严重的还将导致服务器内存溢出。当然,这种情况可以通过线程池机制改善,但并不能从本质上消除这个弊端。
- NIO:在JDK1.4以前,Java的IO模型一直是BIO,但从JDK1.4开始,JDK引入的新的IO模型NIO,它是同步非阻塞的。而服务器的实现模式是多个请求一个线程,即请求会注册到多路复用器Selector上,多路复用器轮询到连接有IO请求时才启动一个线程处理。
- AIO:JDK1.7发布了NIO2.0,这就是真正意义上的异步非阻塞,服务器的实现模式为多个有效请求一个线程,客户端的IO请求都是由OS先完成再通知服务器应用去启动线程处理(回调)。
445

被折叠的 条评论
为什么被折叠?



