学习Linux中的dup/dup2函数。

0简介

1NAME

dup, dup2, dup3 - duplicate a file descriptor

这类函数是用于复制文件描述符的。

2SYNOPSIS

1#include <unistd.h>
2
3int dup(int oldfd);
4int dup2(int oldfd, int newfd);

3DESCRIPTION

 1dup()
 2系统调用创建文件描述符oldfd的副本,为新的描述符使用编号最低的未使用的描述符。
 3成功返回后,新旧文件描述符可以互换使用。它们引用相同的打开文件描述
 4(参见open(2)),从而共享文件偏移量和文件状态标志;例如,如果在一个文件上使用lseek(2)修改文件偏移量
 5在描述符中,另一个的偏移量也改变了。
 6这两个描述符不共享文件描述符标志(close-on-exec标志)。关闭执行标志(FD_CLOEXEC;它看到fcntl (2))
 7为重复描述符关闭。
 8
 9dup2()
10dup2()系统调用执行与dup()相同的任务,但它不是使用编号最低的未使用文件描述符,而是使用
11在newfd中指定的描述符编号。如果描述符newfd以前是打开的,那么在重用它之前,它将被无声地关闭。
12关闭和重用文件描述符newfd的步骤是原子地执行的。这很重要,因为试图实现
13使用close(2)和dup()的等效功能将受到竞争条件的影响,因此newfd可能在两者之间被重用
14步骤。发生这种重用的原因可能是主程序被分配文件描述符的信号处理程序中断
15因为并行线程分配一个文件描述符。
16
17注意以下几点:
18*如果oldfd不是有效的文件描述符,则调用失败,newfd不会关闭。
19*如果oldfd是一个有效的文件描述符,并且newfd与oldfd具有相同的值,那么dup2()不做任何事情,并返回newfd。

具体再解释下,由dup返回的新文件描述符一定是当前可用文件描述符中的最小数值。对于dup2,可以用fd2参数指定新描述符的值。如果fd2已经打开,则先将其关闭。如若fd等于fd2,则dup2返回fd2,而不关闭它。否则,fd2FD_CLOEXEC文件描述符标志就被清除,这样fd2在进程调用exec时是打开状态。

4RETURN VALUE

 1如果成功,这些系统调用将返回新的描述符。如果出现错误,则返回-1,并适当地设置errno。
 2
 3错误
 4EBADF oldfd不是一个打开的文件描述符。
 5EBADF newfd超出了文件描述符的允许范围(请参阅getrlimit(2)中关于RLIMIT_NOFILE的讨论)。
 6EBUSY(仅限Linux)这可能由dup2()或dup3()在open(2)和dup()的竞态条件下返回。
 7EINTR dup2()或dup3()调用被信号中断;看到信号(7)。
 8EINVAL (dup3())标志包含无效值。
 9EINVAL (dup3()) oldfd等于newfd。
10EMFILE每个进程打开的文件描述符数量的限制已经达到(参见getr‐中RLIMIT_NOFILE的讨论)
11限制(2))。

1举例

1.1复制文件描述符(重定向)

 1#include <stdio.h>
 2#include <unistd.h>
 3#include <stdlib.h>
 4#include <sys/stat.h>
 5#include <fcntl.h>
 6
 7int main(int argc, char* argv[])
 8{
 9    int i_fd = open("hello.txt", O_CREAT|O_APPEND|O_RDWR, 0666);
10
11    if(i_fd < 0)
12    {
13        printf("open error!\n");
14        return 0;
15    }
16
17    if(write(i_fd, "hello fd\n", 9) != 9)
18     {
19        printf("write fd error\n");
20
21    }
22	//这里操作之后,原本句柄3就被句柄4复制,也就是句柄3和句柄4都指向真实的hello.txt,关掉任意一个对另外一个都没影响
23    int i_dup_fd = dup(i_fd);
24    if(i_dup_fd < 0)
25    {
26        printf("dup error!\n");
27        return 0;
28    }
29
30    printf("i_dup_fd = %d \t i_fd = %d\n", i_dup_fd, i_fd);
31    close(i_fd);
32
33    char c_buffer[100];
34    int n = 0;
35    while((n = read(STDIN_FILENO, c_buffer, 1000)) != 0)
36    {
37        if(write(i_dup_fd, c_buffer, n) != n)
38        {
39            printf("write dup fd error!\n");
40            return 0;
41        }
42    }
43    return 0;
44}

1.2当前可用文件描述符中的最小数值

 1#include <stdio.h>
 2#include <unistd.h>
 3#include <stdlib.h>
 4#include <sys/stat.h>
 5#include <fcntl.h>
 6
 7int main(int argc, char* argv[])
 8{
 9    int i_fd = open("hello.txt", O_CREAT|O_APPEND|O_RDWR, 0666);
10
11    if(i_fd < 0)
12    {
13        printf("open error!\n");
14        return 0;
15    }
16
17    if(write(i_fd, "hello fd\n", 9) != 9)
18     {
19        printf("write fd error\n");
20
21    }
22    //在重定向新句柄前先把默认的句柄0关闭了,那么复制的句柄将是当前可用最小的句柄,即为0
23    close(STDIN_FILENO);
24    int i_dup_fd = dup(i_fd);
25    if(i_dup_fd < 0)
26    {
27        printf("dup error!\n");
28        return 0;
29    }
30
31    printf("i_dup_fd = %d \t i_fd = %d\n", i_dup_fd, i_fd);
32    close(i_fd);
33    close(i_dup_fd);
34
35    return 0;
36}

1.3 dup2

这里的dup2,会通过后者设置的标准输出,重定向为文件的句柄file_fd,然后在重定向回来为标准输出。

 1#include <stdio.h>
 2#include <unistd.h>
 3#include <stdlib.h>
 4#include <sys/stat.h>
 5#include <fcntl.h>
 6
 7int main(int argc, char *argv[])
 8{
 9    //先调用dup将标准输出拷贝一份,指向真正的标准输出
10    int stdout_copy_fd = dup(STDOUT_FILENO);
11    printf("stdout_copy_fd = %d \t STDOUT_FILENO = %d\n", stdout_copy_fd, STDOUT_FILENO);
12    int file_fd = open("hello_dup2.txt", O_CREAT|O_APPEND|O_RDWR, 0666);
13    //让标准输出指向文件
14    int dup2_fd = dup2(file_fd, STDOUT_FILENO);
15    //printf("before dup2_fd = %d \t file_fd = %d\n", dup2_fd, file_fd);
16    //实际上printf("hello\n")也可以写成fprintf(STDOUT_FILENO, "hello\n");
17    printf("hello\n");
18    //刷新缓冲区
19	//fflush(stdout);
20    //恢复标准输出
21    int dup2_recovery_fd =dup2(stdout_copy_fd, STDOUT_FILENO);
22    printf("world\n");
23    //printf("after dup2_fd = %d \t file_fd = %d \t dup2_recovery_fd = %d\n", dup2_fd, file_fd, dup2_recovery_fd);
24    close(file_fd);
25    close(stdout_copy_fd);
26    return 0;
27}

注:如果出现调用的两次printf仍然出现在屏幕上。。hello并没有写入到文件中。

这个时候需要刷新缓冲区,再重新输出。

1//让标准输出指向文件
2dup2(file_fd, STDOUT_FILENO);
3printf("hello\n");
4//刷新缓冲区
5fflush(stdout);
6//恢复标准输出
7dup2(stdout_copy_fd, STDOUT_FILENO);

2源码(与pipe一起使用)

2.1libc中的debuggerd

以Native crash中的源码为例

 1//system/core/debuggerd/handler/debuggerd_handler.cpp
 2static int debuggerd_dispatch_pseudothread(void* arg) {
 3  //首先把标准输出和标准输入都置为null,这里的/dev/null用于丢弃整个输出中的标准输出或标准错误
 4  int devnull = TEMP_FAILURE_RETRY(open("/dev/null", O_RDWR));
 5  TEMP_FAILURE_RETRY(dup2(devnull, 1));
 6  TEMP_FAILURE_RETRY(dup2(devnull, 2));
 7  //这里设置管道,管道的用途,一端的管道写,一段的管道读	
 8  unique_fd input_read, input_write;
 9  unique_fd output_read, output_write;
10  if (!Pipe(&input_read, &input_write) != 0 || !Pipe(&output_read, &output_write)) {
11    fatal_errno("failed to create pipe");
12  }
13  //这种数据结构调用中读、写多个非连续缓冲区  
14  struct iovec iovs[] = {
15      {.iov_base = &version, .iov_len = sizeof(version)},
16      {.iov_base = thread_info->siginfo, .iov_len = sizeof(siginfo_t)},
17      {.iov_base = thread_info->ucontext, .iov_len = sizeof(ucontext_t)},
18      {.iov_base = &thread_info->abort_msg, .iov_len = sizeof(uintptr_t)},
19      {.iov_base = &thread_info->fdsan_table, .iov_len = sizeof(uintptr_t)},
20      {.iov_base = &thread_info->gwp_asan_state, .iov_len = sizeof(uintptr_t)},
21      {.iov_base = &thread_info->gwp_asan_metadata, .iov_len = sizeof(uintptr_t)},
22  };
23  //将数据以iov的形式传递
24  ssize_t rc = TEMP_FAILURE_RETRY(writev(output_write.get(), iovs, arraysize(iovs)));
25  //将debuggerd_handler所在进程fork复制一份
26  pid_t crash_dump_pid = __fork();
27  if (crash_dump_pid == -1) {
28    async_safe_format_log(ANDROID_LOG_FATAL, "libc",
29                          "failed to fork in debuggerd signal handler: %s", strerror(errno));
30  } else if (crash_dump_pid == 0) {
31    //这个是子进程  
32    TEMP_FAILURE_RETRY(dup2(input_write.get(), STDOUT_FILENO));
33    TEMP_FAILURE_RETRY(dup2(output_read.get(), STDIN_FILENO));
34    input_read.reset();
35    input_write.reset();
36    output_read.reset();
37    output_write.reset();
38    //这个是crash_dump进程
39    execle(CRASH_DUMP_PATH, CRASH_DUMP_NAME, main_tid, pseudothread_tid, debuggerd_dump_type,
40           nullptr, nullptr);
41
42    return 1;
43  }
44  //外面是父进程
45  input_write.reset();
46  output_read.reset();
47  char buf[4];
48  rc = TEMP_FAILURE_RETRY(read(input_read.get(), &buf, sizeof(buf)));
49  if (rc == -1) {
50    async_safe_format_log(ANDROID_LOG_FATAL, "libc", "read of IPC pipe failed: %s", strerror(errno));
51    return 1;
52  } else if (rc == 0) {
53    async_safe_format_log(ANDROID_LOG_FATAL, "libc", "crash_dump helper failed to exec");
54    return 1;
55  } else if (rc != 1) {
56    async_safe_format_log(ANDROID_LOG_FATAL, "libc",
57                          "read of IPC pipe returned unexpected value: %zd", rc);
58    return 1;
59  } else if (buf[0] != '\1') {
60    async_safe_format_log(ANDROID_LOG_FATAL, "libc", "crash_dump helper reported failure");
61    return 1;
62  }
63}

把父子进程分成进程A和进程B

考虑到其中四个句柄是比较特殊的句柄,它们是管道,管道的一边是写,管道的一边是读

最终合并起来的父子进程表项

2.2crash_dump进程

 1int main(int argc, char** argv) {
 2  //将当前进程的标准输出复制为output_pipe,将便准输入复制为input_pipe
 3  unique_fd output_pipe(dup(STDOUT_FILENO));
 4  unique_fd input_pipe(dup(STDIN_FILENO));
 5  //出现的crash_dump进程之后又开始fork出一个新进程,将原先的进程退出  
 6  pid_t forkpid = fork();
 7  if (forkpid == -1) {
 8    PLOG(FATAL) << "fork failed";
 9  } else if (forkpid == 0) {
10    //子进程不退出
11    fork_exit_read.reset();
12  } else {
13    // We need the pseudothread to live until we get around to verifying the vm pid against it.
14    // The last thing it does is block on a waitpid on us, so wait until our child tells us to die.
15    //父进程退出
16    fork_exit_write.reset();
17    char buf;
18    TEMP_FAILURE_RETRY(read(fork_exit_read.get(), &buf, sizeof(buf)));
19    _exit(0);
20  }
21    
22  {
23    ATRACE_NAME("ptrace");
24    ...
25      if (thread == g_target_thread) {
26        //这里重要是的ReadCrashInfo,将之前保存的CrashInfo读取出来,通过input_pipe可以获取iov传输的数据
27        ReadCrashInfo(input_pipe, &siginfo, &info.registers, &abort_msg_address,
28                      &fdsan_table_address, &gwp_asan_state, &gwp_asan_metadata);
29        info.siginfo = &siginfo;
30        info.signo = info.siginfo->si_signo;
31      } else {
32        ...
33        }
34      }
35
36      thread_info[thread] = std::move(info);
37    }
38  }  
39  //将数据写入到output_pipe句柄中
40  if (TEMP_FAILURE_RETRY(write(output_pipe.get(), "\1", 1)) != 1) {
41    PLOG(FATAL) << "failed to write to pseudothread";
42  }
43}

crash_dump进程fork出一个进程C之后

最终进程B死掉,进程A和进程C通过管道通信

2.3总结

之所以上述的Native crash中的源码大费周章的将input_writeoutput_read重定向为标准输出和标准输入,然后又将标准输入和表述输出句柄copy到进程的新句柄output_pipeinput_pipe中。主要原因是这里用到了父子进程,其中子进程执行exec之后,变成了新的进程,原先的句柄无法直接通过变量带过来,可以通过标准输入和标准输出的形式将句柄复制过来。

参考

[1] 非正经程序员, 浅谈dup和dup2的用法, 2017.

[2] Hackergin, dup和dup2用法小结, 2016.

[3] mayue_csdn, readv和writev函数, 2022.