乐趣区

关于linux:浅析-Linux-中的零拷贝技术

本文探讨 Linux 中 次要的几种零拷贝技术 以及零拷贝技术 实用的场景。为了迅速建设起零拷贝的概念,咱们拿一个罕用的场景进行引入:

引文

在写一个服务端程序时(Web Server 或者文件服务器),文件下载是一个基本功能。这时候服务端的工作是:将服务端主机磁盘中的文件不做批改地从已连贯的 socket 收回去,咱们通常用上面的代码实现:

  1. while((n = read(diskfd, buf, BUF_SIZE)) > 0)
  2. write(sockfd, buf , n);

基本操作就是循环的从磁盘读入文件内容到缓冲区,再将缓冲区的内容发送到 socket。然而因为 Linux 的 I / O 操作默认是缓冲 I /O。这外面次要应用的也就是 read 和 write 两个零碎调用,咱们并不知道操作系统在其中做了什么。实际上在以上 I / O 操作中,产生了屡次的数据拷贝。

当应用程序拜访某块数据时,操作系统首先会查看,是不是最近拜访过此文件,文件内容是否缓存在内核缓冲区,如果是,操作系统则间接依据 read 零碎调用提供的 buf 地址,将内核缓冲区的内容拷贝到 buf 所指定的用户空间缓冲区中去。如果不是,操作系统则首先将磁盘上的数据拷贝的内核缓冲区,这一步目前次要依附 DMA 来传输,而后再把内核缓冲区上的内容拷贝到用户缓冲区中。

接下来,write 零碎调用再把用户缓冲区的内容拷贝到网络堆栈相干的内核缓冲区中,最初 socket 再把内核缓冲区的内容发送到网卡上。说了这么多,不如看图分明:

数据拷贝

从上图中能够看出,共产生了四次数据拷贝,即便应用了 DMA 来解决了与硬件的通信,CPU 依然须要解决两次数据拷贝,与此同时,在用户态与内核态也产生了屡次上下文切换,无疑也减轻了 CPU 累赘。

在此过程中,咱们没有对文件内容做任何批改,那么在内核空间和用户空间来回拷贝数据无疑就是一种节约,而零拷贝次要就是为了解决这种低效性。

什么是零拷贝技术(zero-copy)?

零拷贝次要的工作就是防止 CPU 将数据从一块存储拷贝到另外一块存储,次要就是利用各种零拷贝技术,防止让 CPU 做大量的数据拷贝工作,缩小不必要的拷贝,或者让别的组件来做这一类简略的数据传输工作,让 CPU 解脱进去专一于别的工作。这样就能够让系统资源的利用更加无效。

咱们持续回到引文中的例子,咱们如何缩小数据拷贝的次数呢?一个很显著的着力点就是缩小数据在内核空间和用户空间来回拷贝,这也引入了零拷贝的一个类型:

让数据传输不须要通过 user space。

应用 mmap

咱们缩小拷贝次数的一种办法是调用 mmap()来代替 read 调用:

  1. buf = mmap(diskfd, len);
  2. write(sockfd, buf, len);

应用程序调用 mmap(),磁盘上的数据会通过 DMA 被拷贝的内核缓冲区,接着操作系统会把这段内核缓冲区与应用程序共享,这样就不须要把内核缓冲区的内容往用户空间拷贝。应用程序再调用 write(), 操作系统间接将内核缓冲区的内容拷贝到 socket 缓冲区中,这所有都产生在内核态,最初,socket 缓冲区再把数据发到网卡去。同样的,看图很简略:

mmap

应用 mmap 代替 read 很显著缩小了一次拷贝,当拷贝数据量很大时,无疑晋升了效率。然而应用 mmap 是有代价的。当你应用 mmap 时,你可能会遇到一些暗藏的陷阱。例如,当你的程序 map 了一个文件,然而当这个文件被另一个过程截断 (truncate) 时, write 零碎调用会因为拜访非法地址而被 SIGBUS 信号终止。SIGBUS 信号默认会杀死你的过程并产生一个 coredump, 如果你的服务器这样被停止了,那会产生一笔损失。

通常咱们应用以下解决方案防止这种问题:

1. 为 SIGBUS 信号建设信号处理程序

当遇到 SIGBUS 信号时,信号处理程序简略地返回,write 零碎调用在被中断之前会返回曾经写入的字节数,并且 errno 会被设置成 success, 然而这是一种蹩脚的解决方法,因为你并没有解决问题的本质外围。

2. 应用文件租借锁

通常咱们应用这种办法,在文件描述符上应用租借锁,咱们为文件向内核申请一个租借锁,当其它过程想要截断这个文件时,内核会向咱们发送一个实时的 RTSIGNALLEASE 信号,通知咱们内核正在毁坏你加持在文件上的读写锁。这样在程序拜访非法内存并且被 SIGBUS 杀死之前,你的 write 零碎调用会被中断。write 会返回曾经写入的字节数,并且置 errno 为 success。

咱们应该在 mmap 文件之前加锁,并且在操作完文件后解锁:

  1. if(fcntl(diskfd, F_SETSIG, RT_SIGNAL_LEASE) == -1) {
  2. perror("kernel lease set signal");
  3. return -1;
  4. }
  5. /* l_type can be F_RDLCK F_WRLCK 加锁 */
  6. /* l_type can be F_UNLCK 解锁 */
  7. if(fcntl(diskfd, F_SETLEASE, l_type)){
  8. perror("kernel lease set type");
  9. return -1;
  10. }

应用 sendfile

从 2.1 版内核开始,Linux 引入了 sendfile 来简化操作:

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

零碎调用 sendfile()在代表输出文件的描述符 infd 和代表输入文件的描述符 outfd 之间传送文件内容(字节)。描述符 outfd 必须指向一个套接字,而 infd 指向的文件必须是能够 mmap 的。这些局限限度了 sendfile 的应用,使 sendfile 只能将数据从文件传递到套接字上,反之则不行。

应用 sendfile 不仅缩小了数据拷贝的次数,还缩小了上下文切换,数据传送始终只产生在 kernel space。

sendfile 零碎调用过程

在咱们调用 sendfile 时,如果有其它过程截断了文件会产生什么呢?假如咱们没有设置任何信号处理程序,sendfile 调用仅仅返回它在被中断之前曾经传输的字节数,errno 会被置为 success。如果咱们在调用 sendfile 之前给文件加了锁,sendfile 的行为依然和之前雷同,咱们还会收到 RTSIGNALLEASE 的信号。

目前为止,咱们曾经缩小了数据拷贝的次数了,然而依然存在一次拷贝,就是页缓存到 socket 缓存的拷贝。那么能不能把这个拷贝也省略呢?

借助于硬件上的帮忙,咱们是能够办到的。之前咱们是把页缓存的数据拷贝到 socket 缓存中,实际上,咱们仅仅须要把缓冲区描述符传到 socket 缓冲区,再把数据长度传过来,这样 DMA 控制器间接将页缓存中的数据打包发送到网络中就能够了。

总结一下,sendfile 零碎调用利用 DMA 引擎将文件内容拷贝到内核缓冲区去,而后将带有文件地位和长度信息的缓冲区描述符增加 socket 缓冲区去,这一步不会将内核中的数据拷贝到 socket 缓冲区中,DMA 引擎会将内核缓冲区的数据拷贝到协定引擎中去,防止了最初一次拷贝。

带 DMA 的 sendfile

不过这一种收集拷贝性能是须要硬件以及驱动程序反对的。

应用 splice

sendfile 只实用于将数据从文件拷贝到套接字上,限定了它的应用范畴。Linux 在 2.6.17 版本引入 splice 零碎调用,用于在两个文件描述符中挪动数据:

  1. #define _GNU_SOURCE /* See feature_test_macros(7) */
  2. #include<fcntl.h>
  3. ssize_t splice(int fd_in, loff_t *off_in, int fd_out, loff_t *off_out, size_t len, unsignedint flags);

splice 调用在两个文件描述符之间挪动数据,而不须要数据在内核空间和用户空间来回拷贝。他从 fdin 拷贝 len 长度的数据到 fdout,然而有一方必须是管道设施,这也是目前 splice 的一些局限性。flags 参数有以下几种取值:

  • SPLICEFMOVE:尝试去挪动数据而不是拷贝数据。这仅仅是对内核的一个小提示:如果内核不能从 pipe 挪动数据或者 pipe 的缓存不是一个整页面,依然须要拷贝数据。Linux 最后的实现有些问题,所以从 2.6.21 开始这个选项不起作用,前面的 Linux 版本应该会实现。
  • SPLICEFNONBLOCK:splice 操作不会被阻塞。然而,如果文件描述符没有被设置为不可被阻塞形式的 I/O,那么调用 splice 有可能依然被阻塞。
  • SPLICEFMORE:前面的 splice 调用会有更多的数据。

splice 调用利用了 Linux 提出的管道缓冲区机制,所以至多一个描述符要为管道。

以上几种零拷贝技术都是缩小数据在用户空间和内核空间拷贝技术实现的,然而有些时候,数据必须在用户空间和内核空间之间拷贝。这时候,咱们只能针对数据在用户空间和内核空间拷贝的机会上下功夫了。Linux 通常利用写时复制 (copy on write) 来缩小零碎开销,这个技术又时常称作 COW。

因为篇幅起因,本文不具体介绍写时复制。大略形容下就是:如果多个程序同时拜访同一块数据,那么每个程序都领有指向这块数据的指针,在每个程序看来,本人都是独立领有这块数据的,只有当程序须要对数据内容进行批改时,才会把数据内容拷贝到程序本人的利用空间里去,这时候,数据才成为该程序的公有数据。如果程序不须要对数据进行批改,那么永远都不须要拷贝数据到本人的利用空间里。这样就缩小了数据的拷贝。写时复制的内容能够再写一篇文章了。。。

除此之外,还有一些零拷贝技术,比方传统的 Linux I/ O 中加上 O_DIRECT 标记能够间接 I /O,防止了主动缓存,还有尚未成熟的 fbufs 技术,本文尚未笼罩所有零拷贝技术,只是介绍常见的一些,如有趣味,能够自行钻研,个别成熟的服务端我的项目也会本人革新内核中无关 I / O 的局部,进步本人的数据传输速率。

作者:卡巴拉的树_
https://www.jianshu.com/p/fad…

退出移动版