从用户模式角度看零拷贝

​ 到目前为止,每一个人都据说过Linux下所谓的零拷贝功能,但我遇到有些人对这个主题没有彻底理解,正由于如此,我决定写几篇文章来更深刻研究下这个问题,但愿可以阐明这个有用的特性;这本文中,咱们将从用户模式的应用程序角度来看看零拷贝,故省去复杂的内核级别细节。api

什么是零拷贝?缓存

为了更好的理解问题的解决方案,咱们须要首先来理解下问题自己,让咱们来看看网络客户端下载存储在dæmon服务器的一个文件的简单过程,下面是一些实例代码:服务器

read(file, tmp_buf, len);
write(socket, tmp_buf, len);

看起来很简单,你可能认为只有两个系统调用并无太多的开销,实际上这与实际状况相差甚远。在这两个调用背后,数据至少被复制了四次,而且执行了几乎数量同样多的用户/内核上下文的切换(实际上这个过程还要更加复杂,但这里我只想保持简单),为了更好的了解涉及的过程,请看图1,上半部分表示上下文切换,下半部分表示数据复制操做网络

图1所示,两个系统调用过程当中的数据复制。异步

第一步: 读系统调用致使用户空间切换到内核空间,第一次数据复制由DMA引擎执行,该引擎读取文件内容而且存储到内核地址空间缓冲区socket

第二步:数据从内核缓冲区复制到用户缓冲区,而后读系统调用返回。调用返回致使上下文从内核切换回用户模式,如今数据存储在用户地址空间的缓冲区,而且能够再次开始向下复制数据。性能

第三步:写系统调用致使上下文从用户模式切换到内核模式。第三次数据复制是再次执行把数据复制到内核地址空间的缓冲区,不过这一次,数据被放到了不一样的缓冲区,这个缓冲区是跟套接字相关联的。spa

第四步:写系统调用返回,创造了第四次上下文切换。第四次数据复制是由DMA引擎独立、异步的从内核缓冲区传递到协议引擎。你可能会问本身,独立和异步是什么意思?数据不是在系统调用以前传输的吗?系统调用返回,实际上并不能保证传输,甚至不能保证传输的开始。这仅仅意味着以太网驱动程序在队列中有空闲的描述符,而且接受了咱们的数据进行传输,在咱们以前可能有不少的数据包在排队,除非驱动程序/硬件实现优先级的环或队列,不然数据是以先到先出的方式传输的(上图中DMA复制说明了最后一个复制能够延迟的事实)操作系统

正如您看到的,不少的数据复制并非须要的,能够消除一些重复的复制,以减小开销并提升性能;做为一个驱动程序开发人员,我使用一些硬件的高级特性,能够彻底绕开主存储器直接传输数据到另外一个设备,这个特性消除了系统内存中的数据复制,是一个好东西,但不是全部的硬件都支持它。还存在磁盘数据必须从新转换成网络数据的问题,这带来了一些复杂性;为了消除开销,咱们能够从消除内核与用户缓冲区之间的一些数据复制开始。code

消除复制的一种方法就是跳过read调用,转而调用mmap,列如:

tmp_buf = mmap(file, len);
write(socket, tmp_buf, len);

为了更好的了解所涉及的过程,请看图2,上下文切换保持不变。

图2,mmap调用

第一步:mmap系统调用致使文件内容被DMA引擎复制到内核缓冲区中。而后与用户进程共享缓冲区,而不须要在内核和用户内存空间之间执行任何数据复制。

第二步:写系统调用使内核将原始内核缓冲区中的数据复制到与套接字关联的内核缓冲区中。

第三步:第三次复制发生在DMA引擎将数据从内核套接字缓冲区传递到协议引擎时。

使用mmap代替read,咱们将内核的数据复制减小了一半,这在传输大量数据时产生了至关好的效果,然而这种改进并非没有代价的,使用mmap+write方式存在一些隐藏的缺陷。当您在内存映射了一个文件时, 这时若是正好有一个进程使用write修改了这个文件使之变小了,这时有可能会访问到映射文件以外的内存,进程将收到SIGBUS信号而退出,这不是网络服务器最理想的操做,有两种方法能够解决这个问题 。

第一种方法是为SIGBUS信号安装一个信号处理程序,而后在处理程序中简单地调用return。经过这样作,write系统调用返回它在被中断以前所写的字节数,errno设置为成功。让我指出,这将是一个坏的解决方案,只看到了问题的表面而没有解决问题的本质,由于SIGBUS信号代表这个过程出了严重问题,因此我不鼓励将此做为解决方案使用。

第二种解决方案涉及从内核中租借文件(在Microsoft Windows中称为“opportunistic locking”)。这是解决这个问题的正确方法。经过在文件描述符上使用租借,能够在特定文件上使用内核。而后,您能够从内核请求读/写租约。当另外一个进程试图截断您要传输的文件时,内核会向您发送实时信号RT_SIGNAL_LEASE信号。它告诉您,内核正在破坏该文件上的写或读租约。在程序访问无效地址并被SIGBUS信号杀死以前,写调用被中断。write调用的返回值是在中断以前写入的字节数,errno将被设置为success。下面是一些示例代码,展现了如何从内核得到租约:

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;
}

您应该在mmaping文件以前得到您的租约,并在完成以后破坏您的租约。这是经过使用F_UNLCK的租赁类型调用fcntl F_SETLEASE来实现的。

Sendfile

在内核版本2.1中,引入了sendfile系统调用,以简化经过网络和两个本地文件之间的数据传输。sendfile的引入不只减小了数据复制,也减小了上下文切换。像这样使用它:

sendfile(socket, file, len);

为了更好地了解所涉及的流程,请看图3

图3,用Sendfile代替读和写

第一步:sendfile系统调用致使文件内容被DMA引擎复制到内核缓冲区中。而后内核将数据复制到与套接字关联的内核缓冲区中。

第二步:第三次复制发生在DMA引擎将数据从内核套接字缓冲区传递到协议引擎时。

您可能想知道,若是另外一个进程截断了咱们经过sendfile系统调用传输的文件,会发生什么状况。若是咱们不注册任何信号处理程序,sendfile调用只返回它在中断以前传输的字节数,errno将被设置为成功。

可是,若是在调用sendfile以前从内核得到文件的租约,则行为和返回状态彻底相同。在sendfile调用返回以前,咱们还获得了RT_SIGNAL_LEASE信号。

到目前为止,咱们已经可以避免让内核复制几个副本,可是仍然只剩下一个副本。这也能避免吗?固然,在硬件的帮助下。为了消除内核所作的全部数据重复,咱们须要一个支持收集操做的网络接口。这仅仅意味着等待传输的数据不须要在连续内存中;它能够分散在不一样的内存位置。在内核版本2.4中,套接字缓冲区描述符被修改,以适应这些需求——Linux下称为零拷贝。这种方法不只减小了多个上下文切换,还消除了处理器所作的数据重复。对于用户级应用程序,没有任何变化,因此代码仍然是这样的:

sendfile(socket, file, len);

为了更好地了解所涉及的流程,请看图4

图4,支持gather的硬件能够从多个内存位置组装数据,从而消除另外一个副本

第一步:sendfile系统调用致使文件内容被DMA引擎复制到内核缓冲区中。

第二步:没有数据复制到套接字缓冲区中。相反,只有包含有关数据位置和长度信息的描述符被附加到套接字缓冲区,DMA引擎直接将数据从内核缓冲区传递到协议引擎,从而消除了剩余的最终副本。

由于数据实际上仍然是从磁盘复制到内存,从内存写出去,有些人可能会说这不是一个真正的零拷贝。可是,从操做系统的角度来看,这是零副本,由于数据不会在内核缓冲区之间重复。当使用零副本时,除了避免复制以外,还能够得到其余性能优点,例如更少的上下文切换、更少的CPU数据缓存污染和没有CPU校验和计算。

Linux下的zero copy的实现还远未完成,在不久的未来可能会发生变化。应该添加更多的功能。例如,sendfile调用不支持向量传输,服务器(如Samba和Apache)必须使用多个sendfile调用并设置TCP_CORK标志。TCP_CORK也与TCP_NODELAY不兼容,当咱们想给数据添加头信息时使用。这是一个很好的例子,说明了一个vectored调用能够消除对多个sendfile调用的需求和当前实现强制执行的延迟。

当前sendfile中一个至关使人不快的限制是,在传输大于2GB的文件时不能使用它。这样大的文件在今天并很多见,并且在退出时必须复制全部的数据是至关使人失望的。由于sendfile和mmap方法在本例中都不可用,因此sendfile64在将来的内核版本中很是有用。

结论

尽管有一些缺点,可是zero-copy sendfile是一个有用的特性,我但愿您已经发现本文提供了足够的信息,能够开始在您的程序中使用它

相关文章
相关标签/搜索