架构系列二十三(全面理解IO)

1.引言

在编程界,IO一直是一个难点和痛点,不管是对于刚入行的小伙伴,还是有一定工作经验的朋友!今天这篇文章,我索性花点时间,争取让大家以后但凡提到IO,都不在迷糊。

这篇文章,我们试图需要搞清楚以下问题:

  • 什么是IO
  • 经常听到同步/异步,阻塞/非阻塞这样的词,它们到底说了什么
  • 什么是IO模型,IO模型解决了什么问题
  • 零拷贝是怎么一回事
  • sendfile又是什么鬼

2.什么是IO

I是输入(Input的缩写),O是输出(Output的缩写),所以IO即是输入输出。当然这样的描述过于抽象了,从哪儿输入?输出到什么地方?

前面我分享过计算机的体系结构,我们今天的计算机都是冯诺依曼机,下面这个图

即计算机的组成:

  • 输入设备:鼠标,键盘
  • 输出设备:显示器
  • 存储器:内存,磁盘
  • 运算器+控制器:cpu

你看到了有输入输出设备,有存储器,有CPU。更多场景下,我们说到IO都跟内存,与外部设备相关,所以我们可以通过一句话来描述一下。

什么是IO?即,IO是指计算机内存,与外部设备之间数据拷贝的过程。

3.同步OR异步,阻塞OR非阻塞

刚参加工作那会儿,项目组有几个工作久一点的同事,每次讨论方案或者问题的时候,都在哪儿说

  • 你那个是同步的,为什么不用异步?
  • 性能瓶颈?阻塞了嘛

一开始,不知道说的是什么"鸟语",等自己工作久一点以后,终于搞明白!

首先要申明一下,IO的同步异步,与并发编程中的同步异步,不是一回事儿。要说清楚这个问题,有几个概念要先说明一下

  • 进程:每个应用程序,即是一个进程,进程拥有自己的资源,是操作系统分配资源的基本单位
  • 线程:线程是操作系统调度的最小单位,线程没有自己的资源,它使用进程的资源
  • 用户空间与内核空间:进程的虚拟地址空间,分为用户空间,与内核空间

前面我们说明了IO的定义是数据拷贝过程 。这里再聚焦一下,数据在应用程序,与内核之间拷贝。拷贝这个事情,只能是内核来做,出于安全的考虑,操作系统是不允许应用程序直接操作内核的

问题是由谁来触发拷贝?

这就是我们要说的同步,和异步。同步,是指由应用程序来触发拷贝;相应的异步,是指由内核来触发拷贝

你看到了,IO中的同步异步,是不同于并发编程中的同步异步的。并发编程中的同步,强调的是加锁互斥,保障数据安全一致性

说清楚了同步异步,再来看阻塞OR非阻塞。拷贝数据是一个重量级的操作,在计算机的世界里,是一个漫长的等待过程。那么相应的

  • 阻塞:是指应用程序在发起IO相关的系统调用,比如read调用后,得在这儿,干不了别的事
  • 非阻塞:是指应用程序在发起IO相关的系统调用后,立即返回,不需要在这里傻傻的等着。比如你下班回家,在路上带个饭,点好菜你告诉餐馆老板,过会儿来拿,然后你去买了点水果。

4.IO模型解决了什么问题

unix系统给我们提供了五种IO模型,分别是

  • 同步阻塞IO
  • 同步非阻塞IO
  • IO多路复用
  • 信号驱动IO
  • 异步IO

在具体详解每一种IO模型前,我们需要先高清楚几个事实

  • 是在应用程序,与内核之间进行IO交互
  • 通常由应用程序发起IO操作
  • 应用程序发起IO操作后,内核有三个过程:准备数据,数据就绪,拷贝数据

下面我带你具体详细分析每一种IO模型的特点。

4.1.同步阻塞IO

同步阻塞IO的特点:

  • 应用程序发起read调用,直到read返回之间,一直处于阻塞状态
  • 即要等内核准备好数据,将数据拷贝到用户空间后,应用程序才能继续执行

你看到了同步阻塞IO模型,应用处理能力是个大问题,一直阻塞在那儿呢!这也是为什么java直到jdk1.4 支持NIO前,在服务端通信领域一直被诟病,毕竟NIO以前的BIO就是同步阻塞模型,怪不了别人。

4.2.同步非阻塞IO

同步非阻塞IO的特点:

  • 应用程序发起read调用,在内核准备好数据前,立即返回,不会阻塞
  • 内核准备数据的过程中,应用程序不间断发起read调用,来询问:数据准备好了吗
  • 直到内核数据准备就绪,注意:拷贝数据的过程中,应用程序还是要阻塞的

你看到了,同步非阻塞IO,相对于同步阻塞IO有了很大的提升空间,它只在拷贝数据的过程中阻塞。

4.3.IO多路复用

IO多路复用的特点:

  • 应用程序发起系统调用select,注意:不在是read调用
  • select用于检测IO事件:读事件,写事件,连接事件等
  • 与同步非阻塞IO模型一样,内核准备数据过程中不阻塞,拷贝数据过程中任然阻塞

初一看,这不还是同步非阻塞IO吗?不过就是将read调用,换成了select调用。事实上这里面有讲究。同步非阻塞IO模型有一个大的问题,它的编程模型是一线程,一连接。对于服务端编程来说,服务器资源是有瓶颈的,成百上千万的客户端连接打进来,服务器没有办法开这么多线程去应对,难以处理高并发,大流量场景。通过池化技术可以一定程度缓解,但是治标不治本!

那你说IO多路复用就解决这个问题了吗?关键就在于多路复用这几个字眼上。什么是多路复用?通俗点讲是让IO等待都发生在一个地方,一个服务端线程,可以同时处理多个客户端连接

对应到java编程上,NIO即是IO多路复用模型,java的NIO编程有三个核心组件:

  • Selector:选择器,用于检测Channel事件的发生,通知Channel处理IO事件。与Channel的关系是一对多
  • Channel:通道,一个客户端连接Socket,即是一个Channel
  • Buffer:数据缓冲区,Channel必须通过Buffer来进行数据的读写操作

4.4.信号驱动IO

信号驱动IO的特点:

  • 应用程序发起read调用,并注册一个回调函数后,立即返回
  • 内核准备数据,数据就绪后,通过回调函数通知应用程序:数据准备好了
  • 应用程序再次发起read调用:读取数据,在拷贝数据过程中,应用程序阻塞

你看到了,信号驱动IO模型,还是同步非阻塞编程模型。只不过它不需要应用程序不间断发起read调用,来询问内核:数据准备好了吗?而是通过注册一个回调函数,当内核数据就绪后,通过回调函数通知应用程序。最后再次发起read调用,拷贝数据。

4.5.异步IO

异步IO的特点:

  • 应用程序发起read调用,并注册一个回调函数。你需要注意,异步IO中注册的回调函数,不同于信号驱动IO中的回调函数
  • 异步IO注册的回调函数,关注两个事情:告诉内核数据拷贝到什么地方,告诉内核数据拷贝完成后通知应用程序
  • 在整个IO过程中:准备数据,数据就绪,拷贝数据,应用程序都不需要阻塞

你看到了,异步IO才是终极BOSS,应用程序至始至终都没有被阻塞,完全一个甩手掌柜!java的NIO2即采用了异步IO编程模型,不过你需要注意一点,java的异步IO编程是借助Select中的epoll来模拟的,并没有真正意义上的封装操作系统提供的异步IO,感兴趣的同学可以去深入了解一下。

5.零拷贝是怎么一回事

找工作的时候,如果你面试的是高级,资深一类的岗位,一定经常会被问到这个问题。我之前在面试候选人的时候,如果对方有三五年,或者七八年工作经验,经常也会问一问这类问题。因为像这样的问题,可以很好的直观考察候选人在专业深度,广度上的一些见解。工作年限不等于工作经验和能力!

那么到底什么是零拷贝呢?先看图,一图以蔽之。

上图是一个java应用程序,与内核进行IO交互,并且假设是一个网络应用程序。很多同学应该都熟悉左边java内存结构部分的内容。

  • 线程私有区域:程序计数器,jvm栈,本地方法栈
  • 线程共享区域:方法区(jdk8以后叫元空间),堆

对于java应用程序来说,除了jvm内存,还有一块很重要的内存空间:本地内存,常叫做堆外内存或者直接内存。

那么本地内存,用于做什么呢?它跟我们本篇文章的主题IO直接相关。我们知道一个网络应用,读取数据的过程是这样的

shell 复制代码
网卡------>内核------>应用程序(用户空间)

对于java应用来说,数据不能直接从内核,拷贝到java堆内存中为什么?

一切的根源在于GC,如果在拷贝数据的过程中,发生了GC,意味着可能拷贝数据的Buffer地址会发生变化,那就麻烦了,后面的数据还怎么拷贝?

只能先把数据从内核,拷贝到本地内存,因为本地内存没有GC,可以放心的拷贝。这就是我们上面那个图数据的拷贝流程。

于是你看到了,一次IO数据拷贝流程

shell 复制代码
网卡------>内核------>本地内存------>堆内存

在用户空间中,内核与堆之间,需要经过本地内存中转。

你需要注意的是,IO拷贝是非常重量级的操作,能少一次就尽量少一次!于是jdk给我们提供了两个操作数据的API

  • HeapByteBuffer
  • DirectByteBuffer

Buffer即缓冲区,是一个字节数组Byte[]。HeapByteBuffer的意思是说,这是一个堆缓冲区,它以及它内部的Byte[]都是在堆上分配的。这个时候,如果要操作数据,只能从本地内存,到堆内存之间做一次数据拷贝

DirectByteBuffer是直接缓冲区,它的意思是说,我直接指向本地内存的地址,直接操作本地内存的数据就好了,不用拷贝。你看少了一次拷贝不是。

回到主题,通过DirectByteBuffer减少的这次本地内存,与堆内存之间的数据拷贝,就是零拷贝的由来!

你可能会有这样的一个疑问,既然零拷贝这么优秀!就用DirectByteBuffer就好了嘛,干嘛还用弄一个HeapByteBuffer呢?

这是因为本地内存,不像堆内存。堆内存有GC帮我们管理,要使用本地内存,只能你自己来管理,一不小心什么内存溢出,内存泄漏都来了,自己看着办。

世间万物都是一把双刃剑,有利有弊!要做的就是一个取舍。当然实际应用中,有很多优秀的框架就很好的应用了本地内存来提升应用性能,比如netty,通过池化技术,提供内存池的方式来管理和使用本地内存,感兴趣的同学建议去研究研究。

6.sendfile是什么

文章最后,终于聊到sendfile了。最早关注sendfile是在几年前,公司项目中用到kafka,在kafka高性能特性中,说用到了sendfile,于是就特别想知道sendfile是个什么鬼!

老规矩,一图以蔽之。

看上图,假设这是一个网络应用,在一次读取本地文件,发送到网络上的操作,整个流程需要6次IO拷贝

  • 第一次:从磁盘读取本地文件内容,到内核缓冲区
  • 第二次:从内核缓冲区,拷贝数据到本地内存缓冲区
  • 第三次:从本地内存缓冲区,拷贝数据到堆内存
  • 第四次:从堆内存,拷贝数据到本地内存缓冲区
  • 第五次:从本地内存缓冲区,拷贝数据到内核缓冲区
  • 第六次:从内核缓冲区,拷贝数据到网卡

天呐!六次拷贝,成本太高!用sendfile技术吧,谁用谁好使。

通过sendfile,应用程序只需要发起一次sendfile系统调用,数据直接通过内核缓冲区,与本地磁盘,网卡完成交互。整个操作只有两次IO拷贝,极大的提升了效率,这就是sendfile!

相关推荐
焚 城10 分钟前
.NET8关于ORM的一次思考
后端·.net
purrrew24 分钟前
【Java ee初阶】网络编程 UDP socket
java·网络·网络协议·udp·java-ee
上海合宙LuatOS43 分钟前
全栈工程师实战手册:LuatOS日志系统开发指南!
java·开发语言·单片机·嵌入式硬件·物联网·php·硬件工程
多敲代码防脱发44 分钟前
导出导入Excel文件(详解-基于EasyExcel)
java·开发语言·jvm·数据库·mysql·excel
一刀到底2111 小时前
做为一个平台,给第三方提供接口的时候,除了要求让他们申请 appId 和 AppSecret 之外,还应当有哪些安全选项,要过等保3级
java·网络·安全
wjcurry1 小时前
我的实习日报
java·redis·mysql
我喜欢山,也喜欢海2 小时前
Jenkins Maven 带权限 搭建方案2025
java·jenkins·maven
明天更新2 小时前
Java处理压缩文件的两种方式!!!!
java·开发语言·7-zip
铁锚2 小时前
一个WordPress连续登录失败的问题排查
java·linux·服务器·nginx·tomcat