乐趣区

关于java:操作系统的IO模型

IO 操作依据设施类型个别分为内存 IO, 网络 IO, 和磁盘 IO。其中内存 IO 的速度大大快于后两者,计算机的性能瓶颈个别不在于内存 IO. 只管网络 IO 可通过购买独享带宽和高速网卡来晋升速度,能够应用 RAID 磁盘阵列来晋升磁盘 IO 的速度,然而因为 IO 操作都是由零碎内核调用来实现,而零碎调用是通过 cpu 来调度的,而 cpu 的速度远远快于 IO 操作,导致会节约 cpu 的宝贵时间来期待慢速的 IO 操作。为了让 cpu 和慢速的 IO 设施更好的协调工作,缩小 CPU 在 IO 调用上的耗费,逐步倒退出各种 IO 模型。

<!–more–>

IO 模型

IO 步骤

I/ O 次要为:网络 IO(实质是 socket 文件读取)、磁盘 IO
每次 IO,对于一次 IO 拜访,数据会先被拷贝到内核的缓冲区中,而后才会从内核的缓冲区拷贝到应用程序的地址空间。须要经验两个阶段:

  • 第一步:将数据从文件先加载至内核内存空间(缓冲区),期待数据筹备实现,工夫较长
  • 第二步:将数据从内核缓冲区复制到用户空间的过程的内存中,工夫较短

阻塞 / 非阻塞和同步 / 异步

IO 模型总是离不开阻塞 / 非阻塞、同步 / 异步这些概念。

  • 阻塞 / 非阻塞:阻塞和非阻塞是对调用方线程状态的形容,如果一次 IO 过程中,调用方线程须要阻塞线程期待数据的达到,那么说这次 IO 是阻塞式 IO。
  • 同步 / 异步:同步和异步是对调用方获取数据形式的形容,如果调用方被动去查问并复制数据,那么称 IO 是同步的。如果是操作系统在数据筹备实现 (复制到用户缓存区) 之后通知调用方有数据筹备好了,那么称 IO 是异步的。

IO 模型分类

发动零碎调用的是运行在零碎上的某个利用的过程、对象是磁盘上的数据、获取数据须要通过 I /O、整个过程就是利用期待获取磁盘数据。针对整个过程中利用过程的状态不同,能够分为:同步阻塞型,同步非阻塞型,同步复用型,信号驱动型,异步。

同步阻塞型 IO

类比:老李去火车站买票,排队三天买到一张退票。消耗:在车站吃喝拉撒睡 3 天,其余事一件没干。

同步阻塞 IO 模型是最简略的 IO 模型,用户线程在内核进行 IO 操作时被阻塞,等到数据读取实现之后在持续解决后续逻辑,其步骤如下所示(以 read()接口为例):

read(file, tmp_buf, len);
  1. 用户程序须要读取数据,调用 read 办法,把读取数据的指令交给 CPU 执行。
  2. CPU 收回指令给 DMA,通知 DMA 须要读取磁盘的哪些数据,而后返回,线程进入阻塞状态
  3. DMA 向磁盘控制器收回 IO 申请,通知磁盘控制器须要读取哪些数据,而后返回;
  4. 磁盘控制器收到 IO 申请之后,把数据读取到磁盘缓存区,当磁盘缓存读取实现之后,中断 DMA;
  5. DMA 收到磁盘的中断信号,将磁盘缓存区的数据读取到 PageCache 缓存区,而后中断 CPU;
  6. CPU 响应 DMA 中断信号,晓得数据读取实现,而后将 PageCache 缓存区中的数据读取到用户缓存中;
  7. 用户程序从内存中读取到数据,能够继续执行后续逻辑。

同步阻塞 IO 的优缺点

长处:程序简略,在阻塞期待数据期间过程 / 线程挂起,根本不会占用 CPU 资源。
毛病:每个连贯须要独立的过程 / 线程独自解决,当并发申请量大时为了维护程序,内存、线程切换开销较大,这种模型在理论生产中很少应用。

同步非阻塞型 IO

类比:老李去火车站买票,隔 12 小时去火车站问有没有退票,三天后买到一张票。消耗:往返车站 6 次,路上 6 小时,其余工夫做了好多事。

非阻塞 IO 就是当调用方发动读取数据申请时,如果内核数据没有筹备好会即刻通知调用方,不须要调用方线程阻塞期待。

以 recvfrom 办法为例,调用方调用 recvfrom 读取数据时,如果该缓冲区没有数据的话,就会间接返回一个 EWOULDBLOCK 谬误,不会让利用始终期待中。在没有数据的时候会即刻返回谬误标识,那也意味着如果利用要读取数据就须要一直的调用 recvfrom 申请,直到读取到它数据要的数据为止。其读取步骤如下所示:

  1. 调用方调用 recvfrom 办法尝试获取数据;
  2. 如果 recvfrom 办法返回 EWOULDBLOCK 谬误,执行步骤 1;如果 revifrom 办法发现缓存区有数据,那么执行步骤 3;
  3. CPU 将 PageCache 缓存区中的数据读取到用户缓存中;
  4. 用户程序从内存中读取到数据,能够继续执行后续逻辑。

种形式在编程中对 socket 设置 O_NONBLOCK 即可。但此形式仅仅针对网络 IO 无效,对磁盘 IO 并没有作用。因为本地文件 IO 默认是阻塞,咱们所说的网络 IO 的阻塞是因为网路 IO 有有限阻塞的可能,而本地文件除非是被锁住,否则是不可能有限阻塞的,因而只有锁这种状况下,O_NONBLOCK 才会有作用。而且,磁盘 IO 时要么数据在内核缓冲区中间接能够返回,要么须要调用物理设施去读取,这时候过程的其余工作都须要期待。因而,后续的 IO 复用和信号驱动 IO 对文件 IO 也是没有意义的。

IO 复用模型

IO 复用,也叫多路 IO 就绪告诉。这是一种过程事后告知内核的能力,让内核发现过程指定的一个或多个 IO 条件就绪了,就告诉过程。使得一个过程能在一连串的事件上期待。IO 复用的实现形式目前次要有 select、poll 和 epoll。

select/poll

类比:老李去火车站买票,委托黄牛,而后每隔 6 小时电话黄牛询问,黄牛三天内买到票,而后老李去火车站交钱领票。消耗:往返车站 2 次,路上 2 小时,黄牛手续费 100 元,打电话 17 次

select 和 poll 的原理基本相同:

  1. 注册待侦听的 fd(这里的 fd 创立时最好应用非阻塞)
  2. 每次调用都去查看这些 fd 的状态,当有一个或者多个 fd 就绪的时候返回
  3. 返回后果中包含已就绪和未就绪的 fd

相比 select,poll 解决了单个过程可能关上的文件描述符数量有限度这个问题:select 受限于 FD_SIZE 的限度,如果批改则须要批改这个宏从新编译内核;而 poll 通过一个 pollfd 数组向内核传递须要关注的事件,避开了文件描述符数量限度。

此外,select 和 poll 独特具备的一个很大的毛病就是蕴含大量 fd 的数组被整体复制于用户态和内核态地址空间之间,开销会随着 fd 数量增多而线性增大。

epoll

老李去火车站买票,委托黄牛,黄牛买到后即告诉老李去领,而后老李去火车站交钱领票。消耗:往返车站 2 次,路上 2 小时,黄牛手续费 100 元,无需打电话

epoll 是 poll 的一种改良:

  1. 基于事件驱动的形式,防止了每次都要把所有 fd 都扫描一遍。
  2. epoll_wait 只返回就绪的 fd。
  3. epoll 应用 nmap 内存映射技术防止了内存复制的开销。
  4. epoll 的 fd 数量下限是操作系统的最大文件句柄数目, 这个数目个别和内存无关,通常远大于 1024。

目前,epoll 是 Linux2.6 下最高效的 IO 复用形式,也是 Nginx、Node 的 IO 实现形式。而在 freeBSD 下,kqueue 是另一种相似于 epoll 的 IO 复用形式。

此外,对于 IO 复用还有一个程度触发和边缘触发的概念:

  • 程度触发:当就绪的 fd 未被用户过程解决后,下一次查问依旧会返回,这是 select 和 poll 的触发形式。
  • 边缘触发:无论就绪的 fd 是否被解决,下一次不再返回。实践上性能更高,然而实现相当简单,并且任何意外的失落事件都会造成申请处理错误。epoll 默认应用程度触发,通过相应选项能够应用边缘触发。

因为同步非阻塞形式须要一直被动轮询,轮询占据了很大一部分过程,轮询会耗费大量的 CPU 工夫,而“后盾”可能有多个工作在同时进行,人们就想到了循环查问多个工作的实现状态,只有有任何一个工作实现,就去解决它。如果轮询不是过程的用户态,而是有人帮忙就好了。那么这就是所谓的“IO 多路复用”。UNIX/Linux 下的 select、poll、epoll 就是干这个的(epoll 比 poll、select 效率高,做的事件是一样的)。

IO 多路复用有两个特地的零碎调用 select、poll、epoll 函数。select 调用是内核级别的,select 轮询绝对非阻塞的轮询的区别在于 — 前者能够期待多个 socket,能实现同时对多个 IO 端口进行监听,当其中任何一个 socket 的数据准好了,就能返回进行可读,而后过程再进行 recvform 零碎调用,将数据由内核拷贝到用户过程,当然这个过程是阻塞的。select 或 poll 调用之后,会阻塞过程,与 blocking IO 阻塞不同在于,此时的 select 不是等到 socket 数据全副达到再解决, 而是有了一部分数据就会调用用户过程来解决。如何晓得有一部分数据达到了呢?监督的事件交给了内核,内核负责数据达到的解决。也能够了解为 ” 非阻塞 ” 吧。

I/ O 复用模型会用到 select、poll、epoll 函数,这几个函数也会使过程阻塞,然而和阻塞 I / O 所不同的的,这两个函数能够同时阻塞多个 I / O 操作。而且能够同时对多个读操作,多个写操作的 I / O 函数进行检测,直到有数据可读或可写时(留神不是全副数据可读或可写),才真正调用 I / O 操作函数。

对于多路复用,也就是轮询多个 socket。多路复用既然能够解决多个 IO,也就带来了新的问题,多个 IO 之间的程序变得不确定了,当然也能够针对不同的编号。具体流程,如下图所示:

信号驱动模型

类比:老李去火车站买票,给售票员留下电话,有票后,售票员电话告诉老李,而后老李去火车站交钱领票。消耗:往返车站 2 次,路上 2 小时,免黄牛费 100 元,无需打电话

信号驱动 IO 模型,利用过程通知内核:当数据报筹备好的时候,给我发送一个信号,对 SIGIO 信号进行捕获,并且调用我的信号处理函数来获取数据报。流程如下:

  1. 开启套接字信号驱动 IO 性能;
  2. 零碎调用 sigaction 执行信号处理函数(非阻塞,立即返回),通知零碎数据就绪式调用哪个函数;
  3. 数据就绪,生成 sigio 信号,通过信号回调告诉利用来读取数据。

此种 io 形式存在的一个很大的问题:Linux 中信号队列是有限度的,如果超过这个数字问题就无奈读取数据。

Linux 信号的解决:如果这个过程正在用户态忙着做别的事(例如在计算两个矩阵的乘积),那就强行打断之,调用当时注册的信号处理函数,这个函数能够决定何时以及如何解决这个异步工作。因为信号处理函数是忽然闯进来的,因而跟中断处理程序一样,有很多事件是不能做的,因而保险起见,个别是把事件“注销”一下放进队列,而后返回该过程原来在做的事。
如果这个过程正在内核态忙着做别的事,例如以同步阻塞形式读写磁盘,那就只好把这个告诉挂起来了,等到内核态的事件忙完了,快要回到用户态的时候,再触发信号告诉。
如果这个过程当初被挂起了,例如无事可做 sleep 了,那就把这个过程唤醒,下次有 CPU 闲暇的时候,就会调度到这个过程,触发信号告诉。

异步 API 说来笨重,做来难,这次要是对 API 的实现者而言的。Linux 的异步 IO(AIO)反对是 2.6.22 才引入的,还有很多零碎调用不反对异步 IO。Linux 的异步 IO 最后是为数据库设计的,因而通过异步 IO 的读写操作不会被缓存或缓冲,这就无奈利用操作系统的缓存与缓冲机制。

很多人把 Linux 的 O_NONBLOCK 认为是异步形式,但事实上这是后面讲的同步非阻塞形式。须要指出的是,尽管 Linux 上的 IO API 略显毛糙,但每种编程框架都有封装好的异步 IO 实现。操作系统少做事,把更多的自在留给用户,正是 UNIX 的设计哲学,也是 Linux 上编程框架百花齐放的一个起因。

从后面 IO 模型的分类中,咱们能够看出 AIO 的动机:

  • 同步阻塞模型须要在 IO 操作开始时阻塞应用程序。这意味着不可能同时重叠进行解决和 IO 操作。
  • 同步非阻塞模型容许解决和 IO 操作重叠进行,然而这须要应用程序依据重现的规定来查看 IO 操作的状态。
  • 这样就剩下异步非阻塞 IO 了,它容许解决和 IO 操作重叠进行,包含 IO 操作实现的告诉。

异步 IO

类比:老李去火车站买票,给售票员留下电话,有票后,售票员电话告诉老李并快递送票上门。消耗:往返车站 1 次,路上 1 小时,免黄牛费 100 元,无需打电话

当应用程序调用 aio_read 时,内核一方面去取数据报内容返回,另一方面将程序控制权还给利用过程,利用过程持续解决其余事件,是一种非阻塞的状态。

当内核中有数据报就绪时,由内核将数据报拷贝到应用程序中,返回 aio_read 中定义好的函数处理程序。

很少有 Linux 零碎反对,Windows 的 IOCP 就是该模型。能够看出,阻塞水平:阻塞 IO> 非阻塞 IO> 多路转接 IO> 信号驱动 IO> 异步 IO,效率是由低到高的。

欢送关注御狐神的微信公众号

参考文档

IO 和零拷贝
异步 IO、epoll、零拷贝
IO 概念和五种 IO 模型

本文由博客一文多发平台 OpenWrite 公布!

退出移动版