IO阻塞与非阻塞

同步:
      
所谓同步,就是在发出一个功能调用时,在没有得到结果之前,该调用就不返回。也就是必须一件一件事做,等前一件做完了才能做下一件事。

例如普通B/S模式(同步):提交请求->等待服务器处理->处理完毕返回 这个期间客户端浏览器不能干任何事

异步:
      
异步的概念和同步相对。当一个异步过程调用发出后,调用者不能立刻得到结果。实际处理这个调用的部件在完成后,通过状态、通知和回调来通知调用者。

     例如 ajax请求(异步)请求通过事件触发->服务器处理(这是浏览器仍然可以作其他事情)->处理完毕

阻塞
     
阻塞调用是指调用结果返回之前,当前线程会被挂起线程进入非可执行状态,在这个状态下,cpu不会给线程分配时间片,即线程暂停运行)。函数只有在得到结果之后才会返回。

     有人也许会把阻塞调用和同步调用等同起来,实际上他是不同的。对于同步调用来说,很多时候当前线程还是激活的,只是从逻辑上当前函数没有返回而已。 例如,我们在socket中调用recv函数,如果缓冲区中没有数据,这个函数就会一直等待,直到有数据才返回。而此时,当前线程还会继续处理各种各样的消息。

非阻塞
      
非阻塞和阻塞的概念相对应,指在不能立刻得到结果之前,该函数不会阻塞当前线程,而会立刻返回。
对象的阻塞模式和阻塞函数调用
对象是否处于阻塞模式和函数是不是阻塞调用有很强的相关性,但是并不是一一对应的。阻塞对象上可以有非阻塞的调用方式,我们可以通过一定的API去轮询状 态,在适当的时候调用阻塞函数,就可以避免阻塞。而对于非阻塞对象,调用特殊的函数也可以进入阻塞调用。函数select就是这样的一个例子。

IO多路转接

      它也是会阻塞,不过它是可以阻塞多个IO,实现近似于非阻塞IO,先构造一张有关描述符的数据表,然后调用一个函数,仅当有一个或多个描述符已准备可以进行IO操作时才返回,否则一直阻塞。在返回时,它会告诉进程那些描述符已准备好可以进行IO

  select函数的参数将告诉内核:

  (1) 我们所关心的描述符。

  (2) 对于每个描述符我们所关心的条件(是否读一个给定的描述符?是否想写一个给定的

描述符?是否关心一个描述符的异常条件?)。

  (3) 希望等待多长时间(可以永远等待,等待一个固定量时间,或完全不等待)

        select从内核返回后内核会告诉我们:

  (1) 已准备好的描述符的数量。

  (2) 哪一个描述符已准备好读、写或异常条件。

        select 用于查询设备的状态,以便用户程序获知是否能对设备进行非阻塞的访问,需要设备驱动程序中的poll 函数支持。 驱动程序中 poll 函数中最主要用到的一个 API 是 poll_wait,其原型如下:

  void poll_wait(struct file *filp, wait_queue_heat_t *queue, poll_table * wait);

  poll_wait 函数所做的工作是把当前进程添加到 wait 参数指定的等待列表(poll_table)中。

  需要说明的是,poll_wait 函数并不阻塞,程序中 poll_wait(filp, &outq, wait)这句话的意思并不是说一直等待 outq 信号量可获得,真正的阻塞动作是上层的 select/poll 函数中完成的。select/poll 会在一个循环中对每个需要监听的设备调用它们自己的 poll 支持函数以使得当前进程被加入各个设备的等待列表。若当前没有任何被监听的设备就绪,则内核进行调度(调用 schedule)让出 cpu 进入阻塞状态,schedule 返回时将再次循环检测是否有操作可以进行,如此反复;否则,若有任意一个设备就绪,select/poll 都立即返回。

  内核中实现select是用轮询方法

epoll

  1>支持一个进程打开大数目的socket描述符(FD)

  2>IO效率不随FD数目增加而线性下降,它的内核实现只会对"活跃"的socket进行 操作,而不是全部轮询

  3>使用mmap加速内核与用户空间的消息传递。epoll是通过内核于用户空间mmap同一块内存实现来进行消息通知的。

 

Nginx并发策略

  ginx使用的是一个进程处理多个连接、非阻塞IO模式 ,这种模式最特别的是设计了独立的listener进程,专门负责接收新的连接,再分配给各个worker,当然为了减少任务调度的开销,一般都是由worker进程来进行接收。在listener进程中使用epoll来进行非阻塞IO。

信号驱动IO

    简介:两次调用,两次返回;

    首先我们允许套接口进行信号驱动I/O,并安装一个信号处理函数,进程继续运行并不阻塞。当数据准备好时,进程会收到一个SIGIO信号,可以在信号处理函数中调用I/O操作函数处理数据。

异步IO模型

         简介:数据拷贝的时候进程无需阻塞。

     当一个异步过程调用发出后,调用者不能立刻得到结果。实际处理这个调用的部件在完成后,通过状态、通知和回调来通知调用者的输入输出操作

5个I/O模型的比较:

 

 java NIO

  nio 是java nonblocking(非阻塞) IO 的简称

  *为所有的原始类型提供(Buffer)缓存支持。

  *字符集编码解码解决方案。

  *Channel :一个新的原始I/O 抽象

  *支持锁和内存映射文件的文件访问接口。

  *提供多路(non-bloking) 非阻塞式的高伸缩性网络I/O 。

  1.缓冲Buffer

  是一个固定数据量的指定基本类型的数据容器。除内容之外,缓冲区还具有位置 和界限,其中位置是要读写的下一个元素的索引,界限是第一个应该读写的元素的索引。基本 Buffer 类定义了这些属性以及清除、反转 和重绕 方法,用以标记 当前位置,以及将当前位置重置 为前一个标记处。
  每个非布尔基本类型都有一个缓冲区类。每个类定义了一系列用于将数据移出或移入缓冲区的 get 和 put 方法,用于压缩、复制 和切片 缓冲区的方法,以及用于分配 新缓冲区和将现有数组包装 到缓冲区中的静态方法。
  因为字节缓冲区可以用作 I/O 操作的源缓冲区和目标缓冲区,所以可以对它们加以区分。它们还支持其他缓冲区类所没有的几个特性:可以将字节缓冲区分配为一个直接 缓冲区,在这种情况下,Java虚拟机将最大限度地直接在缓冲区上执行本机 I/O 操作。
  可以通过 mapping 将文件区域直接包装到内存中来创建字节缓冲区,在这种情况下,可以使用 MappedByteBuffer 类中定义的几个其他文件相关的操作。
  字节缓冲区提供了对其内容的访问(其内容作为任何非布尔基本类型的异类或同类二进制数据序列),访问要么是以 big-endian字节顺序进行,要么是以 little-endian 字节顺序进行。

  2.通道

  Channel是一个对象,可以通过它读取和写入数据。拿 NIO 与原来的 I/O 做个比较,通道就像是流,而且他们面向缓冲区的。
  所有数据都通过 Buffer 对象来处理。您永远不会将字节直接写入通道中,相反,您是将数据写入包含一个或者多个字节的缓冲区。同样,您不会直接从通道中读取字节,而是将数据从通道读入缓冲区,再从缓冲区获取这个字节。
  通道与流的不同之处在于通道是双向的。而流只是在一个方向上移动(一个流必须是 InputStream 或者 OutputStream 的子类), 而 通道 可以用于读、写或者同时用于读写。
  因为它们是双向的,所以通道可以比流更好地反映底层操作系统的真实情况。特别是在 UNIX 模型中,底层操作系统通道是双向的。 
 

  NIO Socket编程中有一个主要的类Selector,这个类似一个观察者,只要我们把需要探知的套接字通道socketchannel注册到Selector,程序不用阻塞等待,可以并行做别的事情,当有事件发生时,Selector会通知程序,传回一组SelectionKey,程序读取这些Key,就会获得注册过的socketchannel,然后,从这个Channel中读取和处理数据。

  Selector内部原理实际是在做一个对所注册的channel的轮询访问,不断的轮询(目前就这一个算法),一旦轮询到一个channel有所注册的事情发生,比如数据来了,他就会站起来报告,交出一把钥匙,让我们通过这把钥匙来读取这个channel的内容。

  eg:

 

[java] view plaincopy
 
    1. import java.net.*;  
    2. import java.util.*;  
    3. import java.io.*;  
    4. import java.nio.*;  
    5. import java.nio.channels.*;  
    6. import java.nio.charset.*;  
    7.   
    8. public class NIOSocketServer{  
    9.     public static final int PORT = 8080;  
    10.     public static void main(String[] args)throws IOException{  
    11.         //NIO的通道channel中内容读取到字节缓冲区ByteBuffer时是字节方式存储的,  
    12.         //对于以字符方式读取和处理的数据必须要进行字符集编码和解码  
    13.     String encoding = System.getProperty(“file.encoding”);  
    14.     //加载字节编码集  
    15.     Charset cs = Charset.forName(encoding);  
    16.     //分配两个字节大小的字节缓冲区  
    17.     ByteBuffer buffer = ByteBuffer.allocate(16);  
    18.     SocketChannel ch = null;  
    19.     //打开服务端的套接字通道  
    20.     ServerSocketChannel ssc = ServerSocketChannel.open();  
    21.     //打开通道选择器  
    22.     Selector sel = Selector.open();  
    23.     try{  
    24.         //将服务端套接字通道连接方式调整为非阻塞模式  
    25.       ssc.configureBlocking(false);  
    26.       //将服务端套接字通道绑定到本机服务端端口  
    27.       ssc.socket().bind(new InetSocketAddress(PORT));  
    28.          //将服务端套接字通道OP_ACCEP事件注册到通道选择器上  
    29.          SelectionKey key = ssc.register(sel, SelectionKey.OP_ACCEPT);  
    30.          System.out.println(“Server on port:” + PORT);  
    31.          while(true){  
    32.                //通道选择器开始轮询通道事件  
    33.               sel.select();  
    34.               Iterator it = sel.selectedKeys().iterator();  
    35.               While(it.hasNext()){  
    36.                     //获取通道选择器事件键  
    37.                      SelectionKey skey = (SelectionKey)it.next();  
    38.                       it.remove();  
    39.                       //服务端套接字通道发送客户端连接事件,客户端套接字通道尚未连接  
    40.                        if(skey.isAcceptable()){  
    41.                                   //获取服务端套接字通道上连接的客户端套接字通道  
    42.                                    ch = ssc.accept();  
    43.                                     System.out.println(“Accepted connection from:” + ch.socket());  
    44.                                   //将客户端套接字通过连接模式调整为非阻塞模式  
    45.                                   ch.configureBlocking(false);  
    46.                                  //将客户端套接字通道OP_READ事件注册到通道选择器上  
    47.                                  ch.register(sel, SelectionKey.OP_READ);  
    48.                         }  
    49.           //客户端套接字通道已经连接  
    50.                      else{  
    51.                                    //获取创建此通道选择器事件键的套接字通道  
    52.                                    ch = (SocketChannel)skey.channel();  
    53.                                    //将客户端套接字通道数据读取到字节缓冲区中  
    54.                                    ch.read(buffer);  
    55.                                     //使用字符集解码字节缓冲区数据  
    56.                                    CharBuffer cb = cs.decode((ByteBuffer)buffer.flip());  
    57.                                    String response = cb.toString();  
    58.                                    System.out.println(“Echoing:” + response) ;  
    59.                                    //重绕字节缓冲区,继续读取客户端套接字通道数据  
    60.                                    ch.write((ByteBuffer)buffer.rewind());  
    61.                                    if(response.indexOf(“END”) != -1) ch.close();  
    62.                                    buffer.clear();  
    63.                         }  
    64.                  }  
    65.             }  
    66.     }finally{  
    67.            if(ch != null) ch.close();  
    68.            ssc.close();  
    69.            sel.close();  
    70.     }  
    71.   }  
    72. }  

   

java NIO 和阻塞I/O的区别 

1. 阻塞I/O通信模型 

  假如现在你对阻塞I/O已有了一定了解,我们知道阻塞I/O在调用InputStream.read()方法时是阻塞的,它会一直等到数据到来时(或超时)才会返回;同样,在调用ServerSocket.accept()方法时,也会一直阻塞(线程让出CPU)到有客户端连接才会返回,每个客户端连接过来后,服务端都会启动一个线程去处理该客户端的请求。阻塞I/O的通信模型示意图如下:

 

 

  如果你细细分析,一定会发现阻塞I/O存在一些缺点。根据阻塞I/O通信模型,两点缺点:
  1. 当客户端多时,会创建大量的处理线程。且每个线程都要占用栈空间和一些CPU时间

  2. 阻塞可能带来频繁的上下文切换,且大部分上下文切换可能是无意义的。

  这种情况下非阻塞式I/O就有了它的应用前景。

2. 
java NIO原理及通信模型 

Java NIO是在jdk1.4开始使用的,它既可以说成“新I/O”,也可以说成非阻塞式I/O。下面是java NIO的工作原理:

  1. 由一个专门的线程来处理所有的 IO 事件,并负责分发。 
  2. 事件驱动机制:事件到的时候触发,而不是同步的去监视事件。 
  3. 线程通讯:线程之间通过 wait,notify 等方式通讯。保证每次上下文切换都是有意义的。减少无谓的线程切换。 

阅读过一些资料之后,下面贴出我理解的java NIO的工作原理图:

 

 

(注:每个线程的处理流程大概都是读取数据、解码、计算处理、编码、发送响应。)

  Java NIO的服务端只需启动一个专门的线程来处理所有的 IO 事件,这种通信模型是怎么实现的呢?呵呵,我们一起来探究它的奥秘吧。java NIO采用了双向通道(channel)进行数据传输,而不是单向的流(stream),在通道上可以注册我们感兴趣的事件。一共有以下四种事件:

 

事件名 对应值
服务端接收客户端连接事件 SelectionKey.OP_ACCEPT(16)
客户端连接服务端事件 SelectionKey.OP_CONNECT(8)
读事件 SelectionKey.OP_READ(1)
写事件 SelectionKey.OP_WRITE(4)

 

  服务端和客户端各自维护一个管理通道的对象,我们称之为selector,该对象能检测一个或多个通道 (channel) 上的事件。我们以服务端为例,如果服务端的selector上注册了读事件,某时刻客户端给服务端发送了一些数据,阻塞I/O这时会调用read()方法阻塞地读取数据,而NIO的服务端会在selector中添加一个读事件。服务端的处理线程会轮询地访问selector,如果访问selector时发现有感兴趣的事件到达,则处理这些事件,如果没有感兴趣的事件到达,则处理线程会一直阻塞直到感兴趣的事件到达为止。下面是我理解的java NIO的通信模型示意图:

 

参考:http://blog.csdn.net/hguisu/article/details/7453390

http://www.cnblogs.com/zhuyp1015/archive/2012/05/31/2529079.html

http://weixiaolu.iteye.com/blog/1479656

http://www.tuicool.com/articles/AzmiY3

posted on 2013-11-12 16:48  依蓝jslee  阅读(446)  评论(0编辑  收藏  举报

导航