用dup2系统调用实现stdin重定向

Last Updated: 2023-11-05 08:21:23 Sunday

-- TOC --

在shell命令上频繁使用的管道功能,伴随着第管道末端进程的stdin重定向功能,实现的关键,就是dup2这个接口。(对stdout的重定向,同理)

// dup2 - duplicate a file descriptor
// man 2 dup
#include <unistd.h>
int dup2(int oldfd, int newfd);
// oldfd --> newfd

If the file descriptor newfd was previously open, it is closed before being reused; the close is performed silently (i.e., any errors during the close are not reported by dup2()). newfd会被dup2关闭,不会有任何错误上报。

The steps of closing and reusing the file descriptor newfd are performed atomically. This is important, because trying to implement equivalent functionality using close(2) and dup() would be subject to race conditions, whereby newfd might be reused between the two steps. Such reuse could happen because the main program is interrupted by a signal handler that allocates a file descriptor, or because a parallel thread allocates a file descriptor. 以前的先close再dup,可能存在race condition,不要用了。

下面的测试代码,用管道与子进程的stdin对接(从原理上看,只是复用了stdin相同的fd号码),子进程读取stdin,实际上读取的是来自管道的数据,这相当于对子进程实现了shell的重定向功能。

子进程:

$ cat pt.py
while True:
    try:
        line = input().strip()
        if line != '':
            print('[recv]', line)
    except EOFError:
        break
print('pt.py end with EOF!')

主进程:

$ cat test_pipe.c
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#include <stdlib.h>
#include <string.h>
#include <sys/wait.h>


#define _PEXIT \
    do {\
        char ebuf[64] = {0};\
        sprintf(ebuf, "%s: %d", __FILE__, __LINE__);\
        perror(ebuf);\
        exit(errno);\
    }while(0)


int main(void) {
    pid_t c1;
    int pipefd[2];

    /* create pipe */
    if (pipe(pipefd)) 
        _PEXIT;

    /* fork */
    if ((c1=fork()) == -1)
        _PEXIT;
    else if (c1 == 0) {  // child
        /* close write end */
        close(pipefd[1]);
        /* dup2 pipe read end to stdin */
        dup2(pipefd[0], STDIN_FILENO);
        /* close original read end */
        close(pipefd[0]);
        /* exec */
        execlp("python3", "python3", "pt.py", NULL);
        /* if error */
        _PEXIT;
    }

    /* close read end */
    close(pipefd[0]);
    int i = 8;
    char msg[64] = {0};
    while (i--) {
        sprintf(msg, "%s%d\n", "hello pipe and dup2..", i);
        if (write(pipefd[1],msg,strlen(msg)) == -1)
            _PEXIT;
        sleep(1);
    }

    /* close write end, EOF of pipe */
    close(pipefd[1]);
    wait(NULL);  // NULL means don't care the wait status,
                 // the returned pid is also ignored.
    return 0;
}

标准头文件中的三个macro:STDIN_FILENO, STDOUT_FILENO, STDERR_FILENO

执行效果:

$ gcc -Wall -Wextra test_pipe.c -o test_pipe
$ ./test_pipe
[recv] hello pipe and dup2..7
[recv] hello pipe and dup2..6
[recv] hello pipe and dup2..5
[recv] hello pipe and dup2..4
[recv] hello pipe and dup2..3
[recv] hello pipe and dup2..2
[recv] hello pipe and dup2..1
[recv] hello pipe and dup2..0
pt.py end with EOF!

用shell重定向调用子进程的代码:

$ echo '123123' > a.txt
$ echo 'abcabc' >> a.txt
$ python3 pt.py < a.txt
[recv] 123123
[recv] abcabc
pt.py end with EOF!

更复杂的重定向功能,也可以用过dup2复制更多的fd来实现。比如可以将管道的write end同时复制到子进程STDOUT_FILENO和STDERR_FILENO,以此来获取子进程所有向stdout和stderr的输出。

前面的主进程,也可以用Python来实现:

$ cat test_dup2.py
import os
import sys
from time import sleep


rfd, wfd = os.pipe()
if os.fork() == 0:
    try:
        os.close(wfd)
        os.dup2(rfd, sys.stdin.fileno())
        os.close(rfd)
        os.execlp('python3', 'python3', 'pt.py')
    except OSError as e:
        print(os.strerror(e.errno))
        sys.exit(e.errno)

os.close(rfd)
i = 4
while i>0:
    os.write(wfd, f'hello pipe and dup2...{i}\n'.encode())
    sleep(1)
    i -= 1

os.close(wfd)
print(os.waitstatus_to_exitcode(os.wait()[1]))

执行效果:

$ python3 test_dup2.py 
[recv] hello pipe and dup2...4
[recv] hello pipe and dup2...3
[recv] hello pipe and dup2...2
[recv] hello pipe and dup2...1
pt.py end with EOF!
0

本文链接:https://cs.pynote.net/sf/linux/prog/202207082/

-- EOF --

-- MORE --