Linux

[Linux] IPC - 1 (파이프의 개념 - 이름 없는 파이프 pipe)

개발로 먹고 살자 2022. 8. 10. 11:49

IPC는 Inter-Prcess Communication의 약어로 프로세스 간 통신을 말한다.

 

하나의 시스템에서는 프로세스와 다른 프로세스와의 통신만 하면 된다.

여기에 사용되는 것이 pipes, FiFOs, message queue 등이 있다.

 

 

네트워크 시스템에서는 시스템과 시스템의 연결이 우선적으로 필요하다.

네트워크 시스템 연결은 일반적으로 socket 을 사용한다.

 

 

오늘은 파이프에 대해 써보려고 한다.

 

 

파이프는 두 프로세스 사이에서 한 방향으로 통신할 수 있도록 지원하는 것이다.

셸에서는 | 기호가 파이프를 의미한다.

 

 

파이프는 크게 이름 없는 파이프와 이름 있는 파이프로 구분이 된다.

 

이름 없는 파이프 : pipe

별 다른 말 없이 파이프라고 하면 이름 없는 파이프를 의미한다.

 

 

이름 없는 파이프는 부모 - 자식 프로세스 간에 통신을 할 수 있게 해준다.

message pasing 방식이라고도 하는데 우리가 아는 fork() 함수가 이에 해당한다.

 

fork() 함수를 통해 부모 프로세스와 자식 프로세스 간에 통신을 하는 것이다.

 

pipe 는 부모 자식 간에 통신이기 때문에 자신이 만든 파이프에 이름이 존재하지 않는다.

부모 자식 간에 통신이기에 서로 다른 프로세스임에도 값을 공유하고 있다.

 

 

파이프는 기본적으로 단방향 통신이다.

따라서 부모 프로세스가 출력한 내용을 자식 프로세스에서 읽을 것인지

자식 프로세스가 출력한 내용을 부모 프로세스에서 읽을 것인지

둘 중 한 방향을 선택해야 한다.

 

 

양방향 통신을 하고 싶을 때는 어떻게 해야할까?

그런 경우 파이프를 2개 생성해서 사용하면 된다.

 

 

이름 없는 파이프 생성 함수

간단한 파이프 생성

FILE *popen(const char *command, const char *type);
int pclose(FILE *stream);

command는 셸 명령을 의미하고

type은 read, write인 r 또는 w을 의미한다.

 

쓰기 전용 파이프 생성

해당 코드는 popen을 통해 자식 프로세스가 wc -l 명령을 수행한다.

wc -l 명령어는 입력되는 데이터의 행 수를 출력하는 명령어이다.

 

부모 프로세스에서는 for 문을 통해 문자열을 파이프로 출력한다.

 

즉, 부모 프로세스에서 파이프로 출력되는 문자열을 개수를 자식 프로세스가

출력하는 것이다.

 

실행 결과

부모 프로세스에서 for 문을 통해 파이프로 100번 출력한 것을

자식 프로세스에서 개수를 확인할 수 있다.

 

popen 함수로 읽기 전용 파이프를 생성하는 것도 동일한 방법을 이용하면 된다.

 

 

복잡한 파이프 생성

int pipe(int fd[2]);

pipefd[2] 는 파이프로 사용할 파일 기술자 2개를 뜻한다.

 

popen() 함수를 사용하여 파이프를 생성하는 것은 간단하지만

셸을 실행해야 하기 때문에 비효율적이다.

 

그렇기 때문에 pipe() 함수를 사용하는 것이 좀 더 효율적인 생성을 가능하다.

 

 

pipe() 함수를 통신하는 과정을 살펴보자.

우선 pipe() 함수를 호출하여 파이프에 사용할 파일 기술자를 얻는다.

파이프도 파일의 일종이기 때문에 읽고 쓸 수 있는 파일 기술자가 필요하다.

이를 pipe() 함수가 생성해준다. 사용 시 그림과 같이 fd[0]과 fd[1]이 생성된다.

 

fd[0] 은 읽기 전용으로 열고 fd[1]은 쓰기 전용으로 연다.

 

 

여기서 fork() 함수를 수행하여 자식 프로세스를 생성한다.

이 때 pipe() 함수에서 생성한 파일 기술자 또한 자식 프로세스에 복사된다.

 

하지만 파이프는 단방향 통신이므로 하나의 파이프로는 양방향으로

통신하지 못한다. 그렇기 때문에 어느 방향으로 통신할지 통신 방향을 결정한다.

 

예를 들어 부모 프로세스에서 쓰고 자식 프로세스에서 읽는다고 한다.

그렇다면 부모 프로세스는 쓰기만 할 것이기 때문에 읽기용 파일 기술자인 fd[0]을 닫는다.

 

반대로 자식 프로세스는 읽기만 하기 때문에 쓰기용 파일 기술자인 fd[1]을 닫는다.

 

이제 부모 프로세스가 fd[1]에 쓰기 자식 프로세스에서 fd[0]으로 읽으면 된다.

 

 

부모 프로세스에서 쓰고 자식 프로세스에서 읽는 프로그램을 만들어보겠다.

우선 pipe(fd)로 파일 기술자를 만든다.

 

fork() 함수를 사용해 자식 프로세스를 만들고,

자식 프로세스는 쓰기인 fd[1]을 닫고 부모 프로세스는 읽기인 fd[0]을 닫는다.

이렇게 되면 위의 그림과 같은 상황이 된 것이다.

 

이제 부모 프로세스에서 fd[1]을 이용하여 쓰고

자식 프로세스에서 fd[0]을 이용하여 읽으면 된다.

 

부모 프로세스에서는 fd[1]에 test message 라는 문자을 쓴다.

fd[1]인 파일 기술자가 자식 프로세스와 파이프로 연결되어 있기 때문에

자식 프로세스에서 fd[0]을 통해 읽어 buf에 담고 buf를 출력한다.

 

 

실행 결과

부모 프로세스에서 쓰고 자식 프로세스가 받아 출력되는 것을 볼 수 있다.

 

전체 소스 코드

#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/wait.h>

int main(int argc, char* argv[]) {

    int fd[2];
    pid_t pid;
    char buf[256];
    int status;
    
    if(pipe(fd) == -1) {
    	perror("pipe");
        exit(1);
    }
    
    pid = fork();
    
    if(pid < 0) {
    	perror("fork");
        exit(1);
    } else if(pid == 0) {
    	close(fd[1]);
        read(fd[0], buf, 256);
        printf("%s", buf);
        close(fd[0]);
    } else {
    	close(fd[0]);
        write(fd[1], ""test message\n", 256);
        close(fd[1]);
        wait(&status);
    }
    
	return 0;
}

 

 

 

그럼 양방향으로도 통신을 해보자.

우선 양방향으로 통신을 하기 위해서는 파이프가 두 개 필요하다.

 

단방향에서 파이프 하나를 더 추가하여 양방향으로 만든다.

파이프가 하나라면 파일 기술자 배열을 하나만 만들면 되지만

두 개일 경우 파일 기술자 배열도 두 개를 만들어야 양방향으로 읽고 쓸 수 있다.

 

위의 소스 코드에서 파일 기술자 배열을 하나 더 추가하여 파이프를 생성한다.

 

자식 프로세스에서 각각의 파이프를 읽기, 쓰기 하나씩만 열어놓는다.

부모 프로세스 또한 마찬가지다.

 

이렇게 작성했다면 위 그림과 같은 상태가 된다.

 

여기서 자식 프로세스는 read 한 것을 출력 후 write 한다.

부모 프로세스는 write 이후 read 한 것을 출력한다.

 

 

만약 둘 다 먼저 read를 하거나 write를 하게 된다면 무한정 대기 상태에 빠지게 된다.

그렇기 때문에 둘 다 서로 겹치지 않게 읽고 써야한다.

 

실행 결과

부모 프로세스에서 작성한 im parent를 자식 프로세스에서 출력하고

자식 프로세스에서 작성한 im child를 부모 프로세스에서 출력한다.

 

 

전체 소스 코드

#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/wait.h>
#include<string.h>

int main(int argc, char* argv[]) {
    int fd1[2], fd2[2];
    pid_t pid;
    char buf[256];
    int status;
    
    if(pipe(fd1) == -1) {
    	perror("pipe");
        exit(1);
    }
    
    if(pipe(fd2) == -1) {
    	perror("pipe");
        exit(1);
    }
    
    pid = fork();
    
    if(pid < 0) {
    	perror("fork");
        exit(1);
    } else if(pid == 0) { // 자식 프로세스
    	close(fd1[1]);
        close(fd2[0]);
        
        read(fd1[0], buf, 256);
        printf("from parent process : %s\n", buf);
        
        write(fd2[1], "im child", 256);
        
        close(fd1[0]);
        close(fd2[1]);
    } else {
    	close(fd1[0]);
        close(fd2[1]);
        
        write(fd1[1], "im parent", 256);
        
        read(fd2[0], buf, 256);
        printf("from child process : %s\n", buf);
        
        close(fd1[1]);
        close(fd2[0]);
    }
    
    return 0;
}

 

파일 기술자를 여닫는 과정이 다소 복잡하지만 그림과 같이 보면 훨씬 더 쉽게

이해할 수 있을 것이다.

pipe를 사용하면 쉘에 직접 접근하지도 않고 파이프를 2개 생성하면 양방향 통신이 가능하다.

 

 

다음에는 이름 없는 파이프 pipe가 아닌,

이름 있는 파이프 FIFO에 대해 작성해보도록 하겠다.