[Linux] 파이프 개요 및 이름 없는 파이프

jiwon·2023년 12월 4일

Linux

목록 보기
12/12

1. 파이프 개요


  • 물을 보내는 수도 파이프와 비슷
  • 파이프는 두 프로세스 사이에서 한 방향으로 통신할 수 있도록 지원하는 것
  • 한 프로세스는 쓰기용 파일 디스크립터를 이용하여 파이프에 데이터를 보내고
  • 다른 프로세스는 읽기용 파일 디스크립터를 이용하여 그 파이프에서 데이터를 받음
  • 한 방향(one way) 통신
cat test.c | more_
  • 앞에 있는 명령인 cat test.c 의 표준 출력을 다음 명령인 more의 표준 입력으로 사용
  • 파이프는 이름 없는 파이프와 이름 있는 파이프로 구분

이름 없는 파이프 : pipe

  • 특별한 수식어 없이 그냥 파이프라고 하면 일반적으로 이름 없는 파이프(익명 파이프)를 의미
  • 부모 - 자식 프로세스 간에 통신을 할 수 있게 해줌
  • 부모 프로세스에서 fork() 함수를 통해 자식 프로세스를 생성, 부모 프로세스와 자식 프로세스 간에 통신하는 것

이름 없는 파이프 생성 함수

간단한 파이프 생성
FILE *popen(const char *command, const char *type);
int pclose(FILE *stream);
복잡한 파이프 생성
int pipe(int pipefd[2]);

이름 있는 파이프 : FIFO

  • 특수 파일의 한 종류
  • 모든 프로세스가 이름 있는 파이프를 이용해 통신할 수 있음

이름 있는 파이프 생성 명령과 함수

FIFO 생성 명령
mknod 파일명 p
mkfifo [-m mode] ...NAME...
FIFO 생성 함수
int mknod(const char *pathname, mode_t mode, dev_t dev);
int mkfifo(const char *pathname, mode_t mode);

2. 이름 없는 파이프


  • 파이프는 두 프로세스 간에 통신을 할 수 있도록 인터페이스를 제공
  • 수식어가 없는 파이프, 즉 이름 없는 파이프는 부모 - 자식 프로세스 간에 통신을 할 수 있게 함
  • 파이프는 기본적으로 단방향
  • 따라서 부모 프로세스가 출력한 내용을 자식 프로세스에서 읽을 것인지, 자식 프로세스가 출력한 내용을 부모 프로세스에서 읽을 것인지 둘 중 한 방향을 선택
  • 이는 수도관에 물을 보낼 때 양쪽에서 서로 보낼 수 없는 이치와 마찬가지임

2.1 간단한 파이프 생성

파이프를 만드는 가장 간단한 방법은 popen() 함수를 사용하는 것

파이프 만들기 : popen(3)

#include<stdio.h>

FILE *popen(const char *command, const char *type);

* command : 셀 명령
* type : "r" 또는 "w"
  • popen() 함수는 다른 프로세스와 통신하기 위해 파이프를 생성
  • command에는 셀 명령을, 두 번째 인자인 type에는 "r"이나 "w"를 지정
  • "r"를 읽기 전용으로, "w"는 쓰기 전용으로 엶
execl("/bin/sh", "sh", "-c", command, (char *)0);
  • popen() 함수는 자식 프로세스와 파이프를 만들고 mode의 값에 따라 표준 입출력을 연결함
  • 리턴 값은 파일 포인터
  • 파일 입출력 함수에서 이 파일 포인터를 사용하면 파이프를 읽거나 쓸 수 있음
  • popen() 함수는 파이프 생성에 실패하면 널 포인터를 반환

파이프 닫기 : pclose

#include<stdio.h>

int pclose(FILE *stream);

* stream : popen 함수에서 리턴한 파일 포인터
* return : 자식 프로세스의 종료 상태(exit status) / 실패 시 -1

popen() 함수를 쓰기 전용 모드로 사용해 파이프를 생성해보자

#include<stdlib.h>
#include<stdio.h>

int main() {
	FILE *fp;
    int a;
    
    fp = popen("wc -l", "w");
    if (fp == NULL) {
    fprintf(stderr, "popen failed\n");
    exit(1);
    }
    
    for (a = 0; a < 100; a++)
    	fprintf(fp, "test line\n");
      
    pclose(fp);
}
    
결과 값 : 100
  • 08행 "w" 모드를 사용해 쓰기 전용 파이프를 생성하고 자식 프로세스는 wc -l 명령을 수행하도록 함. wc -l은 입력되는 데이터의 데이터의 행 수를 출력하는 명령
  • 14~15행 부모 프로세스에서는 반복문을 사용해 문자열을 파이프로 출력함. 앞서 언급했듯 이 자식 프로세스는 파이프로 입력되는 문자열을 읽어서 wc -l 명령을 수행함.

이번에는 popen() 함수를 읽기 전용 모드로 사용해 파이프를 생성해보자. 자식 프로세스가 파이프에 기록한 데이터를 부모 프로세스가 읽어서 처리한다.

#include<stdlib.h>
#include<stdio.h>

int main() {
	FILE *fp;
    char buf[256];
    
    fp = popen("date", "r");
    if(fp == NULL) {
    	fprintf(stderr, "popen failed\n");
        exit(1);
   }
   if (fgets(buf, sizeof(buf), fp) == NULL) {
   		fprintf(stderr, "No data from pipe!\n");
        exit(1);
   }
   
   printf("line : %s\n", buf);
   pclose(fp);
}
결과 값 : line : 현재 날짜 및 시간 정보
  • 08행 자식 프로세스에서는 date 명령을 수행함
  • 14행, 19행 부모 프로세스에서는 자식 프로세스가 기록한 데이터를 14행에서 읽고 저장해 19행에서 출력함
  • 실행 결과 현재 날짜와 시각이 출력되었음. 자식 프로세스가 실행한 date 명령의 결과를 부모 프로세스가 읽어서 출력한 것

2.2 복잡한 파이프 생성

  • popen() 함수를 사용해 파이프를 생성하는 일은 간단, 셸을 실행해야 하므로 비효율적이고 주고받을 수 있는 데이터도 제한적
  • popen() 함수 대신 pipe() 함수를 사용하면 과정은 복잡해지지만 파이프를 좀 더 효율적으로 생성 가능

파이프 만들기 : pipe(2)

#include<unistd.h>

int pipe(int pipefd[2]);
  • 인자 설명
    pipefd[2] : 파이프로 사용할 파일 기술자(2개)
  • pipe() 함수는 인자로 크기가 2인 정수형 배열을 받음
  • pipe() 함수는 이 배열에 파일 기술자 2개를 저장함
  • pipefd[0]은 읽기 전용으로 열고 pipefd[1]은 쓰기 전용으로 엶
  • pipe() 함수는 파이프를 생성하는 데 성공하면 0을, 실패하면 -1을 리턴함

pipe() 함수로 통신하는 과정

pipe() 함수로 생성한 파이프로 통신하는 과정을 살펴보자

  • 파이프를 생성하면 일반적으로 fork() 함수를 호출해 자식 프로세스를 생성함
  • 자식 프로세스는 부모 프로세스가 pipe() 함수로 생성한 파일 기술자도 복사함
  • 이 파이프를 이용해 한 프로세스에서는 쓰기를 수행하고 다른 프로세스에서는 읽기를 수행하면 통신이 됨

이 과정을 단계적으로 살펴보자
1. pipe() 함수를 호출해 파이프에 사용할 파일 기술자를 얻음. 파이프도 파일의 일종이므로 파일(파이프)을 읽고 쓸 수 있는 파일 기술자가 필요한데, 이를 pipe() 함수가 생성해줌.

2. fork() 함수를 수행해 자식 프로세스를 생성함. 이때 pipe() 함수에서 생성한 파일 기술자도 자식 프로세스로 복사됨. 같은 파일 기술자를 부모 프로세스와 자식 프로세스가 모두 가지고 있음

3. 파이프는 단방향 통신이므로 통신 방향을 결정함. 예를 들어, 부모 프로세스는 쓰기를 하고 자식 프로세스는 읽기를 해야한다고 했을 때, 각각의 파일 기술자에서 쓰지 않는 읽기 또는 쓰기의 파일 기술자를 닫아야 함

만약! 파이프의 쓰기 부분이 닫혀 있다면 파이프에서 읽으려고 할 때 0이나 EOF가 리턴함
파이프의 읽기 부분이 닫혀 있다면 파이프에 쓰려고 할 때 SIGPIPE 시그널이 발생함

부모 프로세스와 자식 프로세스 사이에서 파이프를 사용하여 통신하는 예제
#include <sys/wait.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>

int main() {
    int fd[2]; // 파이프 파일 디스크립터를 저장할 배열
    pid_t pid; // 프로세스 ID를 저장할 변수
    char buf[257]; // 메시지를 저장할 버퍼
    int len, status;

    // 파이프 생성
    if (pipe(fd) == -1) {
        perror("pipe"); // 에러 메시지 출력
        exit(1); // 프로그램 종료
    }

    // 자식 프로세스 생성
    switch (pid = fork()) {
        case -1 : // fork 실패 시
            perror("fork"); // 에러 메시지 출력
            exit(1); // 프로그램 종료
            break;
        case 0 : /* 자식 프로세스 */
            close(fd[1]); // 쓰기용 파이프 종료
            write(1, "Child Process:", 15); // 자식 프로세스 메시지 출력
            len = read(fd[0], buf, 256); // 읽기용 파이프에서 메시지 읽기
            write(1, buf, len); // 읽은 메시지 출력
            close(fd[0]); // 읽기용 파이프 종료
            break;
        default : /* 부모 프로세스 */
            close(fd[0]); // 읽기용 파이프 종료
            write(fd[1], "Test Message\n", 14); // 쓰기용 파이프에 메시지 쓰기
            close(fd[1]); // 쓰기용 파이프 종료
            waitpid(pid, &status, 0); // 자식 프로세스가 종료될 때까지 대기
            break;
    }
}
결과 값 : Child Process: Test Message
부모 프로세스가 ps -ef명령을 실행한 뒤 그 결과를 자식 프로세스로 전달, 자식 프로세스는 그 결과 중 "ssh"를 포함한 라인만 필터링하여 출력하는 예제
#include <sys/wait.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>

int main() {
    int fd[2]; // 파이프 파일 디스크립터를 저장할 배열
    pid_t pid; // 프로세스 ID를 저장할 변수

    // 파이프 생성
    if (pipe(fd) == -1) {
        perror("pipe"); // 에러 메시지 출력
        exit(1); // 프로그램 종료
    }

    // 자식 프로세스 생성
    switch (pid = fork()) {
        case -1 : // fork 실패 시
            perror("fork"); // 에러 메시지 출력
            exit(1); // 프로그램 종료
            break;
        case 0 : /* 자식 프로세스 */
            close(fd[1]); // 쓰기용 파이프 종료
            if (fd[0] != 0) {
                dup2(fd[0], 0); // 읽기용 파이프를 표준 입력으로 복제
                close(fd[0]); // 복제된 읽기용 파이프 종료
            }
            execlp("grep", "grep", "ssh", (char *)NULL); // "ssh"를 포함한 라인만 필터링하는 grep 명령 실행
            exit(1); // 프로그램 종료
            break;
        default : /* 부모 프로세스 */
            close(fd[0]); // 읽기용 파이프 종료
            if (fd[1] != 1) {
                dup2(fd[1], 1); // 쓰기용 파이프를 표준 출력으로 복제
                close(fd[1]); // 복제된 쓰기용 파이프 종료
            }
            execlp("ps", "ps", "-ef", (char *)NULL); // 모든 프로세스의 정보를 출력하는 ps 명령 실행
            wait(NULL); // 자식 프로세스가 종료될 때까지 대기
            break;
    }
}

양방향 파이프의 활용

  • 파이프는 기본적으로 단방향 통신을 수행함
  • 따라서 양방향 통신을 하려면 파이프를 두 개 생성하면 됨
  • 상수도와 하수도가 각각 별도의 파이프로 구성되어 있는 것과 같은 이치임
부모 프로세스와 자식 프로세스가 양방향으로 통신하는 예제
#include <sys/wait.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>

int main() {
    int fd1[2], fd2[2]; // 양방향 통신을 위한 두 개의 파이프 파일 디스크립터 배열
    pid_t pid; // 프로세스 ID를 저장할 변수
    char buf[257]; // 메시지를 저장할 버퍼
    int len, status;

    // 첫 번째 파이프 생성
    if (pipe(fd1) == -1) {
        perror("pipe"); // 에러 메시지 출력
        exit(1); // 프로그램 종료
    }

    // 두 번째 파이프 생성
    if (pipe(fd2) == -1) {
        perror("pipe"); // 에러 메시지 출력
        exit(1); // 프로그램 종료
    }

    // 자식 프로세스 생성
    switch (pid = fork()) {
        case -1 : // fork 실패 시
            perror("fork"); // 에러 메시지 출력
            exit(1); // 프로그램 종료
            break;
        case 0 : /* 자식 프로세스 */
            close(fd1[1]); // 첫 번째 파이프의 쓰기용 종료
            close(fd2[0]); // 두 번째 파이프의 읽기용 종료
            len = read(fd1[0], buf, 256); // 첫 번째 파이프에서 메시지 읽기
            write(1, "Child Process:", 15); // 자식 프로세스 메시지 출력
            write(1, buf, len); // 읽은 메시지 출력

            strcpy(buf, "Good\n"); // 버퍼에 "Good\n" 문자열 복사
            write(fd2[1], buf, strlen(buf)); // 두 번째 파이프에 메시지 쓰기
            break;
        default : /* 부모 프로세스 */
            close(fd1[0]); // 첫 번째 파이프의 읽기용 종료
            close(fd2[1]); // 두 번째 파이프의 쓰기용 종료
            write(fd1[1], "Hello\n", 6); // 첫 번째 파이프에 메시지 쓰기

            len = read(fd2[0], buf, 256); // 두 번째 파이프에서 메시지 읽기
            write(1, "Parent Process:", 15); // 부모 프로세스 메시지 출력
            write(1, buf, len); // 읽은 메시지 출력
            waitpid(pid, &status, 0); // 자식 프로세스가 종료될 때까지 대기
            break;
    }
}
결과 값 : username   1234  0.0  0.0   1234   5678 pts/0    Ss+  Dec04   0:00 ssh example@example.com
profile
BSSM

0개의 댓글