零拷贝之splice( )函数和tee( )函数

splice( )函数
在两个文件描述符之间移动数据,同sendfile( )函数一样,也是零拷贝。
函数原型:



#include
ssize_t splice(int fdin, loff_t *offin, int fdout, loff_t *offout, size_t len, unsigned int flags);
参数意义:



fdin参数:待读取数据的文件描述符。
offin参数:指示从输入数据的何处开始读取,为NULL表示从当前位置。如果fdin是一个管道描述符,则offin必须为NULL。
fdout参数:待写入数据的文件描述符。
offout参数:同offin,不过用于输出数据。
len参数:指定移动数据的长度。
flags参数:表示控制数据如何移动,可以为以下值的按位或:



SPLICE_F_MOVE:按整页内存移动数据,存在bug,自内核2.6.21后,实际上没有效果。
SPLICE_F_NONBLOCK:非阻塞splice操作,实际会受文件描述符本身阻塞状态影响。
SPLICE_F_MORE:提示内核:后续splice将调用更多数据。
SPLICE_F_GIFT:对splice没有效果。
fdin和fdout必须至少有一个是管道文件描述符。



返回值:



返回值>0:表示移动的字节数。
返回0:表示没有数据可以移动,如果从管道中读,表示管道中没有被写入数据。
返回-1;表示失败,并设置errno。



errno值如下:



EBADF:描述符有错。
EINVAL:目标文件不支持splice,或者目标文件以追加方式打开,或者两个文件描述符都不是管道描述符。
ENOMEM:内存不够。
ESPIPE:某个参数是管道描述符,但其偏移不是NULL。
tee( )函数
在两个管道文件描述符之间复制数据,同是零拷贝。但它不消耗数据,数据被操作之后,仍然可以用于后续操作。
函数原型:



#include
ssize_t tee(int fdin, int fdout, size_t len, unsigned int flags);
1
2
参数意义:



fdin参数:待读取数据的文件描述符。
fdout参数:待写入数据的文件描述符。
len参数:表示复制的数据的长度。
flags参数:同splice( )函数。



fdin和fdout必须都是管道文件描述符。
返回值:



返回值>0:表示复制的字节数。
返回0:表示没有复制任何数据。
返回-1:表示失败,并设置errno。



代码实例:
/splice()和tee()实现将文件”./1.txt”同时拷贝到文件”./2.txt”和”./3.txt”中/
#include
#include
#include
#include
#include



int main(){
int fd1 = open(“./1.txt”, O_RDONLY);
int fd2 = open(“./2.txt”, O_RDWR| O_CREAT | O_TRUNC, 0666);
int fd3 = open(“./3.txt”, O_RDWR| O_CREAT | O_TRUNC, 0666);



/*用于向"./2.txt"输入数据*/
int pipefd2[2];
/*用于向"./3.txt"输入数据*/
int pipefd3[2];
pipe(pipefd2);
pipe(pipefd3);

/*将fd1文件的内容输入管道pipefd2中*/
splice(fd1, NULL, pipefd2[1], NULL, 10086, SPLICE_F_MORE);
/*将管道pipefd2的内容复制到管道pipefd3中,不消耗管道pipefd2上的数据,管道pipefd2上的数据可以用于后续操作*/
tee(pipefd2[0], pipefd3[1], 10086, SPLICE_F_NONBLOCK);
/*将管道pipefd2的内容写入fd2文件中*/
splice(pipefd2[0], NULL, fd2, NULL, 10086, SPLICE_F_MORE);
/*将管道pipefd3的内容写入fd3文件中*/
splice(pipefd3[0], NULL, fd3, NULL, 10086, SPLICE_F_MORE);

close(fd1);
close(fd2);
close(fd3);
close(pipefd2[0]);
close(pipefd2[1]);
close(pipefd3[0]);
close(pipefd3[1]);
return 0; } <!-- more --> pipe在linux的实现中,用的是生产者消费者的模型,在linux/pipe_fs_i.h中我们能看到一下的代码: #define PIPE_DEF_BUFFERS 16


//… struct pipe_inode_info { struct mutex mutex; wait_queue_head_t wait; unsigned int nrbufs, curbuf, buffers; unsigned int readers; unsigned int writers; unsigned int files; unsigned int waiting_writers; unsigned int r_counter; unsigned int w_counter; struct page *tmp_page; struct fasync_struct *fasync_readers; struct fasync_struct *fasync_writers; struct pipe_buffer *bufs; };



其中bufs就是一个指向管道缓冲区的指针,而管道缓冲区的结构如下:
struct pipe_buffer { struct page *page; unsigned int offset, len; const struct pipe_buf_operations *ops; unsigned int flags; unsigned long private; };



其中page是指向包含pipe buffer的页,是物理上的页地址,不是虚拟地址,这样方便进程间的通信。



在创建管道缓冲区时,会创建PIPE_DEF_BUFFERS个pipe_buffer大小的空间给bufs,也就是bufs指向一个大小是PIPE_DEF_BUFFERS的pipe_buffer数组。一个页大小是4k,那么linux的管道缓冲区大小就是64k了。
在使用管道缓冲区时,就和生产者消费者的模型一样,一边把数据写进去,另一边把数据取出来,慢时写阻塞,空时读阻塞。在写入的时候,为了效率,linux会倾向于以页为单位的写,因此缓冲区满时未必是64k的数据。



接下来就是说splice了,
#include
ssize_t splice(int fd_in, loff_t *off_in, int fd_out,loff_t *off_out, size_t len, unsigned int flags);
成功返回spliced的字节数,出错-1
这个函数中,fd_in和fd_out中有一个要是管道,off_in、off_out分别是两个文件描述符的偏移,如果其对应的文件描述符不是普通文件,那么就不能有偏移量,就要设为NULL,当是NULL时,就是从文件当前位置读/写,结束后会更新偏移的位置。len就是要移动的数据,至于flags自己看manpage。



它之所以能零复制,就是利用了管道作为中介,先把数据“复制”管道,然后再从管道中读取即可:
pipe(fd_pair[2]);
splice(source_file,…,fd_pair[1],…);
splice(fd_pair[0],…,destination_file,…);
可是注意的是,其实我们并没有真的把数据复制进管道缓冲区,我们只是修改了管道缓冲区的page指针、偏移、长度,使它指向源数据的实际物理地址,然后再从管道中读出来,整个过程都没有设计用户空间和内核空间的复制,在内核中也没有多余的复制,因此是零复制(复制了一次,但术语是叫零复制)。



我们要注意用splice传送超过缓冲区64k的文件时,要更新:
while (filesize > 0) {



len = splice(sourcefd,&off_in,pipe_pair[1],NULL,filesize-off_in,SPLICE_F_MOVE); splice(pipe_pair[0], NULL,dstfd,&off_out,len, SPLICE_F_MOVE); if (len < 0) { perror(“splice”); break; }



filesize -= len;



}



还有一个tee函数,这也是一个零复制函数
#include
ssize_t tee(int fd_in, int fd_out, size_t len, unsigned int flags);
成功返回“复制”的字节数,出错-1
EINVAL fd_in or fd_out does not refer to a pipe; or fd_in and fd_out refer to the same pipe.
这个函数是用于两个不同管道之间的零复制,就相当于把两个管道连通



测试
下面是段性能比较,测试程序是这样的,读入一个文件,然后分别用read-write、mmap、splice三种方法复制这个文件,我使用的是一个300多mb的视频文件来测试,最终得到的测试结果是:
最上面的数字是真正复制所用的时间,下面的是time的输出,因为还有其他的影响,因此用户时间+系统时间!=函数工作时间
read-write:
0.820000
real 0m11.919s
user 0m0.028s
sys 0m0.996s



mmap:
0.830000
real 0m10.109s
user 0m0.312s
sys 0m0.676s



splice:
0.550000
real 0m10.643s
user 0m0.000s
sys 0m0.732s



mmap的时间居然和read-write差不多。。。不过我们可以看出用mmap的系统调用时间比read-write少30%左右,可是用户调用时间比较大,因为mmap这个操作本身就是一个消耗很大的函数,如果与要长时间使用这个文件的话,那么就可以冲淡mmap消耗。splice是最快的方法,用户调用时间很少,因为它的工作就是直接在内核完成,不需要频繁的在用户空间和内核空间之间切换。
更新:
发现mmap分段的复制比较直接复制的快,不过还是慢过splice


Category linux