欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

大话IO

程序员文章站 2022-10-04 08:44:09
大话IO1. 基础知识1.1 一次网络IO1.1.1 网络IO流程1.1.2 什么是DMA?1.1.3 CPU如何知道接收数据?1.2 CPU调度1.2.1 阻塞为什么不消耗CPU?1.2.1.1 工作队列1.2.1.2 等待队列2.BIO2.1 简介下什么是BIO?2.2 什么是C10K问题?2.3 BIO代码示范2.4 BIO存在的问题3.1 简介下什么是NIO?3.2 NIO核心3.2.1 通道3.2.2 缓冲区3.2.2.1 Buffer简介3.2.2.2 缓冲区分类3.2 如何提...

1. 基础知识

1.1 一次网络IO

大话IO

1.1.1 网络IO流程

  • 用户发送数据到服务器【网卡
  • 网卡使用DMA方式将数据拷贝到内核的Socket缓冲区的RevBuffer 【内核Socket缓冲区
  • 应用程序读数据时,将内核Socket缓冲区中的数据拷贝到当前线程的缓冲区里【用户缓冲区
  • 应用程序执行相应的业务处理,如果要操作磁盘文件,需要切换到内核态,调用系统函数,将磁盘上的数据拷贝到内核缓冲区中,然后再从内核缓冲区拷贝到用户缓冲区中。
  • 应用程序写数据时,先将用户缓冲区里的数据拷贝到内核的Socket缓冲区的sendBuffer中
  • 内核程序再通过DMA方式将数据拷贝到网卡上

1.1.2 什么是DMA?

  • DMA: Direct Memory Acces的缩写,直接内存存取的意思。
  • 一种允许【硬件直接访问内存】的机制
  • 基于DMA访问方式,可以【省去CPU调度

1.1.3 CPU如何知道接收数据?

  • 问题:数据从网卡到内核Socket缓冲区中采用DMA方式,那么CPU是如何感知的?
  • 解析:中断, CPU实现线程切换就是基于时钟中断。中断可以分为多种:①外部中断,计算机外设发出的中断,比如:键盘、鼠标、网卡 ②内部中断,计算机内部产生问题,比如:运算出错(除数为零)。 ③软中断:用户程序主动调用中断指令。 当DMA拷贝完数据,就会给CPU发送一个外部中断,这样CPU就能感知到数据已经准备好了,执行相应的处理逻辑。

1.2 CPU调度

1.2.1 阻塞为什么不消耗CPU?

1.2.1.1 工作队列

  • 操作系统为了实现进程调度,维护了一个【工作队列
  • 工作队列中存放的是【运行态】的进程
  • 操作系统会采用时间片的方式执行运行态的进程

1.2.1.2 等待队列

  • 客户端连接服务器后,服务器会为其创建一个Socket对象,也就是对应的文件描述符【Socket文件描述符】。
  • Socket文件描述符中包含【发送缓冲区】、【接收缓冲区】、【等待队列
  • 如果客户端还未发送数据,读取Socket数据时就会阻塞。此时会将当前线程从工作队列移出到SocketFD的等待队列中。这样,阻塞的线程就不会消费CPU资源。
  • 当Socket接收到数据后,DMA会发送个中断请求给CPU,CPU就会执行中断程序,将Socket等待队列中的线程移出到工作队列中,此时线程再继续执行代码,就会获取到Socket中的数据了。

2.BIO

2.1 简介下什么是BIO?

大话IO

  • BIO中的B指的是”Blocking“的意思,即”阻塞的IO"
  • 我们开发服务端时,我们先使用ServerSocket绑定要监听端口,然后调用accept方法获取一个连接,该方法底层调用了内核的recvFrom函数。如果客户端此时还未连接,该方法会阻塞。
  • 当我们通过accept方法获取到Socket后,就可以获取这个Socket的输入输出流,对应就是read和write方法。但是如果客户端没有输入数据,该方法会阻塞,此时其他客户端连接服务器也会阻塞,服务器无法及时响应。
  • 为了解决BIO阻塞的问题,我们一般采用多线程的模式,为每个请求建立一个线程来处理。即BIO是【线程驱动模型
  • BIO最大的问题是:线程开销大,很容易导致系统故障。

2.2 什么是C10K问题?

  • 我们每接到一个客户端请求,服务器就需要分配一个进程来处理这个请求。假如有10K个请求,我们就需要创建1万个进程,显然,我们单机系统是承受不住的。
  • 当我们的线程多了,线程上下文切换开销会增大,导致系统崩溃。
  • 解决思路:一个线程负责多个连接,即IO多路复用。

2.3 BIO代码示范

  • 服务端代码
    static int port = 8080;
    public static void main(String[] args) {
        //1.建立服务器对象
        ServerSocket serverSocket = null;
        //2.建立客户端对象
        Socket socket = null;
        InputStream inputStream = null;
        OutputStream outputStream = null;
        try {
            //3.监听指定端口
            serverSocket = new ServerSocket(port);
            System.out.println("start  server socket....");
            while (true){
                //4.获取连接,该方法会阻塞
                socket = serverSocket.accept();
                System.out.println("获取到socket连接...");
                //5.读取数据
                inputStream = socket.getInputStream();
                byte[] buffer = new byte[1024];
                int length = 0;
                //read方法会阻塞
                while ((length = inputStream.read(buffer)) > 0) {
                    String msg = new String(buffer, 0, length);
                    System.out.println("接收到客户端数据:" + msg);
                    //6.发送回执
                    outputStream = socket.getOutputStream();
                    outputStream.write("server get data".getBytes());
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                serverSocket.close();
                socket.close();
                inputStream.close();
                outputStream.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
  • 客户端测试
telnet localhost 8080

2.4 BIO存在的问题

  • C10K问题:由于read方法会阻塞当前线程,因此我们一般会为每个客户端创建一个线程。如果我们的客户端请求很多,我们服务器就会因为创建多个线程而OOM,或者直接系统崩溃。虽然我们可以使用线程池来管理线程,但是如果客户端访问量很大,那么用户体验会非常差,线程池满了后,后续的请求就无法处理。这就是C10K问题。
  • 如果要解决C10K问题,我们就需要引入:NIO
  1. NIO

3.1 简介下什么是NIO?

  • Java中的NIO指的是NewIO,给我们提供了一套非阻塞的接口,底层通过JVM虚拟机调用操作系统kernel去实现。.
  • NIO三大核心组件:Buffer(缓冲区)、Selector(选择器)、Channel(通道)
  • 操作系统的NIO指的是Not Blocking IO,也就是非阻塞IO.NIO不会为每个请求创建一个线程,而是一个线程取监听多个Socket请求。
  • 操作流程: NIO包中提供了一个selector,我们需要先打开服务器端的管道,然后绑定端口,通过open方法找到Selector,再把需要检查的Socket注册到这个selector中。主线程会阻塞在selctor的select方法里,然后当select发现某个socket就绪了,就会唤醒主线程。然后主线程就可以通过selector获取到就绪状态的Socket,进行相应的处理。
  • NIO是基于事件驱动模型

3.2 NIO核心

3.2.1 通道

  • 通道-channel,表示打开到IO设备的连接,比如:Socket连接

3.2.2 缓冲区

  • 缓冲区-buffer,数据容器,用于存储数据。

3.2.2.1 Buffer简介

大话IO

  • buffer主要负责存储数据,底层实现是数组。
  • 根据数据类型不同,可以分为不同类型的缓冲区(boolean除外),ByteBuffer、CharBuffer、ShortBuffer、IntBuffer、LongBuffer、FloatBuffer、DoubleBuffer
  • buffer四大属性: ①capacity:容量,表示缓冲区最大容量,一旦声明就不可改变。 ②limit:界限,表示可操作数据大小 ③position:位置,表示正在操作数据的位置 ④mark:存档,记录当前操作的位置,通过reset恢复到mark的位置 ⑤0<=mark <= postion <=limt <=capacity
  • buffer核心方法:①allocate:分配缓冲区 ②put:存储数据到缓冲区 ③get:获取缓冲区中的数据 ④flip:切换到写模式
/**
 * @Author pei.sun
 * @Date 2020/7/11 23:24
 * @Description 1.buffer-负责存储数据,可以存储不同类型的数据。底层是数组。
 * 根据数据类型不同,提供了相应的类型的缓冲区(除了boolean)
 * ByteBuffer、CharBuffer、ShortBuffer、IntBuffer
 * LongBuffer、FloatBuffer、DoubleBuffer
 * 2.缓冲区核心方法
 * allocate:分配缓冲区
 * put:存储数据到缓冲区
 * get:获取缓冲区中的数据
 * 3.缓冲区四大核心属性
 * capacity:容量,表示缓冲区中最大存储数据容量,一旦声明就不能改变。
 * limit:界限,表示缓冲区中可以操作数据大小。limit后的数据不能操作.
 * position:位置,表示正在操作数据的位置
 * mark:标记,记录当前position位置,通过reset()恢复到mark的位置
 * 0<=mark <=position<= limit <= capacity
 */
public class TestBuffer {
    public static void main(String[] args) {
        System.out.println("=======allocate()=======");
        ByteBuffer buf = ByteBuffer.allocate(1024);
        System.out.println("position:" + buf.position());
        System.out.println("limit:" + buf.limit());
        System.out.println("capacity:" + buf.capacity());
        //写数据
        System.out.println("=======put()=======");
        String msg = "newio";
        buf.put(msg.getBytes());
        System.out.println("position:" + buf.position());
        System.out.println("limit:" + buf.limit());
        System.out.println("capacity:" + buf.capacity());
        //flip 切换读写模式
        System.out.println("=======flip()=======");
        buf.flip();
        System.out.println("position:" + buf.position());
        System.out.println("limit:" + buf.limit());
        System.out.println("capacity:" + buf.capacity());
        System.out.println("=======get()=======");
        byte[] dst = new byte[buf.limit()];
        buf.get(dst);
        System.out.println("data:" + new String(dst, 0, dst.length));
        System.out.println("position:" + buf.position());
        System.out.println("limit:" + buf.limit());
        System.out.println("capacity:" + buf.capacity());
        //rewind: 重复读
        System.out.println("=======rewind()=======");
        buf.rewind();
        System.out.println("position:" + buf.position());
        System.out.println("limit:" + buf.limit());
        System.out.println("capacity:" + buf.capacity());
        //clear:清空缓冲区,实际只改变了指针,实际数据还存在,只是处于“被遗忘状态”
        System.out.println("=======clear()=======");
        buf.clear();
        System.out.println("position:" + buf.position());
        System.out.println("limit:" + buf.limit());
        System.out.println("capacity:" + buf.capacity());
        //mark:存档
        System.out.println("=======mark()=======");
        buf.put(msg.getBytes());
        buf.flip();
        byte[] result = new byte[buf.limit()];
        buf.get(result, 0, 2);
        System.out.println("result:" + new String(result, 0, 2));
        System.out.println("position:" + buf.position());
        buf.mark();
        buf.get(result, 2, 2);
        System.out.println("result:" + new String(result, 2, 2));
        System.out.println("position:" + buf.position());
        System.out.println("=======reset()=======");
        buf.reset();
        System.out.println("position:" + buf.position());
        //缓冲区中是否有剩余数据
        System.out.println("=======remaining()=======");
        if (buf.hasRemaining()) {
            System.out.println("remaining:"+buf.remaining());
        }
    }
}

3.2.2.2 缓冲区分类

大话IO

  • 缓冲区可以分为【直接缓冲区】和【非直接缓冲区
  • 使用【allocate】方法创建的是非直接缓冲区,使用【allocateDirect】方法创建的是直接缓冲区
  • 非直接缓冲区分配在【JVM堆】内存中,直接缓冲区分配在【物理内存】中
  • 直接缓冲区底层实现了【零拷贝
  • 直接缓冲区缺陷: ①开销大 ②不归JVM管

3.2 如何提高NIO性能?

3.2.1 reactor

3.3 NIO与IO的区别

  • BIO是面向流的,NIO是面向缓冲区的。

4. IO多路复用

1.4.1 select函数

1.4.1.1 简介下select函数的工作原理?

  • 我们每次调用kenel的select函数,都需要切换用户态和内核态,还要把需要检查的Socket集合传过去,其实就是Socket的文件描述符。
  • Linux系统中,一切皆文件,操作系统会为每个socket都生成一个文件描述符。
  • 操作系统会根据传过去的Socket集合,去检查内存中Socket套接字的状态,这个复杂度是O(N)级别的,检查一遍后,如果有就绪状态的Socket,会为socket对应文件描述符打上一个标记,表示这个socket就绪了,然后返回就绪Socket数量。否则会阻塞调用线程,直到有某个Socket有数据后,才会唤醒调用线程
  • 调用者

1.4.1.2 select函数监听socket时,socket有没有数量限制?

  • select函数默认最大可以监听1024个socket,实际肯定是比这个数小的。
  • 由于select函数有个参数,是传进来的socket集合。这个集合长度是1024,如果需要这个长度,比较麻烦,需要重新编译操作系统内核。
  • 这个长度设置为1024,应该是处于性能考虑

1.4.1.3 第一次查找未发现就绪Socket,后续Socket就绪后,select如何感知,是轮询吗?

  • 知识铺垫: ①操作系统调度:一个CPU在同一时刻只能运行一个进程,CPU会在不同进程间来回切换。没有挂起的进程都在工作队列里,是有机会获取到CPU执行权。挂起的进程,会从工作队列内移除出去,就是阻塞了,没有机会去获取CPU执行权。 ②操作系统中断: 首先介绍下时钟中断,这个底层是借助晶振实现的,CPU每收到一个时钟中断就会切换下进程。然后是IO中断,比如:我们使用键盘打字,如果CPU正执行其他程序一直不释放,那么我们就无法打字,事实肯定不是这样的。我们摁下键时,会产生一个IO中断,触发CPU中断,然后CPU会保存现在执行线程的信息,然后处理紧急需求。这样中断程序就拿到了CPU执行权。
  • select第一次轮询,如果没有发现就绪状态的Socket,就把这个进程保存到socket的等待队列中,然后会把这个当前进程从工作队列移除了,然后就阻塞了。select函数也不会执行了。
  • 客户端发送数据,通过网卡到达内存中,整个过程CPU不参与,自然也无法感知。数据完成传输后,会触发网络中断,CPU就会处理这个中断程序,然后根据数据包中的端口信息,分析出来是哪个Socket的数据,然后把数据复制到Socket的读缓冲区里。数据导入完成后,就会检查Socket的等待队列,看是不是有等待者,如果有,就把等待着移动到工作队列中,然后select函数就会再执行了,然后再次检查,就发现有就绪的socket了,然后就会返回就绪数量给Java客户端。
  • 由于select函数返回的是就绪的数量,客户端没法获取就绪进程信息,因此还要再次遍历,检查socket是否就绪,如果就绪就处理。

1.4.2 poll

1.4.2.1 poll和select的区别是啥?

  • 参数不一致,select使用的是bitmap表示需要检查的socket集合,poll使用数组表示需要检查的Socket集合,这样就解决了select最多只能监听1024个socket缺陷。

1.4.2.2 select 和 poll的缺陷是啥?

  • select和poll的返回值是个整型值,表示有几个socket就绪了,无法表达具体是哪个socket就绪了。程序被唤醒后,要新的一轮系统调用去检查哪个socket是就绪状态。系统调用设计用户态和内核态切换。

1.4.3 epoll

1.4.3.1 为什么会有poll?

1.4.3.1 epoll的工作原理是啥?

  • epoll中有自己的数据结构是eventpoll对象,可以通过系统函数epoll_create去创建,创建返程回,返回这个对象的文件号。
  • Socket对象有三块区域,读缓冲区、写缓冲区和等待队列。
  • EventPoll对象有两个核心区域,一块用于存放需要监听的socket文件描述符列表,另一块存放就绪状态的Socket信息。EventPoll对象有两个核心函数,epoll_ctl和epoll_wait。epoll_ctl用于维护需要关注的Socket文件描述符列表,

本文地址:https://blog.csdn.net/sunpeiv/article/details/107371575