IO操作根据设备类型一般分为内存IO,网络IO,和磁盘IO。其中内存IO的速度大大快于后两者,计算机的性能瓶颈一般不在于内存IO. 尽管网络IO可通过购买独享带宽和高速网卡来提升速度,可以使用RAID磁盘阵列来提升磁盘IO的速度,但是由于IO操作都是由系统内核调用来完成,而系统调用是通过cpu来调度的,而cpu的速度远远快于IO操作,导致会浪费cpu的宝贵时间来等待慢速的IO操作。为了让cpu和慢速的IO设备更好的协调工作,减少CPU在IO调用上的消耗,逐渐发展出各种IO模型。

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模型

本文最先发布至微信公众号,版权所有,禁止转载!

操作系统的IO模型的更多相关文章

  1. IO模型-java版

    描述IO,我们需要从两个层面: 编程语言 实现原理 底层基础 从编程语言层面 BIO | NIO | AIO 以Java的角度,理解,linux c里也有AIO的概念(库),本文只从Java角度入手. ...

  2. 从 Linux 操作系统谈谈 IO 模型(终)

    Linux 为什么要区分内核空间与用户空间? Linux 操作系统的 IO 模型有哪几种?有啥区别? 常说的阻塞现象,到底是咋回事? 网络编程研发时,那块到底耗时最多,代码是否还有优化空间? 前几期的 ...

  3. 从养孩子谈谈 IO 模型(一)

    同步/异步.阻塞/非阻塞 说的是一回事儿吗? 同步/异步.阻塞/非阻塞 你能通俗易懂的讲清楚吗? Java 中的 BIO.NIO.AIO 你了解吗? Socket 编程你还会吗? Linux 操作系统 ...

  4. IO模型

    前言 说到IO模型,都会牵扯到同步.异步.阻塞.非阻塞这几个词.从词的表面上看,很多人都觉得很容易理解.但是细细一想,却总会发现有点摸不着头脑.自己也曾被这几个词弄的迷迷糊糊的,每次看相关资料弄明白了 ...

  5. Linux 网络编程(IO模型)

    针对linux 操作系统的5类IO模型,阻塞式.非阻塞式.多路复用.信号驱动和异步IO进行整理,参考<linux网络编程>及相关网络资料. 阻塞模式 在socket编程(如下图)中调用如下 ...

  6. 网络IO模型:同步IO和异步IO,阻塞IO和非阻塞IO

    同步(synchronous) IO和异步(asynchronous) IO,阻塞(blocking) IO和非阻塞(non-blocking)IO分别是什么,到底有什么区别?这个问题其实不同的人给出 ...

  7. Linux IO模型和网络编程模型

    术语概念描述: IO有内存IO.网络IO和磁盘IO三种,通常我们说的IO指的是后两者. 阻塞和非阻塞,是函数/方法的实现方式,即在数据就绪之前是立刻返回还是等待. 以文件IO为例,一个IO读过程是文件 ...

  8. JAVA基础知识之网络编程——-网络通信模型(IO模型)

    <Unix网络编程:卷1>中介绍了5中I/O模型,JAVA作为运行在宿主机上的程序,底层也遵循这5中I/O模型规则.这5中I/O模型分别是: 阻塞式IO 非阻塞式IO I/O复用 信号驱动 ...

  9. 聊聊 Linux 中的五种 IO 模型

    本文转载自: http://mp.weixin.qq.com/s?__biz=MzAxODI5ODMwOA==&mid=2666538919&idx=1&sn=6013c451 ...

随机推荐

  1. python3中文件/IO编程

    python3的文件操作可谓是我见过所有语言中最舒服的,那我们来一起看一下py3中的文件操作. 1:文件的打开方式有以下几种: 注:以上图表参考菜鸟教程 2:定位读写文件  f = open(&quo ...

  2. jvm源码解读--13 gc_root中的栈中oop的mark 和copy 过程分析

    粘贴源码 package com.test; import java.util.Random; public class Test { static int number=12; private in ...

  3. 图解java多线程设计模式之一一synchronized实例方法体

    synchronized实例方法体和synchronized代码块 synchronied void method(){ ....... } 这个等同于下面将方法体用synchronized(this ...

  4. PDMan使用

    场景: 这几天项目要完结交付,需要补很多文档.此时发现甲方要求提供数据库设计文档,尽管我觉得他们不会看,但是人家要求,还是补一下吧!时间紧迫,要赶出整个项目的数据库设计文档比较麻烦,每个两三天不行.于 ...

  5. 栅栏密码(The Rail-Fence Cipher)详解

    最近训练CTF的时候,发现密码学这块的知识不太系统,所以自己接下来会陆陆续续整理出来 今天学习了栅栏密码,BugkuCTF里面的一道叫做"聪明的小羊"的题就与栅栏密码相关 特点 栅 ...

  6. Java练习——加减乘除计算器实现

    Java练习--计算器(加减乘除)  package method; import java.util.Scanner; /*  写一个计算器 实现加减乘除四个功能   并且能够用循环接收新的数据,通 ...

  7. 两年Android开发三面上岸腾讯,这些核心知识点建议收藏

    概述 感觉毕业后时间过得真快啊,从 19 年 7 月本科毕业入职后,到现在快两年了,前段时间金三银四期间想着找一个新的工作,前前后后花了一个多月的时间复习以及面试,面试好几家大厂,最后选择了腾讯.也祝 ...

  8. Docker部署Zookeeper部署集群实践(2)

    注:本文使用docker for windows模拟构建zookeeper集群,在linux系统下,可使用同样的docker命令构建 参考url:https://github.com/31z4/zoo ...

  9. etcd学习(6)-etcd实现raft源码解读

    etcd中raft实现源码解读 前言 raft实现 看下etcd中的raftexample newRaftNode startRaft serveChannels 领导者选举 启动并初始化node节点 ...

  10. 【错误】element cannot be mapped to a null key

    element cannot be mapped to a null key的解决方法 报错: ERROR [o.a.c.c.C.[.[.[/sa].[dispatcherServlet]] - Se ...