C언어에서 프로세스 간 통신(IPC, Inter-Process Communication)은 운영체제의 중요한 기능 중 하나로, 부모-자식 프로세스 간 데이터를 주고받는 데 사용됩니다. 그중 pipe() 시스템 호출은 간단하면서도 효과적인 단방향 통신 수단을 제공합니다. 이 문서에서는 pipe() 함수의 동작 원리와 구현 방법, 그리고 부모-자식 프로세스 간 데이터 전송을 위한 실제 활용법을 단계적으로 설명합니다. 이를 통해 프로세스 간 데이터 교환의 기초를 익히고, 실무에서 유용한 응용 사례를 이해할 수 있습니다.
pipe() 함수란?
pipe()는 유닉스 계열 운영체제에서 제공하는 시스템 호출로, 부모와 자식 프로세스 간 단방향 통신을 가능하게 합니다. 이 함수는 파이프(PIPE)라는 임시 버퍼를 생성하여 데이터를 쓰고 읽는 데 사용됩니다.
pipe() 함수의 역할
pipe()는 두 개의 파일 디스크립터를 생성하며, 이 디스크립터를 통해 데이터를 주고받습니다.
- 읽기 끝 (read-end): 파이프에서 데이터를 읽는 데 사용됩니다.
- 쓰기 끝 (write-end): 파이프에 데이터를 쓰는 데 사용됩니다.
pipe() 함수의 기본 문법
#include <unistd.h>
int pipe(int pipefd[2]);
- 매개변수:
pipefd
는 정수형 배열로,[0]
은 읽기 끝,[1]
은 쓰기 끝을 나타냅니다. - 반환 값: 성공 시 0을 반환하고, 실패 시 -1을 반환하며
errno
를 설정합니다.
pipe()의 주요 특징
- 단방향 통신: 데이터를 한 방향으로만 전달할 수 있습니다.
- 익명 파이프: 부모-자식 프로세스 간에만 사용 가능하며, 이름이 없습니다.
- 임시 버퍼: 운영체제가 자동으로 관리하며, 용량이 제한됩니다.
파이프 통신의 예
부모 프로세스에서 데이터를 작성하면, 자식 프로세스가 이를 읽을 수 있습니다. 이 단순한 구조는 데이터 흐름을 명확히 정의하며, 동기화와 리소스 관리를 쉽게 해줍니다.
pipe() 함수는 이러한 특징 덕분에 간단한 IPC 요구사항을 처리하는 데 자주 사용됩니다.
부모-자식 프로세스의 통신 흐름
파이프를 이용한 데이터 흐름
pipe() 함수는 부모와 자식 프로세스 간에 데이터가 흐르는 단방향 통신 경로를 설정합니다. 통신의 일반적인 흐름은 다음과 같습니다.
- pipe() 호출: 부모 프로세스가 pipe()를 호출하여 두 개의 파일 디스크립터를 생성합니다.
pipefd[0]
: 읽기 끝pipefd[1]
: 쓰기 끝
- fork() 호출: 부모 프로세스는 fork()를 호출하여 자식 프로세스를 생성합니다. 이때 부모와 자식은 동일한 파일 디스크립터를 공유합니다.
- 파일 디스크립터 관리:
- 부모는 읽기 끝(
pipefd[0]
)을 닫고 쓰기 끝(pipefd[1]
)을 통해 데이터를 씁니다. - 자식은 쓰기 끝(
pipefd[1]
)을 닫고 읽기 끝(pipefd[0]
)을 통해 데이터를 읽습니다.
- 데이터 전송: 부모가 쓰기 끝에 데이터를 작성하면, 자식은 읽기 끝을 통해 데이터를 수신합니다.
통신 구조의 예
다음은 부모에서 데이터를 전송하고 자식이 이를 수신하는 구조를 보여줍니다.
#include <stdio.h>
#include <unistd.h>
#include <string.h>
int main() {
int pipefd[2];
pid_t pid;
char write_msg[] = "Hello from parent!";
char read_msg[100];
// 파이프 생성
if (pipe(pipefd) == -1) {
perror("pipe");
return 1;
}
// 프로세스 생성
pid = fork();
if (pid < 0) {
perror("fork");
return 1;
}
if (pid > 0) { // 부모 프로세스
close(pipefd[0]); // 읽기 끝 닫기
write(pipefd[1], write_msg, strlen(write_msg) + 1); // 데이터 전송
close(pipefd[1]); // 쓰기 끝 닫기
} else { // 자식 프로세스
close(pipefd[1]); // 쓰기 끝 닫기
read(pipefd[0], read_msg, sizeof(read_msg)); // 데이터 수신
printf("Child received: %s\n", read_msg); // 출력
close(pipefd[0]); // 읽기 끝 닫기
}
return 0;
}
데이터 흐름의 특징
- 파이프는 FIFO(First In, First Out) 방식으로 작동합니다.
- 부모가 데이터를 전송하면 자식이 순차적으로 읽습니다.
- 프로세스는 더 이상 필요하지 않은 파일 디스크립터를 반드시 닫아야 리소스 누수를 방지할 수 있습니다.
이와 같은 통신 흐름은 프로세스 간 데이터 교환의 기본적인 원리를 이해하는 데 중요한 토대가 됩니다.
pipe() 함수의 구현 방법
pipe() 함수의 단계별 구현
pipe()를 사용하여 부모-자식 프로세스 간 데이터를 전송하려면 다음 단계를 따릅니다.
- 파이프 생성:
pipe()
함수를 호출하여 두 개의 파일 디스크립터를 생성합니다. - 프로세스 생성:
fork()
를 호출하여 부모와 자식 프로세스를 만듭니다. - 파일 디스크립터 닫기: 각 프로세스에서 필요하지 않은 읽기 또는 쓰기 끝을 닫아줍니다.
- 데이터 전송 및 수신: 부모는 쓰기 끝으로 데이터를 전송하고, 자식은 읽기 끝으로 데이터를 수신합니다.
코드 예제
아래는 pipe() 함수를 사용하여 부모 프로세스가 데이터를 전송하고 자식 프로세스가 이를 읽는 구현 예제입니다.
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
int main() {
int pipefd[2];
pid_t pid;
char write_msg[] = "Message from parent to child.";
char read_msg[100];
// 1. 파이프 생성
if (pipe(pipefd) == -1) {
perror("pipe failed");
return 1;
}
// 2. 프로세스 생성
pid = fork();
if (pid < 0) { // fork 실패
perror("fork failed");
return 1;
}
if (pid > 0) { // 부모 프로세스
close(pipefd[0]); // 읽기 끝 닫기
printf("Parent: Writing to pipe...\n");
write(pipefd[1], write_msg, strlen(write_msg) + 1); // 데이터 전송
close(pipefd[1]); // 쓰기 끝 닫기
} else { // 자식 프로세스
close(pipefd[1]); // 쓰기 끝 닫기
read(pipefd[0], read_msg, sizeof(read_msg)); // 데이터 수신
printf("Child: Received from pipe: %s\n", read_msg); // 출력
close(pipefd[0]); // 읽기 끝 닫기
}
return 0;
}
코드 설명
- 파이프 생성:
pipe(pipefd)
는 두 개의 파일 디스크립터를 생성합니다.
pipefd[0]
: 읽기 끝pipefd[1]
: 쓰기 끝
- fork 호출: 부모와 자식 프로세스는 동일한 파일 디스크립터를 공유합니다.
- 부모는 쓰기 끝(
pipefd[1]
)을 사용하여 데이터를 씁니다. - 자식은 읽기 끝(
pipefd[0]
)을 사용하여 데이터를 읽습니다.
- 리소스 관리: 더 이상 사용하지 않는 파일 디스크립터를 닫아 리소스 누수를 방지합니다.
출력 예
프로그램 실행 시 다음과 같은 출력이 나타납니다.
Parent: Writing to pipe...
Child: Received from pipe: Message from parent to child.
주요 포인트
fork()
호출 후, 부모와 자식은 별도의 실행 경로를 가집니다.- 파일 디스크립터 관리가 중요하며, 사용하지 않는 끝을 닫아야 합니다.
- 버퍼 크기 초과 또는 동기화 문제를 방지하려면 데이터를 신중히 처리해야 합니다.
이 기본 구현은 프로세스 간 데이터 전송의 기초를 익히는 데 적합하며, 더 복잡한 통신 구조로 확장할 수 있습니다.
pipe() 함수의 주요 제한 사항
단방향 통신
pipe()는 단방향 통신만 지원합니다. 즉, 파이프의 한쪽 끝은 읽기에만 사용되고 다른 쪽 끝은 쓰기에만 사용됩니다.
- 문제점: 양방향 통신이 필요하면 두 개의 파이프를 생성해야 하며, 코드의 복잡성이 증가합니다.
- 해결 방법: 양방향 통신은 두 개의 파이프를 조합하거나, 소켓을 사용하는 방식으로 대체할 수 있습니다.
익명 파이프의 한계
익명 파이프는 부모와 자식 프로세스 간에만 사용할 수 있습니다.
- 문제점: 독립적인 두 프로세스 간에는 익명 파이프를 사용할 수 없습니다.
- 해결 방법: 독립적인 프로세스 간 통신이 필요하다면 이름 있는 파이프(FIFO) 또는 소켓을 사용할 수 있습니다.
버퍼 크기 제한
파이프의 버퍼 크기는 운영체제에 따라 고정되어 있으며, 일반적으로 4KB에서 64KB 사이입니다.
- 문제점: 버퍼가 가득 차면 쓰기 작업이 블록됩니다. 반대로 버퍼가 비어 있으면 읽기 작업이 블록됩니다.
- 해결 방법:
- 비동기 방식:
O_NONBLOCK
플래그를 설정하여 블로킹을 방지합니다. - 데이터 분할: 데이터를 작은 청크로 나누어 처리합니다.
블로킹 동작
기본적으로 pipe()는 블로킹 I/O로 동작합니다.
- 문제점:
- 쓰기 작업은 버퍼가 가득 찼을 때 블록됩니다.
- 읽기 작업은 버퍼에 데이터가 없을 때 블록됩니다.
- 해결 방법:
fcntl()
을 사용하여 파일 디스크립터를 비차단 모드로 설정합니다.- 다중 작업 처리 시
select()
또는poll()
을 사용하여 I/O를 관리합니다.
데이터 순서 문제
파이프는 FIFO(First In, First Out) 방식을 따르지만, 동시에 여러 쓰기 작업이 이루어질 경우 데이터 순서가 꼬일 수 있습니다.
- 문제점: 여러 프로세스가 동일한 파이프에 데이터를 쓸 때 예상치 못한 순서로 데이터가 읽힐 수 있습니다.
- 해결 방법: 각 프로세스가 독립적인 파이프를 사용하거나, 데이터에 메타정보를 추가해 순서를 명시합니다.
프로세스 종료 시 파일 디스크립터 처리
파이프는 프로세스 종료 시 자동으로 닫히지만, 잘못된 파일 디스크립터 관리로 문제가 발생할 수 있습니다.
- 문제점: 쓰기 끝이 모두 닫히면 읽기 끝은 EOF를 반환합니다. 반대로 읽기 끝이 닫히면 쓰기 작업이
SIGPIPE
오류를 발생시킵니다. - 해결 방법: 프로세스 종료 전에 사용하지 않는 파일 디스크립터를 명시적으로 닫아야 합니다.
주요 포인트
pipe()는 간단한 프로세스 간 통신에는 유용하지만, 고성능이나 복잡한 요구사항을 충족하기에는 한계가 있습니다. 이 함수의 제한을 이해하고 적절한 대안을 선택하면 효과적인 IPC 구현이 가능합니다.
양방향 통신을 위한 대안
두 개의 파이프를 사용한 양방향 통신
pipe()는 단방향 통신만 가능하므로, 양방향 통신을 구현하려면 두 개의 파이프를 생성해야 합니다.
- 구조:
- 첫 번째 파이프는 부모에서 자식으로 데이터를 전송.
- 두 번째 파이프는 자식에서 부모로 데이터를 전송.
예제 코드
#include <stdio.h>
#include <unistd.h>
#include <string.h>
int main() {
int parent_to_child[2], child_to_parent[2];
pid_t pid;
char parent_msg[] = "Hello from parent";
char child_msg[] = "Hello from child";
char buffer[100];
// 두 개의 파이프 생성
if (pipe(parent_to_child) == -1 || pipe(child_to_parent) == -1) {
perror("pipe");
return 1;
}
pid = fork();
if (pid < 0) {
perror("fork");
return 1;
}
if (pid > 0) { // 부모 프로세스
close(parent_to_child[0]); // 부모는 첫 번째 파이프의 읽기 끝 닫기
close(child_to_parent[1]); // 부모는 두 번째 파이프의 쓰기 끝 닫기
// 자식에게 메시지 전송
write(parent_to_child[1], parent_msg, strlen(parent_msg) + 1);
close(parent_to_child[1]); // 쓰기 끝 닫기
// 자식의 응답 읽기
read(child_to_parent[0], buffer, sizeof(buffer));
printf("Parent received: %s\n", buffer);
close(child_to_parent[0]); // 읽기 끝 닫기
} else { // 자식 프로세스
close(parent_to_child[1]); // 자식은 첫 번째 파이프의 쓰기 끝 닫기
close(child_to_parent[0]); // 자식은 두 번째 파이프의 읽기 끝 닫기
// 부모의 메시지 읽기
read(parent_to_child[0], buffer, sizeof(buffer));
printf("Child received: %s\n", buffer);
close(parent_to_child[0]); // 읽기 끝 닫기
// 부모에게 응답 전송
write(child_to_parent[1], child_msg, strlen(child_msg) + 1);
close(child_to_parent[1]); // 쓰기 끝 닫기
}
return 0;
}
출력
Child received: Hello from parent
Parent received: Hello from child
소켓을 사용한 양방향 통신
소켓은 네트워크 통신뿐만 아니라 동일한 시스템 내의 프로세스 간 양방향 통신에도 사용할 수 있는 유연한 방법입니다.
- 장점: 단방향 파이프보다 간결하고 효율적으로 양방향 통신을 구현할 수 있습니다.
- 구현 예:
socketpair()
를 사용하면 양방향 통신이 가능한 소켓을 생성할 수 있습니다.
socketpair() 예제
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/socket.h>
int main() {
int sv[2];
pid_t pid;
char parent_msg[] = "Hello from parent via socket";
char child_msg[] = "Hello from child via socket";
char buffer[100];
// 소켓 페어 생성
if (socketpair(AF_UNIX, SOCK_STREAM, 0, sv) == -1) {
perror("socketpair");
return 1;
}
pid = fork();
if (pid < 0) {
perror("fork");
return 1;
}
if (pid > 0) { // 부모 프로세스
close(sv[1]); // 부모는 자식의 소켓 닫기
// 자식에게 메시지 전송
write(sv[0], parent_msg, strlen(parent_msg) + 1);
// 자식의 응답 읽기
read(sv[0], buffer, sizeof(buffer));
printf("Parent received: %s\n", buffer);
close(sv[0]); // 소켓 닫기
} else { // 자식 프로세스
close(sv[0]); // 자식은 부모의 소켓 닫기
// 부모의 메시지 읽기
read(sv[1], buffer, sizeof(buffer));
printf("Child received: %s\n", buffer);
// 부모에게 응답 전송
write(sv[1], child_msg, strlen(child_msg) + 1);
close(sv[1]); // 소켓 닫기
}
return 0;
}
출력
Child received: Hello from parent via socket
Parent received: Hello from child via socket
메시지 큐나 공유 메모리의 활용
- 메시지 큐: 파이프와 유사하지만, 메시지를 명시적으로 분리하여 관리할 수 있는 더 정교한 방법.
- 공유 메모리: 고속 데이터 교환을 위한 효율적인 방법으로, 세마포어를 함께 사용하여 동기화를 보장해야 합니다.
결론
양방향 통신은 pipe()만으로 구현할 수도 있지만, 복잡한 요구 사항을 처리하려면 소켓, 메시지 큐, 공유 메모리와 같은 대안을 사용하는 것이 더 적합할 수 있습니다. 사용 시 성능 요구사항과 코드 복잡성을 고려하여 적절한 방식을 선택하는 것이 중요합니다.
pipe() 함수와 오류 처리
pipe() 호출 중 발생할 수 있는 오류
pipe() 함수는 호출 실패 시 -1을 반환하고, errno
에 오류 정보를 설정합니다. 주요 오류는 다음과 같습니다.
- EMFILE
- 원인: 프로세스가 열린 파일 디스크립터의 최대 한도에 도달한 경우.
- 해결 방법:
- 사용하지 않는 파일 디스크립터를 닫아 리소스를 확보합니다.
- 시스템의 파일 디스크립터 한도를 확인하고 필요 시 늘립니다(
ulimit -n
명령 사용).
- ENFILE
- 원인: 시스템 전체의 열린 파일 디스크립터 개수가 한도를 초과한 경우.
- 해결 방법: 시스템 관리자에게 문의하여 한도를 늘리거나, 파일 디스크립터 사용을 최적화합니다.
- EFAULT
- 원인: 잘못된 포인터가
pipefd
매개변수로 전달된 경우. - 해결 방법:
pipefd
배열이 올바르게 선언되고 초기화되었는지 확인합니다.
읽기 및 쓰기 중 발생할 수 있는 오류
- 쓰기 오류
- SIGPIPE 시그널:
- 원인: 파이프의 읽기 끝이 닫힌 상태에서 쓰기를 시도하면
SIGPIPE
시그널이 발생합니다. - 해결 방법:
write()
호출 시 반환값을 확인하여 오류를 처리합니다.signal(SIGPIPE, SIG_IGN)
을 사용하여 시그널을 무시합니다(단, 안전한 코드 설계를 위해 권장하지 않음).
- 원인: 파이프의 읽기 끝이 닫힌 상태에서 쓰기를 시도하면
- EAGAIN 또는 EWOULDBLOCK:
- 원인: 파이프가 비차단 모드일 때, 버퍼가 가득 차서 데이터를 기록할 수 없는 경우.
- 해결 방법:
- 데이터를 다시 시도하거나, 블로킹 모드로 전환합니다.
- 버퍼 크기를 고려하여 데이터를 나누어 씁니다.
- 읽기 오류
- EOF(End of File):
- 원인: 파이프의 쓰기 끝이 닫히고 더 이상 데이터가 없는 경우.
- 해결 방법:
read()
반환값이 0인지 확인하여 EOF를 감지합니다.- 파일 디스크립터 관리가 제대로 이루어졌는지 점검합니다.
- EAGAIN 또는 EWOULDBLOCK:
- 원인: 파이프가 비차단 모드일 때, 읽을 데이터가 없는 경우.
- 해결 방법:
- 블로킹 모드를 사용하거나, 데이터를 기다리는 방식으로 코드를 설계합니다.
오류 처리 구현 예
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>
#include <signal.h>
int main() {
int pipefd[2];
char buffer[100];
pid_t pid;
// 파이프 생성
if (pipe(pipefd) == -1) {
perror("pipe failed");
return 1;
}
pid = fork();
if (pid < 0) {
perror("fork failed");
return 1;
}
if (pid > 0) { // 부모 프로세스
close(pipefd[0]); // 읽기 끝 닫기
if (write(pipefd[1], "Hello, Child", 13) == -1) {
if (errno == EPIPE) {
fprintf(stderr, "Write failed: Broken pipe\n");
}
}
close(pipefd[1]); // 쓰기 끝 닫기
} else { // 자식 프로세스
close(pipefd[1]); // 쓰기 끝 닫기
ssize_t bytes_read = read(pipefd[0], buffer, sizeof(buffer));
if (bytes_read == -1) {
perror("Read failed");
} else if (bytes_read == 0) {
printf("EOF reached\n");
} else {
printf("Child received: %s\n", buffer);
}
close(pipefd[0]); // 읽기 끝 닫기
}
return 0;
}
오류 처리의 핵심 포인트
- 파일 디스크립터를 적절히 닫아 리소스 누수를 방지합니다.
- SIGPIPE 시그널이나
EAGAIN
오류를 사전에 처리하여 프로그램이 예기치 않게 종료되는 것을 방지합니다. read()
및write()
의 반환값과errno
를 주의 깊게 점검합니다.
결론
pipe() 함수는 효율적이지만, 오류를 적절히 처리하지 않으면 예상치 못한 동작이나 프로그램 충돌이 발생할 수 있습니다. 파일 디스크립터 관리, 시그널 처리, 반환값 점검과 같은 기법을 통해 안정적인 통신 구현이 가능합니다.
부모-자식 프로세스 통신 실습
실습 목표
이번 실습에서는 pipe()
를 사용하여 부모 프로세스가 데이터를 전송하고, 자식 프로세스가 이를 수신하여 출력하는 예제를 구현합니다. 이를 통해 프로세스 간 데이터 교환의 기본적인 구조와 동작 방식을 이해할 수 있습니다.
실습 코드
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
int main() {
int pipefd[2]; // 파이프 파일 디스크립터
pid_t pid; // 프로세스 ID
char write_msg[] = "Hello from parent process!";
char read_msg[100]; // 읽기 버퍼
// 파이프 생성
if (pipe(pipefd) == -1) {
perror("pipe failed");
return 1;
}
// 프로세스 생성
pid = fork();
if (pid < 0) {
perror("fork failed");
return 1;
}
if (pid > 0) { // 부모 프로세스
close(pipefd[0]); // 읽기 끝 닫기
printf("Parent: Sending message to child...\n");
write(pipefd[1], write_msg, strlen(write_msg) + 1); // 메시지 전송
close(pipefd[1]); // 쓰기 끝 닫기
} else { // 자식 프로세스
close(pipefd[1]); // 쓰기 끝 닫기
read(pipefd[0], read_msg, sizeof(read_msg)); // 메시지 읽기
printf("Child: Received message: %s\n", read_msg);
close(pipefd[0]); // 읽기 끝 닫기
}
return 0;
}
코드 설명
- 파이프 생성
pipe(pipefd)
는 읽기와 쓰기 끝을 나타내는 두 개의 파일 디스크립터를 생성합니다.
pipefd[0]
: 읽기 끝pipefd[1]
: 쓰기 끝
- 프로세스 생성
fork()
호출로 부모와 자식 프로세스를 생성합니다.
- 부모는 쓰기 끝(
pipefd[1]
)을 통해 데이터를 전송합니다. - 자식은 읽기 끝(
pipefd[0]
)을 통해 데이터를 수신합니다.
- 파일 디스크립터 관리
- 부모는 읽기 끝을 닫고(
close(pipefd[0])
), 자식은 쓰기 끝을 닫아 서로 간섭하지 않도록 합니다.
- 데이터 전송 및 수신
- 부모는
write()
로 데이터를 파이프에 기록합니다. - 자식은
read()
로 데이터를 파이프에서 읽습니다.
실행 출력
Parent: Sending message to child...
Child: Received message: Hello from parent process!
실습 확장: 양방향 통신
아래는 부모와 자식 간 양방향 데이터 교환을 추가한 코드입니다.
#include <stdio.h>
#include <unistd.h>
#include <string.h>
int main() {
int parent_to_child[2], child_to_parent[2];
char parent_msg[] = "Message from parent";
char child_msg[] = "Message from child";
char buffer[100];
// 두 개의 파이프 생성
if (pipe(parent_to_child) == -1 || pipe(child_to_parent) == -1) {
perror("pipe failed");
return 1;
}
pid_t pid = fork();
if (pid > 0) { // 부모 프로세스
close(parent_to_child[0]); // 읽기 끝 닫기
close(child_to_parent[1]); // 쓰기 끝 닫기
// 자식에게 메시지 전송
write(parent_to_child[1], parent_msg, strlen(parent_msg) + 1);
close(parent_to_child[1]); // 쓰기 끝 닫기
// 자식의 응답 읽기
read(child_to_parent[0], buffer, sizeof(buffer));
printf("Parent received: %s\n", buffer);
close(child_to_parent[0]); // 읽기 끝 닫기
} else { // 자식 프로세스
close(parent_to_child[1]); // 쓰기 끝 닫기
close(child_to_parent[0]); // 읽기 끝 닫기
// 부모의 메시지 읽기
read(parent_to_child[0], buffer, sizeof(buffer));
printf("Child received: %s\n", buffer);
// 부모에게 응답 전송
write(child_to_parent[1], child_msg, strlen(child_msg) + 1);
close(parent_to_child[0]); // 읽기 끝 닫기
close(child_to_parent[1]); // 쓰기 끝 닫기
}
return 0;
}
확장 코드 출력
Child received: Message from parent
Parent received: Message from child
주요 학습 포인트
pipe()
를 사용한 단방향 및 양방향 통신의 기본 구조를 이해할 수 있습니다.- 파일 디스크립터 관리의 중요성을 학습할 수 있습니다.
- IPC(프로세스 간 통신)의 실무적 활용 가능성을 탐구할 수 있습니다.
이 실습을 통해 pipe()를 활용한 데이터 교환의 기본 원리를 충분히 익힐 수 있습니다.
파이프를 활용한 응용 프로그램
응용 프로그램: 명령어 실행기
이 예제에서는 pipe()
를 사용하여 부모 프로세스가 명령어를 전달하고, 자식 프로세스가 이를 실행한 결과를 부모 프로세스에 반환하는 간단한 명령어 실행기를 구현합니다.
코드 예제
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
int main() {
int parent_to_child[2], child_to_parent[2];
pid_t pid;
char command[100];
char result[500];
// 두 개의 파이프 생성
if (pipe(parent_to_child) == -1 || pipe(child_to_parent) == -1) {
perror("pipe failed");
return 1;
}
pid = fork();
if (pid < 0) {
perror("fork failed");
return 1;
}
if (pid > 0) { // 부모 프로세스
close(parent_to_child[0]); // 부모는 첫 번째 파이프의 읽기 끝 닫기
close(child_to_parent[1]); // 부모는 두 번째 파이프의 쓰기 끝 닫기
printf("Enter a command to execute: ");
fgets(command, sizeof(command), stdin);
command[strcspn(command, "\n")] = '\0'; // 개행 문자 제거
// 자식에게 명령어 전송
write(parent_to_child[1], command, strlen(command) + 1);
close(parent_to_child[1]); // 쓰기 끝 닫기
// 자식 프로세스의 결과 수신
read(child_to_parent[0], result, sizeof(result));
printf("Output:\n%s", result);
close(child_to_parent[0]); // 읽기 끝 닫기
} else { // 자식 프로세스
close(parent_to_child[1]); // 자식은 첫 번째 파이프의 쓰기 끝 닫기
close(child_to_parent[0]); // 자식은 두 번째 파이프의 읽기 끝 닫기
char cmd[100];
read(parent_to_child[0], cmd, sizeof(cmd)); // 부모로부터 명령어 수신
// 명령어 실행 및 결과 저장
FILE *fp = popen(cmd, "r");
if (fp == NULL) {
perror("popen failed");
exit(1);
}
char buffer[100];
size_t len = 0;
while (fgets(buffer, sizeof(buffer), fp) != NULL) {
len += snprintf(result + len, sizeof(result) - len, "%s", buffer);
}
pclose(fp);
// 결과를 부모 프로세스에 전송
write(child_to_parent[1], result, strlen(result) + 1);
close(parent_to_child[0]); // 읽기 끝 닫기
close(child_to_parent[1]); // 쓰기 끝 닫기
exit(0);
}
return 0;
}
코드 동작 설명
- 파이프 생성
parent_to_child
: 부모에서 자식으로 명령어 전송.child_to_parent
: 자식에서 부모로 명령어 결과 반환.
- 명령어 입력 및 전송
- 부모는 사용자로부터 명령어를 입력받아
parent_to_child
의 쓰기 끝으로 전송합니다.
- 명령어 실행
- 자식은
parent_to_child
의 읽기 끝에서 명령어를 수신하고,popen()
을 사용하여 명령어를 실행한 결과를 읽습니다.
- 결과 전송
- 자식은 실행 결과를
child_to_parent
의 쓰기 끝을 통해 부모에 전달합니다.
- 결과 출력
- 부모는
child_to_parent
의 읽기 끝에서 결과를 수신하고 출력합니다.
프로그램 실행 예
입력:
Enter a command to execute: ls -l
출력:
Output:
total 12
-rw-r--r-- 1 user user 1234 Jan 24 10:00 example.c
-rw-r--r-- 1 user user 5678 Jan 24 11:00 program.c
응용 프로그램의 장점
- 유연성: 다양한 명령어를 실행하고 결과를 처리할 수 있습니다.
- 확장성: 결과를 파일로 저장하거나, 추가 처리를 통해 복잡한 명령어 실행 로직을 구축할 수 있습니다.
실습을 통한 학습 포인트
- pipe()를 사용한 양방향 통신 구현: 부모와 자식 간 데이터를 주고받는 방법을 이해할 수 있습니다.
- popen() 활용: 명령어 실행 결과를 읽고 파이프를 통해 전송하는 실용적인 예제를 학습할 수 있습니다.
이 프로그램은 프로세스 간 통신과 명령어 실행을 결합하여 파이프의 실용성을 체험할 수 있는 좋은 연습 사례를 제공합니다.
요약
이 글에서는 C언어의 pipe()
시스템 호출을 활용하여 부모-자식 프로세스 간 통신을 구현하는 방법을 다루었습니다. pipe()의 기본 개념과 구현 방법, 단방향 및 양방향 통신의 구조를 설명했으며, 주요 제한 사항과 오류 처리 기법을 소개했습니다. 또한 명령어 실행기와 같은 실용적인 응용 프로그램 예제를 통해 pipe()의 활용 사례를 탐구했습니다. 이를 통해 프로세스 간 데이터 교환의 기초와 실무 적용 방법을 익힐 수 있습니다.