IO模式
对于一次IO访问(以read举例),数据会先被拷贝到操作系统内核的缓冲区中,然后才会从操作系统内核的缓冲区拷贝到应用程序的地址空间。
所以说,当一个read操作发生时,它会经历两个阶段:
1. 等待数据准备 (Waiting for the data to be ready)
2. 将数据从内核拷贝到进程中 (Copying the data from the kernel to the process)
阻塞 I/O
阻塞IO执行的两个阶段都被block。
非阻塞 I/O
- nonblocking IO:用户进程需要不断的主动询问kernel数据好了没有,如果kernel中的数据还没有准备好,那么它并不会block用户进程,而是立刻返回0,用户进程发现是0,可以过会再询问,一旦kernel中的数据准备好了,并且又再次收到了用户进程的system call,,那么它马上就将数据拷贝到了用户内存,然后返回。注意在第二阶段其实仍旧是阻塞的。
- NIO的读写函数可以立刻返回,这就给了我们不开线程利用CPU的最好机会:如果一个连接不能读写(socket.read()返回0或者socket.write()返回0),我们可以把这件事记下来,记录的方式通常是在Selector上注册标记位,然后切换到其它就绪的连接(channel)继续进行读写。
- NIO是多路复用IO的基础。
- 成熟的NIO框架,如Netty,MINA等
异步 I/O
没啥好说的,顾名思义。
I/O 多路复用
也称事件驱动IO。
它的基本原理就是select,poll,epoll这个function会不断的轮询所负责的所有socket,当某个socket有数据到达了,就通知用户进程。
当用户进程调用了select,那么整个进程会被block,保证cpu不会空转。
select,poll,epoll本质上都是同步I/O,因为他们都需要在读写事件就绪后自己负责进行读写,也就是说这个读写过程是阻塞的。
select
缺点在于单个进程能够监视的文件描述符的数量存在最大限制,在Linux上一般为1024,可以通过修改宏定义甚至重新编译内核的方式提升这一限制,但 是这样也会造成效率的降低。
poll
和select一样通过遍历文件描述符来获取已经就绪的socket,虽然没有最大数量限制,但是数量多了会造成性能下降,因为要遍历socket。
epoll
epoll对文件描述符的操作有两种模式:LT(level trigger)和ET(edge trigger)。LT模式是默认模式
LT模式:当epoll_wait检测到描述符事件发生并将此事件通知应用程序,应用程序可以不立即处理该事件。下次调用epoll_wait时,会再次响应应用程序并通知此事件。
ET模式:当epoll_wait检测到描述符事件发生并将此事件通知应用程序,应用程序必须立即处理该事件。如果不处理,下次调用epoll_wait时,不会再次响应应用程序并通知此事件。
如果没有大量的idle -connection或者dead-connection,epoll的效率并不会比select/poll高很多,但是当遇到大量的idle- connection,就会发现epoll的效率大大高于select/poll。
Proactor与Reactor
一般情况下,I/O 复用机制需要事件分发器(event dispatcher)。 事件分发器的作用,即将那些读写事件源分发给各读写事件的处理者,就像送快递的在楼下喊: 谁谁谁的快递到了, 快来拿吧!开发人员在开始的时候需要在分发器那里注册感兴趣的事件,并提供相应的处理者(event handler),或者是回调函数;事件分发器在适当的时候,会将请求的事件分发给这些handler或者回调函数。
涉及到事件分发器的两种模式称为:Reactor和Proactor。 Reactor模式是基于同步I/O的,而Proactor模式是和异步I/O相关的。
Reactor模式
- 简单实现:注册所有感兴趣的事件处理器,单线程轮询选择就绪事件,执行事件处理器。
interface ChannelHandler{
void channelReadable(Channel channel);
void channelWritable(Channel channel);
}
class Channel{
Socket socket;
Event event;//读,写或者连接
}
//IO线程主循环:
class IoThread extends Thread{
public void run(){
Channel channel;
while(channel=Selector.select()){//选择就绪的事件和对应的连接
if(channel.event==accept){
registerNewChannelHandler(channel);//如果是新连接,则注册一个新的读写处理器
}
if(channel.event==write){
getChannelHandler(channel).channelWritable(channel);//如果可以写,则执行写事件
}
if(channel.event==read){
getChannelHandler(channel).channelReadable(channel);//如果可以读,则执行读事件
}
}
}
Map<Channel,ChannelHandler> handlerMap;//所有channel的对应事件处理器
}
//由原来的阻塞读写(占用线程)变成了单线程轮询事件,找到可以进行读写的网络描述符进行读写。除了事件的轮询是阻塞的(没有可干的事情必须要阻塞),剩余的I/O操作都是纯CPU操作,没有必要开启多线程。
- Java的Selector对于Linux系统来说,有一个致命限制:同一个channel的select不能被并发的调用。因此,如果有多个I/O线程,必须保证:一个socket只能属于一个IoThread,而一个IoThread可以管理多个socket。
- Redis基于Reactor模式开发了以单线程的方式运行的网络事件处理器,通过IO复用来监听多个套接字,实现了高性能的网络通讯模型,又很好的与Redis服务器中其他同样以单线程方式运行的模块进行对接,保持了内部线程设计的简单性(参考Redis设计与实现第12章12.1文件事件)。使用单线程+队列,保证服务端是全局串行的,能够保证同一连接的所有请求与返回顺序一致。
图中的IO多路复用程序即网络事件处理器,它是单线程的,基于Reactor模式。另外命令的处理过程也是主线程处理的,如果此时有新的请求到来,会暂时不理,等到event_loop执行到下一次从缓冲区读取新的数据, 然后再处理,期间新的请求数据只能在socket缓冲区里standby。具体看http://www.hoterran.info/redis_eventlibrary - 使用该IO != 高性能,当连接数<1000,并发程度不高或者局域网环境下该IO类型并没有显著的性能优势。
多路复用并没有完全屏蔽平台差异,它仍然是基于各个操作系统的I/O系统实现的,差异仍然存在。
Proactor
区别
- 同步情况下(Reactor),回调handler时,表示I/O设备可以进行某个操作(can read 或 can write)。异步情况下(Proactor),当回调handler时,表示I/O操作已经完成,即IO的第二阶段由操作系统来完成了,有些系统不支持底层异步API,可以去模拟实现。
- 对于一个读事件
标准/典型的Reactor:
步骤1:等待事件到来(Reactor负责)。
步骤2:将读就绪事件分发给用户定义的处理器(Reactor负责)。
步骤3:读数据(用户处理器负责)。
步骤4:处理数据(用户处理器负责)。
改进实现的模拟Proactor:
步骤1:等待事件到来(Proactor负责)。
步骤2:得到读就绪事件,执行读数据(现在由Proactor负责)。
步骤3:将读完成事件分发给用户处理器(Proactor负责)。
步骤4:处理数据(用户处理器负责)。
参考
https://segmentfault.com/a/1190000003063859
http://tech.meituan.com/nio.html
http://blog.jobbole.com/59676/