当前位置: 首页 > 科技观察

一篇文章看懂零拷贝技术原理

时间:2023-03-12 19:34:55 科技观察

spliceReview先来回顾一下splice的原理:如上图所示,使用splice拷贝数据时,需要经过pipeline作为中转。splice首先将pagecache绑定到pipeline的写端,然后通过pipeline的读端读取pagecache中的数据,复制到socketbuffer中。pipeline有一个ringbuffer,需要绑定真实的物理内存页。而splice就是将pipeline的ringbuffer绑定到文件的pagecache上,如下图所示:将filepagecache绑定到pipeline的ringbuffer后,可以通过read读取文件pagecache管道的末端数据。拼接代码实现了拼接的使用过程。将文件内容发送到客户端连接的步骤如下:首先,使用splice()系统调用将文件内容绑定到管道。然后,使用splice()系统调用将管道的数据复制到客户端连接套接字。让我们看一下splice()系统调用的实现。代码如下:asmlinkage(fd_in,*off_in,fd_out,*off_out,len,flags){error;*,*;fput_in,fput_out;...错误=-EBADF;in=fget_light(fd_in,&fput_in);//1.获取数据输入文件对象(in){(in->f_mode&FMODE_READ){out=fget_light(fd_out,&fput_out);//2.获取数据输出文件对象(out){(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);}error;}splice()系统调用主要调用下一步的do_splice()函数。我们来分析一下do_splice()函数的实现。do_splice()函数主要处理两种情况,代码如下:(structfile*in,*off_in,structfile*out,*off_out,len,flags){*;偏移量,*关闭;退役;//情况1:如果输入是管道?pipe=pipe_info(in->f_path.dentry->d_inode);(pipe){...//调用do_splice_from()函数将管道数据复制到目标文件handleret=do_splice_from(pipe,out,off,len,flags);...退役;}//情况2:如果输出是管道?pipe=pipe_info(out->f_path.dentry->d_inode);(pipe){...//调用do_splice_to()函数将文件内容绑定到pipe中ret=do_splice_to(in,off,pipe,len,flags);...退役;}-EINVAL;}如上代码所示,do_splice()函数分为两种第一种情况处理如下:如果输入端是管道,则调用do_splice_from()函数进行处理。如果输出是管道,调用do_splice_to()函数进行处理。下面我们分别描述这两种情况的处理。1、输入端为管道。如果输入端是管道(即从管道复制数据到输出端句柄),那么会调用do_splice_from()函数进行处理。do_splice_from()函数的实现如下:(structpipe_inode_info*pipe,structfile*out,*ppos,len,flags){...out->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(),如下:(structpipe_inode_info*pipe,structfile*out,*ppos,len,flags){...ret=__splice_from_pipe(pipe,&sd,pipe_to_file);...ret;}下面分析一下__splice_from_pipe()函数的实现:__splice_from_pipe(structpipe_inode_info*pipe,structsplice_desc*sd,splice_actor*actor){...(;;){(pipe->nrbufs){//1.获取管道环形缓冲区*=->+->;*=->;...//2.获取管道环缓冲区数据复制到输出文件。//actor指针指向pipe_to_file()函数,由generic_file_splice_write()函数传入err=actor(pipe,buf,sd);(err<=0){(!ret&&err!=-ENODATA)ret=err;;}...}...}...ret;}简化__splice_from_pipe()函数后,逻辑就很简单了。主要流程如下:获取pipelineringbuffer(pipeline的实现可以参考文章《图解 | Linux进程通信 - 管道实现》)。调用pipe_to_file()函数将piperingbuffer中的数据复制到输出文件中。因此,输入为管道的调用链如下:sys_splice()└→do_splice()└→do_splice_from()└→generic_file_splice_write()└→__splice_from_pipe()└→pipe_to_file()2.输出为管道如果output是一个pipeline(也就是将输入端绑定到pipeline上),那么就会调用do_splice_to()函数进行处理。do_splice_to()函数的实现如下:staticlongdo_splice_to(structfile*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()函数。如果输出是一个socket,那么in->f_op->splice_read()会指向sock_splice_read()函数。下面将使用generic_file_splice_read()函数作为分析对象,generic_file_splice_read()函数会调用__generic_file_splice_read()进行下一步的处理,如下:staticint__generic_file_splice_read(structfile*in,loff_t*ppos,structpipe_inode_info*pipe,size_tlen,unsignedintflags){...structpage*pages[PIPE_BUFFERS];structsplice_pipe_descspd={.pages=pages,...};...//1.找到页面缓存中已经存在的页面spd.nr_pages=find_get_pages_contig(mapping,index,nr_pages,pages);index+=spd.nr_pages;...//2.如果部分页面缓存不存在,则申请新的页面缓存while(spd.nr_pagesreadpage(in,page);//从硬盘读取数据...}...spd.nr_pages++;索引++;}...//4.将页面缓存绑定到管道if(spd.nr_pages)returnsplice_to_pipe(pipe,&spd);returnerror;}__generic_file_splice_read()函数的代码比较长。为了便于分析,对其进行了简化。从简化代码可以看出,__generic_file_splice_read()函数主要完成4个步骤:查找要绑定的pagecache是??否已经存在(已经从硬盘同步到pagecache)。如果还有未同步到内核的pagecache,则申请新的pagecache。如果pagecache中的数据和硬盘不一致,先从硬盘同步到pagecache。调用splice_to_pipe()函数将页面缓存绑定到管道。所以最后会调用splice_to_pipe()函数将pagecache绑定到pipeline上。我们看一下splice_to_pipe()函数的实现:intret,do_wakeup,page_nr;...for(;;){...if(pipe->nrbufscurbuf+pipe->nrbufs)&(PIPE_BUFFERS-1);structpipe_buffer*buf=pipe->bufs+newbuf;//将环形缓冲区绑定到页面缓存buf->page=spd->pages[page_nr];buf->offset=spd->partial[page_nr].offset;buf->len=spd->partial[page_nr].len;buf->private=spd->partial[page_nr].private;buf->ops=spd->ops;如果(spd->flags&SPLICE_F_GIFT)buf->flags|=PIPE_BUF_FLAG_GIFT;管道->nrbufs++;page_nr++;ret+=buf->len;...如果(pipe->nrbufs