zoukankan      html  css  js  c++  java
  • 【IO 和 NIO】

    仅针对网络IO的介绍,要搞懂IO和NIO的大致原理,我们要介绍两个概念:

    一.【总体流程】

    【IO】
    read系统调用,是把数据从内核缓冲区复制到进程缓冲区;write系统调用,是把数据从进程缓冲区复制到内核缓冲区。
    这个两个系统调用,都不负责数据在内核缓冲区和磁盘之间的交换。底层的读写交换,是由操作系统kernel内核完成的。
     
    【内核缓冲和进程缓冲区】
    1.缓冲区的目的,是为了减少频繁的系统IO调用。
    2.系统调用需要保存之前的进程数据和状态等信息,而结束调用之后回来还需要恢复之前的信息,为了减少这种损耗时间、也损耗性能的系统调用,于是出现了缓冲区。有了缓冲区,操作系统使用read函数把数据从内核缓冲区复制到进程缓冲区,write把数据从进程缓冲区复制到内核缓冲区中。等待缓冲区达到一定数量的时候,再进行IO的调用,提升性能。至于什么时候读取和存储则由内核来决定,用户程序不需要关心。
    在linux系统中,系统内核也有个缓冲区叫做内核缓冲区。每个进程有自己独立的缓冲区,叫做进程缓冲区。
    所以,用户程序的IO读写程序,大多数情况下,并没有进行实际的IO操作,而是在读写自己的进程缓冲区。
     
    看个大致例子,流程图如下,先介绍链接,再介绍读写:

     1. server创建一个socket,并进行端口的绑定,设定监听端口,这时候就可以接受client的网络链接请求了。

             这时候我们有两种选择,用户的进程/线程是阻塞 等待连接还是一直主动询问操作系统,接口有没有链接进来。

                  ServerSocketChannel sc = ServerSocketChannel.open();
    sc.bind(new InetSocketAddress(PORT));
    sc.configureBlocking(false);//可以阻塞/非阻塞

       1.1 【等待连接】即 我们常说的BIO,我们线程阻塞,挂起,并等待操作系统的唤醒。

       1.2 【主动询问】即 NIO(非阻塞IO,不是java NIO,java的NIO其实指的是IOM-多路复用),不断的询问操作系统,是否有链接进来。

    2.client 发起链接,数据通过网卡触发CPU的终端,CPU根据socket(源IP,源端口,目标IP,目标端口),定位到具体的进程。

    3.一方面通过触发CPU的中断,进行排队,一方面网络数据写入内核缓冲区。

    4.CPU唤醒对应的进程(如果是阻塞的话,非阻塞,则通过一直询问内核来获取)。

    5.进程进行CPU分片的排队,轮到进程A时候,进行数据的读取。

    6.触发进行数据读取,把对应的需要读取的数据,从内核缓冲区,读到进程缓冲区之后,进行读取。

    那单个 简单的网络IO流程如上,那就引发具体的问题:

    1. BIO 线程进行阻塞,时效性肯定是更高的,但线程阻塞无法做其他事情了,吞吐量就会小很多了。

    2. NIO 如果有1000个链接,读写 每次询问操作系统,会触发上下文切换,同样会极大的影响性能。

    二.【细分IO】

    接下来就详细讨论下各个IO的具体情况,涉及的是 大纲图 6的流程,即触发 流程6的时候,用户的进程是在做什么的。

    开始介绍6流程时候,我们先了解现有计算机结构。目前网卡数据的写入一般由DMA进行完成的,因此网络的IP包不会触发CPU的上下文切换。

    2.1【BIO】 

     1 public static final Integer PORT = 8080;
     2 
     3     public static void main(String[] args) throws Exception{
     4         ServerSocket serverSocket = new ServerSocket(PORT);
     5         System.out.println("-----1. 等待连接-------");
     6 
     7         while (true) {
     8             Socket socket = serverSocket.accept();//阻塞
     9             System.out.println("-----2. 已连接,客户端--" + socket.getRemoteSocketAddress() + "-----");
    10             InputStream inputStream = socket.getInputStream();
    11             BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));//阻塞
    12             System.out.println("----客户端信息:" + reader.readLine());
    13             inputStream.close();
    14             socket.close();
    15         }
    16     }

    以上是BIO的Demo

    1.首先我们看到,Socket绑定端口之后,accept获取链接的时候是阻塞方式的。

    2.通过字节流方式读取网络数据也是阻塞的。即我们通过inputStream获取字节流的时候,如果无数据的时候读取不到,代码是不会往下执行的。

         如下图所示:即,我们代码触发read网络IO流时候,会等待网卡写入数据到内核缓冲区的数据(①阶段),等缓冲区数据够了(比如 等待一个完整的socket数据包),进行复制到用户的缓冲区(②阶段),复制完成之后,用户线程即可往下执行代码,进行数据的读取了。

    BIO只是网络IO的一种作业模型,有对应的优缺点如下,但是目前不常用,一般再本地的IO上使用,性能不下于NIO,特别是JDK1.5之后有优化过。

    BIO的优点:

    程序简单,在阻塞等待数据期间,用户线程挂起。用户线程基本不会占用 CPU 资源。
     
    BIO的缺点:
    一般情况下,会为每个连接配套一条独立的线程,或者说一条线程维护一个连接成功的IO流的读写。在并发量小的情况下,这个没有什么问题。但是,当在高并发的场景下,需要大量的线程来维护大量的网络连接,内存、线程切换开销会非常巨大。因此,基本上,BIO模型在高并发场景下是不可用的。

    2.2【BIO】 

     1 public static final Integer PORT = 8080;
     2 
     3     public static final LinkedList<SocketChannel> clients = new LinkedList<>();
     4     public static void main(String[] args) throws Exception{
     5         ServerSocketChannel sc = ServerSocketChannel.open();
     6         sc.bind(new InetSocketAddress(PORT));
     7         sc.configureBlocking(false);//可以阻塞/非阻塞
     8         System.out.println("-----1. 等待连接-------");
     9         while (true) {
    10             Thread.sleep(1000);
    11             SocketChannel client = sc.accept();//非阻塞
    12             //处理链接
    13             if (client == null) {
    14                 System.out.println("-----1.1 未有连接-------");
    15             } else {
    16                 client.configureBlocking(false);//可以阻塞/非阻塞
    17                 System.out.println("-----2. 已连接,客户端--" + client.socket().getRemoteSocketAddress() + "-----");
    18                 clients.add(client);
    19             }
    20 
    21             for(SocketChannel temp:clients){
    22                 ByteBuffer buffer = ByteBuffer.allocateDirect(4096);
    23                 int num = temp.read(buffer);
    24                 while (num <= 0){
    25                     Thread.sleep(1000);
    26                     System.out.println("----2.1 未有消息-------");
    27                     num = temp.read(buffer);//非阻塞
    28                 }
    29 
    30                 buffer.flip();
    31                 byte[] msg = new byte[buffer.limit()];
    32                 buffer.get(msg);
    33                 System.out.println("----客户端信息:"+new String(msg));
    34                 clients.remove(temp);
    35                 buffer.clear();
    36                 temp.close();
    37             }
    38 
    39         }
    40 
    41     }

    以上是NIO的Demo(注:是Non Blocking IO,不是JAVA说的 NewIO)

    1.首先我们看到,Socket绑定端口之后,accept获取链接的时候是非阻塞方式的。会一直循环,并不会卡住代码流程。

    2.获取连接之后,设置客户端也是非阻塞的,由于为了测试方便,因此都在一个循环流程里面。刚兴趣的可以自己跑一跑这个demo,看看场景。

         如下图所示:即,我们代码触发read网络IO流时候,会一直以上代码的while里面循环,会等待网卡写入数据到内核缓冲区的数据(①阶段),等缓冲区数据够了(比如 等待一个完整的socket数据包),进行复制到用户的缓冲区(②阶段),复制完成之后,获取到数据(即 num > 0),然后进行数据的读取了。

    NIO的特点如下: 应用程序的线程需要不断的进行 I/O 系统调用,轮询数据是否已经准备好,如果没有准备好,继续轮询,直到完成系统调用为止。

    NIO的优点:
    每次发起的 IO 系统调用,在内核的等待数据过程中可以立即返回。用户线程不会阻塞。
    NIO的缺点:
    需要不断的重复发起IO系统调用,这种不断的轮询,将会不断地询问内核,这将占用大量的 CPU 时间,系统资源利用率较低。
     
     
    那是不是有NIO就万事大吉了?这里引出一个计算机的难题 C10K,
    假设有1000个链接,为了处理这些链接有几种方式:
    1.是不是要1000个线程去轮询等待处理,每个线程询问操作系统,该链接是否具备读的条件。
    2.我们用其他方式,通过线程池,去处理读写,但是轮询这1000个链接,有读写事件的,再进行读写,但是每次还得去调用操作系统询问链接情况。
     
    不论采用哪种方式,都会存在一个问题,即,这1000个链接,得询问操作系统1000次,这种也是非常耗时的操作,所以基本不会用在正式环境上。
    那操作系统能不能提供一个方式,每次我调用询问的时候,直接给我返回可用的(链接/读写事件)。这就是我们经常用的IO多路复用(我习惯叫IOM,即我们常说的java NIO,即 New IO),为什么叫IO多路复用呢?即,操作系统开辟一路进程监视这些链接描述符,我们只需询问这个进程,就可以知道哪些链接具备 链接/读写事件了。即多个链接用一个进程进行监听,称为 IO 多路复用。liunx 多路复用,有select,poll和epoll,下面再详细介绍,这边不展开。
     
    2.3【IOM】
     1 public static final Integer PORT = 8080;
     2 
     3     public static void main(String[] args) throws Exception{
     4 
     5         //SelectorProvider.provider().openServerSocketChannel();
     6         //sun.nio.ch.DefaultSelectorProvider.create()
     7         //这边windows是 WindowsSelectorProvider()
     8         ServerSocketChannel sc = ServerSocketChannel.open();
     9         sc.bind(new InetSocketAddress(PORT));
    10         sc.configureBlocking(false);//为什么要设置非阻塞,因为再我们调用多路复用器selector询问这个是否有链接进来时候,期望serverSocket是立即答复。
    11 
    12         //多路复用器
    13         Selector selector = Selector.open();
    14         System.out.println("-----1. 等待连接-------");
    15         while (true){
    16             Thread.sleep(1000);
    17             SocketChannel client = sc.accept();//非阻塞
    18             //处理链接
    19             if (client == null) {
    20                 System.out.println("-----1.1 未有连接-------");
    21             } else {
    22                 //把sc注册进去,仅注册读事件
    23                 client.configureBlocking(false);
    24                 ByteBuffer buffer = ByteBuffer.allocateDirect(4096);
    25                 client.register(selector, SelectionKey.OP_READ,buffer);
    26                 System.out.println("-----2. 已连接,客户端--" + client.socket().getRemoteSocketAddress() + "-----");
    27             }
    28 
    29             //这里为什么要阻塞
    30             while (selector.select(1000) > 0){
    31                 //调用多路复用器获取
    32                 System.out.println("-----3 调用多路复用器-------");
    33                 Set<SelectionKey> selectionKeySet = selector.selectedKeys(); //从多路复用器,取出有效的链接(仅注册read事件),调用多路复用器这个是阻塞的,等待多路复用器返回链接
    34 
    35                 Iterator<SelectionKey> iterator = selectionKeySet.iterator();
    36                 while(iterator.hasNext()){
    37                     SelectionKey key = iterator.next();
    38                     iterator.remove();
    39                     SocketChannel temp = (SocketChannel)key.channel();
    40                     ByteBuffer buffer = ByteBuffer.allocateDirect(4096);
    41                     int num = temp.read(buffer);//为什么要设置非阻塞,因为再我们调用多路复用器selector询问这个是否有链接进来时候,期望serverSocket是立即答复。
    42 
    43                     //循环等待有消息
    44                     while (num <= 0){
    45                         Thread.sleep(1000);
    46                         System.out.println("----2.1 未有消息-------");
    47                         num = temp.read(buffer);//非阻塞
    48                     }
    49 
    50                     buffer.flip();
    51                     byte[] msg = new byte[buffer.limit()];
    52                     buffer.get(msg);
    53                     System.out.println("----客户端信息"+temp.socket().getPort()+":"+new String(msg));
    54                     buffer.clear();
    55                 }
    56             }
    57 
    58         }
    59     }

    以上是IOM的Demo(注:就是JAVA说的 NewIO)

    1.首先我们看到,跟之前IO不同的地方在于 。

           Selector selector = Selector.open();
           client.register(selector, SelectionKey.OP_READ,buffer);

    2.打开Selector,即多路复用器,然后把我们关心的事件注册到多路复用器上,这里为了区分,我仅仅把读事件注册进去,这个即告诉操作系统,如果发现该端口的链接存在读事件返回回来即可。

    3.selector.select() 我们再调用多路复用器询问的时候,如果存在读事件时候,即返回对应的key,我们循环Key,进行读。

         如下图所示:即,类似BIO,因为由操作系统帮我们去校验链接的状态了,不用一个个链接循环去询问(一个个循环触发上下文切换,非常耗时),同样会等待网卡写入数据到内核缓冲区的数据(①阶段),等缓冲区数据够了(比如 等待一个完整的socket数据包),进行复制到用户的缓冲区(②阶段),复制完成之后,获取到数据,然后进行数据的读取了。由于有IO多路复用器已经帮我们过滤了,获取到的都是具备完善的条件的(即 可读/可写),因此通过阻塞进行数据的读,时效性更高。

    多路复用IO的优点:

    用select/epoll的优势在于,它可以同时处理成千上万个连接(connection)。与一条线程维护一个连接相比,I/O多路复用技术的最大优势是:系统不必创建线程,也不必维护这些线程,从而大大减小了系统的开销。
    Java的NIO(new IO)技术,使用的就是IO多路复用模型。在linux系统上,使用的是epoll系统调用。
    多路复用IO的缺点:
    本质上,select/epoll系统调用,属于同步IO,也是阻塞IO。都需要在读写事件就绪后,自己负责进行读写,也就是说这个读写过程是阻塞的。
     
    以上,本质上,都是用户的进程主动询问操作系统的,因此都是属于 同步IO,但是区分阻塞和非阻塞。目前异步IO,即AIO,由于需要操作系统的支持,但是目前操作系统支持还不完善,因此还未大规模使用。

     
    AIO的晚点再更新
     
     

  • 相关阅读:
    .NET实现Excel文件的读写 未测试
    权限管理设计
    struts1中配置应用
    POJ 2139 Six Degrees of Cowvin Bacon(floyd)
    POJ 1751 Highways
    POJ 1698 Alice's Chance
    POJ 1018 Communication System
    POJ 1050 To the Max
    POJ 1002 4873279
    POJ 3084 Panic Room
  • 原文地址:https://www.cnblogs.com/HA-Tinker/p/13826978.html
Copyright © 2011-2022 走看看