Linux小知识--零拷贝技术相关函数

本文探讨了零拷贝技术在数据搬运中的优势,介绍了sendfile、splice和tee三个关键函数的区别与用途,包括sendfile用于网络文件传输,splice利用管道缓存加速数据交换,tee实现数据复制而不影响原始数据。
摘要由CSDN通过智能技术生成

工作时间越来越长,从开始面对怎么做,到现在面对如何做的更好,都是一个必然的过程。所以还是要勤于学习,善于记录。
在这里插入图片描述

首先看一张常见的数据搬运过程图,这个图描述的是将文件发送到socket的过程
在这里插入图片描述
从图中看出,数据经理了四次搬运,虽然我们不直接面对这个过程,但是在大量的数据搬运过程中,是相当耗费时间和资源的,因此应运而生了一种技术–零拷贝。

零复制(英语:Zero-copy;也译零拷贝)技术是指计算机执行操作时,CPU不需要先将数据从某处内存复制到另一个特定区域。这种技术通常用于通过网络传输文件时节省CPU周期和内存带宽。

说是零拷贝,要注意是减少,并不是不拷贝!!
主要是减少了kernel到用户态的拷贝过程。
在这里插入图片描述

目前直接相关的函数,有三个,分别是sendfile、splice和tee函数,首先说一下大概的区别

函数输入输出特点
sendfile文件描述符在linux kernel 2.6.33之前out_fd must be socket, 之后 out_fd就可以是任意的文件了专门为网络传输文件而生,也可以作为文件拷贝方式
splice文件描述符文件描述符输入输出必须有一个是管道,因为是利用了管道的缓存机制
tee管道管道在两个管道文件描述符之间复制数据,同是零拷贝。但它不消耗数据,数据被操作之后,仍然可以用于后续操作。

sendfile

前面也说到了,这个是一个专门用来进行网络文件传输的接口,用户的拷贝行为只发生在kernel层面
在这里插入图片描述
函数定义

#include<sys/sendfile.h>
 
ssize_t sendfile(int out_fd,int in_fd , off_t* offset ,size_t count);
参数含义
in_fd待读出内容的文件描述符,
out_fd参数是待写入内容的文件描述符,在linux kernel 2.6.33之前out_fd must be socket, 之后 out_fd就可以是任意的文件了
offset参数执行从读入文件流的哪个位置开始读,如果为空,则使用读入文件流的默认起始位置。
count参数指定在文件描述符in_fd和out_fd之间传输的字节数
返回值成功时返回传输的字节数,失败则返回-1并设置errno。

参考代码为发送文件至远端服务器。

#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <assert.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/sendfile.h>
#include <libgen.h>
int main( int argc, char* argv[] )
{
    struct stat stat_buf;
	char* ip =NULL;
	int port = 0;
	char* file_name =NULL;
	int filefd = 0;

    struct sockaddr_in address;
	int sock = 0;
	int ret = 0;
    struct sockaddr_in client;
    socklen_t client_addrlength=0;

	int connfd=0;
	
    if( argc <= 3 )
    {
        printf( "usage: %s ip_address port_number filename\n", basename( argv[0] ) );
        return 1;
    }
	ip = argv[1];
	port = atoi( argv[2] );
	file_name = argv[3];
 
	filefd = open( file_name, O_RDONLY );
    assert( filefd > 0 );
    fstat( filefd, &stat_buf );
 
    bzero( &address, sizeof( address ) );
    address.sin_family = AF_INET;
    inet_pton( AF_INET, ip, &address.sin_addr );
    address.sin_port = htons( port );
 
	sock = socket( PF_INET, SOCK_STREAM, 0 );
    assert( sock >= 0 );
 
	ret = bind( sock, ( struct sockaddr* )&address, sizeof( address ) );
    assert( ret != -1 );
 
    ret = listen( sock, 5 );
    assert( ret != -1 );
 
	client_addrlength = sizeof( client );
	connfd = accept( sock, ( struct sockaddr* )&client, &client_addrlength );
    if ( connfd < 0 )
    {
        printf( "errno is: %d\n", errno );
    }
    else
    {
        sendfile( connfd, filefd, NULL, stat_buf.st_size );
        close( connfd );
    }
 
    close( sock );
    return 0;
}

splice

这个是用来在文件描述符及一个管道之间拷贝数据。因为这个函数是利用管道的缓存属性实现,所以必须有一头是管道。
在这里插入图片描述
函数定义


#include<fcntl.h>
 
ssize_t splice(int fd_in,loff_t* off_in,int fd_out,loff_t* off_out,size_t len ,unsigned int flags)
参数含义
fd_in待读取数据的文件描述符。
off_in表示从输入数据流的何处开始读取数据,如果是管道,必须为NULL
fd_out待写入数据的文件描述符。
off_out表示写入数据流的位置,如果是管道,必须为NULL
len指定移动数据的长度
flag参数则控制数据如何移动,它可以设置为下表中的某些值的按位或。
SPLICE_F_MOVE------------如果合适的话,按整页内存移动数据。这只是给内核提示,不过,因为它的实现存在BUG,所以自内核2.6.21后,他实际上没有任何效果
SPLICE_F_NONBLOCK------------非阻塞的splice操作,但是实际效果还是会受文件描述符本身的阻塞状态的影响
SPLICE_F_MORE------------给内核一个提示,后续的splice调用将读取更多的数据
SPLICE_F_GIFT------------对splice没有效果
返回值成功返回移动字节的数量,失败返回-1,并设置errno
EBADF:描述符有错。
EINVAL:目标文件不支持splice,或者目标文件以追加方式打开,或者两个文件描述符都不是管道描述符。
ENOMEM:内存不够。
ESPIPE:某个参数是管道描述符,但其偏移不是NULL。

例子为一个回射服务

回射服务:接收客户端的消息,再把消息原封不动的返回去

#define _GNU_SOURCE /* See feature_test_macros(7) */
#include<stdio.h>
#include<stdbool.h>
#include<sys/types.h>
#include<unistd.h>
#include<sys/socket.h>
#include<arpa/inet.h>
#include<stdlib.h>

#include<fcntl.h>
#include <sys/sendfile.h>

#define LEN 655
int main(int argc,char *argv[])
{
	char *ip = NULL;
	int port = 0;
    int sockfd,connfd;
    struct sockaddr_in sockaddr,connaddr;
	socklen_t connaddr_len=0;
	int ret = 0;
    int pipefd[2];

    if(argc < 3)
    {   
        printf("usage: %s ip port\n",argv[0]);
        exit(1);
    }   
	ip = argv[1];
    port = atoi(argv[2]);

    connaddr_len = sizeof(connaddr);
    sockaddr.sin_family = AF_INET;
    sockaddr.sin_port = htons(port);
    inet_pton(AF_INET,ip,&sockaddr.sin_addr);

    sockfd = socket(AF_INET,SOCK_STREAM,0);

    ret = bind(sockfd,(struct sockaddr*)&sockaddr,sizeof(sockaddr));
    if(ret == -1) 
    {   
        perror("bind error");
        exit(1);
    }   

    listen(sockfd,15);

    connfd = accept(sockfd,(struct sockaddr*)&connaddr,&connaddr_len);
    if(connfd == -1) 
    {   
        perror("accept error");
        exit(1);
    }   

    pipe(pipefd);
    while(true)
    {
    	int n = 0;
        //用splice函数的回射服务

        n = splice(connfd,NULL,pipefd[1],NULL,LEN,SPLICE_F_MOVE);
        if(n > 0)
        {
            splice(pipefd[0],NULL,connfd,NULL,n,SPLICE_F_MOVE);
        }
        else if(n == 0)
        {
            printf("client close\n");
            close(pipefd[0]);
            close(pipefd[1]);
            close(connfd);
            close(sockfd);
            break;
        }
        else{
            perror("splice error");
            exit(1);
        }
    }
    return 0;
}

tee

主要用来复制一份管道描述符的内容到另外的管道,并且原有管道内容不受影响,可以继续使用

#include<fcntl.h>

ssize_t tee(int fd_in ,int fd_out,size_t len ,unsigned int flags);
参数含义
fdin待读取数据的文件描述符。
fdout待写入数据的文件描述符。
len表示复制的数据的长度。
flags同splice( )函数。
返回值返回值>0:表示复制的字节数。 返回0:表示没有复制任何数据。 返回-1:表示失败,并设置errno。

如下代码利用tee函数和splice函数,实现了linux下的tee程序(同时输出数据到终端和文件的程序)

#define _GNU_SOURCE /* See feature_test_macros(7) */

#include <assert.h>
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <fcntl.h>
 
int main( int argc, char* argv[] )
{
	int filefd=0;
	int pipefd_stdout[2];
	int ret=0;
	int pipefd_file[2];
	
	if ( argc != 2 )
	{
		printf( "usage: %s <file>\n", argv[0] );
		return 1;
	}
	filefd = open( argv[1], O_CREAT | O_WRONLY | O_TRUNC, 0666 );
	assert( filefd > 0 );
 
    ret = pipe( pipefd_stdout );
	assert( ret != -1 );
 
	ret = pipe( pipefd_file );
	assert( ret != -1 );
 

	//将标准输入写入管道pipefd_stdout
	ret = splice( STDIN_FILENO, NULL, pipefd_stdout[1], NULL, 32768, SPLICE_F_MORE | SPLICE_F_MOVE );
	assert( ret != -1 );

	//将pipefd_stdout写入pipefd_file
	ret = tee( pipefd_stdout[0], pipefd_file[1], 32768, SPLICE_F_NONBLOCK ); 
	assert( ret != -1 );

	//将pipefd_file写入文件
	ret = splice( pipefd_file[0], NULL, filefd, NULL, 32768, SPLICE_F_MORE | SPLICE_F_MOVE );
	assert( ret != -1 );

	//将pipefd_stdout写入标准输出
	ret = splice( pipefd_stdout[0], NULL, STDOUT_FILENO, NULL, 32768, SPLICE_F_MORE | SPLICE_F_MOVE );
	assert( ret != -1 );

	//整体来看就是将标准输入 分别写入文件和标准输出
	
	close(filefd );
    close(pipefd_stdout[0]);
    close(pipefd_stdout[1]);
    close(pipefd_file[0]);
    close(pipefd_file[1]);
	return 0;
}

以上就是关于零拷贝的几个常用函数的简单介绍,以后工作中再遇到这种环境的话,可以尝试使用,虽不见奇效,但也会有偶尔致胜。
参考大佬文章传送门
参考代码有些修改,centos7下保证编译通过。
在这里插入图片描述
最近一段时间,儿子每天都在不停的唱着一首歌,是从姥姥的播放器里面听到的,TFBOYS的《大梦想家》
在这里插入图片描述
我也跟着听了好久,发现这首歌还挺好听,突然感觉几年前,大家一股脑的用一个很难听的称呼来称呼这三个小孩,也没有发现他们所代表的儿歌与成人歌曲之间的少年学生的成长阶段,他们这些音乐《青春修炼手册》《大梦想家》《少年中国说》,恰恰是适合少年小朋友来传唱的。
一股惭愧的感觉萦绕在脑海很久,希望家里有孩子的朋友也可以给他们听一听这些阳光的歌曲。不要张嘴闭嘴都是抖音的那些歌了。
推荐一首《不完美小孩》。

全世界在等我飞更高
你却心疼我小小翅膀
为我撑起
沿途休息的地方

全世界在催着我长大
你却总能捧我在手掌
为我遮挡
未知的那些风浪

当我努力做个完美的小孩
满足所有人的期待
你却不讲 你的愿望
怕增添我肩上重量

他们也没想到歌词会出现在脚本里
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

胖哥王老师

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值