当前位置: 首页 >> 程序设计 >> Linux中的"零拷贝"剖析
 

Linux中的"零拷贝"剖析

作者:      来源:http://blog.csdn.net/lovekatherine     发表时间:2007-03-25     浏览次数:      字号:    

Zero Copy I: User-Mode Perspective

Author By Dragan Stancevic

Translated By Love. Katherine

 

(解释Linux中的"零拷贝"特性,为什么有用以及何处需要改进之处)

 

如今几乎每个人都听说过Linux中所谓的"零拷贝"特性,然而我经常碰到没有充分理解这个问题的人们。因此,我决定写一些文章略微深入的讲述这个问题,希望能将这个有用的特性解释清楚。在本文中,将从用户空间应用程序的角度来阐述这个问题,因此有意忽略了复杂的内核实现。

 

什么是零拷贝

 

为了更好的理解问题的解决法,我们首先需要理解问题本身。首先我们以一个网络服务守护进程为例,考虑它在将存储在文件中的信息通过网络传送给客户这样的简单过程中,所涉及的操作。下面是其中的部分简单代阿:

 

       read(file, tmp_buf, len);

       write(socket, tmp_buf, len);

 

看起来不能更简单了。你也许认为执行这两个系统调用并未产生多少开销。实际上,这简直错的一塌糊涂。在执行这两个系统调用的过程中,目标数据至少被复制了4次,同时发生了同样多次数的用户/内核空间的切换(实际上该过程远比此处描述的要复杂,但是我希望以简单的方式描述之,以更好的理解本文的主题)

 

为了更好的理解这两句代码所涉及的操作,请看图1。图的上半部展示了上下文切换,而下半部展示了复制操作。

Figure 1. Copying in Two Sample System Calls

 

 

 

步骤一:系统调用read导致了从用户空间到内核空间的上下文切换。DMA模块从磁盘中读取文件内容,并将其存储在内核空间的缓冲区内,完成了第1次复制。

 

步骤二:数据从内核空间缓冲区复制到用户空间缓冲区,之后系统调用read返回,这导致了从内核空间向用户空间的上下文切换。此时,需要的数据已存放在指定的用户空间缓冲区内(参数tmp_buf),程序可以继续下面的操作。

 

步骤三:系统调用write导致从用户空间到内核空间的上下文切换。数据从用户空间缓冲区被再次复制到内核空间缓冲区,完成了第3次复制。不过,这次数据存放在内核空间中与使用的socket相关的特定缓冲区中,而不是步骤一中的缓冲区。

 

步骤四:系统调用返回,导致了第4次上下文切换。第4次复制在DMA模块将数据从内核空间缓冲区传递至协议引擎的时候发生,这与我们的代码的执行是独立且异步发生的。你可能会疑惑:“为何要说是独立、异步?难道不是在write系统调用返回前数据已经被传送了?write系统调用的返回,并不意味着传输成功——它甚至无法保证传输的开始。调用的返回,只是表明以太网驱动程序在其传输队列中有空位,并已经接受我们的数据用于传输。可能有众多的数据排在我们的数据之前。除非驱动程序或硬件采用优先级队列的方法,各组数据是依照FIFO的次序被传输的(1中叉状的DMA copy表明这最后一次复制可以被延后)

 

正如你所看到的,上面的过程中存在很多的数据冗余。某些冗余可以被消除,以减少开销、提高性能。作为一名驱动程序开发人员,我的工作围绕着拥有先进特性的硬件展开。某些硬件支持完全绕开内存,将数据直接传送给其他设备的特性。这一特性消除了系统内存中的数据副本,因此是一种很好的选择,但并不是所有的硬件都支持。此外,来自于硬盘的数据必须重新打包(地址连续)才能用于网络传输,这也引入了某些复杂性。为了减少开销,我们可以从消除内核缓冲区与用户缓冲区之间的复制入手。

 

消除复制的一种方法是将read系统调用,改为mmap系统调用,例如:

 

tmp_buf = mmap(file, len);

write(socket, tmp_buf, len);

 

为了更好的理解这其中设计的操作,请看图2。上下文切换部分与图1保持一致。

Figure 2. Calling mmap

 

 

步骤一:mmap系统调用导致文件的内容通过DMA模块被复制到内核缓冲区中,该缓冲区之后与用户进程共享,这样就内核缓冲区与用户缓冲区之间的复制就不会发生。

 

步骤二:write系统调用导致内核将数据从内核缓冲区复制到与socket相关联的内核缓冲区中。

 

步骤三:DMA模块将数据由socket的缓冲区传递给协议引擎时,第3次复制发生。

 

通过调用mmap而不是read,我们已经将内核需要执行的复制操作减半。当有大量数据要进行传输是,这将有相当良好的效果。然而,性能的改进需要付出代价的;是用mmapwrite这种组合方法,存在着一些隐藏的陷阱。例如,考虑一下在内存中对文件进行映射后调用write,与此同时另外一个进程将同一文件截断的情形。此时write系统调用会被进程接收到的SIGBUS信号中断,因为当前进程访问了非法内存地址。对SIGBUS信号的默认处理是杀死当前进程并生成dump core文件——而这对于网络服务器程序而言不是最期望的操作。

有两种方式可用于解决该问题:

 

第一种方式是为SIGBUS信号设置信号处理程序,并在处理程序中简单的执行return语句。在这样处理方式下,write系统调用返回被信号中断前已写的字节数,并将errno全局变量设置为成功。必须指出,这并不是个好的解决方式——治标不治本。由于收到SIGBUS信号意味着进程发生了严重错误,我不鼓励采取这种解决方式。

 

第二种方式应用了文件租借(在Microsoft Windows系统中被称为“机会锁”)。这才是解劝前面问题的正确方式。通过对文件描述符执行租借,可以同内核就某个特定文件达成租约。从内核可以获得读/写租约。当另外一个进程试图将你正在传输的文件截断时,内核会向你的进程发送实时信号——RT_SIGNAL_LEASE。该信号通知你的进程,内核即将终止在该文件上你曾获得的租约。这样,在write调用访问非法内存地址、并被随后接收到的SIGBUS信号杀死之前,write系统调用就被RT_SIGNAL_LEASE信号中断了。write的返回值是在被中断前已写的字节数,全局变量errno设置为成功。下面是一段展示如何从内核获得租约的示例代码。

 

if(fcntl(fd, F_SETSIG, RT_SIGNAL_LEASE) == -1) {

    perror("kernel lease set signal");

    return -1;

}

/* l_type can be F_RDLCK F_WRLCK */

if(fcntl(fd, F_SETLEASE, l_type)){

    perror("kernel lease set type");

    return -1;

}

 

在对文件进行映射前,应该先获得租约,并在结束write操作后结束租约。这是通过在fcntl调用中指定租约类型为F_UNLCK了来实现的。

 

Sendfile

 

sendfile系统调用在内核版本2.1中被引入,目的是简化通过网络在两个本地文件之间进行的数据传输过程。sendfile系统调用的引入,不仅减少了数据复制,还减少了上下文切换的次数。使用方法如下:

sendfile(socket, file, len);

 

为了更好的理解所涉及的操作,请看图3

Figure 3. Replacing Read and Write with Sendfile

 

步骤一:sendfile系统调用导致文件内容通过DMA模块被复制到某个内核缓冲区,之后再被复制到与socket相关联的缓冲区内。

 

步骤二:当DMA模块将位于socket相关联缓冲区中的数据传递给协议引擎时,执行第3次复制。

 

你可能会在想,我们在调用sendfile发送数据的期间,如果另外一个进程将文件截断的话,会发生什么事情?如果进程没有为SIGBUS注册任何信号处理函数的话,sendfile系统调用返回被信号中断前已发送的字节数,并将全局变量errno置为成功。

 

然而,如果在调用sendfile之前,从内核获得了文件租约,那么类似的,在sendfile调用返回前会收到RT_SIGNAL_LEASE

 

[1] [2]

责任编辑 webmaster

 
 
 
 
 
评论更多>>
 
 
 
发表
 
姓名: QQ:
性别: MSN:
E-mail: 主页:
评分: 1 2 3 4 5
评论内容:
验证码:
  
  • 请遵守《互联网电子公告服务管理规定》及中华人民共和国其他各项有关法律法规。
  • 严禁发表危害国家安全、损害国家利益、破坏民族团结、破坏国家宗教政策、破坏社会稳定、侮辱、诽谤、教唆、淫秽等内容的评论 。
  • 用户需对自己在使用本站服务过程中的行为承担法律责任(直接或间接导致的)。
  • 本站管理员有权保留或删除评论内容。
  • 评论内容只代表网友个人观点,与本网站立场无关。
  •