架构系列二十三(全面理解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!

相关推荐
demonlg01122 分钟前
Go 语言标准库中Channels,Goroutines详细功能介绍与示例
开发语言·后端·golang
王强你强5 分钟前
Spring Boot 启动参数终极解析:如何优雅地控制你的应用?
java·spring boot·后端
vener_10 分钟前
基于Flask的通用登录注册模块,并代理跳转到目标网址
后端·python·flask
Lonwayne12 分钟前
当编程语言有了人格
java·javascript·c++·python·php
Asthenia041224 分钟前
git的回退:revert还是reset?来个例子看看吧!
后端
qq_5895681032 分钟前
java学习笔记——多线程
java·笔记·学习·intellij-idea
Asthenia041239 分钟前
接口速度太慢,排除DB影响,试试通过异步来优化吧!
后端
编程的大耳朵41 分钟前
Java 实现将Word 转换成markdown
java·word
Asthenia041242 分钟前
Lombok注解详解:从朴素构造到高效开发
后端