IO进化史

发布于:2023-01-22 ⋅ 阅读:(15) ⋅ 点赞:(0) ⋅ 评论:(0)

I/O

IO概述
   IO (Input/Output,输入/输出)即数据的读取(接收)或写入(发送)操作,通常用户进程中的一个完整IO分为两阶段:用户进程空间<-->内核空间、内核空间<-->设备空间(磁盘、网络等)。IO有内存IO、网络IO和磁盘IO三种,通常我们说的IO指的是后两者。
    LINUX中进程无法直接操作I/O设备,其必须通过系统调用请求kernel来协助完成I/O动作;内核会为每个I/O设备维护一个缓冲区。
    对于一个输入操作来说,进程IO系统调用后,内核会先看缓冲区中有没有相应的缓存数据,没有的话再到设备中读取,因为设备IO一般速度较慢,需要等待;内核缓冲区有数据则直接复制到进程空间。
    所以,对于一个网络输入操作通常包括两个不同阶段:
	(1)等待网络数据到达网卡→读取到内核缓冲区,数据准备好;
	(2)从内核缓冲区复制数据到进程空间。
  • 程序进行IO基本都会用到底层(操作系统)的read&write,只是调用方法的名称可能不同
  • read&write操作不直接与物理设备交互,而是与缓冲区交互—read是把数据从(操作系统)内核缓冲区复制到进程缓冲区,write是进程到内核
  • 传统IO模型都是同步阻塞IO
  • java默认创建的socket都是阻塞的
  • java的NIO是New IO ,属于IO多路复用
  • 操作系统底层用一个文件描述符(fd: file descriptor)来表示一个网络连接
  • JVM: 一个线程的成本默认1MB,线程多了调度成本CPU浪费内存成本高
  • linux查看指令: yum install man man-pages
  1. BIO:同步阻塞IO(Blocking)
    • 由用户空间的线程主动发起IO请求(同步),需要内核IO操作彻底完成后才返回到用户空间执行用户的操作(阻塞)。

    • 一次调用发起后,用户程序空间阻塞,等待内核缓冲数据准备好并复制到用户缓冲区,复制完返回给用户程序空间,才解除阻塞

    • 程序调用内核的read命令读取文件描述符,单线程情况下会阻塞,所以就需要使用多线程来读取

    • 每次对fd的操作都需要重新启动一个线程,线程以及线程切换会消耗内存

    • 进程阻塞挂起不消耗CPU资源,及时响应每个操作

    • 实现难度低,开发应用较容易,适合并发量小的网络应用开发

      图片

  2. NIO:同步非阻塞IO(Non-blocking)
    • linux上查看指令: man socket

    • 内核向前发展,文件描述符是非阻塞的

    • 进程发起IO系统调用后,如果内核缓冲区没有数据,需要到IO设备中读取,进程返回一个错误而不会被阻塞;进程发起IO系统调用后,如果内核缓冲区有数据,内核就会把数据返回进程。

    • 进行轮询调用,消耗CPU的资源

    • 实现难度低,开发应用相对阻塞IO模式较难

    • 适用并发量较少,且不需要及时响应的网络应用开发

    • 复杂度是O(n),有1000fd,代表用户进程空间轮询调用1000次kernel

      图片

  3. 多路复用NIO(IO Multiplexing)

    IO多路复用有三种实现方式:select、poll、epoll,其中poll、epoll是linux的函数,而Linux 2.5.44版本后poll被epoll取代。select和poll的时间复杂度都是O(N),select是数组有大小限制,poll是链表无大小限制;epoll的时间复杂度是O(1)

    特点:

    • 专一进程解决多个进程IO的阻塞问题,性能好
    • 实现,开发应用难度较大
    • 适用高并发服务应用开发:一个进程(线程)响应多个请求

    图片

    1. selcet

      • linux上查看指令: man selector

      • 多个的进程的IO可以注册到一个复用器(select)上,然后用一个进程调用该select, select会监听所有注册进来的IO

      • 如果select没有监听的IO在内核缓冲区都没有可读数据,select调用进程会被阻塞;而当任意IO在内核缓冲区中有可读数据时,select调用就会返回

      • 而后select调用进程可以自己或通知另外的进程(注册进程)来再次发起读取IO,读取内核中准备好的数据。

      • 可以看到,多个进程注册IO后,只有另一个select调用进程被阻塞。

      • 减少了程序到内核调用,由内核进行遍历

      • fd相关数据拷来拷去,用户态和内核态之间互相传输数据,相互拷贝

      • 注册IO、阻塞扫描,监听的IO最大连接数不能多于FD_SIZE

    2. poll

      • 原理和Select相似,没有数量限制,但IO数量大扫描线性性能下降
    3. epoll

      • linux上查看指令: man epoll

      • 解决内核正向遍历的复杂度

      • 共享空间 mmap

        1. 用户态和内核态 拥有一个共享空间 内核mmap
        2. 通过epoll_create add 注册到共享空间
        3. 一个值在共享空间中,用户态和内核态都有对应的地址指向
        4. 共享空间是进程空间的一部分,也是内核空间的一部分,有一个红黑树以及一个链表
      • 事件驱动不阻塞,mmap实现内核与用户空间的消息传递,数量很大,Linux2.6后内核支持

    4. 0 拷贝(sendfile)

      • 文件数据先到内核中,进程读取read,并且在write到内核中,有了sendfile之后,就是指内核直接调用sendfile读取到缓冲区,然后发送出去,不再拷贝了,就叫0拷贝
      • kafka就是由mmap+o拷贝
  4. 信号驱动IO模型

    图片

    • 当进程发起一个IO操作,会向内核注册一个信号处理函数,然后进程返回不阻塞;当内核数据就绪时会发送一个信号给进程,进程便在信号处理函数中调用IO读取数据。
    • 回调机制,实现,开发应用难度大
  5. 异步IO(Asynchronous IO)

    图片

    • 当进程发起一个IO操作,进程返回(不阻塞),但也不能返回果结;内核把整个IO处理完后,会通知进程结果。如果IO操作成功则进程直接获取到数据
    • 典型应用:JAVA7 AIO、高性能服务器应用
    • 不阻塞,数据一步到位;Proactor模式
    • 需要操作系统的底层支持,Linux2.5版本内核首现,2.6 版本产品的内核标准特性
    • 实现,开发应用难度大
    • 非常适合高性能高并发应用
  6. 文件描述符

    文件描述符(FileDescriptor、FD):文件句柄,也叫文件描述符。文件描述符是内核为了高效管理已被打开的文件所创建的索引,它 是一个非负整数(通常是小整数),用于指代被打开的文件。所有的IO系统调用,包括socket的读写调用,都是通过文件描述符完成的。
    linux默认文件句柄数为1024。
    在Linux系统中, 文件可分为:普通文件、目录文件、链接文件 和设备文件。
    linux获取单个进程能打开的最大文件句柄数量:ulimit -n
    临时修改:ulimit -n 10000
    永久修改:编辑/etc/rc.local,添加一句话:ulimit -SHn 10000,该方式不可大于硬极限值
    终极修改:编辑/etc/security/limits.conf,添加:
    soft nofile 100000
    hard nofile 100000

7. 总结比较

图片

1. 阻塞IO调用:在用户进程(线程)中调用执行的时候,进程会等待该IO操作,而使得其他操作无法执行

2. 非阻塞IO调用:在用户进程中调用执行的时候,无论成功与否,该IO操作会立即返回,之后进程可以进行其他操作(当然如果是读取到数据,一般就接着进行数据处理)

3. 阻塞IO模型:阻塞IO模型是一个阻塞IO调用,而非阻塞IO模型是多个非阻塞IO调用+一个阻塞IO调用,因为多个IO检查会立即返回错误,不会阻塞进程

4. 非阻塞IO模型:非阻塞IO模型对于阻塞IO模型来说区别就是,内核数据没准备好需要进程阻塞的时候,就返回一个错误,以使得进程不被阻塞。

5. 同步IO:

  • 导致请求进程阻塞,直到I/O操作完成
  • 双方的动作是经过双方协调的,步调一致的
  • 用户进程发出IO调用,去获取IO设备数据,双方的数据要经过内核缓冲区同步,完全准备好后,再复制返回到用户进程。而复制返回到用户进程会导致请求进程阻塞,直到I/O操作完成。

6. 异步IO:

  • 不导致请求进程阻塞
  • 双方并不需要协调,都可以随意进行各自的操作。
  • 用户进程发出IO调用,去获取IO设备数据,并不需要同步,内核直接复制到进程,整个过程不导致请求进程阻塞。

这里我们的双方是指,用户进程和IO设备

所以, 阻塞IO模型、非阻塞IO模型、IO复用模型、信号驱动的IO模型者为同步IO模型,只有异步IO模型是异步IO。