Linux传统I/O操作是一种缓冲I/O,在数据传输中,操作系统会将 I/O 的数据缓存在文件系统的页缓存中,即操作系统内核缓冲区中。
比如:在网络中传输一个文件时,发送端应用程序会先检查内核缓冲区中有没有需要发送的这个文件的数据,如果没有,则会将这个文件从磁盘拷贝到内核缓冲区中,然后再从内核缓冲区拷贝到应用程序的用户缓冲区,如果应用程序不对数据进行处理或处理完毕之后,再将文件拷贝到内核中的socket发送缓冲区(比如TCP发送缓冲区),待内核socket缓冲区中有足够的数据时,就会把数据发送到网卡上,然后在网络上进行传输
其过程至少发生了四次数据的拷贝,其频繁的读写对CPU的使用和内存的带宽开销是非常大的。
零拷贝技术
零拷贝技术相对传统I/O技术来说,主要是避免数据传输过程中频繁的数据拷贝操作,提高传输效率,并且使CPU有更多时间执行其它任务。
零拷贝技术分类
直接I/O机制:
通过上面的介绍,普通I/O(即缓冲I/O)会被内核缓存。相对于普通I/O机制,直接I/O机制对文件的访问不经过内核的缓存,数据直接在磁盘和应用程序地址空间进行传输。这就避免了内核缓冲区和用户缓冲区的数据拷贝,降低了读写操作对CPU的使用以及对内存带宽的占用。
但是直接 I/O 不能提供缓存 I/O 的优势。缓存 I/O 的读操作可以从高速缓冲存储器中快速获取数据,而直接 I/O 的读数据操作会造成磁盘的同步读,导致进程需要较长的时间才能执行完。
不经过用户缓冲区:
主要是指不需要将数据拷贝或者映射到应用程序地址空间中,直接在内核中传输。
比如sendfile( )系统调用:sendfile( ) 系统调用利用 DMA 将文件中的数据拷贝到操作系统内核缓冲区中,然后数据被拷贝到与 socket 相关的内核缓冲区中。接下来,DMA 将数据从内核 socket 缓冲区中拷贝到网卡中去。如果在用户调用 sendfile ( ) 系统调用进行数据传输的过程中有其他进程截断了该文件,那么 sendfile ( ) 系统调用会简单地返回给用户应用程序中断前所传输的字节数,errno 会被设置为 success。
由于sendfile( )函数只能往socket上写数据,因此它几乎是专门为了在网络上传输文件而设计的。
DMA简介:Direct Memory Access(存储器直接访问)。这是指一种高速的数据传输操作,允许在外部设备和存储器之间直接读写数据,既不通过CPU,也不需要CPU干预。整个数据传输操作在一个称为”DMA控制器”的控制下进行的。CPU除了在数据传输开始和结束时做一点处理外,在传输过程中CPU可以进行其他的工作。这样,在大部分时间里,CPU和输入输出都处于并行操作。因此,使整个计算机系统的效率大大提高。
优化数据在内核缓冲区和用户缓冲区之间的传输:
保留了传统的在用户应用程序地址空间和操作系统内核地址空间之间传递数据的技术,但却在传输上进行优化。
比如写时复制技术:如果多个应用程序同时访问同一块数据,但这块数据只有一份,那么可以为这些应用程序分配指向这块数据的指针,在每一个应用程序看来,它们都拥有这块数据的一份数据拷贝。若一个应用程序需要访问但不修改该数据时,直接读这个数据而不复制,但当应用程序需要对这块数据进行修改的时候,就需要将数据真正地拷贝到该应用程序的地址空间中去,也就是说,该应用程序拥有了一份真正的私有数据拷贝,对这份私有数据进行修改。这样做是为了避免该应用程序对这块数据做的更改被其他应用程序看到。这个过程对于应用程序来说是透明的,如果应用程序永远不会对所访问的这块数据进行任何更改,那么就永远不需要将数据拷贝到应用程序自己的地址空间中去
Linux内核有zero copy的函数。nginx和proftpd中用到sendfile(文件到socket),haproxy则用到slice(socket到socket),比较下来,haproxy仍然需要调用两次system call(与read,write一样)
sendfile
http://linux.die.net/man/2/sendfile
http://lxr.linux.no/linux+v3.5.4/fs/read_write.c#L1000
在两个文件描述符之间传输数据,不用拷贝。 但 输入的描述符必须是真正的文件, 输出的文件描述符可以是 socket。 这也是sendfile的由来吧。他是从文件的缓存页 page cache里面直接把数据传输到另外一个描述符里面去,省去用户空间和内核空间的复制。 看当前代码他是使用一个专门的do_splice_direct 函数来实现的。 思路跟 splice是一样的,也需要使用pipe来做中介,但他这个do_splice_direct 使用一个每个进程缓存(在 corrent指针的 splice_pipe)的一个pipe,可以少用一次系统调用(正常的splice需要从 文件到 pipe,然后再从pipe到socket,有两次调用)。
这个sendfile应用场合,比如像http服务器,直接把htm源文件读出来发送给客户可对应的socket时,用这个sendfile就很合适。减少数据复制的同时,应该 系统调用的次数也减少了。 看网上共识,这种应用环境使用sendfile可以提到性能是不争的事实。
splice
http://linux.die.net/man/2/splice
http://lwn.net/Articles/119680/
http://yarchive.net/comp/linux/splice.html
http://lxr.linux.no/linux+v3.5.4/fs/splice.c
在两个文件描述符之间传输数据,不用拷贝。但输入和输出文件描述符必须有一个是pipe。也就是说如果你需要从一个socket 传输数据到另外一个socket,是需要使用 pipe来做为中介的。 pipe buffer被抽象出来,当作 “内核缓存结构”, 一种流缓冲,可以理解成你的数据从写入 “内核流缓存”里面,然后在从 一个”内核流缓存“复制到另外一个比如说socket的缓存。全部数据都是在内核空间进行。 当然你的数据复制也是不用复制,他那个pipe buffer本来就是 使用page去管理缓存的,就是 缓存地址加偏移地址的办法,只是Linus 觉splice的需要很像之前的pipe思想,所以splice就用这个个pipe来作为”内核缓存结构“了。
看起来splice是可以避免数据的复制,应该能获得更好的性能。但好像网上的评测,一些人使用了splice之后性能反而下降。其实大家最想要的还是从socket到socket的 ”零拷贝“技术,这样那些代理服务器啊什么的转发数据的时候就可以提高性能。但这个splice用到这个场合的话, 还是需要一个pipe来做中介的,要调用两个splice才能把数据从一个socket移到另外一个socket。
splice (socket1_fd, pipe_fd
splice (pipl_fd, socket2_fd 这样,系统调用同样需要两次。read write也需要两次。系统调用次数没有减少,不像sendfile那样可以减少一个系统调用。 数据复制的代价,可能数据包比较小时,影响应该是比较小的,所以 splice没有起到意想的作用。有人用性能工具比较了splice的时候的内核调用的区别,说只是性能消耗的地方转移了而已。这个需要那个高人来使用最新的内核测试分析一下了。
haproxy有配置使不使用splice的选项,splice的使用也可以去参考一下 haproxy的代码。 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。