IO类型

  • Buffered IO

每次读取都需要经过 PageCache 缓存

  • Direct IO

读取不经过 PageCache 缓存,调用系统应用时需要指定 O_DIRECT 参数。因为没有经过内存缓存的缘故,虽然数据本身会直接落地到磁盘,但是一些文件的其他元数据还是会缓存在内存中,因此我们在使用 Direct IO 时,需要配合使用 **fsync**进行强制文件写入磁盘。
某些具有自己缓存方案的应用,如数据库,则需要使用 Direct IO 来加速数据写入磁盘的速度

  • AIO

异步IO,linux 的 AIO 只能指定为 Direct IO 的形式,即不能经过 PageCache

PageCache 技术

文件一般是存放在磁盘中的,当需要读取磁盘中的数据时,都需要先把磁盘中的数据读入内存,才能被 CPU 访问。
为了避免每次读写文件时,都需要对硬盘进行读写操作,因此 linux 使用了PageCache(页缓存)机制对文件中的数据进行缓存。
当 IO 的访问优先经过 PageCache 时,称为Buffered IO。而可以通过使用O_DIRECT参数,使访问不经过 PageCache,则称为Direct IO
linux 系统默认使用的是Buffered IO
当我们使用mmap时,所映射的内核的内存空间也是位于 PageCache 中
image.png

页缓存读取

当 linux 系统在读取文件时,读取的流程如下:

  1. 进程调用read函数发起文件读取请求,这会发生用户态到内核态的切换
  2. 从内核文件系统VFS中找到文件的iNode信息,然后计算出要读取的具体的页
  3. PageCache中查找对应页的缓存,1)如果页缓存命中,则直接返回文件内容;2)如果没有对应的页缓存,则会产生一个缺页异常。这时系统会创建新的空的页缓存并从磁盘中读取文件内容,更新页缓存,再跳转到读PageCache的流程
  4. 读取文件成功并返回

    对于所有文件的读取,无论最初有没有命中页缓存,最终都会直接来源于页缓存

页缓存写入

当 linux 系统在写入文件时,写入的流程如下:

  1. 进程调用write函数发起文件写入请求,这会发生用户态到内核态的切换
  2. 如果写请求不命中PageCache,而且写入的 offset 或者数据的大小不与 4KB 对齐,则会先进行读IO
  3. PageCache中找到对应的文件页,将更新的内容写入到文件缓存,然后将其标记为dirty

    脏页回写

    linux 内核有单独的线程负责定时回写页缓存,在以下的三种情况下会触发回写:

  4. 空闲内存低于阈值,需要释放掉部分页缓存,此时如果页缓存为dirty,则会写回磁盘,此时依据的是LRU规则

  5. 脏页在缓存的时间超过了阈值
  6. 用户进程主动调用syncfsync系统调用时,会强制进程回写

    int sync():不等待写入结果; int fsync(int filedes):阻塞等待到对应的文件写入成功后才会返回

优缺点

PageCache 主要有以下两个优点:

  1. 缓存最近被访问的数据,从而避免了频繁读取同文件时的重复加载问题
  2. 预读功能,提升读取性能,默认最大是32个Page(128KB)

但是,也具有以下的缺点:

  1. 额外占用了内存
  2. 当读取大文件时,会因为占用了整个 PageCache 而导致其他程序无法利用 PageCache 带来的缓存功能,反而降低了性能。因此对于大文件的读取,应该使用Direct IO + 异步IO,在 nginx 中,就可使用该配置方式
    1. location /video/ {
    2. aio on;
    3. directio 1024m;
    4. }

    DMA 技术

    在未有DMA技术之前,IO 读取的每个环节都需要 CPU 参与,这导致了总体性能是不高的
    image.png

为了减轻 CPU 的压力,因此发明了DMA技术,即直接内存访问技术,在进行 IO 设备和内存数据传输时,数据的搬运都交给 DMA 控制器,而 CPU 不再需要参与数据的搬运。
因此,现在的数据传输流程是这样的:
image.png

传统的文件传输

在使用零拷贝技术之前,尽管有了DMA技术,但是文件的读取还是有很大的开销,伴随着频繁的用户空间和内核空间切换,以及额外的数据复制。
一般我们使用以下两个系统调用:

  1. read(file, tmp_buf, len);
  2. write(socket, tmp_buf, len);

在这两个系统调用背后,文件数据的读取流程如图:
image.png整个流程共发生了4次用户态和内核态的切换,当系统调用时都会先从用户态切换到内核态,而等内核完成处理之后,就又从内核态切换回用户态
在这个过程中,还发生了4次数据拷贝,其中两次是DMA拷贝,而另外的两次需要通过CPU拷贝

零拷贝技术

零拷贝技术通常有两种实现方式:

  1. mmap + write
  2. sendfile

    mmap + write

    当使用mmap替换 read 函数之后,调用的系统函数如下:
    1. buf = mmap(file, len);
    2. write(sockfd, buf, len);
    mmap函数会直接把内核缓冲区的数据直接映射到用户空间,这样就可减少一次CPU拷贝
    image.png
    整个流程共发生了4次用户态和内核态的切换,以及3次数据拷贝

    sendfile

    在 linux 内核版本 2.1 以上,提供了sendfile系统函数,该函数签名如下: ```cpp

    include

    ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count);

/ int out_fd:目的端的文件描述符 int in_fd:输入端的文件描述符 off_t offset:输入端的文件偏移量 size_t count:目标复制数据的长度

return: 实际复制数据的长度 */

  1. 通过该系统函数,即可减少一次系统函数的调用,从而减少了`2次`用户态和内核态的切换<br />![image.png](https://cdn.nlark.com/yuque/0/2022/png/27017064/1655948664574-d30bc093-2183-4f1e-a550-e04a84334889.png#clientId=u7bdb8f4f-79c8-4&crop=0&crop=0&crop=1&crop=1&from=paste&id=u957694ec&margin=%5Bobject%20Object%5D&name=image.png&originHeight=686&originWidth=1100&originalType=url&ratio=1&rotation=0&showTitle=false&size=116767&status=done&style=none&taskId=ub5f50ebe-a535-4d41-84d8-b05b5f5f9f8&title=)
  2. <a name="YfwLH"></a>
  3. ## SG-DMA 技术
  4. 如果网卡本身支持`SG-DMA`技术,则可以减少通过 CPU 把内核缓冲区中的数据拷贝到 socket 缓冲区的过程<br />可通过以下命令,查看本机网卡是否支持 scatter-gather 技术:
  5. ```shell
  6. rxsi@VM-20-9-debian:~$ /usr/sbin/ethtool -k eth0 | grep scatter-gather
  7. scatter-gather: off
  8. tx-scatter-gather: off [fixed]
  9. tx-scatter-gather-fraglist: off [fixed]

从 linux 内核2.4版本开始起,在网卡支持SG-DMA技术下,sendfile系统调用即可减少1次CPU 拷贝
image.png

使用零拷贝的技术

RocketMQ

以 RMQ 的消息写入源码为例,可以看到其中的实现如下:

  1. public PutMessageResult putMessage(final MessageExtBrokerInner msg) {
  2. // ...省略部分代码
  3. //获取内存映射文件句柄
  4. MappedFile mappedFile = this.mappedFileQueue.getLastMappedFile();
  5. //...省略部分代码
  6. //重点来了,调用MappedFile.appendMessage方法将消息字节追加到共享内存中,由操作系统或者后台刷盘线程完成刷盘的动作
  7. result = mappedFile.appendMessage(msg, this.appendMessageCallback);
  8. //...省略部分代码
  9. }

可见生产者在把数据持久化到磁盘时,使用的是mmap + write方式

再看 RMQ 的消息读取源码:

  1. public GetMessageResult getMessage(final String group, final String topic, final int queueId, final long offset,
  2. final int maxMsgNums,
  3. final MessageFilter messageFilter) {
  4. // ...省略部分代码
  5. //根据 offset 找到对应的 ConsumeQueue 的 MappedFile
  6. SelectMappedBufferResult bufferConsumeQueue = consumeQueue.getIndexBuffer(offset);
  7. // ...省略部分代码
  8. }

消费者的消息读取也主要依赖于mmap + write形式

kafka

当 Producer 生产者发送数据到 broker 时,采用mmap + write的形式,实现顺序的快速写入
当 Consumer 消费者从 boker 读取数据时,采用sendfile的形式,将磁盘文件读到系统内核后,直接通过 socket buffer 进行数据发送

nginx

nginx 可以通过配置开启零拷贝技术

  1. http {
  2. ...
  3. sendfile on
  4. ...
  5. }

大文件传输

前面在 PageCache 一节介绍其缺点时,指出了在操作大文件时,会由于占用了 PageCache 而导致后续的操作无法利用到 PageCache 提供的缓存作用,因此可以使用 Direct IO 的方式绕过。
同时为了避免同步 IO 的阻塞问题,可以使用 AIO 进行异步处理,而 linux 的 AIO 只能使用 Direct IO 形式,因此对于大文件的操作,我们应该使用AIO + Direct IO的形式
image.png