0%

linux零拷贝技术讲解

磁盘可以说是计算机系统最慢的硬件之一,读写速度相差内存 10 倍以上,所以针对优化磁盘的技术非常的多,比如零拷贝、直接 I/O、异步 I/O 等等,这些优化的目的就是为了提高系统的吞吐量,另外操作系统内核中的磁盘高速缓存区,可以有效的减少磁盘的访问次数。

1. DMA

1.1 DMA前(CPU参与搬运数据)

在没有 DMA 技术前,I/O 的过程是这样的:

  • CPU 发出对应的指令给磁盘控制器,然后返回;
  • 磁盘控制器收到指令后,于是就开始准备数据,会把数据放入到磁盘控制器的内部缓冲区中,然后产生一个中断;
  • CPU 收到中断信号后,停下手头的工作,接着把磁盘控制器的缓冲区的数据一次一个字节地读进自己的寄存器,然后再把寄存器里的数据写入到内存,而在数据传输的期间 CPU 是无法执行其他任务的。
img

整个数据的传输过程,都要需要 CPU 亲自参与搬运数据的过程,而且这个过程,CPU 是不能做其他事情的。计算机科学家们发现了事情的严重性后,于是就发明了 DMA 技术,也就是直接内存访问(Direct Memory Access) 技术。

1.2 引入DMA(减少CPU搬运)

在进行 I/O 设备和内存的数据传输的时候,数据搬运的工作全部交给 DMA 控制器,而 CPU 不再参与任何与数据搬运相关的事情,这样 CPU 就可以去处理别的事务。

img

具体过程:

  • 用户进程调用 read 方法,向操作系统发出 I/O 请求,请求读取数据到自己的内存缓冲区中,进程进入阻塞状态;
  • 操作系统收到请求后,进一步将 I/O 请求发送 DMA,然后让 CPU 执行其他任务;
  • DMA 进一步将 I/O 请求发送给磁盘;
  • 磁盘收到 DMA 的 I/O 请求,把数据从磁盘读取到磁盘控制器的缓冲区中,当磁盘控制器的缓冲区被读满后,向 DMA 发起中断信号,告知自己缓冲区已满;
  • DMA 收到磁盘的信号,将磁盘控制器缓冲区中的数据拷贝到内核缓冲区中,此时不占用 CPU,CPU 可以执行其他任务;
  • 当 DMA 读取了足够多的数据,就会发送中断信号给 CPU;
  • CPU 收到 DMA 的信号,知道数据已经准备好,于是将数据从内核拷贝到用户空间,系统调用返回;

1.3 read() 和 write()

从服务器获取文件的代码通常如下,一般会需要两个系统调用:

1
2
read(file, tmp_buf, len);
write(socket, tmp_buf, len);
img
  1. 发生了两次系统调用,一次是 read() ,一次是 write()
  2. 数据搬运了4次,DMA拷贝了2次,CPU拷贝了两次。

2. 零拷贝

2.1 mmap 后 write(2系统调用3拷贝)

不算真正的零拷贝。

mmap() 系统调用函数会直接把内核缓冲区里的数据「映射」到用户空间,操作系统直接将内核缓冲区的数据拷贝到 socket 缓冲区中,这一切都发生在内核态。

1
2
buf = mmap(file, len);
write(sockfd, buf, len);
mmap + write 零拷贝
  1. 发生了两次系统调用,一次是 mmap() ,一次是 write()
  2. 数据搬运了3次,DMA拷贝了2次,CPU拷贝了1次。

2.2 sendfile (1系统调用2拷贝)

在 Linux 内核版本 2.1 中,提供了一个专门发送文件的系统调用函数 sendfile(),函数形式如下:

1
2
#include <sys/socket.h>
ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count);

从 Linux 内核 2.4 版本开始起,对于支持网卡支持 SG-DMA 技术的情况下, sendfile() 系统调用的过程发生了点变化,具体过程如下:这个过程之中,只进行了 2 次数据拷贝。

从DMA 将磁盘上的数据拷贝到内核缓冲区里;缓冲区描述符和数据长度传到 socket 缓冲区,这样网卡的 SG-DMA 控制器就可以直接将内核缓存中的数据拷贝到网卡的缓冲区里。

  1. 发生了1次系统调用,sendfile()。

  2. 数据搬运了2次。 DMA一次,SG-DMA一次。

img

没有通过 CPU 来搬运数据,所有的数据都是通过 DMA 来进行传输的。这就是所谓的零拷贝(Zero-copy)技术。

3. 头脑风暴

3.1 使用零拷贝技术的项目

事实上,Kafka 这个开源项目,就利用了「零拷贝」技术,从而大幅提升了 I/O 的吞吐率,这也是 Kafka 在处理海量数据为什么这么快的原因之一。

如果你追溯 Kafka 文件传输的代码,你会发现,最终它调用了 Java NIO 库里的 transferTo 方法,如果 Linux 系统支持 sendfile() 系统调用,那么 transferTo() 实际上最后就会使用到 sendfile() 系统调用函数。

另外,Nginx 也支持零拷贝技术,一般默认是开启零拷贝技术,这样有利于提高文件传输的效率,是否开启零拷贝技术的配置如下:

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

sendfile 配置的具体意思:

  • 设置为 on 表示,使用零拷贝技术来传输文件:sendfile ,这样只需要 2 次上下文切换,和 2 次数据拷贝。
  • 设置为 off 表示,使用传统的文件传输技术:read + write,这时就需要 4 次上下文切换,和 4 次数据拷贝。

3.2 传输大文件不要零拷贝

传输大文件的时候,由于大文件难以命中 PageCache 缓存,而且会占满 PageCache 导致「热点」文件无法充分利用缓存,从而增大了性能开销,因此,这时应该使用直接 I/O。

所以,传输文件的时候,我们要根据文件的大小来使用不同的方式:

  • 传输大文件的时候,使用「异步 I/O + 直接 I/O」;
  • 传输小文件的时候,则使用「零拷贝技术」;

在 nginx 中,我们可以用如下配置,来根据文件的大小来使用不同的方式:

1
2
3
4
5
location /video/ { 
sendfile on;
aio on;
directio 1024m;
}

当文件大小大于 directio 值后,使用「异步 I/O + 直接 I/O」,否则使用「零拷贝技术」。

3.3 总结

  • 没有零拷贝会发生 4 次拷贝。DMA 从磁盘到内核缓存,内核到用户缓存,用户缓存到 Socket 缓存,Socket 缓存到网卡。
  • mmap(内核缓存和用户缓存映射)+write,减少一次 cpu 拷贝。
  • sendfile 给 socket 发送描述符和长度,内核缓存直接到网卡。

4. 参考资料

给作者打赏,可以加首页微信,咨询作者相关问题!