IO 模型知多少

1. 引言

同步异步I/O,阻塞非阻塞I/O是程序员老生常谈的话题了,也是自己一直以来懵懵懂懂的一个话题。比如:何为同步异步?何为阻塞与非阻塞?二者的区别在哪里?阻塞在何处?为什么会有多种IO模型,分别用来解决问题?常用的框架采用的是何种I/O模型?各种IO模型的优劣势在哪里,适用于何种应用场景?

简而言之,对于I/O的认知,不能仅仅停留在字面上认识,了解内部玄机,才能深刻理解I/O,才能看清I/O相关问题的本质。

2. I/O 的定义

I/O 的全称是Input/Output。虽常谈及I/O,但想必你也一时不能给出一个完整的定义。搜索了谷歌,发现也尽是些冗长的论述。要想厘清I/O这个概念,我们需要从不同的视角去理解它。

2.1. 计算机视角

冯•诺伊曼计算机的基本思想中有提到计算机硬件组成应为五大部分:控制器,运算器,存储器,输入和输出。其中输入是指将数据输入到计算机的设备,比如键盘鼠标;输出是指从计算机中获取数据的设备,比如显示器;以及既是输入又是输出设备,硬盘,网卡等。

用户通过操作系统才能完成对计算机的操作。计算机启动时,第一个启动的程序是操作系统的内核,它将负责计算机的资源管理和进程的调度。换句话说:操作系统负责从输入设备读取数据并将数据写入到输出设备。

所以I/O之于计算机,有两层意思:

  1. I/O设备

  2. 对I/O设备的数据读写

对于一次I/O操作,必然涉及2个参与方,一个输入端,一个输出端,而又根据参与双方的设备类型,我们又可以分为磁盘I/O,网络I/O(一次网络的请求响应,网卡)等。

2.2. 程序视角

应用程序作为一个文件保存在磁盘中,只有加载到内存到成为一个进程才能运行。应用程序运行在计算机内存中,必然会涉及到数据交换,比如读写磁盘文件,访问数据库,调用远程API等等。但我们编写的程序并不能像操作系统内核一样直接进行I/O操作。

因为为了确保操作系统的安全稳定运行,操作系统启动后,将会开启保护模式:将内存分为内核空间(内核对应进程所在内存空间)和用户空间,进行内存隔离。我们构建的程序将运行在用户空间,用户空间无法操作内核空间,也就意味着用户空间的程序不能直接访问由内核管理的I/O,比如:硬盘、网卡等。

但操作系统向外提供API,其由各种类型的系统调用(System Call)组成,以提供安全的访问控制。所以应用程序要想访问内核管理的I/O,必须通过调用内核提供的系统调用(system call)进行间接访问。

所以I/O之于应用程序来说,强调的通过向内核发起系统调用完成对I/O的间接访问。换句话说应用程序发起的一次IO操作实际包含两个阶段:

  1. IO调用阶段:应用程序进程向内核发起系统调用

  2. IO执行阶段:内核执行IO操作并返回

    1. 准备数据阶段:内核等待I/O设备准备好数据

    2. 拷贝数据阶段:将数据从内核缓冲区拷贝到用户空间缓冲区

怎么理解准备数据阶段呢?对于写请求:等待系统调用的完整请求数据,并写入内核缓冲区;对于读请求:等待系统调用的完整请求数据;(若请求数据不存在于内核缓冲区)则将外围设备的数据读入到内核缓冲区。

而应用程序进程在发起IO调用至内核执行IO返回之前,应用程序进程/线程所处状态,就是我们下面要讨论的第二个话题阻塞IO与非阻塞IO。

3. IO 模型之阻塞I/O(BIO)

应用程序中进程在发起IO调用后至内核执行IO操作返回结果之前,若发起系统调用的线程一直处于等待状态,则此次IO操作为阻塞IO。阻塞IO简称BIO,Blocking IO。其处理流程如下图所示:

从上图可知当用户进程发起IO系统调用后,内核从准备数据到拷贝数据到用户空间的两个阶段期间用户调用线程选择阻塞等待数据返回。

因此BIO带来了一个问题:如果内核数据需要耗时很久才能准备好,那么用户进程将被阻塞,浪费性能。为了提升应用的性能,虽然可以通过多线程来提升性能,但线程的创建依然会借助系统调用,同时多线程会导致频繁的线程上下文的切换,同样会影响性能。所以要想解决BIO带来的问题,我们就得看到问题的本质,那就是阻塞二字。

4. IO 模型之非阻塞I/O(NIO)

那解决方案自然也容易想到,将阻塞变为非阻塞,那就是用户进程在发起系统调用时指定为非阻塞,内核接收到请求后,就会立即返回,然后用户进程通过轮询的方式来拉取处理结果。也就是如下图所示:

应用程序中进程在发起IO调用后至内核执行IO操作返回结果之前,若发起系统调用的线程不会等待而是立即返回,则此次IO操作为非阻塞IO模型。非阻塞IO简称NIO,Non-Blocking IO。

然而,非阻塞IO虽然相对于阻塞IO大幅提升了性能,但依旧不是完美的解决方案,其依然存在性能问题,也就是频繁的轮询导致频繁的系统调用,会耗费大量的CPU资源。比如当并发很高时,假设有1000个并发,那么单位时间循环内将会有1000次系统调用去轮询执行结果,而实际上可能只有2个请求结果执行完毕,这就会有998次无效的系统调用,造成严重的性能浪费。有问题就要解决,那NIO问题的本质就是频繁轮询导致的无效系统调用

5. IO模型之IO多路复用

解决NIO的思路就是降解无效的系统调用,如何降解呢?我们一起来看看以下几种IO多路复用的解决思路。

5.1. IO多路复用之select/poll

Select是内核提供的系统调用,它支持一次查询多个系统调用的可用状态,当任意一个结果状态可用时就会返回,用户进程再发起一次系统调用进行数据读取。换句话说,就是NIO中N次的系统调用,借助Select,只需要发起一次系统调用就够了。其IO流程如下所示:

但是,select有一个限制,就是存在连接数限制,针对于此,又提出了poll。其与select相比,主要是解决了连接限制。

select/epoll 虽然解决了NIO重复无效系统调用用的问题,但同时又引入了新的问题。问题是:

  1. 用户空间和内核空间之间,大量的数据拷贝

  2. 内核循环遍历IO状态,浪费CPU时间

换句话说,select/poll虽然减少了用户进程的发起的系统调用,但内核的工作量只增不减。在高并发的情况下,内核的性能问题依旧。所以select/poll的问题本质是:内核存在无效的循环遍历。

5.2. IO多路复用之epoll

针对select/pool引入的问题,我们把解决问题的思路转回到内核上,如何减少内核重复无效的循环遍历呢?变主动为被动,基于事件驱动来实现。其流程图如下所示:

epoll相较于select/poll,多了两次系统调用,其中epollcreate建立与内核的连接,epollctl注册事件,epoll_wait阻塞用户进程,等待IO事件。

epoll,已经大大优化了IO的执行效率,但在IO执行的第一阶段:数据准备阶段都还是被阻塞的。所以这是一个可以继续优化的点。

6. IO 模型之信号驱动IO(SIGIO)

信号驱动IO与BIO和NIO最大的区别就在于,在IO执行的数据准备阶段,不会阻塞用户进程。如下图所示:当用户进程需要等待数据的时候,会向内核发送一个信号,告诉内核我要什么数据,然后用户进程就继续做别的事情去了,而当内核中的数据准备好之后,内核立马发给用户进程一个信号,说”数据准备好了,快来查收“,用户进程收到信号之后,立马调用recvfrom,去查收数据。

乍一看,信号驱动式I/O模型有种异步操作的感觉,但是在IO执行的第二阶段,也就是将数据从内核空间复制到用户空间这个阶段,用户进程还是被阻塞的。

综上,你会发现,不管是BIO还是NIO还是SIGIO,它们最终都会被阻塞在IO执行的第二阶段。那如果能将IO执行的第二阶段变成非阻塞,那就完美了。

7. IO 模型之异步IO(AIO)

异步IO真正实现了IO全流程的非阻塞。用户进程发出系统调用后立即返回,内核等待数据准备完成,然后将数据拷贝到用户进程缓冲区,然后发送信号告诉用户进程IO操作执行完毕(与SIGIO相比,一个是发送信号告诉用户进程数据准备完毕,一个是IO执行完毕)。其流程如下:

所以,之所以称为异步IO,取决于IO执行的第二阶段是否阻塞。因此前面讲的BIO,NIO和SIGIO均为同步IO。

8. 总结

梳理完这些IO模型后,之前一直处于懵懂状态的阻塞,非阻塞,同步异步IO,终于算是有个概念了。同时也纠正了自己一直以来的误解,所以一路走来,愈发觉得返璞归真的重要性,只有如此,才能在快速更迭的技术演进中,以不变应万变。

本文综合多方资料写就,难免纰漏,但只有写下来,才能得以指正。所以,烦请各位看官不吝赐教。

参考资料:

  1. 程序员应该这样理解IO

  2. IO复用模型同步,异步,阻塞,非阻塞及实例详解

  3. 服务器网络编程之 IO 模型

  4. http://www.c-jump.com/CIS77/CPU/VonNeumann/lecture.html

  5. 同步I/O(阻塞I/O,非阻塞I/O),异步I/O

  6. 马士兵:权威讲解nio,epoll,多路复用

  7. Linux 内核详解以及内核缓冲区技术

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/310549.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

[剑指offer]面试题22:栈的压入、弹出序列

面试题22:栈的压入、弹出序列 题目:输入两个整数序列,第一个序列表示栈的压入顺序,请判断第二个序列是否为该栈的弹出顺序。假设压入栈的所有数字均不相等。例如序列1、2、3、4、5 是某栈的压栈序列,序列4、5、3、2、1…

Ubuntu上安装使用Docker

Docker简介 Docker是一个开源的容器引擎,他有助于更快的交付应用。Docker可以将应用程序和基础设施层做隔离,并且能将基础设施当做程序一样进行管理。使用Docker,可以更快的打包,测试以及部署应用程序,并且可以缩短从…

让 .NET 轻松构建中间件模式代码

让 .NET 轻松构建中间件模式代码Intro在 asp.net core 中中间件的设计令人叹为观止,如此高大上的设计何不集成到自己的代码里呢。于是就有了封装了一个简单通用的中间件模板的想法,以后有需要的时候就可以拿来即用。接口定义这里按执行的委托是同步还是异…

[剑指offer]面试题26:复杂链表的复制

面试题26:复杂链表的复制 题目:请实现函数ComplexListNodeClone(ComplexListNodepHead),复制一个复杂链表。在复杂链表中,每个结点除了有一个m_pNext指针指向下一个结点外,还有一个m_pSibling 指…

SpringCloud + Docker

Dockerfile构建Docker 镜像 注意这里说的Dockerfile是指的一个文本文件,类似txt,只不过名字是Dockerfile,里面编辑Docker的一些指令,指令作用在于描述构建镜像的细节。如下一个简单的案例,用上一节中下载的nginx镜像来…

红帽借“订阅”模式成开源一哥,首创者升任总裁

4 月 6 日,红帽公司宣布,产品和技术总裁 Paul Cormier 即日起任红帽总裁,并兼任首席执行官。Paul Cormier 是开源商业化“订阅”模式的提出者,这一模式促使红帽达成连续超70个季度的盈利,身价升至340亿美金&#xff0c…

Docker中数据管理

Docker数据管理 生产环境中,对数据进行持久化,或者需要在多个容器直接进行数据共享,这必然涉及到容器的一些数据管理的操作。容器中数据管理主要有两种方式: 数据卷(Data Volumes):容器内数据直…

C#两大知名Redis客户端连接哨兵集群的姿势

前言前面《Docker-Compose搭建Redis高可用哨兵集群》,我的思路是将Redis、Sentinel、Redis Client App链接到同一个网桥网络,这个网桥内的Redis Client App就可以使用ContainerIP访问网桥内任意redis节点。同一网桥网络访问规避了Docker上发生的NAT&…

Docker之Dockerfile详解

使用Dockerfile创建镜像 Dockerfile是一个文本格式的配置文件,我们可以利用Dockerfile来快速的创建一个自定义的镜像。 基本结构 Dockerfile由一行命令语句组成,并且支持以#开头的注释一般包括四个部分:基础镜像信息,维护者信息…

创业的N种死法

点击蓝字关注,回复“职场进阶”获取职场进阶精品资料一份互联网的江湖一直流传着大佬们的创业传奇。马云上厕所几分钟敲定几千万美金融资。王石走投无路靠倒卖玉米赚到上百万从此逆袭。扎克伯格为了获得哈佛美女照片,开发一个小玩意从此改变了世界。传奇…

Docker容器实战思维

Docker成功的基础 Docker的实现用到的基础技术(cgroups, namespace,分层文件系统)在Docker之前已经存在很多年,并且 Linux Containers(LXC)也在很多企业的环境中得到了大量的应用实践,并得到明…

dotNET Core 3.X 请求处理管道和中间件的理解

理解 dotNET Core 中的管道模型,对我们学习 dotNET Core 有很大的好处,能让我们知其然,也知其所以然,这样在使用第三方组件或者自己写一些扩展时,可以避免入坑,或者说避免同样的问题多次入坑。本文分为以下…

了解.NET中的垃圾回收

原文来自互联网,由长沙DotNET技术社区编译。尽管这是一篇来自2009年的古老的文章,但或许能够对你理解GC产生一些作用。 了解.NET中的垃圾回收一旦了解了.NET的垃圾收集器是如何工作的,那么可能会触及.NET应用程序的一些更为神秘的问题时&…

数据结构与算法--数组:二维数组中查找

数组 数组最简单的是数据结构,占据一整块连续的内存并按照顺序存储数据,创建数组时候,我们需要首先指定数组的容量大小,然后根据大小分配内存。即使我们只在数组中存储一个元素,亚需要为所有数据预先分配内存&#xf…

数据结构与算法--字符串:字符串替换

数据结构与算法–字符串:字符串替换 字符串的优化 由于字符串在编程时候使用的评率非常高,为了优化,很多语言都对字符串做了特殊的规定。下面我们讨论java中字符串的特性java中的字符数组以’\0’ 结尾,我们可以利用这个特性来找…

数据结构与算法--经典10大排序算法(动图演示)【建议收藏】

十大经典排序算法总结(动图演示) 算法分类 十大常见排序算法可分为两大类: 比较排序算法:通过比较来决定元素的位置,由于时间复杂度不能突破O(nlogn),因此也称为非线性时间比较类排序非比较类型排序&…

如何查找,修复和避免C#.NET中内存泄漏的8个最佳实践

原文来自互联网,由长沙DotNET技术社区编译。本文来源:https://michaelscodingspot.com/find-fix-and-avoid-memory-leaks-in-c-net-8-best-practices/从事大型企业项目的任何人都知道内存泄漏就像是大型酒店中的老鼠。当它们很少时,您可能不会…

ASP.NET Core技术研究-探秘依赖注入框架

ASP.NET Core在底层内置了一个依赖注入框架,通过依赖注入的方式注册服务、提供服务。依赖注入不仅服务于ASP.NET Core自身,同时也是应用程序的服务提供者。毫不夸张的说,ASP.NET Core通过依赖注入实现了各种服务对象的注册和创建,…

Redis遍历方式思考--字典扩容方式

全量遍历keys 工作中线上Redis维护,有时候我们需要查询特定前缀的缓存key列表来手动处理数据。可能是修改值,删除key。那么怎么才能快速的从海量的key中查找到对应的前缀匹配项。Redis提供了一下简单的指令,例如keys用来满足特定正则下的key…

从项目到产品: 软件时代需要价值流架构师 | IDCF

译者:无敌哥原文地址: https://thenewstack.io/the-age-of-software-needs-value-stream-architects/ 本文翻译仅供学习交流之用。原文作者 Mik Kersten 出版了《Project to Product》本系列共四篇文章,分别是01 从项目到产品:软件需要从物理…