Linux下C语言文件描述符操作(dup / dup2 / sendfile / splice / tee)

Linux的哲学是一切皆文件,而操作文件是通过文件描述符来进行。本文梳理一下dup / dup2 / sendfile / splice/ tee函数对文件描述符的操作。

目录

1.dup

2.dup2

3.sendfile

4.splice

5.tee


1.dup

#include <unistd.h>
int  dup(int fd);

复制一个现有的文件描述符,dup会返回一个新的描述符,这个描述一定是当前可用文件描述符中的最小值。我们知道,一般的0,1,2描述符分别被标准输入、输出、错误占用,所以在程序中如果close掉标准输出1后,调用dup函数,此时返回的描述符就是1。函数返回后fd和dup返回值指向同一个文件,只是文件描述符不同。

以下下代码演示将标准输出复制成普通文件:

#include <assert.h>
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <fcntl.h>

int main(int argc, char* argv[]) {
    if (argc != 2) {
        printf("usage: %s <file>\n", argv[0]);
        return 1;
    }
    int filefd = open(argv[1], O_CREAT | O_WRONLY | O_TRUNC, 0666);
    assert(filefd > 0);
	close(STDOUT_FILENO);
    printf("=== %d\n", dup(filefd));
    write(filefd, "hello world\n", 12);
    close(filefd);
	printf("dup函数,你好啊");
   
    return 0;
}

运行结果:

2.dup2

#include <unistd.h>
int dup2(int fd, int fd2);

复制一个现有的文件描述符,用fd2指定新描述符的值,如果fd2本身已经打开了,则会先将其关闭。如果fd等于fd2,则返回fd2,并不关闭它。函数返回后fd和fd2指向同一个文件,只是文件描述符不同。

一下代码演示将标准输出复制成普通文件:

#include <assert.h>
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <fcntl.h>

int main(int argc, char* argv[]) {
    if (argc != 2) {
        printf("usage: %s <file>\n", argv[0]);
        return 1;
    }
    int filefd = open(argv[1], O_CREAT | O_WRONLY | O_TRUNC, 0666);
    assert(filefd > 0);
	write(filefd, "hello liudehua\n", 15);
    printf("=== %d\n", dup2(filefd, STDOUT_FILENO));
	write(filefd, "hello world\n", 12);
    close(filefd);
   
    return 0;
}

文件内容如下:

 使用strace跟踪:

3.sendfile

#include<sys/sendfile.h>
ssize_t sendfile(int out_fd,int in_fd , off_t* offset ,size_t count);
功能:在两个文件描述符之间传递数据(完全在内核中操作),从而避免内核缓冲区和用户缓冲区之间的数据拷贝,效率很高,这就是传说中的零拷贝。

参数:
in_fd:参数是待读出内容的文件描述符
out_fd:参数是待写入内容的文件描述符
offset:参数执行从读入文件流的哪个位置开始读,如果为空,则使用读入文件流的默认起始位置
count:参数指定在文件描述符in_fd和out_fd之间传输的字节数
返回值:成功时返回传输的字节数,失败则返回-1并设置errno

重点说明:该函数的man手册明确指出,in_fd必须是一个支持mmap函数的文件描述符,即它必须指向真实的文件,而不能是socket和管道,
而out_fd则必须是一个socket。所以sendfile几乎是专门为在网络上传输文件而设计的

以下测试代码编写一个简单的tcp服务器。通过sendfile向客户端发送文件的例子。

#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>

int main( int argc, char* argv[] ) {
    if( argc <= 3 ) {
        printf("usage: %s ip port filename\n", basename(argv[0]));
        return 1;
    }
    const char* ip = argv[1];
    int port = atoi(argv[2]);
    const char* file_name = argv[3];
    int filefd = open(file_name, O_RDONLY);
    assert(filefd > 0);
    struct stat stat_buf;
    fstat(filefd, &stat_buf);
    struct sockaddr_in address;
    bzero(&address, sizeof(address));
    address.sin_family = AF_INET;
    inet_pton(AF_INET, ip, &address.sin_addr);
    address.sin_port = htons(port);
    int sock = socket(PF_INET, SOCK_STREAM, 0);
    assert(sock >= 0);
    int ret = bind(sock, (struct sockaddr*)&address, sizeof(address));
    assert(ret != -1);
    ret = listen(sock, 5);
    assert(ret != -1);
    struct sockaddr_in client;
    socklen_t client_addrlength = sizeof(client);
    int connfd = accept(sock, (struct sockaddr*)&client, &client_addrlength);
    if (connfd < 0) {
        printf( "errno is: %d\n", errno);
    }
    else {
        off_t len = 0;
        while (len < stat_buf.st_size) {
            int ret = sendfile(connfd, filefd, &len, stat_buf.st_size - len);
        //printf("ret value %d \n", ret);
            if (-1 == ret) {
                if (EAGAIN == errno) {
                    //printf("no data\n");
                    perror("sendfile");
                }
                else {
                    printf("client quit \n");
                    break;
                }
            }
        
        }
        close(connfd);
    }
    close(sock);

    return 0;

}

使用nc测试,结果如下:

 可以看到文件内容被nc程序正确接收。

4.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);
功能:splice函数用于在两个文件描述符之间移动数据,也是零拷贝

参数:
fd_in/off_in:fd_in为待输入数据的文件描述符, 如果fd_in是一个管道文件描述符,那么off_in参数必须设置为NULL。
如果fd_in不是一个管道文件(比如是一个socket),那么off_in表示从输入数据流的何处开始读取数据。此时,如果off_in被设置为NULL,则表示从输入数据流的当前偏移位置读入;
若off_in不为NULL,则它将指出具体的偏移位置。
fd_out/off_out参数的含义与fd_in/off_in相同,不过用于输出数据流。
len:移动数据的长度
flag:控制数据如何移动,它可以设置为下表中的某些值的按位或。

返回值:

     使用splice函数时,fd_in 和fd_out必须至少有一个是管道文件描述符。splice函数调用成功时返回移动字节的数量,它可能返回0,表示没有数据需要移动,这发生在从管道中读取数据,而该管道没有被写入任何数据时。spice函数失败时返回-1并设置errno.常见的errno如下图   

 以下测试代码编写一个简单的tcp服务器,使用匿名管道再结合splice向客户端回显信息。

#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 <fcntl.h>

int main(int argc, char* argv[]) {

    if (argc <= 2) {
        printf("usage: %s ip port\n", basename(argv[0]));
        return 1;
    }
    const char* ip = argv[1];
    int port = atoi(argv[2]);
    struct sockaddr_in address;
    bzero(&address, sizeof(address));
    address.sin_family = AF_INET;
    inet_pton(AF_INET, ip, &address.sin_addr);
    address.sin_port = htons(port);
    int sock = socket(PF_INET, SOCK_STREAM, 0);
    assert(sock >= 0);
    int ret = bind(sock, (struct sockaddr*)&address, sizeof(address));
    assert(ret != -1);
    ret = listen(sock, 5);
    assert(ret != -1);
    struct sockaddr_in client;
    socklen_t client_addrlength = sizeof(client);
    int connfd = accept(sock, (struct sockaddr*)&client, &client_addrlength);
    if (connfd < 0) {
        printf("errno is: %d\n", errno);
    }
    else {
        //创建匿名队列,pipefd[0]读,pipefd[1]写
        int pipefd[2];
        assert(ret != -1);
        ret = pipe(pipefd);
        // connfd -> pipefd[1]
        ret = splice(connfd, NULL, pipefd[1], NULL, 32768, SPLICE_F_MORE | SPLICE_F_MOVE); 
        assert(ret != -1);
        // pipefd[0] -> connfd
        ret = splice(pipefd[0], NULL, connfd, NULL, 32768, SPLICE_F_MORE | SPLICE_F_MOVE);
        assert(ret != -1);
        close(pipefd[0]);
        close(pipefd[1]);
        close(connfd);
    }
    close(sock);
    
    return 0;
}

运行结果如下:

可以看到客户端输入的内容被正确回显。 

5.tee

#include <fcntl.h>
ssize_t tee(int fd_in, int fd_out, size_t len, unsigned int flags);
功能:在两个管道文件描述符之间复制数据,也是零拷贝操作。它不消耗数据,因此源文件描述符上的数据仍然可以用于后续的读操作。
参数:
fd_in:待输入数据的文件描述符,必须是管道文件。
fd_out:待输出数据的文件描述符,必须是管道文件。
len:赋值的数据长度(字节数)
flags 修饰标志,跟splice(2)/vmsplice(2) 共享命名空间:
1)SPLICE_F_MOVE 当前对tee没有效果。
2)SPLICE_F_NONBLOCK 非阻塞的I/O操作,实际效果还会受文件描述符本身的阻塞状态的影响。
3)SPLICE_F_MORE当前对tee没有效果。
4)SPLICE_F_GIFT 对tee没有效果。
返回值:
成功时,返回两个文件描述符之间复制的数据量(字节数)。返回0表示没有复制任何数据,可能碰到EOF。失败时,返回-1,并设置errno。

如下代码显示从标准输入接收数据,通过管道将数据写入文件,可以看到tee(pipefd_stdout[0]调用两次,pipefd_stdout[0]中数据一直存在。

#include <assert.h>
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <fcntl.h>

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

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

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

    // pipefd_file[0] -> filefd
    ret = splice(pipefd_file[0], NULL, filefd, 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;
}

运行结果如下:

tee和splice的区别
tee类似于splice,都用于两个fd之间数据拷贝。区别在于:
1)对参数fd的要求
splice要求2个fd中至少必须有一个fd是管道文件;
tee要求两个fd都是管道文件。

2)对fd数据的消耗
splice是两个fd之间数据移动,splice会消耗fd数据;
tee是两个fd之间数据复制,tee不会消耗fd数据。

3)flags参数
Linux2.6.21以前,SPLICE_F_MOVE 对splice有效果,之后没效果。SPLICE_F_NONBLOCK 和SPLICE_F_MORE都对splice有效果;
只有SPLICE_F_NONBLOCK 才对tee有效果;

  • 0
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值