当前位置: 首页 > Linux

Linux中零拷贝技术浅析

时间:2023-04-06 04:29:16 Linux

本文讨论了Linux中几种主要的零拷贝技术以及零拷贝技术的适用场景。为了快速建立零拷贝的概念,我们介绍一个常见的场景:在编写服务器程序(WebServer或文件服务器)时,文件下载是一个基本功能。此时服务器的任务是:将服务器主机磁盘中的文件不加修改地从连接的socket中发送出去。我们通常使用如下代码来完成:while((n=read(diskfd,buf,BUF_SIZE))>0)write(sockfd,buf,n);基本操作是从磁盘循环读取文件内容到缓冲区,然后将缓冲区内容发送到socket。但是因为linux的I/O操作默认是bufferedI/O。这里主要用到的两个系统调用是read和write。我们不知道操作系统在其中做了什么。实际上,在上述I/O操作中,发生了多次数据拷贝。当应用程序访问一段数据时,操作系统首先会检查该文件最近是否被访问过,文件内容是否缓存在内核缓冲区中。缓冲区的内容被复制到buf指定的用户空间缓冲区。如果没有,操作系统首先将磁盘上的数据复制到内核缓冲区中。这一步目前主要是依靠DMA进行传输,然后将内核缓冲区的内容复制到用户缓冲区中。接下来write系统调用将用户缓冲区的内容复制到网络栈相关的内核缓冲区,最后socket将内核缓冲区的内容发送给网卡。说了这么多还是看图清楚:数据副本从上图可以看出,一共生成了四个数据副本。即使使用DMA来处理与硬件的通信,CPU仍然需要处理两个数据副本。同时,用户态和内核态之间存在多次上下文切换,这无疑增加了CPU的负担。在这个过程中,我们并没有对文件内容做任何修改,所以在内核空间和用户空间之间来回拷贝数据无疑是一种浪费,而零拷贝主要就是为了解决这种低效率的问题。什么是零拷贝技术(zero-copy)?零拷贝的主要任务是防止CPU将数据从一块存储复制到另一块存储。执行这种简单的数据传输任务可以让CPU专注于其他任务。这允许更有效地使用系统资源。让我们回到引用中的例子,我们如何减少数据副本的数量?一个明显的重点是减少内核空间和用户空间之间来回的数据拷贝,这也引入了一种零拷贝:让数据传输不需要经过用户空间。我们可以使用mmap减少副本数量的一种方法是调用mmap()而不是read调用:buf=mmap(diskfd,len);写(sockfd,buf,len);应用程序调用mmap(),磁盘上的数据会被DMA复制到内核缓冲区,然后操作系统会将这个内核缓冲区共享给应用程序,这样就不需要将内核缓冲区的内容复制给用户空间。应用程序再次调用write(),操作系统直接将内核缓冲区的内容复制到socket缓冲区,这一切都发生在内核态,最后,socket缓冲区将数据发送到网卡。同样,看图也很简单:在mmap中使用mmap代替read,明显减少了一次拷贝,当拷贝的数据量很大时,无疑提高了效率。但是使用mmap是有代价的。当你使用mmap时,你可能会遇到一些隐藏的陷阱。例如,当你的程序映射了一个文件,但是当这个文件被另一个进程截断(truncate)时,write系统调用就会被SIGBUS信号终止,因为它访问了一个非法地址。默认情况下,SIGBUS信号将终止您的进程并生成核心转储。如果你的服务器以这种方式终止,将会造成损失。通常我们使用以下解决方案来避免此类问题:1.为SIGBUS信号建立信号处理程序当遇到SIGBUS信号时,信号处理程序简单地返回,write系统调用返回中断前写入的字节数,并且errno将被设置为成功,但这是一个糟糕的做法,因为你没有解决问题的真正核心。2.使用文件租用锁通常我们使用这种方法是对文件描述符使用租用锁。我们向内核申请文件的租约锁。当其他进程要截断文件时,内核会实时向我们发送一个RTSIGNALLEASE信号,告诉我们内核正在破坏你对该文件持有的读写锁。这样你的write系统调用就会在程序访问非法内存并被SIGBUS杀死之前被中断。write将返回写入的字节数并将errno设置为成功。我们应该在mmap之前锁定文件,操作完文件后解锁:if(fcntl(diskfd,F_SETSIG,RT_SIGNAL_LEASE)==-1){perror("kernelleasesetsignal");return-1;}/*l_type可以是F_RDLCKF_WRLCKlock*//*l_type可以是F_UNLCKunlock*/if(fcntl(diskfd,F_SETLEASE,l_type)){perror("kernelleasesettype");return-1;}从2.1版本开始使用sendfile从在内核的开始,Linux引入了sendfile来简化操作:#includessize_tsendfile(intout_fd,intin_fd,off_t*offset,size_tcount);系统调用sendfile()在表示输入文件的描述符infd中,在表示输出文件的描述符outfd之间传输文件内容(字节)。描述符outfd必须指向套接字,而infd指向的文件必须是可映射的。这些限制限制了sendfile的使用,使得sendfile只能将数据从文件传输到套接字,而不能反之。使用sendfile不仅减少了数据拷贝的次数,也减少了上下文切换,数据传输始终只发生在内核空间。sendfile系统调用进程如果另一个进程在我们调用sendfile时截断了文件,会发生什么情况?假设我们没有设置任何信号处理程序,sendfile调用只返回它在被中断之前传输的字节数,并且errno设置为成功。如果我们在调用sendfile之前锁定文件,sendfile的行为仍然和以前一样,我们仍然会收到RTSIGNALLEASE信号。至此,我们减少了数据副本的数量,但是还有一份,就是从pagecache到socketcache的副本。那么这个副本也可以省略吗?在硬件的帮助下,我们可以做到。之前我们将pagecache的数据复制到socketcache中,其实我们只需要将bufferdescriptor传递给socketbuffer,然后传递数据长度,这样DMA控制器就可以直接将pagecache中的数据进行打包即可将其发送到网络。综上所述,sendfile系统调用使用DMA引擎将文件内容复制到内核缓冲区,然后将带有文件位置和长度信息的缓冲区描述符添加到套接字缓冲区。这一步并没有将内核中的数据复制到socketbuffer中,DMA引擎会将内核buffer中的数据复制到协议引擎中,避免了最后的copy。SendfilewithDMA不过,这种收集和复制功能需要硬件和驱动程序的支持。使用splicesendfile只适用于将数据从文件复制到socket,这限制了它的使用范围。Linux在2.6.17版本中引入了splice系统调用以在两个文件描述符之间移动数据:#define_GNU_SOURCE/*Seefeature_test_macros(7)*/#includessize_tsplice(intfd_in,loff_t*off_in,intfd_out,loff_t*off_out,size_tlen,unsignedintflags);拼接调用在两个文件描述符之间移动数据,而无需在内核空间和用户空间之间来回复制数据。它将len长度的数据从fdin复制到fdout,但是一侧必须是管道设备,这也是splice的一些限制。flags参数具有以下值:SPLICEFMOVE:尝试移动数据而不是复制数据。这只是对内核的一个小提示:如果内核不能从管道中移动数据或者管道的缓存不是整页,数据仍然需要被复制。Linux原来的实现有一些问题,所以这个选项从2.6.21开始不起作用,以后的Linux版本应该实现它。SPLICEFNONBLOCK:拼接操作不会被阻塞。但是,如果未以非阻塞方式为I/O设置文件描述符,则对splice的调用可能仍会阻塞。SPLICEFMORE:后续的拼接调用将有更多的数据。splice调用使用了Linux提出的pipelinebuffer机制,所以至少有一个描述符必须是pipeline。上述零拷贝技术是通过减少用户空间和内核空间的数据拷贝来实现的,但有时,数据必须在用户空间和内核空间之间进行拷贝。这个时候我们只能在用户空间和内核空间的数据拷贝的时机上下功夫了。Linux通常使用写时复制(copyonwrite)来减少系统开销,这种技术通常被称为COW。由于篇幅原因,本文不详细介绍copy-on-write。大致的描述是:如果多个程序同时访问同一条数据,则每个程序都有一个指向这条数据的指针。从每个程序的角度来看,它独立拥有这条数据。当数据内容被修改时,数据内容会被复制到程序自身的应用空间中。这时候数据就变成了程序的私有数据。如果程序不需要修改数据,就永远不需要将数据复制到自己的应用程序空间。这减少了数据复制。写时复制内容可用于编写另一篇文章。..此外,还有一些零拷贝技术,比如在传统的LinuxI/O中加入O_DIRECT标志,实现直接I/O,避免自动缓存,还有不成熟的fbufs技术。本文并未涵盖所有零拷贝技术,只介绍一些常用的。有兴趣的可以自己研究。一般成熟的服务器项目也会对内核的I/O相关部分进行修改,以提高其数据传输速率。作者:卡巴拉之树_https://www.jianshu.com/p/fad...