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!