Files
JavaYouth/docs/os/操作系统-IO与零拷贝.md
2022-07-24 20:30:09 +08:00

399 lines
29 KiB
Markdown
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
title: 操作系统-IO与零拷贝
tags:
- 操作系统
- os
- IO
- 零拷贝
categories:
- 操作系统
keywords: 操作系统IO零拷贝
description: 基本面试会问到的IO进行了详解同时本篇文章也对面试以及平时工作中会看到的零拷贝进行了充分的解析。万字长文系列读到就是赚到。
cover: 'https://npm.elemecdn.com/lql_static@latest/logo/os_logo.jpg'
abbrlink: e959db2e
date: 2021-04-08 15:21:58
---
>
> 1. 本篇文章对于IO相关内容并没有完全讲完不过也讲的差不多了基本面试会问到的都讲了如果想看的更细的推荐**《操作系统导论》**这本书。
> 2. 同时本篇文章也讲了面试以及平时工作中会看到的零拷贝因为和IO有比较大的关系就在这篇文章写一下。
> 3. 零拷贝很多开源项目都用到了nettykafkarocketmq等等。所以还是比较重要的也是面试常问
> 4. 流程图为processOn手工画的
# IO
## 阻塞与非阻塞 I/O 和 同步与异步 I/O
> 这应该是大家看到很多文章对IO的一种分类这只是IO最常见的一种分类。是从是否阻塞以及是否异步的角度来分类的
在这里我们以一个网络IO来的read来举例它会涉及到两个东西一个是产生这个IO的进程另一个就是系统内核(kernel)。当一个read操作发生时它会经历两个阶段
**阶段1**等待数据准备
**阶段2**数据从内核空间拷贝到用户进程缓冲区的过程
### 阻塞IO
1. 当用户进程进行recvfrom这个系统调用内核就开始了IO的第一个阶段等待数据准备。
2. 于network io来说很多时候数据在一开始还没有到达比如还没有收到一个完整的TCP包这个时候**内核**就要等待足够的数据到来。
3. 而在用户进程这边,整 个进程会被阻塞。当**内核**一直等到数据准备好了,它就会将数据从**内核**中拷贝到用户内存,然后**内核**返回果,用户进程才解除 block的状态重新运行起来。
4. **所以blocking IO的特点就是在IO执行的两个阶段都被block了。**
<img src="https://npm.elemecdn.com/youthlql@1.0.8/computer_network/summary/0003.png">
### 非阻塞IO
1. 当用户进程发出read操作时如果kernel中的数据还没有准备好那么它并不会block用户进程而是立刻返回一个error。
2. 从用户进程角度讲 它发起一个read操作后并不需要等待而是马上就得到了一个结果。用户进程判断结果是一个error时它就知道数据还没有准备好。用户线程需要不断地发起IO请求直到数据到达后才真正读取到数据继续执行。
3. 虽然用户线程每次发起IO请求后可以立即返回但是为了等到数据仍需要不断地轮询、重复请求消耗了大量的CPU的资源。一般很少直接使用这种模型而是在其他IO模型中使用非阻塞IO这一特性。
4. **所以,用户进程第一个阶段不是阻塞的,需要不断的主动询问内核数据好了没有;第二个阶段依然总是阻塞的。**
<img src="https://npm.elemecdn.com/youthlql@1.0.8/computer_network/summary/0004.png">
### IO多路复用
> 1. 应用程序每次轮询内核的 I/O 是否准备好,感觉有点傻乎乎,因为轮询的过程中,应用程序啥也做不了,只是在循环。
>
> 2. 为了解决这种傻乎乎轮询方式,于是 **I/O 多路复用**技术就出来了,如 select、poll它是通过 I/O 事件分发,当内核数据准备好时,再以事件通知应用程序进行操作。
>
> 3. 这个做法大大改善了应用进程对 CPU 的利用率,在没有被通知的情况下,应用进程可以使用 CPU 做其他的事情。
>
> 下面是大概的过程
1. IO多路复用模型是建立在内核提供的多路分离函数select基础之上的使用select函数可以避免同步非阻塞IO模型中轮询等待的问题。利用了新的select系统调用由内核来负责本来是请求进程该做的轮询操作
2. 它的基本原理就是select /epoll这个函数会不断的轮询所负责的所有socket当某个socket有数据到达了就通知用户进程正式发起read请求。
3. 从流程上来看使用select函数进行IO请求和同步阻塞模型没有太大的区别甚至还多了添加监视socket以及调用select函数的额外操作效率更差。但是使用select以后最大的优势是用户可以在一个线程内同时处理多个socket的IO请求。用户可以注册多个socket然后不断地调用select读取被激活的socket(也就是数据准备好了的socket)即可达到在同一个线程内同时处理多个IO请求的目的。而在同步阻塞模型中必须通过多线程的方式才能达到这个目的。
<img src="https://npm.elemecdn.com/youthlql@1.0.8/computer_network/summary/0005.png">
**select函数**
> handle_events实现事件循环
>
> handle_event进行读/写等操作
1. 使用select函数的优点并不仅限于此。虽然上述方式允许单线程内处理多个IO请求但是每个IO请求的过程还是阻塞的在select函数上阻塞平均时间甚至比同步阻塞IO模型还要长。
2. 如果用户线程只注册自己感兴趣的socket或者IO请求然后去做自己的事情等到数据到来时再进行处理则可以提高CPU的利用率。
3. IO多路复用模型使用了Reactor设计模式实现了这一机制。
4. 通过Reactor的方式可以将用户线程轮询IO操作状态的工作统一交给handle_events事件循环进行处理。用户线程注册事件处理器之后可以继续执行做其他的工作异步而Reactor线程负责调用内核的select函数检查socket状态。当有socket被激活时(就是数据准备好的时候)则通知相应的用户线程或执行用户线程的回调函数执行handle_event进行数据读取、处理的工作。
5. 由于select函数是阻塞的因此多路IO复用模型也被称为异步阻塞IO模型。注意这里的所说的阻塞是指select函数执行时线程被阻塞而不是指socket。(一般在使用IO多路复用模型时socket都是设置为NONBLOCK的不过这并不会产生影响因为用户发起IO请求时数据已经到达了用户线程一定不会被阻塞。)
<img src="https://npm.elemecdn.com/youthlql@1.0.0/os/IO_and_Zero-copy/0001.png">
### 异步IO
- 实际上,无论是阻塞 I/O、非阻塞 I/O还是基于非阻塞 I/O 的多路复用**都是同步调用。因为它们在 read 调用时,内核将数据从内核空间拷贝到应用进程空间,这个阶段都是需要等待的。**
- 而真正的**异步 I/O** 是「内核数据准备好」和「数据从内核态拷贝到用户态」这两个过程都不用等待
1. 真正”的异步IO需要操作系统更强的支持。在IO多路复用模型中由用户线程自行读取数据、处理数据。
2. 而在异步IO模型中用户进程发起read操作之后立刻就可以开始去做其它的事。
3. 而另一方面,从**内核**的角度,当它受到一个异步读之后,首先它会立刻返回,所以不会对用户进程产生任何阻塞。然后,内核会等待数据准备完成,然后将数据拷贝到用户内存,当这一切都 完成之后,**内核**会给用户进程发送一个信号告诉它read操作完成了用户线程直接使用即可。 在这整个过程中,进程完全没有被阻塞。
4. 异步IO模型使用了Proactor设计模式实现了这一机制。**(具体怎么搞得,看上面的文章链接)**
<img src="https://npm.elemecdn.com/youthlql@1.0.8/computer_network/summary/0007.png">
> 有一篇文章以实际例子讲解的比较形象
>
> [漫画讲IO](https://mp.weixin.qq.com/s?__biz=Mzg3MjA4MTExMw==&mid=2247484746&idx=1&sn=c0a7f9129d780786cabfcac0a8aa6bb7&source=41&scene=21#wechat_redirect)
## 直接与非直接I/O
1. 磁盘 I/O 是非常慢的,所以 Linux 内核通过减少磁盘 I/O 次数来减少I/O时间在系统调用后会把用户数据拷贝到内核中缓存起来这个内核缓存空间也就是【页缓存PageCache】只有当缓存满足某些条件的时候才发起磁盘 I/O 的请求。
2. **根据是「否利用操作系统的页缓存」,可以把文件 I/O 分为直接 I/O 与非直接 I/O**
* 直接 I/O不会发生内核缓存和用户程序之间数据复制跳过操作系统的页缓存直接经过文件系统访问磁盘。
* 非直接 I/O正相反读操作时数据从内核缓存中拷贝给用户程序写操作时数据从用户程序拷贝给内核缓存再由内核决定什么时候写入数据到磁盘。
3. 想要实现直接I/O需要你在系统调用中指定 O_DIRECT 标志。如果没有设置过默认的是非直接I/O。
在进行写操作的时候以下几种场景会触发内核缓存的数据写入磁盘:
> 以下摘自---**《深入linux内核架构》**
>
> **1、**
>
> 可能因不同原因、在不同的时机触发不同的刷出数据的机制。
>
> - 周期性的内核线程,将扫描脏页的链表,并根据页变脏的时间,来选择一些页写回。如果系统不是太忙于写操作,那么在脏页的数目,以及刷出页所需的硬盘访问操作对系统造成的负荷之间,有一个可接受的比例。
>
> - 如果系统中的脏页过多(例如,一个大型的写操作可能造成这种情况),内核将触发进一步的机制对脏页与后备存储器进行同步,直至脏页的数目降低到一个可接受的程度。而“脏页过多”和“可接受的程度”到底意味着什么,此时尚是一个不确定的问题,将在下文讨论。
>
> - 内核的各个组件可能要求数据必须在特定事件发生时同步,例如在重新装载文件系统时。
>
> 前两种机制由内核线程pdflush实现该线程执行同步代码而第三种机制可能由内核中的多处代码触发。
>
>
>
> **2、**
>
> 可以从用户空间通过各种系统调用来启用内核同步机制以确保内存和块设备之间完全或部分的数据完整性。有如下3个基本选项可用。
>
> 1. 使用sync系统调用刷出整个缓存内容。在某些情况下这可能非常耗时。
>
> 2. 各个文件的内容以及相关inode的元数据可以被传输到底层的块设备。内核为此提供了fsync和fdatasync系统调用。尽管sync通常与上文提到的系统工具sync联合使用但fsync和fdatasync则专用于特定的应用程序因为刷出的文件是通过特定于进程的文件描述符在第8章介绍来选择的。因而没有一个通用的用户空间工具可以回写特定的文件。
> 3. msync用于同步内存映射
1、我们先来说脏页
脏页linux内核中的概念因为硬盘的读写速度远赶不上内存的速度系统就把读写比较频繁的数据事先放到内存中以提高读写速度这就叫高速缓存linux是以页作为高速缓存的单位当进程修改了高速缓存里的数据时该页就被内核标记为脏页内核将会在合适的时间把脏页的数据写到磁盘中去以保持高速缓存中的数据和磁盘中的数据是一致的。
2、通过对上面的解读我们用通俗的语言翻译以下
- 周期性的扫描脏页,如果发现脏页存在的时间过了某一时间时,也会把该脏页的数据刷到磁盘上
* 当发现脏页太多的时候,内核会把一定数量的脏页数据写到磁盘上;
* 用户主动调用 `sync``fsync``fdatasync`,内核缓存会刷到磁盘上;
## 缓冲与非缓冲I/O
1. 文件操作的标准库是可以实现数据的缓存,那么**根据「是否利用标准库缓冲」,可以把文件 I/O 分为缓冲 I/O 和非缓冲 I/O**
* 缓冲 I/O利用的是标准库的缓存实现文件的加速访问而标准库再通过系统调用访问文件。
* 非缓冲 I/O直接通过系统调用访问文件不经过标准库缓存。
2. 这里所说的「缓冲」特指标准库内部实现的缓冲。比方说,很多程序遇到换行时才真正输出,而换行前的内容,其实就是被标准库暂时缓存了起来,这样做的目的是,减少系统调用的次数。
3. 非缓冲io因为没有标准库提供的缓冲只能用操作系统的缓存区会造成很多次的系统调用降低效率
4. 带缓存IO也叫标准IO符合ANSI C 的标准IO处理不依赖系统内核所以移植性强我们使用标准IO操作很多时候是为了减少对read()和write()的系统调用次数带缓存IO其实就是在用户层再建立一个缓存区这个缓存区的分配和优化长度等细节都是标准IO库代你处理好了不用去操心。
> 标准 I/O 库提供缓冲的目的是尽可能减少使用 read 和 write 调用的次数(见图 3-6其中显示了在不同缓冲区长度情况下执行 I/O 所需的 CPU 时间量)。它也对每个 I/O流自动地进行缓冲管理从而避免了应用程序需要考虑这一点所带来的麻烦。遗憾的是标准 I/O 库最令人迷惑的也是它的缓冲。
>
> 标准 I/O提供了以下3 种类型的缓冲。
>
> 1. 全缓冲。在这种情况下,在填满标准 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刷清表示丢弃已存储在缓冲区中的数据。
>2. 行缓冲。在这种情况下,当在输入和输出中遇到换行符时,标准 I/O 库执行 I/O 操作。这允许我们一次输出一个字符(用标准 I/O 函数fputc但只有在写了一行之后才进行实际 I/O操作。当流涉及一个终端时如标准输入和标准输出通常使用行缓冲。对于行缓冲有两个限制。第一因为标准 I/O 库用来收集每一行的缓冲区的长度是固定的。所以只要填满了缓冲区,那么即使还没有写一个换行符,也进行 I/O 操作。第二,任何时候只要通过标准 I/O 库要求从a一个不带缓冲的流或者b一个行缓冲的流它从内核请求需要 数据得到输入数据那么就会冲洗所有行缓冲输出流。在b中带了一个在括号中的说明其理由是所需的数据可能已在该缓冲区中它并不要求一定从内核读数据。很明显从一个不带缓冲的流中输入a需要从内核获得数据。
> 3. 不带缓冲。标准 I/O 库不对字符进行缓冲存储,例如,若用标准 I/O 函数 fputs 写 15个字符到不带缓冲的流中我们就期望这 15 个字符能立即输出,很可能使用 3.8 节的write 函数将这些字符写到相关联的打开文件中。
# 零拷贝
> 讲零拷贝前,讲一下前置知识
## 标准设备
1. 来看一个标准设备不是真实存在的相当于一个逻辑上抽象的东西通过它来帮助我们更好地理解设备交互的机制。可以看到一个包含两部分重要组件的设备。第一部分是向系统其他部分展现的硬件接口interface。同软件一样硬件也需要一些接口让系统软件来控制它的操作。因此所有设备都有自己的特定接口以及典型交互的协议。
<img src="https://npm.elemecdn.com/youthlql@1.0.0/os/IO_and_Zero-copy/0002.png"/>
2. 第2部分是它的内部结构internal structure。这部分包含设备相关的特定实现负责具体实现设备展示给系统的抽象接口。
## 标准协议
1. 在上图中一个简化的设备接口包含3个寄存器一个状态status寄存器可以读取并查看设备的当前状态一个命令command寄存器用于通知设备执行某个具体任务一个数据data寄存器将数据传给设备或从设备接收数据。通过读写这些寄存器操作系统可以控制设备的行为
2. 我们现在来描述操作系统与该设备的典型交互,以便让设备为它做某事。协议如下:
```c++
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
```
3. 该协议包含4步。
- 第1步操作系统通过反复读取状态寄存器等待设备进入可以接收命令的就绪状态。我们称之为轮询polling设备基本上就是问它正在做什么
- 第2步操作系统下发数据到数据寄存器。例如你可以想象如果这是一个磁盘需要多次写入操作将一个磁盘块比如4KB传递给设备。如果主CPU参与数据移动就像这个示例协议一样我们就称之为编程的I/OprogrammedI/OPIO
- 第3步操作系统将命令写入命令寄存器这样设备就知道数据已经准备好了它应该开始执行命令。最后一步操作系统再次通过不断轮询设备等待并判断设备是否执行完成命令有可能得到一个指示成功或失败的错误码
4. 这个简单的协议好处是足够简单并且有效。但是难免会有一些低效和不方便。我们注意到这个协议存在的第一个问题就是轮询过程比较低效在等待设备执行完成命令时浪费大量CPU时间如果此时操作系统可以切换执行下一个就绪进程就可以大大提高CPU的利用率。
> 关键问题如何减少轮询开销操作系统检查设备状态时如何避免频繁轮询从而降低管理设备的CPU开销
## 利用中断减少CPU开销
**概念:**有了中断后CPU 不再需要不断轮询设备而是向设备发出一个请求然后就可以让对应进程睡眠切换执行其他任务。当设备完成了自身操作会抛出一个硬件中断引发CPU跳转执行操作系统预先定义好的中断服务例程InterruptService RoutineISR或更为简单的中断处理程序interrupt handler。中断处理程序是一小段操作系统代码它会结束之前的请求比如从设备读取到了数据或者错误码并且唤醒等待I/O的进程继续执行。
**例子:**
1. 没有中断时进程1在CPU上运行一段时间对应CPU那一行上重复的1然后发出一个读取数据的I/O请求给磁盘。如果没有中断那么操作系统就会简单自旋不断轮询设备状态直到设备完成I/O操作对应其中的p。当设备完成请求的操作后进程1又可以继续运行。
<img src="https://npm.elemecdn.com/youthlql@1.0.0/os/IO_and_Zero-copy/0003.png"/>
2. 有了中断后中断允许计算与I/O重叠overlap这是提高CPU利用率的关键。我们利用中断并允许重叠操作系统就可以在等待磁盘操作时做其他事情。
<img src="https://npm.elemecdn.com/youthlql@1.0.0/os/IO_and_Zero-copy/0004.png"/>
- 在这个例子中在磁盘处理进程1的请求时操作系统在CPU上运行进程2。磁盘处理完成后触发一个中断然后操作系统唤醒进程1继续运行。这样在这段时间无论CPU还是磁盘都可以有效地利用。
> 注意使用中断并非总是最佳方案。假如有一个非常高性能的设备它处理请求很快通常在CPU第一次轮询时就可以返回结果。此时如果使用中断反而会使系统变慢切换到其他进程处理中断再切换回之前的进程代价不小。因此如果设备非常快那么最好的办法反而是轮询。如果设备比较慢那么采用允许发生重叠的中断更好。如果设备的速度未知或者时快时慢可以考虑使用混合hybrid策略先尝试轮询一小段时间如果设备没有完成操作此时再使用中断。这种两阶段two-phased的办法可以实现两种方法的好处。
中断仍旧存在的缺点:
<img src="https://npm.elemecdn.com/youthlql@1.0.0/os/IO_and_Zero-copy/0005.png"/>
IO过程简述
1. 用户进程调用 read 方法向cpu发出 I/O 请求
2. cpu向磁盘发起IO请求给磁盘控制器**之后立马返回**。返回之后cpu可以切换到其它进程执行其他任务
3. 磁盘控制器收到指令后于是就开始进行磁盘IO磁盘IO完成后会把数据放入到磁盘控制器的内部缓冲区中然后产生一个**中断**
4. CPU 收到中断信号后停下手头的工作接着把磁盘控制器的缓冲区的数据读进内核的页缓存【这个过程是可以用DMA进行优化的】。
5. 接着将数据从内核页缓存拷贝到用户进程空间【这个过程想要优化只能用到我们上面说的异步IO】
6. 最后read()调用返回。
> 注意:
>
> 1. 这里很多博客画的图是错的讲的也是错的。使用中断减少CPU开销时在进行磁盘IO期间CPU可以执行其他的进程不必等待磁盘IO。【因为这是《操作系统导论》里的原话】
## 利用DMA进行更高效的数据传送
> 这里为什么要特别强调原文呢?因为可以让读者读的安心,这本经典书籍总不会出错吧
> 《操作系统导论》原文:
>
> 标准协议还有一点需要我们注意。具体来说如果使用编程的I/O将一大块数据传给设备CPU又会因为琐碎的任务而变得负载很重浪费了时间和算力本来更好是用于运行其他进程。下面的时间线展示了这个问题
>
> <img src="https://npm.elemecdn.com/youthlql@1.0.0/os/IO_and_Zero-copy/0006.png"/>
>
> 进程1在运行过程中需要向磁盘写一些数据所以它开始进行I/O操作将数据从内存拷贝到磁盘其中标示c的过程。**拷贝结束后磁盘上的I/O操作开始执行此时CPU才可以处理其他请求。**
也就是说在从**内存拷贝到磁盘或者从磁盘拷贝到内存**这个过程是可以使用DMADirect Memory Access优化的怎么优化呢
> 原文:
>
> DMA工作过程如下。为了能够将数据传送给设备操作系统会通过编程告诉DMA引擎数据在内存的位置要拷贝的大小以及要拷贝到哪个设备。在此之后操作系统就可以处理其他请求了。当DMA的任务完成后DMA控制器会抛出一个中断来告诉操作系统自己已经完成数据传输。修改后的时间线如下
>
> <img src="https://npm.elemecdn.com/youthlql@1.0.0/os/IO_and_Zero-copy/0007.png"/>
>
> 从时间线中可以看到数据的拷贝工作都是由DMA控制器来完成的。因为CPU在此时是空闲的所以操作系统可以让它做一些其他事情比如此处调度进程2到CPU来运行。因此进程2在进程1再次运行之前可以使用更多的CPU。
为了更好理解,看图:
<img src="https://npm.elemecdn.com/youthlql@1.0.0/os/IO_and_Zero-copy/0008.png"/>
过程:
1. 用户进程调用 read 方法向cpu发出 I/O 请求
2. cpu将IO请求交给DMA控制器**之后自己立马返回去执行其他进程的任务**
3. DMA向磁盘发起IO请求
4. 磁盘控制器收到指令后于是就开始进行磁盘IO磁盘IO完成后会把数据放入到磁盘控制器的内部缓冲区中然后产生一个**中断**。
5. DMA收到中断后把磁盘控制器的缓冲区的数据读进内核的页缓存接着抛出一个中断
6. 操作系统收到中断后调度cpu回来执行之前的进程将数据从内核页缓存拷贝到用户进程空间【这一步还是只能用异步IO来优化】
7. 最后read()调用返回。
## 零拷贝 - 传统文件IO
场景:将磁盘上的文件读取出来,然后通过网络协议发送给客户端。
<img src="https://npm.elemecdn.com/youthlql@1.0.0/os/IO_and_Zero-copy/0009.png"/>
1. 很明显发生了4次拷贝
* 第一次拷贝,把磁盘上的数据拷贝到操作系统内核的缓冲区里,这个拷贝是通过 DMA 的。
* 第二次拷贝,把内核缓冲区的数据拷贝到用户的缓冲区里,于是应用程序就可以使用这部分数据了,这个拷贝是由 CPU 完成的。
* 第三次拷贝,把刚才拷贝到用户的缓冲区里的数据,再拷贝到内核的 socket 的缓冲区里,这个过程依然由 CPU 完成的。
* *第四次拷贝*,把内核的 socket 缓冲区里的数据,拷贝到协议栈里,这个过程又是由 DMA 完成的。
2. 发生了4次用户上下文切换因为发生了两个系统调用read和write。一个系统调用对应两次上下文切换所以上下文切换次数在一般情况下只可能是偶数。
> 想要优化文件传输的性能就两个方向
>
> 1. 减少上下文切换次数
> 2. 减少数据拷贝次数
>
> 因为这两个是最耗时的
## 零拷贝之mmap
`read()` 系统调用的过程中会把内核缓冲区的数据拷贝到用户的缓冲区里,为了减少这一步开销,我们可以用 `mmap()` 替换 `read()` 系统调用函数。`mmap()` 系统调用函数会直接把内核缓冲区里的数据映射到用户空间,这样,操作系统内核与用户空间共享缓冲区,就不需要再进行任何的数据拷贝操作。
<img src="https://npm.elemecdn.com/youthlql@1.0.0/os/IO_and_Zero-copy/0010.png"/>
总的来说mmap减少了一次数据拷贝总共4次上下文切换3次数据拷贝
## 零拷贝之sendfile
`Linux2.1` 版本提供了 `sendFile` 函数,其基本原理如下:数据根本不经过用户态,直接从内核缓冲区进入到 `SocketBuffer`
<img src="https://npm.elemecdn.com/youthlql@1.0.0/os/IO_and_Zero-copy/0011.png"/>
总的来说有2次上下文切换3次数据拷贝。
## sendfile再优化
`Linux在2.4` 版本中,做了一些修改,避免了从内核缓冲区拷贝到 `Socketbuffer` 的操作,直接拷贝到协议栈,从而再一次减少了数据拷贝
<img src="https://npm.elemecdn.com/youthlql@1.0.0/os/IO_and_Zero-copy/0012.png" />
# 文件传输总结
## 小文件传输
前文一直提到了内核里的页缓存(PageCache),这个页缓存的作用就是用来提升小文件传输的效率
原因:
1. 读写磁盘相比读写内存的速度慢太多了,这个有点基础的人应该都知道,所以我们应该想办法把读写磁盘换成读写内存。于是,我们通过 DMA 把磁盘里的数据拷贝到内存里,这样就可以用读内存替换读磁盘。读磁盘数据的时候,优先在 PageCache 找,如果数据存在则可以直接返回;如果没有,则从磁盘中读取,然后缓存 PageCache 中。这点不是很类似redis和mysql的关系吗所以说操作系统里的一些设计理念和平时工作应用息息相关毕竟操作系统可是无数大牛的结晶
2. 程序运行的时候,具有局部性原理,也就是说刚被访问的数据在短时间内再次被访问的概率很高,通常称为热点数据,于是我们用 PageCache 来缓存这些热点数据,当空间不足时有对应的缓存淘汰策略。
## 大文件传输
QPageCache可以用来大文件传输吗
A不能
Q为什么呢
A
1. 假设你要几G的数据要传输用户访问这些大文件的时候内核会把它们载入 PageCache 中, PageCache 空间很快被这些大文件占满。
2. PageCache 由于长时间被大文件占据,其他热点小文件可能就无法使用到 PageCache就频繁读写磁盘效率低下
3. 而PageCache 中的大文件数据,没有享受到缓存带来的好处,反而却耗费 DMA 多拷贝到 PageCache 一次
4. 这前前后后加起来效率低了很多所以PageCache不适合小文件传输
而想不用到内核缓冲区,我们就想到了**直接IO**这个东西直接IO不经过内核缓存同时经过上面的讲述我们也可以知道异步IO效率是最高的。所以大文件传输最好的解决办法应该是**异步IO+直接IO**
> 读到这里你就会发现,我为何这样安排目录顺序了,前面讲到的,后面都会用到。
# 相关文章
- 《操作系统导论》 强推
- [漫画讲IO](https://mp.weixin.qq.com/s?__biz=Mzg3MjA4MTExMw==&mid=2247484746&idx=1&sn=c0a7f9129d780786cabfcac0a8aa6bb7&source=41&scene=21#wechat_redirect)
* 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](https://blog.csdn.net/younger_china/article/details/51127127)