자식프로세스의 표준출력을 파이프로 부모프로세스에서 받아오기 - PIPE redirection

Kim Minseok·2024년 3월 29일
0

linux-ubuntu

목록 보기
1/1

복습

리눅스의 모든 입출력은 파일이다.

리눅스는 모든 입출력을 파일로 처리한다. USB 장치에 접근할 때도, 네트워크 소켓을 열고 닫을때도, 프로세스 끼리의 통신을 할 때도, 모든 입출력을 파일 입출력과 동일한 매커니즘을 사용한다.

Standard IO (표준 입출력)

리눅스 환경과 응용 프로그램 사이에 미리 연결된 입출력 통로를 표준입출력이라고 한다. 따로 건드리지 않는 이상, 표준입출력은 기본적으로 터미널의 키보드(입력)과 모니터(출력)에 연결되어있다. 표준입출력 또한 파일을 읽고 쓰는 과정와 완전히 동일하게 작동한다.

File Descripter (파일 디스크립터)

File Descripter(이하 fd)는 리눅스 환경에서 파일을 접근하는데 사용되는 일종의 ID와 같은 것이다. 코드로는 int(4byte)형 정수로 처리된다. 프로세스가 시스템콜로 OS에 "이 파일 접근하고 싶어요"라고 시스템 콜을 보내면, OS는 프로세스에게 파일을 접근할 수 있는 통로를 열어줌과 둥시에 프로세스에게 int형 정수(fd)를 하나 리턴한다. 프로세스는 읽기동작, 쓰기동작을 하려면 이 fd 정수값을 시스템콜로 전달해주면서 "이 fd 가 가리키는 파일에 읽기/쓰기를 해주세요"라고 전달하면 된다.

각 프로세스마다 FD는 별도로 할당된다. (A프로세스에게는 17번 File Descripter가 "a.txt"를 읽고쓰는데 사용될 수도 있고, B프로세스에게는 17번 File Descripter가 웹소켓을 여는데 사용될 수도 있음)

리눅스 파일시스템에서 /proc/<PID명>/fd 에서 열린 fd들을 확인할 수 있다. (참고로 커널은 fd를 오름차순으로 할당해준다고 함)

fd로 열었다면 write와 read 함수 를 이용해 입출력을 할 수 있다. 그리고 입출력이 끝났다면, close함수로 닫아주는것이 필요하다.

모든 프로세스는 공통적으로 표준입출력의 FD가 기본할당된다.fd 0(표준입력),1(표준출력),2(표준에러)

POSIX 표준에서 매크로에는 다음과 같이 정의되어있다 :

/* Standard file descriptors.  */
#define	STDIN_FILENO	0	/* Standard input.  */
#define	STDOUT_FILENO	1	/* Standard output.  */
#define	STDERR_FILENO	2	/* Standard error output.  */

https://elixir.bootlin.com/glibc/latest/source/posix/unistd.h#L209

FD는 fork할 때 복제, execl 이후에도 유지

fd는 fork할 때 복제되며, 새로운 프로세스를 만들었을 때도 계속 유지된다.
https://man7.org/linux/man-pages/man3/exec.3p.html

한번 연 파일 디스크립터들은 자식프로세스에서도 그대로 사용이 가능하다는 것 (심지어 exec 계열 함수로 실행 코드를 완전이 바꿔치기 해도)

dup 함수

fd 번호가 가리키는(대표하는) 장치(혹은 파일)을 바꿔주는(복제하는) 시스템콜 함수이다.

예를 들어서 17번 fd가 a.txt라는 파일의 출력을 제어하는데 사용중이라고 가정.

int fd2 = dup(fd); // fd2는 fd가 가리키는 장치를 똑같이 가리킨다.

dup2 함수는 이미 할당된 fd가 다른 방향을 가리키도록 할 수 있다.

// fd1가 a.txt, fd2가 b.txt에 연결되어있었다고 가정.
 
dup2(fd1,fd2); // fd2는 이제 fd1이 가리키는 파일을 연결함. 
//즉 fd2로 a.txt에 접근가능, 기존 b.txt와의 연결은 자동으로 close.

심지어 표준입출력도 건드려줄 수 있다.

// fd가 a.txt출력을 가리키고있었다고 가정
dup(fd,STDOUT_FILENO); // 이제 stdout FD (1번)은 a.txt를 가르키게 됨

위 코드를 실행하면, 이후에 putc, printf같은 함수를 사용하면 터미널에 출력되는 것이 아니라 a.txt에 출력되는 것을 확인할 수 있다. 이 부분이 재미있음.

pipe (파이프)

파이프는 리눅스에서 제공하는 대표적인 IPC(Inter Process Comunication) (= 프로세스끼리 통신하는 방법) 기법 중 하나.

부모와 자식프로세스간 통신방식임. bash shell에서는 | 기호로 파이프를 붙일 수 있음.

pipe() 시스템콜 함수로 pipe를 만들면 인자로 보낸 int fd[2] 배열에 0번 인덱스에는 파이프에서 입력을 받아오고, 1번 인덱스는 파이프로 출력을 함.

pipe는 단방향으로 사용하는 것이 강력하게 권장, 양방향 통신 필요할 시, pipe를 두개 여는 것이 좋음.

redirecting stdout to pipe

파이프를 stdout에 출력시키는 과정

#include <stdio.h>
#include <unistd.h> // fork, execl, pipe, dup2 ...

int main() {
    pid_t pid;
    int fd[2]; // 파이프의 fd
    pipe(fd);
    
    if((pid = fork()) == 0) { // 자식 프로세스
        close(fd[0]); // 자식은 pipe로 출력을 하므로 입력 fd 닫기
        dup2(fd[1], STDOUT_FILENO); // stdout을 파이프로 리다이렉트
        close(fd[1]); // 더 이상 필요 없으므로 닫기
        execl("/bin/ls", "ls", NULL); // 실행파일 실행
    } else { // 부모 프로세스
        close(fd[1]); // 부모는 pipe로 입력을 하므로 출력 fd 닫기
        char c;
        while (read(fd[0], &c, 1) > 0) {
            write(STDOUT_FILENO, &c, 1); // 읽은 내용을 stdout으로 출력
        }
        close(fd[0]); // 읽기 작업 끝난 후 닫기
    }
    
    return 0;
}

사실 훨씬 더 편한방법

그냥 popen함수를 이용하면 된다. 쉘로 명령어를 실행하고 그걸 편하게 FILE 포인터로 받아서 처리할 수 있다.

#include <stdio.h> // popen 함수 위치

int main() {
    FILE *fp;
    char path[1035];

    // "ls" 명령어 실행하여 결과를 읽기 위해 popen 호출
    fp = popen("ls", "r");
    if (fp == NULL) {
        printf("popen failed\n");
        return 1;
    }

    // 결과를 한 줄씩 읽어서 출력
    while (fgets(path, sizeof(path), fp) != NULL) {
        printf("%s", path);
    }

    // popen 닫기
    pclose(fp);

    return 0;
}

심화

자식프로세스가 끝나기 전 까지 출력이 안됨

stdio.h의 출력 함수들은 특이한 행동을 하는데, stdout fd가 기본설정으로 되어있을 때, 즉 그냥 터미널로 평범하게 출력할 때는 line flushing을 한다. 개행문자를 만날 때 마다 버퍼를 비우고 목적지 파일로 전송한다는 것이다. 그러나 stdout fd가 그 외의 장치로 연결이 되어있다면, full-buffering을 한다.

그래서 자식프로세스에서 별도로 printf() 등을 사용한 후 flush를 하지 않는다면, 자식프로세스가 종료되거나, 아니면 버퍼가 꽉 차야지만 출력이 된다.

어떻게 printf는 장치에 따라 구분하여 flush를 다르게 하는가?

이 부분을 찾아보니 좀 재미있다. STDOUT_FILENO를 stat 시스템콜로 조사해서,tty가 아니라면 full buffering으로, tty면 line buffering으로 동작한다고 한다.
https://stackoverflow.com/questions/13932932/why-does-stdout-need-explicit-flushing-when-redirected-to-file

profile
안녕하세요! 대학생 김민석입니다.

0개의 댓글