寻佳构资料,帮你构建Java全栈知识体系 www.jiagoujishu.cn
根本IO如何从数据传输办法理解IO流?从数据传输办法或者说是运输办法角度看,可以将 IO 类分为:
字节是给打算机看的,字符才是给人看的

从数据来源或者说是操为难刁难象角度看,IO 类可以分为:
Java IO设计上利用了什么设计模式?
装饰者模式: 所谓装饰,便是把这个装饰者套在被装饰者之上,从而动态扩展被装饰者的功能。
装饰者举例设计不同种类的饮料,饮料可以添加配料,比如可以添加牛奶,并且支持动态添加新配料。每增加一种配料,该饮料的价格就会增加,哀求打算一种饮料的价格。
下图表示在 DarkRoast 饮料上新增新添加 Mocha 配料,之后又添加了 Whip 配料。DarkRoast 被 Mocha 包裹,Mocha 又被 Whip 包裹。它们都继续自相同父类,都有 cost() 方法,外层类的 cost() 方法调用了内层类的 cost() 方法。
以 InputStream 为例InputStream 是抽象组件;FileInputStream 是 InputStream 的子类,属于详细组件,供应了字节流的输入操作;FilterInputStream 属于抽象装饰者,装饰者用于装饰组件,为组件供应额外的功能。例如 BufferedInputStream 为 FileInputStream 供应缓存的功能。
实例化一个具有缓存功能的字节流工具时,只须要在 FileInputStream 工具上再套一层 BufferedInputStream 工具即可。
FileInputStream fileInputStream = new FileInputStream(filePath);BufferedInputStream bufferedInputStream = new BufferedInputStream(fileInputStream);
DataInputStream 装饰者供应了对更多数据类型进行输入的操作,比如 int、double 等基本类型。
5种IO模型什么是壅塞?什么是同步?壅塞IO 和 非壅塞IO这两个观点是程序级别的。紧张描述的是程序要求操作系统IO操作后,如果IO资源没有准备好,那么程序该如何处理的问题: 前者等待;后者连续实行(并且利用线程一贯轮询,直到有IO资源准备好了)
同步IO 和 非同步IO这两个观点是操作系统级别的。紧张描述的是操作系统在收到程序要求IO操作后,如果IO资源没有准备好,该如何相应程序的问题: 前者不相应,直到IO资源准备好往后;后者返回一个标记(好让程序和自己知道往后的数据往哪里关照),当IO资源准备好往后,再用事宜机制返回给程序。
什么是Linux的IO模型?网络IO的实质是socket的读取,socket在linux系统被抽象为流,IO可以理解为对流的操作。刚才说了,对付一次IO访问(以read举例),数据会先被拷贝到操作系统内核的缓冲区中,然后才会从操作系统内核的缓冲区拷贝到运用程序的地址空间。以是说,当一个read操作发生时,它会经历两个阶段:
第一阶段:等待数据准备 (Waiting for the data to be ready)。第二阶段:将数据从内核拷贝到进程中 (Copying the data from the kernel to the process)。对付socket流而言,
第一步:常日涉及等待网络上的数据分组到达,然后被复制到内核的某个缓冲区。第二步:把数据从内核缓冲区复制到运用进程缓冲区。网络运用须要处理的无非便是两大类问题,网络IO,数据打算。相对付后者,网络IO的延迟,给运用带来的性能瓶颈大于后者。网络IO的模型大致有如下几种:
同步壅塞IO(bloking IO)同步非壅塞IO(non-blocking IO)多路复用IO(multiplexing IO)旗子暗记驱动式IO(signal-driven IO)异步IO(asynchronous IO)什么是同步壅塞IO?运用进程被壅塞,直到数据复制到运用进程缓冲区中才返回。
举例理解你早上去买有现炸油条,你点单,之后一贯等店家做好,期间你啥其它事也做不了。(你便是运用级别,店家便是操作系统级别, 运用被壅塞了不能做其它事)
Linux 中IO图例什么是同步非壅塞IO?运用进程实行系统调用之后,内核返回一个缺点码。运用进程可以连续实行,但是须要不断的实行系统调用来获知 I/O 是否完成,这种办法称为轮询(polling)。
举例理解你早上去买现炸油条,你点单,点完后每隔一段韶光讯问店家有没有做好,期间你可以做点其它事情。(你便是运用级别,店家便是操作系统级别,运用可以做其它事情并通过轮询来看操作系统是否完成)
Linux 中IO图例什么是多路复用IO?系统调用可能是由多个任务组成的,以是可以拆成多个任务,这便是多路复用。
举例理解你早上去买现炸油条,点单收钱和炸油条原来都是由一个人完成的,现在他成了瓶颈,以是专门找了个收银员下单收钱,他则专注在炸油条。(实质上炸油条是耗时的瓶颈,将他职责分离出不是瓶颈的部分,比如下单收银,对应到系统级别也时一样的意思)
Linux 中IO图例利用 select 或者 poll 等待数据,并且可以等待多个套接字中的任何一个变为可读,这一过程会被壅塞,当某一个套接字可读时返回。之后再利用 recvfrom 把数据从内核复制到进程中。
它可以让单个进程具有处理多个 I/O 事宜的能力。又被称为 Event Driven I/O,即事宜驱动 I/O。
有哪些多路复用IO?
目前流程的多路复用IO实现紧张包括四种: select、poll、epoll、kqueue。下表是他们的一些主要特性的比较:
IO模型
相对性能
关键思路
操作系统
JAVA支持情形
select
较高
Reactor
windows/Linux
支持,Reactor模式(反应器设计模式)。Linux操作系统的 kernels 2.4内核版本之前,默认利用select;而目前windows下对同步IO的支持,都是select模型
poll
较高
Reactor
Linux
Linux下的JAVA NIO框架,Linux kernels 2.6内核版本之前利用poll进行支持。也是利用的Reactor模式
epoll
高
Reactor/Proactor
Linux
Linux kernels 2.6内核版本及往后利用epoll进行支持;Linux kernels 2.6内核版本之前利用poll进行支持;其余一定把稳,由于Linux下没有Windows下的IOCP技能供应真正的 异步IO 支持,以是Linux下利用epoll仿照异步IO
kqueue
高
Proactor
Linux
目前JAVA的版本不支持
多路复用IO技能最适用的是“高并发”场景,所谓高并发是指1毫秒内至少同时有上千个连接要求准备好。其他情形下多路复用IO技能发挥不出来它的上风。另一方面,利用JAVA NIO进行功能实现,相对付传统的Socket套接字实现要繁芜一些,以是实际运用中,须要根据自己的业务需求进行技能选择。
什么是旗子暗记驱动IO?运用进程利用 sigaction 系统调用,内核立即返回,运用进程可以连续实行,也便是说等待数据阶段运用进程是非壅塞的。内核在数据到达时向运用进程发送 SIGIO 旗子暗记,运用进程收到之后在旗子暗记处理程序中调用 recvfrom 将数据从内核复制到运用进程中。
比较于非壅塞式 I/O 的轮询办法,旗子暗记驱动 I/O 的 CPU 利用率更高。
举例理解你早上去买现炸油条,门口排队的人多,现在引入了一个叫号系统,点完单后你就可以做自己的事情了,然后等叫号就去拿就可以了。(以是不用再去自己频繁跑去问有没有做好了)
Linux 中IO图例什么是异步IO?相对付同步IO,异步IO不是顺序实行。用户进程进行aio_read系统调用之后,无论内核数据是否准备好,都会直接返回给用户进程,然后用户态进程可以去做别的事情。等到socket数据准备好了,内核直接复制数据给进程,然后从内核向进程发送关照。IO两个阶段,进程都是非壅塞的。
举例理解你早上去买现炸油条, 不用去排队了,打开美团外卖下单,然后做其它事,一会外卖自己送上门。(你便是运用级别,店家便是操作系统级别, 运用无需壅塞,这就是非壅塞;系统还可能在处理中,但是急速相应了运用,这便是异步)
Linux 中IO图例(Linux供应了AIO库函数实现异步,但是用的很少。目前有很多开源的异步IO库,例如libevent、libev、libuv)
什么是Reactor模型?
大多数网络框架都是基于Reactor模型进行设计和开拓,Reactor模型基于事宜驱动,特殊适宜处理海量的I/O事宜。
传统的IO模型?这种模式是传统设计,每一个要求到来时,大致都会按照:要求读取->要求解码->做事实行->编码相应->发送答复 这个流程去处理。
做事器会分配一个线程去处理,如果要求暴涨起来,那么意味着须要更多的线程来处理该要求。若要求涌现暴涨,线程池的事情线程数量满载那么其它要求就会涌现等待或者被抛弃。若每个小任务都可以利用非壅塞的模式,然后基于异步回调模式。这样就大大提高系统的吞吐量,这便引入了Reactor模型。
Reactor模型中定义的三种角色:Reactor:卖力监听和分配事宜,将I/O事宜分派给对应的Handler。新的事宜包含连接建立就绪、读就绪、写就绪等。Acceptor:处理客户端新连接,并分派要求到处理器链中。Handler:将自身与事宜绑定,实行非壅塞读/写任务,完成channel的读入,完成处理业务逻辑后,卖力将结果写出channel。可用资源池来管理。单Reactor单线程模型Reactor线程卖力多路分离套接字,accept新连接,并分派要求到handler。Redis利用单Reactor单进程的模型。
处理流程:
Reactor工具通过select监控连接事宜,收到事宜后通过dispatch进行转发。如果是连接建立的事宜,则由acceptor接管连接,并创建handler处理后续事宜。如果不是建立连接事宜,则Reactor会分发调用Handler来相应。handler会完成read->业务处理->send的完全业务流程。单Reactor多线程模型将handler的处理池化。
多Reactor多线程模型
主从Reactor模型: 主Reactor用于相应连接要求,从Reactor用于处理IO操作要求,读写分离了。
什么是Java NIO?
NIO紧张有三大核心部分:Channel(通道),Buffer(缓冲区), Selector。传统IO基于字节流和字符流进行操作,而NIO基于Channel和Buffer(缓冲区)进行操作,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中。Selector(选择区)用于监听多个通道的事宜(比如:连接打开,数据到达)。因此,单个线程可以监听多个数据通道。
NIO和传统IO(一下简称IO)之间第一个最大的差异是,IO是面向流的,NIO是面向缓冲区的。
零拷贝传统的IO存在什么问题?为什么引入零拷贝的?
如果做事端要供应文件传输的功能,我们能想到的最大略的办法是:将磁盘上的文件读取出来,然后通过网络协议发送给客户端。
传统 I/O 的事情办法是,数据读取和写入是从用户空间到内核空间来回复制,而内核空间的数据是通过操作系统层面的 I/O 接口从磁盘读取或写入。
代码常日如下,一样平常会须要两个别系调用:
read(file, tmp_buf, len);write(socket, tmp_buf, len);
代码很大略,虽然就两行代码,但是这里面发生了不少的事情。
首先,期间共发生了 4 次用户态与内核态的高下文切换,由于发生了两次系统调用,一次是 read() ,一次是 write(),每次系统调用都得先从用户态切换到内核态,等内核完成任务后,再从内核态切换回用户态。
高下文切换到本钱并不小,一次切换须要耗时几十纳秒到几微秒,虽然韶光看上去很短,但是在高并发的场景下,这类韶光随意马虎被累积和放大,从而影响系统的性能。
其次,还发生了 4 次数据拷贝,个中两次是 DMA 的拷贝,其余两次则是通过 CPU 拷贝的,下面说一下这个过程:
第一次拷贝,把磁盘上的数据拷贝到操作系统内核的缓冲区里,这个拷贝的过程是通过 DMA 搬运的。第二次拷贝,把内核缓冲区的数据拷贝到用户的缓冲区里,于是我们运用程序就可以利用这部分数据了,这个拷贝到过程是由 CPU 完成的。第三次拷贝,把刚才拷贝到用户的缓冲区里的数据,再拷贝到内核的 socket 的缓冲区里,这个过程依然还是由 CPU 搬运的。第四次拷贝,把内核的 socket 缓冲区里的数据,拷贝到网卡的缓冲区里,这个过程又是由 DMA 搬运的。我们回过分看这个文件传输的过程,我们只是搬运一份数据,结果却搬运了 4 次,过多的数据拷贝无疑会花费 CPU 资源,大大降落了系统性能。
这种大略又传统的文件传输办法,存在冗余的上文切换和数据拷贝,在高并发系统里是非常糟糕的,多了很多不必要的开销,会严重影响系统性能。
以是,要想提高文件传输的性能,就须要减少「用户态与内核态的高下文切换」和「内存拷贝」的次数。
mmap + write怎么实现的零拷贝?在前面我们知道,read() 系统调用的过程中会把内核缓冲区的数据拷贝到用户的缓冲区里,于是为了减少这一步开销,我们可以用 mmap() 更换 read() 系统调用函数。
buf = mmap(file, len);write(sockfd, buf, len);
mmap() 系统调用函数会直接把内核缓冲区里的数据「映射」到用户空间,这样,操作系统内核与用户空间就不须要再进行任何的数据拷贝操作。
详细过程如下:
运用进程调用了 mmap() 后,DMA 会把磁盘的数据拷贝到内核的缓冲区里。接着,运用进程跟操作系统内核「共享」这个缓冲区;运用进程再调用 write(),操作系统直接将内核缓冲区的数据拷贝到 socket 缓冲区中,这统统都发生在内核态,由 CPU 来搬运数据;末了,把内核的 socket 缓冲区里的数据,拷贝到网卡的缓冲区里,这个过程是由 DMA 搬运的。我们可以得知,通过利用 mmap() 来代替 read(), 可以减少一次数据拷贝的过程。
但这还不是最空想的零拷贝,由于仍旧须要通过 CPU 把内核缓冲区的数据拷贝到 socket 缓冲区里,而且仍旧须要 4 次高下文切换,由于系统调用还是 2 次。
sendfile怎么实现的零拷贝?在 Linux 内核版本 2.1 中,供应了一个专门发送文件的系统调用函数 sendfile(),函数形式如下:
#include <sys/socket.h>ssize_t sendfile(int out_fd, int in_fd, off_t offset, size_t count);
它的前两个参数分别是目的端和源真个文件描述符,后面两个参数是源真个偏移量和复制数据的长度,返回值是实际复制数据的长度。
首先,它可以替代前面的 read() 和 write() 这两个别系调用,这样就可以减少一次系统调用,也就减少了 2 次高下文切换的开销。
其次,该系统调用,可以直接把内核缓冲区里的数据拷贝到 socket 缓冲区里,不再拷贝到用户态,这样就只有 2 次高下文切换,和 3 次数据拷贝。如下图:
但是这还不是真正的零拷贝技能,如果网卡支持 SG-DMA(The Scatter-Gather Direct Memory Access)技能(和普通的 DMA 有所不同),我们可以进一步减少通过 CPU 把内核缓冲区里的数据拷贝到 socket 缓冲区的过程。
你可以在你的 Linux 系统通过下面这个命令,查看网卡是否支持 scatter-gather 特性:
$ ethtool -k eth0 | grep scatter-gatherscatter-gather: on
于是,从 Linux 内核 2.4 版本开始起,对付支持网卡支持 SG-DMA 技能的情形下, sendfile() 系统调用的过程发生了点变革,详细过程如下:
第一步,通过 DMA 将磁盘上的数据拷贝到内核缓冲区里;第二步,缓冲区描述符和数据长度传到 socket 缓冲区,这样网卡的 SG-DMA 掌握器就可以直接将内核缓存中的数据拷贝到网卡的缓冲区里,此过程不须要将数据从操作系统内核缓冲区拷贝到 socket 缓冲区中,这样就减少了一次数据拷贝;以是,这个过程之中,只进行了 2 次数据拷贝,如下图:
这便是所谓的零拷贝(Zero-copy)技能,由于我们没有在内存层面去拷贝数据,也便是说全程没有通过 CPU 来搬运数据,所有的数据都是通过 DMA 来进行传输的。
零拷贝技能的文件传输办法比较传统文件传输的办法,减少了 2 次高下文切换和数据拷贝次数,只须要 2 次高下文切换和数据拷贝次数,就可以完成文件的传输,而且 2 次的数据拷贝过程,都不须要通过 CPU,2 次都是由 DMA 来搬运。
"大众年夜众号《鲁大猿》
寻佳构资料,帮你构建Java全栈知识体系 www.jiagoujishu.cn