介绍

在谈及网络IO的时候总避不开阻塞、非阻塞、同步、异步、IO多路复用、select、poll、epoll等这几个词语。在面试的时候也会被经常问到这几个的区别。本文就来讲一下这几个词语的含义、区别以及使用方式。
Unix网络编程一书中作者给出了五种IO模型:
1、BlockingIO - 阻塞IO
2、NoneBlockingIO - 非阻塞IO
3、IO multiplexing - IO多路复用
4、signal driven IO - 信号驱动IO
5、asynchronous IO - 异步IO
这五种IO模型中前四个都是同步的IO,只有最后一个是异步IO。信号驱动IO使用的比较少,重点介绍其他几种IO以及在Java中的应用。

阻塞、非阻塞、同步、异步以及IO多路复用

在进行网络IO的时候会涉及到用户态和内核态,并且在用户态和内核态之间会发生数据交换,从这个角度来说我们可以把IO抽象成两个阶段:1、用户态等待内核态数据准备好,2、将数据从内核态拷贝到用户态。之所以会有同步、异步、阻塞和非阻塞这几种说法就是根据程序在这两个阶段的处理方式不同而产生的。

同步阻塞


    当在用户态调用read操作的时候,如果这时候kernel还没有准备好数据,那么用户态会一直阻塞等待,直到有数据返回。当kernel准备好数据之后,用户态继续等待kernel把数据从内核态拷贝到用户态之后才可以使用。这里会发生两种等待:一个是用户态等待kernel有数据可以读,另外一个是当有数据可读时用户态等待kernel把数据拷贝到用户态。
    在Java中同步阻塞的实现对应的是传统的文件IO操作以及Socket的accept的过程。在Socket调用accept的时候,程序会一直等待知道有描述符就绪,并且把就绪的数据拷贝到用户态,然后程序中就可以拿到对应的数据。

同步非阻塞


        对比第一张同步阻塞IO的图就会发现,在同步非阻塞模型下第一个阶段是不等待的,无论有没有数据准备好,都是立即返回。第二个阶段仍然是需要等待的,用户态需要等待内核态把数据拷贝过来才能使用。对于同步非阻塞模式的处理,需要每隔一段时间就去询问一下内核数据是不是可以读了,如果内核说可以,那么就开始第二阶段等待。

IO多路复用

IO多路复用也是同步的。
        
    IO多路复用的方式看起来跟同步阻塞是一样的,两个阶段都是阻塞的,但是IO多路复用可以实现以较小的代价同时监听多个IO。通常情况下是通过一个线程来同时监听多个描述符,只要任何一个满足就绪条件,那么内核态就返回。IO多路复用使得传统的每请求每线程的处理方式得到解耦,一个线程可以同时处理多个IO请求,然后交到后面的线程池里处理,这也是netty等框架的处理方式,所谓的reactor模式。IO多路复用的实现依赖于操作系统的select、poll和epoll,后面会详细介绍这几个系统调用。
    IO多路复用在Java中的实现方式是在Socket编程中使用非阻塞模式,然后配置感兴趣的事件,通过调用select函数来实现。select函数就是对应的第一个阶段。如果给select配置了超时参数,在指定时间内没有感兴趣事件发生的话,select调用也会返回,这也是为什么要做非阻塞模式下运行。

异步IO


        异步模式下,前面提到的两个阶段都不会等待。使用异步模式,用户态调用read方法的时候,相当于告诉内核数据发送给我之后告诉我一声我先去干别的事情了。在这两个阶段都不会等待,只需要在内核态通知数据准备好之后使用即可。通常情况下使用异步模式都会使用callback,当数据可用之后执行callback函数。

IO多路复用

现在用Java开发的网络服务器通常采用IO多路复用的方式来加快网络IO操作,例如Netty、Tomcat等。IO多路复用的基础是select、poll和epoll。这三个函数是从操作系统的角度上支持的IO多路复用的操作,下面就分别来看一下这三个函数。

select

函数签名如下:

int select(int maxfdp1, fd_set *readset,fd_set *writeset,fd_set *exceptset,const struct timeval *timeout)

maxfdp1为指定的待监听的描述符的个数,因为描述符是从0开始的,所以需要加1
readset为要监听的读描述符
writeset为要监听的写描述符
exceptset为要监听的异常描述符
timeout监听没有准备好的描述符的话,多久可以返回,支持按照秒或者毫秒来配置时间
    select操作的逻辑是首先将要监听的读、写以及异常描述符拷贝到内核空间,然后遍历所有的描述符,如果有感兴趣的事件发生,那么就返回。
select在使用的过程中有三个问题:
1、被监控的fds(描述符)集合限制为1024,1024太小了
2、需要将描述符集合从用户空间拷贝到内核空间
3、当有描述符可操作的时候都需要遍历一下整个描述符集合才能知道哪个是可操作的,效率很低。

poll

函数签名如下:

int poll(struct pollfd[] fds, unsigned int nfds, int timeout);

poll操作与select操作类似,仍旧避免不了描述符从用户空间拷贝到内核空间,但是poll不再有1024个描述符的限制。对于事件的触发通知还是使用遍历所有描述符的方式,因此在大量连接的情况下也存在遍历低效的问题。poll函数在传递参数的时候统一的将要监听的描述符和事件封装在了pollfd结构体数组中。

epoll

epoll有三个方法:epoll_create、epoll_ctl和epoll_wait。epoll_create是创建一个epoll句柄;epoll_ctl是注册要监听的事件类型;epoll_wait则是等待事件的产生。 通过这三个方法epoll解决了select的三个问题。
1、1024数量限制的问题
通过epoll_create方法来创建一个epoll句柄,这个句柄监听的描述符的数量不再有限制。
2、文件描述符频繁从用户空间拷贝到内核空间的问题
通过观察select的操作会发现描述符从用户空间到内核空间拷贝发生在调用select方法的时候,只要没有注册新的事件或者取消注册事件,每次拷贝的描述符都是一样的。因此epoll引入了epoll_ctl调用,该方法用于注册新事件和取消注册事件。而在epoll_wait的时候并不会拷贝描述符,描述符始终存在于内核空间,当需要修改的时候只要调用epoll_ctl修改一下内核的描述符即可。如此一来便省去了描述符来回拷贝的开销。
3、文件描述符可操作的时候遍历整个描述符集合的问题
在调用epoll_ctl注册感兴趣的事件的时候,实际上会为设置的事件添加一个回调函数,当对应的感兴趣的事件发生的时候,回调函数就会触发,然后将自己加到一个链表中。epoll_wait函数的作用就是去查看这个链表中有没有已经准备就绪的事件,如果有的话就通知应用程序处理,如此操作epoll_wait只需要遍历就绪的事件描述符即可。

epoll在Java中的使用

目前针对Java服务器的非阻塞编程基本都是基于epoll的。在进行非阻塞编程的时候有两个步骤:1、注册感兴趣的事情;2、调用select方法,查找感兴趣的事件。

注册感兴趣的事件

我们在编写Socket的非阻塞代码的时候需要在Selector上注册感兴趣的事情,通常写法是serverSocketChannel.register(selector, SelectionKey.XXX)。来看一下这行代码背后的执行逻辑是什么样的。
        
注册的时候实际执行的是EPollSelectorImp。该方法主要有以下三步:
1、implRegister方法。在fdToKey的Map中插入channel对应的文件描述法和SelectionKey的映射,当做注册Channel、关闭Channel、取消注册等操作是都是操作此Map。
2、往pollWrapper[Epoll实例]中放入channel实例。
3、往keys[HashSet]中放入SelectionKey

select方法

通过Java的Selector.select方法来获取准备好的键的时候实际执行的代码如下:
        
首先调用EPollArrayWrapper的poll方法,该方法做两件事:1、调用epollCtl方法向epoll中注册感兴趣的事件;2、调用epollWait方法返回已就绪的文件描述符集合
然后调用updateSelectedKeys方法调用把epoll中就绪的文件描述符加到ready队列中等待上层应用处理, updateSelectedKeys通过fdToKey查找文件描述符对应的SelectionKey,并在SelectionKey对应的channel中添加对应的事件到ready队列。

水平触发LT与边缘触发ET

epoll支持两种触发模式,分别是水平触发和边缘触发。
    LT是缺省的工作方式,并且同时支持block和no-block socket。在这种做法中,内核告诉你一个文件描述符是否就绪了,然后你可以对这个就绪的fd进行IO操作。如果你不作任何操作,内核还是会继续通知你的。
    ET是高速工作方式,只支持no-block socket。在这种模式下,当描述符从未就绪变为就绪时,内核会通知你一次,并且除非你做了某些操作导致那个文件描述符不再为就绪状态了,否则不会再次发送通知。
    可以看到,本来内核在被DMA中断,捕获到IO设备来数据后,只需要查找这个数据属于哪个文件描述符,进而通知线程里等待的函数即可,但是,LT要求内核在通知阶段还要继续再扫描一次刚才所建立的内核fd和io对应的那个数组,因为应用程序可能没有真正去读上次通知有数据后的那些fd,这种沟通方式效率是很低下的,只是方便编程而已;

JDK并没有实现边缘触发,关于边缘触发和水平触发的差异简单列举如下,边缘触发的性能更高,但编程难度也更高,netty就重新实现了Epoll机制,采用边缘触发方式;另外像nginx等也采用的是边缘触发。

----------------------------------------------------------------

欢迎关注我的微信公众号:yunxi-talk,分享Java干货,进阶Java程序员必备。

一文读懂阻塞、非阻塞、同步、异步IO的更多相关文章

  1. Java IO 学习(一)同步/异步/阻塞/非阻塞

    关于IO,同步/异步/阻塞/非阻塞,这几个关键词是经常听到的,譬如: “Java oio是阻塞的,nio是非阻塞的” “NodeJS的IO是异步的” 但是这些东西听多了就容易迷糊,比方说同步是否就是阻 ...

  2. 同步异步,阻塞非阻塞 和nginx的IO模型

    同步与异步 同步和异步关注的是消息通信机制 (synchronous communication/ asynchronous communication).所谓同步,就是在发出一个*调用*时,在没有得 ...

  3. 进程&线程 同步异步&阻塞非阻塞

    2015-08-19 15:23:38 周三 线程 线程安全 如果你的代码所在的进程中有多个线程在同时运行,而这些线程可能会同时运行这段代码 线程安全问题都是由全局变量及静态变量引起的 若每个线程中对 ...

  4. 【转载】高性能IO设计 & Java NIO & 同步/异步 阻塞/非阻塞 Reactor/Proactor

    开始准备看Java NIO的,这篇文章:http://xly1981.iteye.com/blog/1735862 里面提到了这篇文章 http://xmuzyq.iteye.com/blog/783 ...

  5. 高性能IO设计模式之阻塞/非阻塞,同步/异步解析

    提到高性能,我想大家都喜欢这个,今天我们就主要来弄明白在高性能的I/O设计中的几个关键概念,做任何事最重要的第一步就是要把概念弄的清晰无误不是么?在这里就是:阻塞,非阻塞,同步,异步. OK, 现在来 ...

  6. Python番外之 阻塞非阻塞,同步与异步,i/o模型

    1. 概念理解 在进行网络编程时,我们常常见到同步(Sync)/异步(Async),阻塞(Block)/非阻塞(Unblock)四种调用方式: 同步/异步主要针对C端: 同步:      所谓同步,就 ...

  7. JAVA 中BIO,NIO,AIO的理解以及 同步 异步 阻塞 非阻塞

    在高性能的IO体系设计中,有几个名词概念常常会使我们感到迷惑不解.具体如下: 序号 问题 1 什么是同步? 2 什么是异步? 3 什么是阻塞? 4 什么是非阻塞? 5 什么是同步阻塞? 6 什么是同步 ...

  8. Netty基础系列(2) --彻底理解阻塞非阻塞与同步异步的区别

    引言 在进行I/O学习的时候,阻塞和非阻塞,同步和异步这几个概念常常被提及,但是很多人对这几个概念一直很模糊.要想学好Netty,这几个概念必须要掌握清楚. 同步和异步 同步与异步的区别在于,异步基于 ...

  9. 008. 阻塞&非阻塞、同步&异步

    阻塞 非阻塞:关注的对象是调用者: 阻塞:调用者发起调用后,处于等待状态,直到该调用有返回: 非阻塞:调用者发起调用后,不需要等待返回,可以往下执行: 同步 异步:  关注的对象是被调用者: 同步:服 ...

  10. 操作系统介绍-操作系统历史,IO,进程的三态,同步异步阻塞非阻塞

    1.操作系统历史 2.进程,IO,同步异步阻塞非阻塞 操作系统历史: 手工操作: 1946年第一台计算机诞生--20世纪50年代中期,计算机工作还在采用手工操作方式.此时还没有操作系统的概念. 手工操 ...

随机推荐

  1. Java异常处理示例

    翻译人员: 铁锚 翻译日期: 2013年11月22日 原文链接: Java Exception Handling Example 本文中有两个示例, 第一个演示了所有调用其他方法的地方,都必须处理被调 ...

  2. iOS开发支付集成之支付宝支付

    项目中要用到支付功能,需要支付宝,微信,银联三大支付,所以打算总结一下,写两篇文章,方便以后的查阅, 大家在做的时候也能稍微参考下,用到的地方避免再次被坑.这是第二篇支付宝集成,第一篇银联支付在这里. ...

  3. 16_Android生命周期再介绍,通过androidconfigChanges属性让界面旋转时不改变状态中保留的值

     A  android:configChanges属性 对android:configChanges属性,一般认为有以下几点: 1 不设置Activity的android:configChange ...

  4. CSS中让一个div的高度随着另外个一个统计的div的高度变化而变化的代码

     .w1002 .left_part{overflow:hidden; padding-bottom:9999px; margin-bottom:-9999px;display:inline;}

  5. Erlang 编写 Kafka 客户端之最简单入门

    Erlang 编写 Kafka 客户端之最简单入门 费劲周折,终于测通了 erlang 向kafka 发送消息,使用了ekaf 库,参考: An advanced but simple to use, ...

  6. LIRe 源代码分析 6:检索(ImageSearcher)[以颜色布局为例]

    ===================================================== LIRe源代码分析系列文章列表: LIRe 源代码分析 1:整体结构 LIRe 源代码分析 ...

  7. PS 图像调整算法——饱和度调整

    算法参考自 阿发伯 的博客. http://blog.csdn.net/maozefa 饱和度调整 图像的饱和度调整有很多方法,最简单的就是判断每个象素的R.G.B值是否大于或小于128,大于加上调整 ...

  8. objc:NSDateFormatter使用备忘

    NSDateFormatter类的实例可以将字符串的日期表示转换为NSDate对象或者反向转换. 如果只要显示日期不需要时间,则可以用-setDateStyle方法来设置显示日期的格式,有以下几种: ...

  9. Android开发技巧——高亮的用户操作指南

    Android开发技巧--高亮的用户操作指南 2015-12-15补记: 发现使用PopupWindow进行遮罩层的显示,在华为P7上会有问题.具体表现为:画出来的高亮部分会偏下.原因为:通过view ...

  10. Leetcode(59)-Count Primes

    题目: Description: Count the number of prime numbers less than a non-negative number, n. 思路: 题意:求小于给定非 ...