欢迎加入QQ讨论群258996829
麦子学院 头像
苹果6袋
6
麦子学院

Linux学习之零拷贝技术详解

发布时间:2017-03-24 15:57  回复:0  查看:2258   最后回复:2017-03-24 15:57  
本文探讨 Linux系统 主要的几种零拷贝技术  以及零拷贝技术 适用的场景  。为了迅速建立起零拷贝的概念,我们拿一个常用的场景进行引入:
   引文
  在写一个服务端程序时(Web Server 或者文件服务器),文件下载是一个基本功能。这时候服务端的任务是: 将服务端主机磁盘中的文件不做修改地从已连接的 socket 发出去 ,我们通常用下面的代码完成:
  while(( n = read( diskfd, buf, BUF_SIZE)) > 0)
  write( sockfd, buf , n);
  基本操作就是循环的从磁盘读入文件内容到缓冲区,再将缓冲区的内容发送到  socket  。但是由于Linux 的  I/O  操作默认是缓冲  I/O  。这里面主要使用的也就是  read    write  两个系统调用,我们并不知道操作系统在其中做了什么。实际上在以上  I/O  操作中,发生了多次的数据拷贝。
  当应用程序访问某块数据时,操作系统首先会检查,是不是最近访问过此文件,文件内容是否缓存在内核缓冲区,如果是,操作系统则直接根据  read  系统调用提供的  buf  地址,将内核缓冲区的内容拷贝到  buf  所指定的用户空间缓冲区中去。如果不是,操作系统则首先将磁盘上的数据拷贝的内核缓冲区,这一步目前主要依靠  DMA  来传输,然后再把内核缓冲区上的内容拷贝到用户缓冲区中。
  接下来,  write  系统调用再把用户缓冲区的内容拷贝到网络堆栈相关的内核缓冲区中,最后  socket  再把内核缓冲区的内容发送到网卡上。
  说了这么多,不如看图清楚:
Linux学习之零拷贝技术详解
数据拷贝
  从上图中可以看出,共产生了四次数据拷贝,即使使用了  DMA  来处理了与硬件的通讯,CPU 仍然需要处理两次数据拷贝,与此同时,在用户态与内核态也发生了多次上下文切换,无疑也加重了 CPU 负担。
  在此过程中,我们没有对文件内容做任何修改,那么在内核空间和用户空间来回拷贝数据无疑就是一种浪费,而零拷贝主要就是为了解决这种低效性。
   什么是零拷贝技术(zero-copy)?
  零拷贝主要的任务就是  避免 CPU 将数据从一块存储拷贝到另外一块存储,主要就是利用各种零拷贝技术,避免让 CPU 做大量的数据拷贝任务,减少不必要的拷贝,或者让别的组件来做这一类简单的数据传输任务,让 CPU 解脱出来专注于别的任务。这样就可以让系统资源的利用更加有效。
  我们继续回到引文中的例子,我们如何减少数据拷贝的次数呢?一个很明显的着力点就是减少数据在内核空间和用户空间来回拷贝,这也引入了零拷贝的一个类型:
   让数据传输不需要经过user space
   使用mmap
  我们减少拷贝次数的一种方法是调用mmap() 来代替 read 调用:
  buf = mmap( diskfd, len);
  write( sockfd, buf, len);
  应用程序调用  mmap()  ,磁盘上的数据会通过  DMA  被拷贝的内核缓冲区,接着操作系统会把这段内核缓冲区与应用程序共享,这样就不需要把内核缓冲区的内容往用户空间拷贝。应用程序再调用  write() , 操作系统直接将内核缓冲区的内容拷贝到  socket  缓冲区中,这一切都发生在内核态,最后,  socket  缓冲区再把数据发到网卡去。
  同样的,看图很简单:
Linux学习之零拷贝技术详解
mmap
  使用mmap 替代 read 很明显减少了一次拷贝,当拷贝数据量很大时,无疑提升了效率。但是使用  mmap  是有代价的。当你使用  mmap  时,你可能会遇到一些隐藏的陷阱。例如,当你的程序  map  了一个文件,但是当这个文件被另一个进程截断(truncate) , write 系统调用会因为访问非法地址而被  SIGBUS  信号终止。  SIGBUS  信号默认会杀死你的进程并产生一个  coredump , 如果你的服务器这样被中止了,那会产生一笔损失。
  通常我们使用以下解决方案避免这种问题:
  1.  SIGBUS信号建立信号处理程序
  当遇到  SIGBUS  信号时,信号处理程序简单地返回,  write  系统调用在被中断之前会返回已经写入的字节数,并且  errno  会被设置成success, 但是这是一种糟糕的处理办法,因为你并没有解决问题的实质核心。
  2.  使用文件租借锁
通常我们使用这种方法,在文件描述符上使用租借锁,我们为文件向内核申请一个租借锁,当其它进程想要截断这个文件时,内核会向我们发送一个实时的 RT_SIGNAL_LEASE  信号,告诉我们内核正在破坏你加持在文件上的读写锁。这样在程序访问非法内存并且被  SIGBUS  杀死之前,你的  write  系统调用会被中断。  write  会返回已经写入的字节数,并且置  errno  success
  我们应该在  mmap  文件之前加锁,并且在操作完文件后解锁:
   if(fcntl(diskfd, F_SETSIG, RT_SIGNAL_LEASE) == -1) {
  perror("kernel lease set signal");
   return -1;
  }/* l_type can be F_RDLCK F_WRLCK   加锁 *//* l_type can be  F_UNLCK  解锁 */ if(fcntl(diskfd, F_SETLEASE, l_type)){
  perror("kernel lease set type");
   return -1;
  }
   使用sendfile
  从2.1 版内核开始, Linux 引入了  sendfile  来简化操作:
  # includessize_t sendfile( int out_fd,  int in_fd,  off_t *offset,  size_t count);
  系统调用  sendfile()  在代表输入文件的描述符  in_fd  和代表输出文件的描述符  out_fd  之间传送文件内容(字节)。描述符  out_fd  必须指向一个套接字,而  in_fd  指向的文件必须是可以 mmap  的。这些局限限制了  sendfile  的使用,使  sendfile  只能将数据从文件传递到套接字上,反之则不行。
  使用  sendfile  不仅减少了数据拷贝的次数,还减少了上下文切换,数据传送始终只发生在  kernel space 
Linux学习之零拷贝技术详解
sendfile 系统调用过程
  在我们调用  sendfile  时,如果有其它进程截断了文件会发生什么呢?假设我们没有设置任何信号处理程序,  sendfile  调用仅仅返回它在被中断之前已经传输的字节数,  errno  会被置为success 。如果我们在调用 sendfile 之前给文件加了锁,  sendfile  的行为仍然和之前相同,我们还会收到RT_SIGNAL_LEASE 的信号。
  目前为止,我们已经减少了数据拷贝的次数了,但是仍然存在一次拷贝,就是页缓存到socket 缓存的拷贝。那么能不能把这个拷贝也省略呢?
  借助于硬件上的帮助,我们是可以办到的。之前我们是把页缓存的数据拷贝到socket 缓存中,实际上,我们仅仅需要把缓冲区描述符传到  socket  缓冲区,再把数据长度传过去,这样  DMA  控制器直接将页缓存中的数据打包发送到网络中就可以了。
  总结一下,  sendfile  系统调用利用  DMA  引擎将文件内容拷贝到内核缓冲区去,然后将带有文件位置和长度信息的缓冲区描述符添加socket 缓冲区去,这一步不会将内核中的数据拷贝到 socket 缓冲区中,  DMA  引擎会将内核缓冲区的数据拷贝到协议引擎中去,避免了最后一次拷贝。
Linux学习之零拷贝技术详解
DMA sendfile
  不过这一种收集拷贝功能是需要硬件以及驱动程序支持的。
   使用splice
  sendfile 只适用于将数据从文件拷贝到套接字上,限定了它的使用范围。 Linux 在  2.6.17  版本引入  splice  系统调用,用于在两个文件描述符中移动数据:
  # define _GNU_SOURCE         /* See feature_test_macros(7) */# include  ssize_t splice( int fd_in,  loff_t *off_in,  int fd_out,  loff_t *off_out,  size_t len, unsigned  int flags);
  splice 调用在两个文件描述符之间移动数据,而不需要数据在内核空间和用户空间来回拷贝。他从  fd_in  拷贝  len  长度的数据到  fd_out  ,但是有一方必须是管道设备,这也是目前  splice 的一些局限性。  flags  参数有以下几种取值:
  ·  SPLICE_F_MOVE  :尝试去移动数据而不是拷贝数据。这仅仅是对内核的一个小提示:如果内核不能从  pipe  移动数据或者  pipe  的缓存不是一个整页面,仍然需要拷贝数据。Linux 最初的实现有些问题,所以从  2.6.21  开始这个选项不起作用,后面的Linux 版本应该会实现。
  ·  SPLICE_F_NONBLOCK    splice  操作不会被阻塞。然而,如果文件描述符没有被设置为不可被阻塞方式的 I/O  ,那么调用  splice  有可能仍然被阻塞。
  ·  SPLICE_F_MORE    后面的  splice  调用会有更多的数据。
  splice 调用利用了 Linux 提出的管道缓冲区机制, 所以至少一个描述符要为管道。
  以上几种零拷贝技术都是减少数据在用户空间和内核空间拷贝技术实现的,但是有些时候,数据必须在用户空间和内核空间之间拷贝。这时候,我们只能针对数据在用户空间和内核空间拷贝的时机上下功夫了。Linux 通常利用  写时复制(copy on write)  来减少系统开销,这个技术又时常称作  COW 
  由于篇幅原因,本文不详细介绍写时复制。大概描述下就是:如果多个程序同时访问同一块数据,那么每个程序都拥有指向这块数据的指针,在每个程序看来,自己都是独立拥有这块数据的,只有当程序需要对数据内容进行修改时,才会把数据内容拷贝到程序自己的应用空间里去,这时候,数据才成为该程序的私有数据。如果程序不需要对数据进行修改,那么永远都不需要拷贝数据到自己的应用空间里。这样就减少了数据的拷贝。写时复制的内容可以再写一篇文章了。。。
  除此之外,还有一些零拷贝技术,比如传统的Linux I/O 中机上  O_DIRECT  标记可以直接  I/O ,避免了自动缓存,还有尚未成熟的  fbufs  技术,本文尚未覆盖所有零拷贝技术,只是介绍了常见的一些,如有兴趣,可以自行研究,一般成熟的服务端项目也会自己改造内核,提高自己的数据传输速率。
来源: 简书
您还未登录,请先登录

热门帖子

最新帖子