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的晚点再更新
     
     

  • 相关阅读:
    学习:Intents和Intent Filters(理论部分)
    天书夜读:从汇编语言到Windows内核编程笔记(1)
    学习:Intents和Intent Filters(实例部分)
    寒江独钓(1):内核数据类型和函数
    寒江独钓(2):串口的过滤
    学习:了解WDK目录
    Nginx 414 RequestURI Too Large 海口
    Ansible 批量修改密码 海口
    记一次node进程无法kill 问题 海口
    Vue学习心得新手如何学习Vue(转载)
  • 原文地址:https://www.cnblogs.com/HA-Tinker/p/13826978.html
Copyright © 2011-2022 走看看