copy_file_range()的函数原型如下:
ssize_t copy_file_range(int fd_in, loff_t *off_in, int fd_out, loff_t *off_out, size_t len, unsigned int flags);
copy_file_range()出现的时间比较晚,在4.5内核(2016/3)才引入。参考[2],提到了4.5内核引入了一个新的系统调用copy_file_range。
copy_file_range()最开始要求fd_in和fd_out在同一个文件系统。5.3的一个patch放开了限制,允许跨文件系统。但是Go语言的开发者在其io.Copy()接口中使用copy_file_range()来实现拷贝的时候,引入了一个bug。当其中一个fd是procfs的文件时,没有拷贝任何内容,且报告了成功,因为procfs的文件,在stat的时候,返回的大小是0,但是read()却可以读到内容。这引发了内核开发社区激烈的讨论。最终5.19内核又将限制修改为,不需要是同一个挂载点,但是两个文件系统需要是相同类型,或者文件系统显式的支持copy_file_range()。
参考[3]详细讲述了copy_file_range()的实现机理。
首先,在filesystem层级,添加了一个新的同名file_operations接口,也就是说文件系统可以自己去实现copy_file_range()。
在kernel内部,有3个flag。
COPY_FR_COPY ,正常的拷贝操作。将kernel page cache直接写入底层存储设备,等价于splice()系统调用。
COPY_FR_REFLINK,引用的方式执行拷贝,实现真正的0拷贝,有些文件系统如btrfs,可以支持。
COPY_FR_DEDUP,用来将目标的内容替换为引用,可以用来实现去除文件系统中,重复的内容。
当系统调用的flag为0时,内部使用 flags = COPY_FR_COPY | COPY_FR_REFLINK;
由于文件系统可以自己实现copy_file_range(),所以其真正的杀手锏,是可以实现copy offloading。
对于本地文件系统可能节省的开销不大,毕竟节省的用户态内存拷贝的开销远小于磁盘IO开销。
但是对于NFS,好处就很明显了,不用将数据从nfs server传输到nfs client,在传输回去,可以直接在server实现copy动作。
在没有copy_file_range()的时候,尽管coW文件系统,如btrfs可以通过ioctl来实现copy offloading,现在cp使用--reflink实现。
$ strace -e ioctl cp --reflink a b
ioctl(4, BTRFS_IOC_CLONE or FICLONE, 3) = 0
sendfile()出现的时间非常早,2.6内核就已经有了。
差异1:
sendfile()的一个fd可以是socket,用来实现文件到网络的传输。但是copy_file_range()不行,它的fd只能是文件。
差异2:
当两个fd都是文件的时候,sendfile()是vfs级别的,而copy_file_range()是可以是filesystem级别的。
参考:
【1】How useful should copy_file_range() be? https://lwn.net/Articles/846403/
【2】https://kernelnewbies.org/Linux_4.5
【3】copy_file_range() https://lwn.net/Articles/659523/
【4】Copy offload https://lwn.net/Articles/637436/
【5】https://man7.org/linux/man-pages/man2/copy_file_range.2.html