拼接原理综述在文章《splice使用》中,介绍了拼接的原理和用途。现在我们来分析splice的代码实现。让我们先回顾一下拼接的原理:如上图所示
拼接原理综述
在文章《splice使用》中,介绍了拼接的原理和用途。现在我们来分析splice的代码实现。
让我们先回顾一下拼接的原理:
如上图所示,使用splice复制数据时,需要使用管道作为中转。Splice首先将页面缓存绑定到管道的写端,然后通过管道的读端读取页面缓存的数据并复制到socket缓冲区中。
我们知道流水线有一个环形缓冲区,这个环形缓冲区需要和真实的物理内存页面绑定。Splice是将管道的循环缓冲区绑定到文件的页面缓存,如下图所示:
通过将文件页缓存绑定到流水线的环形缓冲区,可以通过流水线的读取端读取文件页缓存的数据。
拼接代码实现
在文章《splice使用》中,介绍了拼接的使用过程。将文件内容发送到客户端进行连接的步骤如下:
首先,使用splice()系统调用将文件内容绑定到管道。
然后,使用splice()系统调用将管道的数据复制到客户端连接套接字。
让我们来看看splice()系统调用的实现。代码如下:
asmlinkagelongsys _ splice(intfd _ in,loff_t*off_in,intfd_out,loff_t*off_out,size_tlen,unsignedintflags){ long error;structfile*in,* outintfput_in,fput _ out.错误=-EBADF;in=fget_light(fd_in,fput _ in);//1.获取数据输入文件对象if(in){ if(in-f _ mode fmode _ read){ out=fget _ light(FD _ out,fput _ out);//2.获取文件对象if(out){ if(out-f _ mode fmode _ write)//3。调用do_splice()函数进行下一次运算error=do _ splice (in,off _ in,out,off _ out,len,flags);fput_light(out,fput _ out);}}fput_light(in,fput _ in);} returnerror}splice()系统调用主要调用do_splice()函数进行进一步处理。我们来分析一下do_splice()函数的实现。do_splice()函数主要处理两种情况,代码如下:
static long do _ splice(struct file * in,loff_t*off_in,structfile*out,loff_t*off_out,size_tlen,unsignedintflags){ struct pipe _ inode _ info * pipe;loff_toffset,* off朗雷特;//情况一:如果输入端是管道?pipe=pipe _ info(in-f _ path . dentry-d _ inode);如果(管道){.//调用do_splice_from()函数将管道数据复制到目标文件句柄ret=do _ splice _ from (pipe,out,off,len,flags);returnret}//情况二:如果输出端是管道?pipe=pipe _ info(out-f _ path . dentry-d _ inode);如果(管道){.//调用do_splice_to()函数,用管道RET=do _ splice _ to (in,off,pipe,len,flags)绑定文件内容;returnret} return-EINVAL;}如上面的代码所示,do_splice()函数在两种情况下处理,如下所示:
如果输入端是管道,则调用do_splice_from()函数进行处理。
如果输出是管道,则调用do_splice_to()函数进行处理。
下面分别说明这两种情况的处理流程。
1.输入端是管道。
如果输入是管道(即从管道复制数据到输出的句柄),将调用do_splice_from()函数进行处理,do_splice_from()函数实现如下:
static long do _ splice _ from(struct pipe _ inode _ info * pipe,structfile*out,loff_t*ppos,size_tlen,unsignedintflags){.returnout-f _ op-splice _ write(pipe,out,ppos,len,flags);}如果输出的是普通文件,那么out-f_op-splice_write()会指向generic_file_splice_write()函数。如果输出是一个套接字,那么out-f_op-splice_write()将指向generic_splice_sendpage()函数。
generic_file_splice_write()函数将作为分析对象,generic_file_splice_write()函数将调用__splice_from_pipe()进行进一步处理,如下图所示:
ssize _ tgeneric _ file _ splice _ write(struct pipe _ inode _ info * pipe,structfile*out,loff_t*ppos,size_tlen,unsignedintflags){.ret=__splice_from_pipe(管道,sd,管道_到_文件);returnret}然后我们将分析__splice_from_pipe()函数的实现:
ssize_t__splice_from_pipe(结构管道_信息节点_信息*管道,结构拼接_ desc *标清,拼接_演员*演员){.for(;){if(pipe-nrbufs){//1。获取管道环形缓冲结构pipe _ buffer * buf=pipe-bufs pipe-curbuf;const struct pipe _ buf _ operations * ops=buf-ops;//2.将管道环形缓冲区的数据复制到输出文件中。//其中actor指针指向pipe_to_file()函数,err=actor(pipe,buf,sd)由generic_file_splice_write()函数传入;if(err=0){if(!复述!=-ENODATA)ret=err;打破;}.}.}.returnret}简化__splice_from_pipe()函数后,逻辑非常简单。主要流程如下:
获取管道环形缓冲区。
调用pipe_to_file()函数将管道环形缓冲区的数据复制到输出端的文件中。
因此,将输入作为管道的调用链如下所示:
sys _ splice()do _ splice()do _ splice _ from()generic _ file _ splice _ write()_ _ splice _ from _ pipe()
如果输出是管道(即输入绑定到管道),将调用do_splice_to()函数进行处理,do_splice_to()函数实现如下:
static long do _ splice _ to(struct file * in,loff_t*ppos,structpipe_inode_info*pipe,size_tlen,unsignedintflags){.returnin-f_op-splice_read(in,ppos,pipe,len,flags);}如果输入的是普通文件,那么in-f_op-splice_read()会指向generic_file_splice_read()函数。如果输出是套接字,那么in-f_op-splice_read()将指向sock_splice_read()函数。
generic_file_splice_read()函数将作为分析对象,generic_file_splice_read()函数将调用__generic_file_splice_read()进行进一步处理,如下图所示:
static int _ _ generic _ file _ splice _ read(struct file * in,loff_t*ppos,structpipe_inode_info*pipe,size_tlen,unsignedintflags){.struct page * pages[PIPE _ BUFFERS];structsplice_pipe_descspd={。页数=页数,};//1.找到页面SPD。NR _ pages=find _ get _ pages _ contig(mapping,index,NR _ pages,pages)带有现有页面缓存;index=spd.nr _ pages.//2.如果有的页面缓存还不存在,在while(SPD . NR _ page SNR _ pages){ page=find _ get _ page(mapping,index)中申请新的页面缓存;pages[SPD . NR _ pages]=page;指数;}//3.如果页面缓存中的数据与硬盘中的数据不一致,则从硬盘同步到页面缓存,持续(page _ NR=0;page_nra_ops-readpage(in,page);//从硬盘读取数据.}.spd.nr _ pages指数;//4.将页面缓存绑定到管道if(SPD . NR _ pages)return splice _ to _ pipe(pipe,SPD);returnerror}__generic_file_splice_read()函数的代码比较长,为了便于分析,对其进行了简化。从简化代码中可以看出,__generic_file_splice_read()函数主要完成四个步骤:
找出要绑定的页面缓存是否已经存在(已经从硬盘同步到页面缓存)。
如果还有一个页面缓存没有同步到内核,申请一个新的页面缓存。
如果页面缓存与硬盘上的数据不一致,请先从硬盘同步到页面缓存。
调用splice_to_pipe()函数将页面缓存绑定到管道。
因此,最终将调用splice_to_pipe()函数将页面缓存绑定到管道。让我们来看看splice_to_pipe()函数的实现:
ssize _ tsplice _ to _ pipe(struct pipe _ inode _ info * pipe,struct splice _ pipe _ desc * SPD){ unsignedintspd _ pages=SPD-NR _ pages;intret,do_wakeup,page _ nr.for(;){.if(PIPE-nrbufscurbuf PIPE-nrbufs)(PIPE _ BUFFERS-1);struct pipe _ buffer * buf=pipe-bufs new buf;//将环形缓冲区与页面缓存buf-page=spd-pages[page_nr]绑定;buf-offset=SPD-partial[page _ NR]。偏移;buf-len=spd-partial[page_nr]。lenbuf-private=SPD-partial[page _ NR]。私人;buf-ops=SPD-ops;if(SPD-flags splice _ F _ GIFT)BUF-flags |=PIPE _ BUF _ FLAG _ GIFT;pipe-NR bufs;page _ nrret=buf-len;if(pipe-nrbufsPIPE_BUFFERS)继续;打破;}.}.returnret}splice_to_pipe()函数代码比较长,但是逻辑很简单,就是将管道的循环缓冲区与文件的页面缓存绑定,这样就可以通过管道的读取端读取页面缓存的数据。
因此,输出为管道的调用链如下所示:
sys _ splice()do _ splice()do _ splice _ to()generic _ file _ splice _ read()_ _ generic _ file _ splice _ read()
本文主要介绍零拷贝技术的一种实现splice的原理和实现。希望通过这篇文章,让读者对零拷贝技术有更深入的了解。
当然,本文也忽略了很多实现细节,所以当你在阅读过程中对一些细节不理解时,可以直接阅读源代码来解惑。
黄飞
声明本站所有作品图文均由用户自行上传分享,仅供网友学习交流。若您的权利被侵害,请联系我们