- 文章优先发布在Github,其它平台会晚一段时间,文章纠错与更新内容只在Github:https://github.com/youthlql/JavaYouth
- 转载须知:转载请注明GitHub出处,让我们一起维护一个良好的技术创作环境。
- 如果你要提交 issue 或者 pr 的话建议到 Github 提交。笔者会陆续更新,如果对你有所帮助,不妨Github点个Star~。你的Star是我创作的动力。
- 本篇文章对于IO相关内容并没有完全讲完,不过也讲的差不多了,基本面试会问到的都讲了,如果想看的更细的,推荐**《操作系统导论》**这本书。
- 同时本篇文章也讲了面试以及平时工作中会看到的零拷贝,因为和IO有比较大的关系,就在这篇文章写一下。
- 零拷贝很多开源项目都用到了,netty,kafka,rocketmq等等。所以还是比较重要的,也是面试常问
- 流程图为processOn手工画的
这应该是大家看到很多文章对IO的一种分类,这只是IO最常见的一种分类。是从是否阻塞,以及是否异步的角度来分类的
在这里,我们以一个网络IO来的read来举例,它会涉及到两个东西:一个是产生这个IO的进程,另一个就是系统内核(kernel)。当一个read操作发生时,它会经历两个阶段:
**阶段1:**等待数据准备
**阶段2:**数据从内核空间拷贝到用户进程缓冲区的过程
应用程序每次轮询内核的 I/O 是否准备好,感觉有点傻乎乎,因为轮询的过程中,应用程序啥也做不了,只是在循环。
为了解决这种傻乎乎轮询方式,于是 I/O 多路复用技术就出来了,如 select、poll,它是通过 I/O 事件分发,当内核数据准备好时,再以事件通知应用程序进行操作。
这个做法大大改善了应用进程对 CPU 的利用率,在没有被通知的情况下,应用进程可以使用 CPU 做其他的事情。
下面是大概的过程
select函数
handle_events:实现事件循环
handle_event:进行读/写等操作
有一篇文章以实际例子讲解的比较形象
漫画讲IO
磁盘 I/O 是非常慢的,所以 Linux 内核通过减少磁盘 I/O 次数来减少I/O时间,在系统调用后,会把用户数据拷贝到内核中缓存起来,这个内核缓存空间也就是【页缓存:PageCache】,只有当缓存满足某些条件的时候,才发起磁盘 I/O 的请求。
根据是「否利用操作系统的页缓存」,可以把文件 I/O 分为直接 I/O 与非直接 I/O:
在进行写操作的时候以下几种场景会触发内核缓存的数据写入磁盘:
以下摘自—《深入linux内核架构》
1、
可能因不同原因、在不同的时机触发不同的刷出数据的机制。
周期性的内核线程,将扫描脏页的链表,并根据页变脏的时间,来选择一些页写回。如果系统不是太忙于写操作,那么在脏页的数目,以及刷出页所需的硬盘访问操作对系统造成的负荷之间,有一个可接受的比例。
如果系统中的脏页过多(例如,一个大型的写操作可能造成这种情况),内核将触发进一步的机制对脏页与后备存储器进行同步,直至脏页的数目降低到一个可接受的程度。而“脏页过多”和“可接受的程度”到底意味着什么,此时尚是一个不确定的问题,将在下文讨论。
内核的各个组件可能要求数据必须在特定事件发生时同步,例如在重新装载文件系统时。
前两种机制由内核线程pdflush实现,该线程执行同步代码,而第三种机制可能由内核中的多处代码触发。
2、
可以从用户空间通过各种系统调用来启用内核同步机制,以确保内存和块设备之间(完全或部分)的数据完整性。有如下3个基本选项可用。
使用sync系统调用刷出整个缓存内容。在某些情况下,这可能非常耗时。
各个文件的内容(以及相关inode的元数据)可以被传输到底层的块设备。内核为此提供了fsync和fdatasync系统调用。尽管sync通常与上文提到的系统工具sync联合使用,但fsync和fdatasync则专用于特定的应用程序,因为刷出的文件是通过特定于进程的文件描述符(在第8章介绍)来选择的。因而,没有一个通用的用户空间工具可以回写特定的文件。
msync用于同步内存映射
1、我们先来说脏页
脏页-linux内核中的概念,因为硬盘的读写速度远赶不上内存的速度,系统就把读写比较频繁的数据事先放到内存中,以提高读写速度,这就叫高速缓存,linux是以页作为高速缓存的单位,当进程修改了高速缓存里的数据时,该页就被内核标记为脏页,内核将会在合适的时间把脏页的数据写到磁盘中去,以保持高速缓存中的数据和磁盘中的数据是一致的。
2、通过对上面的解读,我们用通俗的语言翻译以下
sync
,fsync
,fdatasync
,内核缓存会刷到磁盘上; 标准 I/O 库提供缓冲的目的是尽可能减少使用 read 和 write 调用的次数(见图 3-6,其中显示了在不同缓冲区长度情况下,执行 I/O 所需的 CPU 时间量)。它也对每个 I/O流自动地进行缓冲管理,从而避免了应用程序需要考虑这一点所带来的麻烦。遗憾的是,标准 I/O 库最令人迷惑的也是它的缓冲。
标准 I/O提供了以下3 种类型的缓冲。
- 全缓冲。在这种情况下,在填满标准 I/O 缓冲区后才进行实际 I/O 操作。对于驻留在磁盘上的文件通常是由标准 IO库实施全缓冲的。在一个流上执行第一次 I/O 操作时,相关标准 I/O函数通常调用 malloc (见7.8 节)获得需使用的缓冲区。 术语冲洗(fush)说明标准 UO 缓冲区的写操作。缓冲区可由标准 I/O 例程自动地冲洗(例如,当填满一个缓冲区时),或者可以调用函数 fflush 冲洗一个流。值得注意的是,在 UNTX环境中,fush有两种意思。在标准 I/O库方面,flush(冲洗)意味着将缓冲区中的内容写到磁盘上(该缓冲区可能只是部分填满的)。在终端驱动程序方面(例如,在第 18章中所述的tcflush函数),flush(刷清)表示丢弃已存储在缓冲区中的数据。
- 行缓冲。在这种情况下,当在输入和输出中遇到换行符时,标准 I/O 库执行 I/O 操作。这允许我们一次输出一个字符(用标准 I/O 函数fputc),但只有在写了一行之后才进行实际 I/O操作。当流涉及一个终端时(如标准输入和标准输出),通常使用行缓冲。 对于行缓冲有两个限制。第一,因为标准 I/O 库用来收集每一行的缓冲区的长度是固定的。所以只要填满了缓冲区,那么即使还没有写一个换行符,也进行 I/O 操作。第二,任何时候只要通过标准 I/O 库要求从(a)一个不带缓冲的流,或者(b)一个行缓冲的流(它从内核请求需要 数据)得到输入数据,那么就会冲洗所有行缓冲输出流。在(b)中带了一个在括号中的说明,其理由是,所需的数据可能已在该缓冲区中,它并不要求一定从内核读数据。很明显,从一个不带缓冲的流中输入(即(a)项)需要从内核获得数据。
- 不带缓冲。标准 I/O 库不对字符进行缓冲存储,例如,若用标准 I/O 函数 fputs 写 15个字符到不带缓冲的流中,我们就期望这 15 个字符能立即输出,很可能使用 3.8 节的write 函数将这些字符写到相关联的打开文件中。
讲零拷贝前,讲一下前置知识
While (STATUS == BUSY);//wait until device is not busy
Write data to DATA register
Write command to COMMAND register
(Doing so starts the device and executes the command)
While (STATUS == BUSY);//wait until device is done with your request
关键问题:如何减少轮询开销操作系统检查设备状态时如何避免频繁轮询,从而降低管理设备的CPU开销?
**概念:**有了中断后,CPU 不再需要不断轮询设备,而是向设备发出一个请求,然后就可以让对应进程睡眠,切换执行其他任务。当设备完成了自身操作,会抛出一个硬件中断,引发CPU跳转执行操作系统预先定义好的中断服务例程(InterruptService Routine,ISR),或更为简单的中断处理程序(interrupt handler)。中断处理程序是一小段操作系统代码,它会结束之前的请求(比如从设备读取到了数据或者错误码)并且唤醒等待I/O的进程继续执行。
例子:
注意,使用中断并非总是最佳方案。假如有一个非常高性能的设备,它处理请求很快:通常在CPU第一次轮询时就可以返回结果。此时如果使用中断,反而会使系统变慢:切换到其他进程,处理中断,再切换回之前的进程代价不小。因此,如果设备非常快,那么最好的办法反而是轮询。如果设备比较慢,那么采用允许发生重叠的中断更好。如果设备的速度未知,或者时快时慢,可以考虑使用混合(hybrid)策略,先尝试轮询一小段时间,如果设备没有完成操作,此时再使用中断。这种两阶段(two-phased)的办法可以实现两种方法的好处。
中断仍旧存在的缺点:
IO过程简述:
注意:
- 这里很多博客画的图是错的,讲的也是错的。使用中断减少CPU开销时,在进行磁盘IO期间,CPU可以执行其他的进程不必等待磁盘IO。【因为这是《操作系统导论》里的原话】
这里为什么要特别强调原文呢?因为可以让读者读的安心,这本经典书籍总不会出错吧
《操作系统导论》原文:
标准协议还有一点需要我们注意。具体来说,如果使用编程的I/O将一大块数据传给设备,CPU又会因为琐碎的任务而变得负载很重,浪费了时间和算力,本来更好是用于运行其他进程。下面的时间线展示了这个问题:
进程1在运行过程中需要向磁盘写一些数据,所以它开始进行I/O操作,将数据从内存拷贝到磁盘(其中标示c的过程)。拷贝结束后,磁盘上的I/O操作开始执行,此时CPU才可以处理其他请求。
也就是说在从内存拷贝到磁盘或者从磁盘拷贝到内存这个过程是可以使用DMA(Direct Memory Access)优化的,怎么优化呢?
原文:
DMA工作过程如下。为了能够将数据传送给设备,操作系统会通过编程告诉DMA引擎数据在内存的位置,要拷贝的大小以及要拷贝到哪个设备。在此之后,操作系统就可以处理其他请求了。当DMA的任务完成后,DMA控制器会抛出一个中断来告诉操作系统自己已经完成数据传输。修改后的时间线如下:
从时间线中可以看到,数据的拷贝工作都是由DMA控制器来完成的。因为CPU在此时是空闲的,所以操作系统可以让它做一些其他事情,比如此处调度进程2到CPU来运行。因此进程2在进程1再次运行之前可以使用更多的CPU。
为了更好理解,看图:
过程:
场景:将磁盘上的文件读取出来,然后通过网络协议发送给客户端。
很明显发生了4次拷贝
发生了4次用户上下文切换,因为发生了两个系统调用read和write。一个系统调用对应两次上下文切换,所以上下文切换次数在一般情况下只可能是偶数。
想要优化文件传输的性能就两个方向
- 减少上下文切换次数
- 减少数据拷贝次数
因为这两个是最耗时的
read()
系统调用的过程中会把内核缓冲区的数据拷贝到用户的缓冲区里,为了减少这一步开销,我们可以用 mmap()
替换 read()
系统调用函数。mmap()
系统调用函数会直接把内核缓冲区里的数据映射到用户空间,这样,操作系统内核与用户空间共享缓冲区,就不需要再进行任何的数据拷贝操作。
总的来说mmap减少了一次数据拷贝,总共4次上下文切换,3次数据拷贝
Linux2.1
版本提供了 sendFile
函数,其基本原理如下:数据根本不经过用户态,直接从内核缓冲区进入到 SocketBuffer
总的来说有2次上下文切换,3次数据拷贝。
Linux在2.4
版本中,做了一些修改,避免了从内核缓冲区拷贝到 Socketbuffer
的操作,直接拷贝到协议栈,从而再一次减少了数据拷贝
前文一直提到了内核里的页缓存(PageCache),这个页缓存的作用就是用来提升小文件传输的效率
原因:
Q:PageCache可以用来大文件传输吗?
A:不能
Q:为什么呢?
A:
而想不用到内核缓冲区,我们就想到了直接IO这个东西,直接IO不经过内核缓存,同时经过上面的讲述,我们也可以知道异步IO效率是最高的。所以大文件传输最好的解决办法应该是:异步IO+直接IO
读到这里你就会发现,我为何这样安排目录顺序了,前面讲到的,后面都会用到。
《操作系统导论》 强推
漫画讲IO
https://blog.csdn.net/sehanlingfeng/article/details/78920423
https://blog.csdn.net/m0_38109046/article/details/89449305
https://www.zhihu.com/question/19732473
linux 同步IO: sync、fsync与fdatasync