프로세스 간 통신(IPC, Inter-Process Communication) 중에서도 파이프(Pipe)는 유닉스/리눅스 시스템에서 가장 기본적인 IPC 메커니즘이다.
파이프(Pipe)
파이프란?
파이프(Pipe)는 두 프로세스 간 데이터를 일시적으로 저장하고 전달하는 커널 내부의 버퍼다. 사용자는 write/read로 데이터를 주고받으며, 이때 파이프는 파일 디스크립터로 다뤄진다.
파이프는 크게 두 종류로 구분된다.
- 이름 없는 파이프 (Anonymous Pipe)
- 이름 있는 파이프 (Named Pipe, FIFO)
이름 없는 파이프 (Anonymous Pipe)
특징
이름 없는 파이프는 익명 파이프라고도 하며, 다음과 같은 특징을 가진다.
- 부모-자식 프로세스 간에만 통신 가능
- 부모 프로세스에서 fork()로 자식 프로세스를 생성한 뒤 통신
- 단방향 통신만 가능 (양방향 통신을 위해서는 두 개의 파이프 필요)
- 부모 → 자식 방향, 자식 → 부모 방향 중 하나를 선택해야 함
popen() 함수
FILE *popen(const char *command, const char *mode);popen() 함수는 쉘 명령어(command)를 실행하고, 그 명령의 입력 또는 출력을 FILE * 스트림 형태로 연결해주는 함수다.
매개변수:
command: 실행할 쉘 명령mode: "r" (읽기) 또는 "w" (쓰기)
주요 역할:
- 쉘 명령어를 실행한다
- 해당 명령의 표준 입력 또는 출력을 프로세스 간 파이프로 연결한다
- 사용자는 FILE *을 통해 그 결과를 읽거나 쓸 수 있다
내부 동작 과정:
-
파이프(pipe) 생성
-
fork()로 자식 프로세스 생성
-
자식 프로세스에서:
- mode가 "r" → 표준 출력을 파이프 출력으로 리디렉션
- mode가 "w" → 표준 입력을 파이프 입력으로 리디렉션
-
exec()로 쉘 명령(command) 실행
exec("/bin/sh", "sh", "-c", command, (char *)NULL); -
부모 프로세스는 FILE * 스트림을 받아 사용
리턴 값:
- 성공 시: FILE *
- 실패 시: NULL
pclose() 함수
int pclose(FILE *stream);popen()으로 생성한 자식 프로세스(쉘 명령 실행)가 종료될 때까지 기다렸다가 종료 상태를 반환하고, 사용한 리소스를 정리하는 함수다.
주요 역할:
- waitpid()로 popen()이 만든 자식의 종료를 기다린다
- 자식 프로세스의 종료 상태를 정수로 리턴한다
- 파이프, FILE 스트림 등을 정리하고 닫는다
리턴 값:
- 성공 시: 자식 프로세스의 종료 상태 코드
- 실패 시: -1
popen() 사용 예제
예제 1: 쓰기 모드 ("w")
#include <stdlib.h>
#include <stdio.h>
int main() {
FILE *fp;
int a;
// wc -l 명령을 쓰기 모드로 열기
fp = popen("wc -l", "w");
if (fp == NULL) {
perror("popen");
exit(1);
}
// 100줄의 테스트 라인을 wc -l 명령에 전달
for (a = 0; a < 100; a++) {
fprintf(fp, "test line %d\n", a);
}
pclose(fp); // 출력: 100 (줄 수)
return 0;
}동작 과정:
popen("wc -l", "w")호출- pipe() 호출 → 파이프 생성
- fork() 호출 → 자식 프로세스 생성
4. 자식 프로세스에서:- stdin을 파이프의 출력 쪽으로 리디렉션
- exec("wc -l") 실행
5. 부모 프로세스에서: - fp는 파이프의 쓰기 스트림
- 이를 통해 자식 프로세스에게 데이터 전달
예제 2: 읽기 모드 ("r")
#include <stdlib.h>
#include <stdio.h>
int main() {
FILE *fp;
char buf[256];
// date 명령을 읽기 모드로 열기
fp = popen("date", "r");
if (fp == NULL) {
perror("popen");
exit(1);
}
// date 명령의 출력을 읽기
if(fgets(buf, sizeof(buf), fp) == NULL) {
fprintf(stderr, "No data from pipe\n");
exit(1);
}
printf("line: %s", buf);
pclose(fp);
return 0;
}pipe() 함수
int pipe(int pipefd[2]);popen() 함수는 쉘을 무조건 실행해야 하므로 비효율적이고 제한적이다.
→ pipe() 함수를 사용하면 파이프를 조금 더 효율적으로 생성할 수 있다.
매개변수:
pipefd[2]: 파이프로 사용할 파일 디스크립터 배열pipefd[0]: 읽기 전용 (프로세스 입장에서 입력)pipefd[1]: 쓰기 전용 (프로세스 입장에서 출력)
pipe()로 통신하는 과정
1단계: 파이프 생성
int fd[2];
pipe(fd); // fd[0]: 읽기, fd[1]: 쓰기2단계: fork()로 자식 프로세스 생성

fork() 이후 자식 프로세스에도 pipe fd가 복제된다(각자의 fd).
3단계: 파이프의 통신 방향 결정

fork() 이후 불필요한 파이프 디스크립터를 close() 해주지 않으면, 읽기/쓰기 블로킹이 발생하여 데드락 가능성이 생긴다.
파이프 상태에 따른 동작:
- 쓰기 부분은 닫혀있고 읽기 부분만 열려있을 때 → 0이나 EOF 리턴
- 쓰기 부분은 열려있고 읽기 부분은 닫혀있을 때 → SIGPIPE 시그널 발생
pipe() 사용 예제
기본적인 부모-자식 통신
#include <sys/wait.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
int main() {
int fd[2];
pid_t pid;
char buf[257];
int len, status;
if (pipe(fd) == -1) {
perror("pipe");
exit(1);
}
switch(pid = fork()) {
**case -1**:
perror("fork");
exit(1);
break;
case 0: /* child process */
close(fd[1]); // 쓰기 파이프 닫기
write(1, "Child Process\n", 15);
len = read(fd[0], buf, 256); // 부모로부터 데이터 읽기
write(1, buf, len);
close(fd[0]);
exit(0);
default: /* parent process */
close(fd[0]); // 읽기 파이프 닫기
write(fd[1], "Test Message\n", 14); // 자식에게 데이터 전송
close(fd[1]);
waitpid(pid, &status, 0);
break;
}
return 0;
}출력:
Child Process
Test Message파이프를 이용한 명령어 체이닝
다음 예제는 ps -ef | grep ssh 명령과 동일한 동작을 파이프로 구현한 것이다.
#include <sys/wait.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
int main() {
int fd[2];
pid_t pid;
if (pipe(fd) == -1) {
perror("pipe");
exit(1);
}
switch(pid = fork()) {
**case -1**:
perror("fork");
exit(1);
break;
case 0: /* child process - grep ssh */
close(fd[1]); // 쓰기 파이프 닫기
if (fd[0] != 0) {
dup2(fd[0], 0); // 표준 입력을 파이프로 리디렉션
close(fd[0]);
}
execlp("grep", "grep", "ssh", (char *)NULL);
exit(1);
break;
default: /* parent process - ps -ef */
close(fd[0]); // 읽기 파이프 닫기
if (fd[1] != 1) {
dup2(fd[1], 1); // 표준 출력을 파이프로 리디렉션
close(fd[1]);
}
execlp("ps", "ps", "-ef", (char *)NULL);
wait(NULL);
break;
}
return 0;
}출력 예시:
root 236999 236998 0 05:25 pts/3 00:00:00 grep ssh데드락 (Deadlock) 문제
데드락이란?
데드락은 프로세스 간 서로가 가진 자원을 기다리며 영원히 기다리고 있는 상태를 의미한다.
파이프는 커널 버퍼를 기반으로 하며, 읽기와 쓰기 작업은 다음 조건에서 블로킹된다.
- 커널 버퍼가 가득 찼을 때 → write() 호출은 블로킹됨
- 커널 버퍼가 비어 있을 때 → read() 호출은 블로킹됨
이러한 블로킹 동작으로 인해 단일 프로세스 내에서 데드락이 발생할 수 있다.
데드락 해결: 논블로킹 모드
fcntl() 함수를 사용하여 파일 디스크립터를 논블로킹 모드(O_NONBLOCK)로 설정하면 데드락을 예방할 수 있다.
#include <fcntl.h>
// 읽기를 논블로킹 모드로 설정
fcntl(pipe[0], F_SETFL, O_NONBLOCK);
// 쓰기를 논블로킹 모드로 설정
fcntl(pipe[1], F_SETFL, O_NONBLOCK);논블로킹 모드에서의 동작
- 비어 있는 파이프에서 read(): -1 반환, errno == EAGAIN
- 가득 찬 파이프에서 write(): -1 반환, errno == EAGAIN
즉, 블로킹 없이 에러 코드로 처리 가능하므로 데드락을 방지할 수 있다.
파이프의 활용
1. 명령어 파이프라인
쉘에서 사용하는 파이프라인을 프로그램으로 구현할 수 있다.
# 쉘 명령
ps aux | grep python | wc -l// C 프로그램으로 구현
FILE *fp = popen("ps aux | grep python | wc -l", "r");2. 로그 처리
실시간으로 생성되는 로그를 파이프를 통해 처리
// 로그 생성 프로세스
FILE *log_pipe = popen("logger -t myapp", "w");
fprintf(log_pipe, "Application started\n");3. 데이터 필터링
대용량 데이터를 파이프를 통해 실시간으로 필터링
// 데이터 필터링 파이프라인
FILE *filter = popen("sort | uniq | head -10", "w");파이프 vs 다른 IPC 방법
| 특징 | 파이프 | 공유메모리 | 메시지큐 | 소켓 |
|---|---|---|---|---|
| 속도 | 보통 | 빠름 | 보통 | 느림 |
| 사용 복잡도 | 간단 | 복잡 | 보통 | 복잡 |
| 데이터 크기 | 제한적 | 대용량 | 보통 | 대용량 |
| 네트워크 지원 | X | X | X | O |
| 프로세스 관계 | 부모-자식 | 무관 | 무관 | 무관 |
마치며
파이프는 유닉스/리눅스 시스템에서 프로세스 간 통신을 위한 가장 기본적이면서도 효과적인 메커니즘이다. 특히 다음과 같은 경우에 유용하다.
파이프를 사용하기 좋은 경우
- 부모-자식 프로세스 간 간단한 데이터 전송
- 명령어 파이프라인 구현
- 스트림 기반의 실시간 데이터 처리
- 필터 프로그램 연결
주의할 점
- 단방향 통신만 가능 (양방향은 두 개의 파이프 필요)
- 부모-자식 관계에서만 사용 가능
- 데드락 위험성 존재 (적절한 close() 필요)
- 버퍼 크기 제한 (커널 버퍼 크기에 의존)