Notice
Recent Posts
Recent Comments
Link
«   2024/04   »
1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30
Archives
Today
Total
04-29 19:41
관리 메뉴

nomad-programmer

[Programming/C] 입출력의 리다이렉션 본문

Programming/C

[Programming/C] 입출력의 리다이렉션

scii 2020. 6. 19. 19:12
파일 디스크립터는 데이터 스트림을 나타내는 숫자이다.

데이터 스트림은 말 그대로 프로세스로 들어가고 나오는 데이터의 흐름이다. 표준 입력, 출력, 에러에 대한 데이터 스트림이 있으며, 파일이나 네트워크 연결과 같은 데이터 스트림도 더 만들 수 있다. 프로세스의 출력을 리다이렉션하면 데이터를 보낼 곳을 바꿀 수 있다. 따라서 표준 출력이 하면 대신 파일에 데이터를 보낼 수 있다.

모든 프로세스는 스택과 힙 데이터 공간 외에도 자신이 실행하는 프로그램을 포함하고 있다. 그런데 표준 출력과 같은 데이터 스트림이 어디에 연결되는지 어딘가에 기록해놓아야 한다. 각 데이터 스트림은 파일 디스크립터(File Descriptor)에 의해 표현되는데, 프로그램에서는 단지 숫자로 나타난다. 프로세스는 파일 디스크립터와 이에 해당하는 데이터 스트림을 디스크립터 테이블(Descriptor Table)에 저장해 처리한다.

#

데이터 스트림

0 (표준 입력)

키보드

1 (표준 출력)

화면

2 (표준 에러)

화면

3 (프로세스가 스트림을 열면 뒤에 추가된다)

데이터베이스 연결

디스크립터 테이블은 한 열로 구성되고 각 파일 디스크립터 숫자마다 한 항목에 대응된다. 이 항목들은 파일 디스크립터라고 불리지만, 하드 디스크에 있는 실제 파일에만 연결되는 것은 아니다. 테이블은 모든 파일 디스크립터에 연관된 데이터 스트림을 기록한다. 데이터 스트림은 키보드나 화면에 대한 연결, 파일 포인터, 네트워크 연결이 될 수 있다.

테이블 앞의 세 항목은 언제나 똑같다. 0번은 표준 입력, 1번은 표준 출력, 2번은 표준 에러이다. 테이블의 다른 항목은 비어 있거나 프로세스가 연 데이터 스트림에 연결된다. 예를 들어 프로그램이 읽거나 쓰기 위해 파일을 열 때마다 디스크립터 테이블의 한 항목이 채워진다.

프로세스가 생성될 때 표준 입력은 키보드에, 표준 출력은과 에러는 화면에 연결된다. 그리고 누군가 표준 입력이나 출력을 리다이렉션할 때까지 그대로 연결되어 있다.

파일 디스크립터가 꼭 파일을 의미하는 건 아니다.

표준 입력, 출력, 에러는 언제나 디스크립터 테이블의 동일한 위치에 고정되어 있다. 그런데 이 항목이 가리키는 데이터 스트림이 바뀔 수 있다.

printf() 함수와 같이 표준 출력으로 데이터를 보내는 모든 함수는 디스크립터 테이블을 검색해 디스크립터 1번이 가리키는 스트림을 찾는다. 그리고 데이터를 해당 데이터 스트림에 출력한다.

이제 왜 "2>" 를 사용해 표준 에러를 리다이렉션했는지 알 것이다. 2는 표준 에러의 디스크립터 테이블 번호를 의미한다. 대부분의 운영체제에서 표준 출력을 리다이렉션하기 위해 1> 를 사용할 수도 있다. 그리고 유닉스 기반 시스템에서는 다음과 같이 표준 에러를 표준 출력과 같은 스트림으로 리다이렉션할 수 있다.

./cmd 2>&1

프로세스는 자신을 리다이렉션 할 수 있다. 프로세스는 디스크립터 테이블을 연결해 자신을 리다이렉션할 수 있다.

fileno() 함수가 디스크립터를 알려준다.

파일을 열 매마다 운영체제는 새로운 항목을 디스크립터 테이블에 등록한다. 가령 아래와 같은 파일을 열었다고 해보자.

FILE *my_file = fopen("guitar.mp3", "rb");

운영체제는 guitar.mp3 파일을 열고 파일 포인터를 반환한다. 그리고 디스크립터 테이블의 앞에서부터 빈 슬롯을 찾을 때까지 훑어보다가 빈 슬롯을 찾으면 그 곳에 새 파일을 등록한다. 그러나 파일 포인터를 받은 후에 어떻게 디스크립터 테이블에서 찾을 수 있을까?

바로 "fileno()" 함수를 호출하면 된다.

...

3

데이터베이스 연결

4

guitar.mp3

// 이 함수는 4를 반환
int descriptor = fileno(my_file);

fileno() 함수는 실패하더라도 -1을 반환하지 않는 몇 안 되는 시스템 함수 중 하나이다. 열린 파일에 대한 포인터로 fileno()를 호출하면 디스크립터 번호를 반환한다.

dup2() 함수는 데이터 스트립을 복제한다.

파일을 열면 디스크립터 테이블의 슬롯을 채우지만, 이미 등록된 데이터 스트림의 디스크립터를 바꾸려면 어떻게 해야 할까?

파일 디스크립터 3이 다른 데이터 스트림을 가리키도록 바꾸려면 "dup2()" 함수를 이용하면 된다.

dup2() 함수는 한 슬롯에 있는 내용을 다른 슬롯에 복사한다. 따라서 guitar.mp3에 대한 파일 포인터가 파일 디스크립터 4에 연결되어 있을 때 다음 코드를 실행하면 이 파일 디스크립터를 슬롯3에도 연결시켜준다.

dup2(4, 3);

...

3

데이터베이스 연결 guitar.mp3

4

guitar.mp3

guitar.mp3 파일은 하나만 있고, 여기에 연결된 데이터 스트림도 하나만 있지만, 이 데이터 스트림(FILE*)이 파일 디스크립터 3번과 4번에 등록되어 있다.

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

int main(int argc, char *argv[]) {
    FILE * fp = fopen("/home/scii/Desktop/test.txt", "wt");
    if(fp == NULL){
        fprintf(stderr, "파일 열기 실패: %s\n", strerror(errno));
        return 1;
    }
    pid_t pid = fork();
    if(-1 == pid){
        fprintf(stderr, "fork 실패: %s\n", strerror(errno));
        return 1;
    }
    if(!pid){
    	// dup2함수로 인하여 디스크립터 1번 항목이 test.txt 파일을 가리키게된다.
        // 즉, 3번의 디스크립터 스트림이 1번에 들어간다.
        if (dup2(fileno(fp), 1) == -1){
            fprintf(stderr, "표준 출력을 리다이렉션할 수 없음: %s\n", strerror(errno));
            return 1;
        }
        if(-1 == execlp("ps", "ps", "aux", NULL)){
            fprintf(stderr, "명령 실패: %s\n", strerror(errno));
            return 1;
        }
    }

    return 0;
}

// 결과
/* 

데스크탑에 test.txt 파일이 만들어지며 ps 결과 값들이 쓰여있다.

*/

위의 예제는 아래와 같은 디스크립터 테이블을 만든다.

#

데이터 스트림

0

키보드

1

화면 test.txt 파일

2

화면

3

test.txt 파일

 

자식 프로세스는 부모 프로세스에 "종속적" 이다. 따라서 운영체제는 자식 프로세스가 완료될 때까지 기다릴 수 있게 해야 한다.

waitpid() 함수

// sys/wait.h 헤더 파일을 인클루드 해야한다.
#include <sys/wait.h>

// pid: 자식 프로세스를 포크했을 때 부모 프로세스가 받은 자식 프로세스의 ID
// pid_status: 프로세스의 종료 정보를 저장. waitpid()가 값을 바꿀 수 있게 포인터형 인자를 전달
// options: waitpid()에 여러 옵션을 설정할 수 있다. 터미널에서 man waitpid를 치면 자세한 정보를 확인할 수 있다.
// 옵션을 0으로 설정하면, 이 함수는 프로세스가 종료될 때까지 기다린다.
waitpid(pid, pid_status, options);

waitpid() 함수는 자식 프로세스가 종료될 때까지 반환하지 않는다.

waitpid() 함수가 종료될 때 프로세스의 상태를 pid_status 변수에 저장한다. 자식 프로세스의 상태를 확인하려면 pid_status 값을 WEXITSTATUS() 라는 매트로에 전달하면 된다.

// 종료 상태가 0이 아니라면
if (WEXITSTATUS(pid_status)) {
	puts("에러 상태가 0이 아니다");
}

pid_status 는 여러 정보를 포함하고 있다. 이 값의 앞에 있는 8비트만 종료 상태를 나타내므로 매크로를 사용해 앞 부분의 8비트 값을 추출한다.

waitpid() 함수는 프로그램에 추가하기가 쉬우며 프로그램을 더욱 신뢰성 있게 만든다. 이 함수를 사용하기 전에는 자식 프로세스가 종료되었는지 장담할 수 없었으므로 제대로 작동되지만 신뢰가 가지 않았다.

입출력을 리다이렉션하고 프로세스가 다른 프로세스를 기다리게 만드는 것은 모두 프로세스 간 통신을 간략화한 형태이다. 데이터를 공유하고 서로 처리를 완료할 때까지 기다리는 등 프로세스가 협력할 때 프로세스는 더욱 강력해진다.

정리

exit()를 사용하면 프로세스를 빨리 종료할 수 있다.

열린 파일은 모두 디스크립터 테이블에 기록된다.

디스크립터 테이블을 바꾸면 입출력을 리다이렉션할 수 있다.

fileno() 함수는 디스크립터 테이블에 있는 스트림의 디스크립터를 찾는다.

dub2() 함수를 사용하면 디스크립터 테이블을 바꿀 수 있다.

waitpid()는 프로세스가 종료할때까지 기다린다.

'Programming > C' 카테고리의 다른 글

[Programming/C] 시그널 (Signal)  (0) 2020.06.20
[Programming/C] pipe() 함수  (0) 2020.06.19
[Programming/C] fork() 함수  (0) 2020.06.19
[Programming/C] errno.h 헤더 파일  (0) 2020.06.19
[Programming/C] exec 함수 사용  (0) 2020.06.19
Comments