本文讨论了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来简化操作:#include
