JAVA中的I/O

可以把整个IO分为两个阶段

  • 等待数据到达
  • 从内核拷贝数据到用户空间

I/O的三种方式

  • 缓存IO
    最常见的一种IO方式,磁盘->内核缓冲区->用户空间
  • 直接IO
    磁盘->用户空间,该方式IO见于数据库系统
  • 内存映射
    用户空间的一部分区域和内核缓冲区共享,比如epoll模式中

UNIX的5种I/O模型

用钓鱼来类比操作系统的IO
钓鱼的时候,刚开始鱼是在鱼塘里面的,我们的钓鱼动作的最终结束标志是鱼从鱼塘中被我们钓上来,放入鱼篓中。这里面的鱼塘就可以映射成磁盘,中间过渡的鱼钩可以映射成内核空间,最终放鱼的鱼篓可以映射成用户空间。一次完整的钓鱼(IO)操作,是鱼(文件)从鱼塘(硬盘)中转移(拷贝)到鱼钩(内核空间)再到鱼篓(用户空间)的过程。

  • 阻塞I/O模型
    等待数据到达+将数据从内核空间复制到用户空间,在等待数据阶段(等待鱼儿上钩阶段)拿着鱼竿啥都不做一直盯着鱼竿看,应用进程通过系统调用 recvfrom 接收数据,但由于内核还未准备好数据报,应用进程就会阻塞住,直到内核准备好数据报,recvfrom 完成数据报复制工作,应用进程才能结束阻塞状态。
  • 非阻塞I/O模型
    我们钓鱼的时候,在等待鱼儿咬钩的过程中,我们可以做点别的事情,比如玩一把王者荣耀、看一集《延禧攻略》等等。但是,我们要时不时的去看一下鱼竿,一旦发现有鱼儿上钩了,就把鱼钓上来。
    应用进程通过 recvfrom 调用不停的去和内核交互,直到内核准备好数据。如果没有准备好,内核会返回error,应用进程在得到error后,过一段时间再发送recvfrom请求。在两次发送请求的时间段,进程可以先做别的事情。这种方式钓鱼,和阻塞IO比,所使用的工具没有什么变化,但是钓鱼的时候可以做些其他事情,增加时间的利用率。
  • 信号驱动模型
    我们钓鱼的时候,为了避免自己一遍一遍的去查看鱼竿,我们可以给鱼竿安装一个报警器。当有鱼儿咬钩的时候立刻报警。然后我们再收到报警后,去把鱼钓起来。
    映射到Linux操作系统中,这就是信号驱动IO。应用进程在读取文件时通知内核,如果某个 socket 的某个事件发生时,请向我发一个信号。在收到信号后,信号对应的处理函数会进行后续处理。
    应用进程预先向内核注册一个信号处理函数,然后用户进程返回,并且不阻塞,当内核数据准备就绪时会发送一个信号给进程,用户进程便在信号处理函数中开始把数据拷贝的用户空间中。这种方式钓鱼,和前几种相比,所使用的工具有了一些变化,需要有一些定制(实现复杂)。但是钓鱼的人就可以在鱼儿咬钩之前彻底做别的事儿去了。等着报警器响就行了。
  • I/O复用
    我们钓鱼的时候,为了保证可以最短的时间钓到最多的鱼,我们同一时间摆放多个鱼竿,同时钓鱼。然后哪个鱼竿有鱼儿咬钩了,我们就把哪个鱼竿上面的鱼钓起来。
    映射到Linux操作系统中,这就是IO复用模型。多个进程的IO可以注册到同一个管道上,这个管道会统一和内核进行交互。当管道中的某一个请求需要的数据准备好之后,进程再把对应的数据拷贝到用户空间中。
    IO多路转接是多了一个select函数,多个进程的IO可以注册到同一个select上,当用户进程调用该select,select会监听所有注册好的IO,如果所有被监听的IO需要的数据都没有准备好时,select调用进程会阻塞。当任意一个IO所需的数据准备好之后,select调用就会返回,然后进程在通过recvfrom来进行数据拷贝。这里的IO复用模型,并没有向内核注册信号处理函数,所以,他并不是非阻塞的。进程在发出select后,要等到select监听的所有IO操作中至少有一个需要的数据准备好,才会有返回,并且也需要再次发送请求去进行文件的拷贝。这种方式的钓鱼,通过增加鱼竿的方式,可以有效的提升效率。
    Ps:其实IO复用和阻塞IO很像,只不过是高效率版本的阻塞IO几种IO多路复用模型
select poll epoll
操作方式 遍历 遍历 回调
底层实现 数组 链表 哈希表
IO效率 线性遍历O(n) 线性遍历O(n) 事件通知方式,每当fd准备就绪,系统注册的回调函数会被调用O(1)
最大连接数 1024(x86) 2048(x64) 无上限 无上限
fd拷贝 每次调用select都需要把fd集合从用户态拷贝到内核态 每次调用poll都需要把fd集合从用户态拷贝到内核态 调用epoll_ct时拷贝进内核并保存,之后每次epoll_wait不拷贝
  • 信号驱动I/O
    我们钓鱼的时候,为了避免自己一遍一遍的去查看鱼竿,我们可以给鱼竿安装一个报警器。当有鱼儿咬钩的时候立刻报警。然后我们在收到报警后,去把鱼钓起来。
    映射到Linux操作系统中,这就是信号驱动IO。应用进程在读取文件时通知内核,如果某个 socket 的某个事件发生时,请向我发一个信号。在收到信号后,信号对应的处理函数会进行后续处理。

    通过系统调用sigaction执行一个信号处理函数(此系统调用立即返回,进程继续工作,它是非阻塞的);当数据准备就绪后,为该进程生成一个SIGIO信号,通知应用程序调用recvfrom读取数据(此时仍然需要进程自身去调用recvfrom)

  • 异步I/O
    我们钓鱼的时候,采用一种高科技钓鱼竿,即全自动钓鱼竿。可以自动感应鱼上钩,自动收竿,更厉害的可以自动把鱼放进鱼篓里。然后,通知我们鱼已经钓到了,他就继续去钓下一条鱼去了。
    映射到Linux操作系统中,这就是异步IO模型。应用进程把IO请求传给内核后,完全由内核去操作文件拷贝。内核完成相关操作后,会发信号告诉应用进程本次IO已经完成。
    告知内核启动某个操作,并让内核在整个操作完成后通知我们(包括将数据从内核复制到用户空间)
    Ps异步I/O模型由内核通知我们I/O操作何时已经完成。而信号驱动I/O由内核告诉我们合适可以在进程内执行一个I/O操作

同步VS异步

数据从内核到用户空间的复制是否需要当前进程参与,换句话讲read的逻辑代码是否在当前进程执行~

阻塞VS非阻塞

等待数据阶段是否是阻塞的(只盯着鱼竿看),这么看的话多路复用也是阻塞的

JAVA中常用的三种IO

可以把JAVA中的IO看作是堆操作系统的各种IO模型的封装。
比如 linux JAVA中的NIO和AIO都是基于epoll实现的,在Windows上是基于IOCP实现的,这对程序员来讲是透明的。

BIO(阻塞IO)

同步阻塞:数据从内核到用户空间的复制在进程内且调用recvfrom后需要等待
传统的服务器使用的就是BIO,每当客户端过来一个请求便开启一个新的线程,这种方式最大的问题就是过度依赖于线程。而线程是相对昂贵的资源

  • 线程本身占用较大的内存,如果成千上万个请求发送到服务端,此时JVM的内存会被占用很多
  • 线程切换的成本很高,甚至比线程本身的业务逻辑花的时间还要多
  • 线程创建和销毁成本也相对较高

客户端个数/IO线程数 = 1:1

NIO(非阻塞IO)

同步非阻塞:数据从内核到用户空间的复制在进程内且调用recvfrom后无需等待
nginx使用的就是NIO用来接收数以千万计的客户端请求,之后再分发到不同的服务端
tomcat容器之前采用bio,现在已经改为nio
客户端个数/IO线程数 = M:1

AIO(异步IO)

异步非阻塞:数据从内核到用户空间的复制不在进程内且当前进程根本无须调用recvfrom也就无须等待
客户端个数/IO线程数 = M:0