Operating System/UNIX

[UNIX] 파이프 (Pipe) 제대로 사용하기

  • -

개념

파이프는 두 프로세스가 통신할 수 있게 하는 전달자로,

UNIX 기반 운영체제에서 제공하는 프로세스 간 통신 (Inter-Process Communication, IPC) 기법 중 하나이다. 

 

멀티 프로세스에 대한 자세한 설명은 아래의 링크 참고.

 

[UNIX] 멀티 프로세스 (Multi Process) 프로그래밍

개념 프로세스란 실행 중인 프로그램(실행 파일)이자, 현대의 컴퓨팅 시스템에서 작업의 단위이다. 프로세스는 실행되는 동안 여러 개의 새로운 프로세스들을 생성할 수 있다. 생성하는 프로세

sikpang.tistory.com

 

 

사용법

 

#include <unistd.h>

int pipe(int pipefd[2]);

 

pipe()의 인자로 파이프의 fd가 들어갈 크기 2개짜리 int 배열을 받는다.

pipe()가 성공하면, 인자로 넣는 배열 0번째 index에 파이프의 읽기 전용 fd, 1번째에 파이프의 쓰기 전용 fd가 저장된다.

 

이러한 파이프는 일반 파이프라고 하며, 단방향으로 사용해야 한다.

데이터를 쓰는 프로세스는 파이프 fd의 1번째에 write()하고,

데이터를 읽는 프로세스는 파이프 fd의 0번째에서 read()해야 한다.

 

read(), write() 및 끝에 나올 close() 대한 설명은 아래의 링크 참고.

 

[UNIX] 입출력 및 리다이렉션 (I/O Redirection)

유닉스 계열 운영체제에서 파일은 연속된 n개의 바이트다. 네트워크, 디스크, 터미널 같은 모든 I/O 디바이스들은 파일로 모델링되며, 모든 입력과 출력은 해당 파일을 읽거나 쓰는 형식으로 수

sikpang.tistory.com

 

일반 파이프는 파이프를 생성한 프로세스만 접근할 수 있다.

따라서 부모 프로세스가 파이프를 생성하고, fork() fd 테이블을 복제하여 자식 프로세스에서도 해당 파이프를 사용할 수 있게 해야 한다.

 

#include <unistd.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <string.h>
#define BUFFER_SIZE 20

int main()
{
	char buf[BUFFER_SIZE];
	int pipe_fd[2];
    
	pipe(pipe_fd);
	pid_t pid = fork();

	if (pid == 0)
	{
		printf("I'm Child Process\n");
		read(pipe_fd[0], buf, BUFFER_SIZE);
		printf("Child Received Data : %s\n", buf);
	}
	else
	{
		printf("I'm Parent Process\n");
		char* data = "Hello World!";
		write(pipe_fd[1], data, strlen(data));
		wait(NULL);
	}
	return 0;
}

 

파이프는 위와 같이 사용이 가능하다.

 

부모 프로세스가 Hello World! 라는 문자열을 파이프의 쓰기 전용 fd write() 하면, 

자식 프로세스가 파이프의 읽기 전용 fd read() 하여 읽어올 수 있다.

 

실행 결과는 아래와 같다.

 

 

 

파이프 사용 시 유의점

pipe()로 생성된 일반 파이프는 사실 양방향으로 사용이 가능은 하다.

하지만 앞서 일반 파이프는 단방향으로 사용해야 한다고 했던 이유를 알아보자.

 

데이터를 보낸 측에서 다시 읽어버릴 가능성

파이프는 fork()를 호출한 순간 자식 프로세스에서 읽기/쓰기 fd가 열리게 되지만,

그 이전에 부모 프로세스에서도 이미 읽기/쓰기 fd가 열려있었다는 점을 유의하자.

 

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

#define BUFFER_SIZE 20

int main()
{
	char buf[BUFFER_SIZE];
	int pipe_fd[2];
    
	pipe(pipe_fd);
	pid_t pid = fork();

	if (pid == 0)
	{
		printf("I'm Child Process\n");
		read(pipe_fd[0], buf, BUFFER_SIZE);
		printf("Child Received Data : %s\n", buf);
		
		char* data = "Hello From Child";
		write(pipe_fd[1], data, strlen(data));
	}
	else
	{
		printf("I'm Parent Process\n");
		char* data = "Hello From Parent";
		write(pipe_fd[1], data, strlen(data));

		read(pipe_fd[0], buf, BUFFER_SIZE);
		printf("Parent Received Data : %s\n", buf);
		wait(NULL);
	}
	return 0;
}

 

 

위의 코드는 부모와 자식이 모두 읽기, 쓰기를 번갈아가며 하고 있고 그 결과는 다음과 같다.

 

 

부모 프로세스에서 write()하여 데이터를 보낸 이후,

자식 프로세스가 읽기 전에 부모 프로세스가 다시 read()하여 읽어버렸다.

 

따라서 자식 프로세스의 read()는 아무 것도 읽지 못하고, 파이프에 데이터가 쌓일 때까지 blocking된다.

 

부모 프로세스의 write()read() 사이에 을 주면 원하는 대로 실행이 가능하다.

다음은 부모 프로세스의 쓰기/읽기 사이에 일정 시간의 usleep()을 호출한 이후 실행 결과이다.

 

 

하지만 fork()의 딜레이와 함께 자식 프로세스가 read()하는 타이밍을 정확히 usleep()에 의존하는 행동은 매우 위험하다.

 

아래는 극단적인 예시를 위해 usleep()의 인자에 1을 줬을 때의 실행 결과다.

 

 

프로그램이 실행될 때마다 프로세스의 결과가 다르게 나타나는 것을 확인할 수 있다.

 

이를 피하기 위해 usleep()의 인자를 매우 크게 준다면, 프로그램의 성능에 큰 영향을 미칠 것이고,

usleep()인자로 넘긴 시간보다 자식 프로세스의 작업이 느려질 경우 이 역시 위와 동일한 현상이 발생할 것이다.


쓰기 작업이 종료되었음에도 읽는 측이 감지하지 못하는 경우

쓰기 작업이 끝났을 때 모든 프로세스에서 파이프의 쓰기 전용 fd를 잘 닫아주지 않으면,

read()가 EOF를 감지하지 못해 무한정 읽는 상황이 벌어진다.

 

아래 코드는 자식 프로세스가 read()를 두 번 하는 경우다.

 

#include <unistd.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <string.h>
#define BUFFER_SIZE 20

int main()
{
	char buf[BUFFER_SIZE];
	int pipe_fd[2];
    
	pipe(pipe_fd);
	pid_t pid = fork();

	if (pid == 0)
	{
		printf("I'm Child Process\n");
		for (int i=0; i<2; ++i)
		{
			if (read(pipe_fd[0], buf, BUFFER_SIZE) == 0)
				break;
			printf("Child Received Data %d : %s\n", i, buf);
		}
	}
	else
	{
		printf("I'm Parent Process\n");
		char* data = "Hello From Parent";
		write(pipe_fd[1], data, strlen(data));
		wait(NULL);
	}
	return 0;
}

 

첫 번째 read()에서는 데이터를 잘 받아왔지만, 두 번째 read()에서는 파이프가 비어있기에 데이터를 받아오지 못했다.

 

이처럼 프로세스 간 통신에서 write()read()의 호출 횟수가 다를 수 있다.

이 때 파이프의 쓰는 fd가 모두 닫히면, 읽는 프로세스에서 read()를 호출했을 때 EOF를 잘 전달받고, 안전하게 종료된다.

 

#include <unistd.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <string.h>
#define BUFFER_SIZE 20

int main()
{
	char buf[BUFFER_SIZE];
	int pipe_fd[2];
    
	pipe(pipe_fd);
	pid_t pid = fork();

	if (pid == 0)
	{
		close(pipe_fd[1]);
		printf("I'm Child Process\n");
		for (int i=0; i<2; ++i)
		{
			if (read(pipe_fd[0], buf, BUFFER_SIZE) == 0)
				break;
			printf("Child Received Data %d : %s\n", i, buf);
		}
	}
	else
	{
		printf("I'm Parent Process\n");
		char* data = "Hello From Parent";
		write(pipe_fd[1], data, strlen(data));
		close(pipe_fd[1]);
		wait(NULL);
	}
	return 0;
}

 

위와 같은 이유들로 인해, 파이프에서 쓰지 않는 fd는 모두 닫아주어야

프로그래머의 의도 대로 데이터의 전달이 원활하게 프로세스가 진행될 수 있다.

 

파이프를 재활용하는 경우

예시로 쉘(Shell)에서 파이프( | ) 사용 시, 파이프 자체를 재활용 한다면 다음과 같은 상황이 벌어진다.

 

 

위의 그림에서 볼 수 있다시피, 1번 파이프의 읽기 측을 사용할 수 있는 프로세스는 총 4개다.

 

 

안 쓰는 파이프의 fd를 모두 닫더라도, 3번 프로세스에서 1번 파이프의 읽기 fd를 사용하기 때문에

1번 자식 프로세스에게 데이터가 제대로 전달되지 않을 가능성이 있다.

 

이러한 이유로 인해 파이프를 재활용하는 것은 바람직하지 않다.

 

 

참고 자료

https://man7.org/linux/man-pages/man2/pipe.2.html
https://man7.org/linux/man-pages/man7/pipe.7.html

 

 

개인 공부용 포스팅인 점을 참고하시고, 잘못된 부분이 있다면 댓글로 남겨주시면 감사드리겠습니다.
Contents

포스팅 주소를 복사했습니다

이 글이 도움이 되었다면 공감 부탁드립니다.