当前位置:   article > 正文

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

linux dup

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返回值指向同一个文件,只是文件描述符不同。

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

  1. #include <assert.h>
  2. #include <stdio.h>
  3. #include <unistd.h>
  4. #include <errno.h>
  5. #include <string.h>
  6. #include <fcntl.h>
  7. int main(int argc, char* argv[]) {
  8. if (argc != 2) {
  9. printf("usage: %s <file>\n", argv[0]);
  10. return 1;
  11. }
  12. int filefd = open(argv[1], O_CREAT | O_WRONLY | O_TRUNC, 0666);
  13. assert(filefd > 0);
  14. close(STDOUT_FILENO);
  15. printf("=== %d\n", dup(filefd));
  16. write(filefd, "hello world\n", 12);
  17. close(filefd);
  18. printf("dup函数,你好啊");
  19. return 0;
  20. }

运行结果:

2.dup2

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

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

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

  1. #include <assert.h>
  2. #include <stdio.h>
  3. #include <unistd.h>
  4. #include <errno.h>
  5. #include <string.h>
  6. #include <fcntl.h>
  7. int main(int argc, char* argv[]) {
  8. if (argc != 2) {
  9. printf("usage: %s <file>\n", argv[0]);
  10. return 1;
  11. }
  12. int filefd = open(argv[1], O_CREAT | O_WRONLY | O_TRUNC, 0666);
  13. assert(filefd > 0);
  14. write(filefd, "hello liudehua\n", 15);
  15. printf("=== %d\n", dup2(filefd, STDOUT_FILENO));
  16. write(filefd, "hello world\n", 12);
  17. close(filefd);
  18. return 0;
  19. }

文件内容如下:

 使用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向客户端发送文件的例子。

  1. #include <sys/socket.h>
  2. #include <netinet/in.h>
  3. #include <arpa/inet.h>
  4. #include <assert.h>
  5. #include <stdio.h>
  6. #include <unistd.h>
  7. #include <stdlib.h>
  8. #include <errno.h>
  9. #include <string.h>
  10. #include <sys/types.h>
  11. #include <sys/stat.h>
  12. #include <fcntl.h>
  13. #include <sys/sendfile.h>
  14. int main( int argc, char* argv[] ) {
  15. if( argc <= 3 ) {
  16. printf("usage: %s ip port filename\n", basename(argv[0]));
  17. return 1;
  18. }
  19. const char* ip = argv[1];
  20. int port = atoi(argv[2]);
  21. const char* file_name = argv[3];
  22. int filefd = open(file_name, O_RDONLY);
  23. assert(filefd > 0);
  24. struct stat stat_buf;
  25. fstat(filefd, &stat_buf);
  26. struct sockaddr_in address;
  27. bzero(&address, sizeof(address));
  28. address.sin_family = AF_INET;
  29. inet_pton(AF_INET, ip, &address.sin_addr);
  30. address.sin_port = htons(port);
  31. int sock = socket(PF_INET, SOCK_STREAM, 0);
  32. assert(sock >= 0);
  33. int ret = bind(sock, (struct sockaddr*)&address, sizeof(address));
  34. assert(ret != -1);
  35. ret = listen(sock, 5);
  36. assert(ret != -1);
  37. struct sockaddr_in client;
  38. socklen_t client_addrlength = sizeof(client);
  39. int connfd = accept(sock, (struct sockaddr*)&client, &client_addrlength);
  40. if (connfd < 0) {
  41. printf( "errno is: %d\n", errno);
  42. }
  43. else {
  44. off_t len = 0;
  45. while (len < stat_buf.st_size) {
  46. int ret = sendfile(connfd, filefd, &len, stat_buf.st_size - len);
  47. //printf("ret value %d \n", ret);
  48. if (-1 == ret) {
  49. if (EAGAIN == errno) {
  50. //printf("no data\n");
  51. perror("sendfile");
  52. }
  53. else {
  54. printf("client quit \n");
  55. break;
  56. }
  57. }
  58. }
  59. close(connfd);
  60. }
  61. close(sock);
  62. return 0;
  63. }

使用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向客户端回显信息。

  1. #include <sys/socket.h>
  2. #include <netinet/in.h>
  3. #include <arpa/inet.h>
  4. #include <assert.h>
  5. #include <stdio.h>
  6. #include <unistd.h>
  7. #include <stdlib.h>
  8. #include <errno.h>
  9. #include <string.h>
  10. #include <fcntl.h>
  11. int main(int argc, char* argv[]) {
  12. if (argc <= 2) {
  13. printf("usage: %s ip port\n", basename(argv[0]));
  14. return 1;
  15. }
  16. const char* ip = argv[1];
  17. int port = atoi(argv[2]);
  18. struct sockaddr_in address;
  19. bzero(&address, sizeof(address));
  20. address.sin_family = AF_INET;
  21. inet_pton(AF_INET, ip, &address.sin_addr);
  22. address.sin_port = htons(port);
  23. int sock = socket(PF_INET, SOCK_STREAM, 0);
  24. assert(sock >= 0);
  25. int ret = bind(sock, (struct sockaddr*)&address, sizeof(address));
  26. assert(ret != -1);
  27. ret = listen(sock, 5);
  28. assert(ret != -1);
  29. struct sockaddr_in client;
  30. socklen_t client_addrlength = sizeof(client);
  31. int connfd = accept(sock, (struct sockaddr*)&client, &client_addrlength);
  32. if (connfd < 0) {
  33. printf("errno is: %d\n", errno);
  34. }
  35. else {
  36. //创建匿名队列,pipefd[0]读,pipefd[1]写
  37. int pipefd[2];
  38. assert(ret != -1);
  39. ret = pipe(pipefd);
  40. // connfd -> pipefd[1]
  41. ret = splice(connfd, NULL, pipefd[1], NULL, 32768, SPLICE_F_MORE | SPLICE_F_MOVE);
  42. assert(ret != -1);
  43. // pipefd[0] -> connfd
  44. ret = splice(pipefd[0], NULL, connfd, NULL, 32768, SPLICE_F_MORE | SPLICE_F_MOVE);
  45. assert(ret != -1);
  46. close(pipefd[0]);
  47. close(pipefd[1]);
  48. close(connfd);
  49. }
  50. close(sock);
  51. return 0;
  52. }

运行结果如下:

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

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]中数据一直存在。

  1. #include <assert.h>
  2. #include <stdio.h>
  3. #include <unistd.h>
  4. #include <errno.h>
  5. #include <string.h>
  6. #include <fcntl.h>
  7. int main(int argc, char* argv[]) {
  8. if (argc != 2) {
  9. printf("usage: %s <file>\n", argv[0]);
  10. return 1;
  11. }
  12. int filefd = open(argv[1], O_CREAT | O_WRONLY | O_TRUNC, 0666);
  13. assert(filefd > 0);
  14. int pipefd_stdout[2];
  15. int ret = pipe(pipefd_stdout);
  16. assert(ret != -1);
  17. int pipefd_file[2];
  18. ret = pipe(pipefd_file);
  19. assert(ret != -1);
  20. // 标准输入 STDIN_FILENO -> pipefd_stdout[1]
  21. ret = splice(STDIN_FILENO, NULL, pipefd_stdout[1], NULL, 32768, SPLICE_F_MORE | SPLICE_F_MOVE);
  22. assert(ret != -1);
  23. // pipefd_stdout[0] -> pipefd_file[1]
  24. ret = tee(pipefd_stdout[0], pipefd_file[1], 32768, SPLICE_F_NONBLOCK);
  25. assert(ret != -1);
  26. ret = tee(pipefd_stdout[0], pipefd_file[1], 32768, SPLICE_F_NONBLOCK);
  27. assert(ret != -1);
  28. // pipefd_file[0] -> filefd
  29. ret = splice(pipefd_file[0], NULL, filefd, NULL, 32768, SPLICE_F_MORE | SPLICE_F_MOVE);
  30. assert(ret != -1);
  31. close(filefd);
  32. close(pipefd_stdout[0]);
  33. close(pipefd_stdout[1]);
  34. close(pipefd_file[0]);
  35. close(pipefd_file[1]);
  36. return 0;
  37. }

运行结果如下:

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有效果;

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/2023面试高手/article/detail/721848
推荐阅读
相关标签
  

闽ICP备14008679号