C 언어에서 프로세스 간 통신(IPC)은 서로 다른 프로세스가 데이터를 교환하거나 협력하여 작업을 수행할 수 있도록 하는 중요한 기술입니다. 이 중 파이프(pipe)는 가장 기본적이고 널리 사용되는 IPC 방식 중 하나로, 한 프로세스가 데이터를 쓰고 다른 프로세스가 읽는 단방향 통신 채널을 제공합니다. 본 기사에서는 파이프의 기본 개념, 사용 방법, 예제 코드, 그리고 효율적인 활용 방안까지 단계적으로 살펴보며, 이를 활용해 더 나은 소프트웨어를 설계할 수 있도록 돕겠습니다.
파이프의 기본 개념
파이프(pipe)는 운영 체제에서 제공하는 프로세스 간 통신(IPC) 메커니즘으로, 데이터를 한 프로세스에서 다른 프로세스로 전달하는 단방향 통신 채널입니다.
파이프의 작동 원리
파이프는 두 프로세스 간에 연결된 버퍼를 사용하여 데이터를 교환합니다. 일반적으로 한 프로세스는 파이프에 데이터를 쓰고(write), 다른 프로세스는 데이터를 읽는(read) 방식으로 동작합니다. 파이프는 FIFO(First In, First Out) 방식으로 데이터를 처리하며, 입력된 데이터가 순서대로 전달됩니다.
파이프의 주요 특징
- 단방향 통신: 기본적으로 하나의 방향으로만 데이터를 주고받을 수 있습니다.
- 익명 파이프: 동일한 부모 프로세스에서 생성된 자식 프로세스 간에만 사용할 수 있습니다.
- 명명된 파이프: 파일 시스템에 이름이 등록되어 서로 다른 프로세스 간에도 통신이 가능합니다.
- 커널 지원: 파이프는 커널에서 관리되며, 프로세스 간 데이터를 안전하게 교환할 수 있습니다.
파이프의 일반적인 용도
- 부모와 자식 프로세스 간의 데이터 교환
- 작은 양의 데이터를 처리하는 간단한 통신
- 명령줄 유틸리티 간의 데이터 파이프라인 구성
파이프는 단순하면서도 강력한 도구로, 다양한 프로세스 통신 시나리오에서 활용됩니다.
파이프를 활용한 IPC의 장점
효율적인 데이터 교환
파이프는 두 프로세스 간 데이터를 교환하기 위한 단순하고 직관적인 방법을 제공합니다. 커널에서 직접 관리되므로 데이터 전송 속도가 빠르며, 프로세스 간의 메모리 공유나 복잡한 설정 없이 통신을 구현할 수 있습니다.
FIFO 구조의 신뢰성
파이프는 FIFO(First In, First Out) 구조를 사용하여 데이터를 처리하므로, 데이터가 입력된 순서대로 출력됩니다. 이는 데이터의 순서 보장이 중요한 애플리케이션에 유용합니다.
운영 체제와의 긴밀한 통합
파이프는 대부분의 현대 운영 체제에서 기본적으로 지원되며, 파일 디스크립터(file descriptor)를 사용하여 통신을 처리합니다. 따라서 표준 입출력(STDIN, STDOUT)과 쉽게 연동할 수 있어 명령줄 기반 애플리케이션에서 활용도가 높습니다.
간단한 구현
파이프는 사용 방법이 비교적 간단하여, IPC를 처음 배우는 개발자도 쉽게 구현할 수 있습니다. 최소한의 코드로 프로세스 간 통신을 설정할 수 있으며, 익명 파이프는 부모-자식 프로세스 간 통신에 최적화되어 있습니다.
응용 범위의 다양성
파이프는 단순한 데이터 교환부터 명령어 파이프라인 구성, 로그 전송, 임시 데이터 저장 등 다양한 용도로 활용됩니다. 또한, 명명된 파이프를 사용하면 프로세스 간 경계를 넘어 더 복잡한 IPC 시나리오를 처리할 수 있습니다.
파이프는 간결함과 신뢰성을 동시에 제공하며, 프로세스 간 통신의 기본적인 빌딩 블록 역할을 합니다.
C 언어에서 파이프 사용 방법
파이프 생성
C 언어에서 파이프는 pipe()
시스템 호출을 사용하여 생성합니다. pipe()
는 두 개의 파일 디스크립터를 저장하는 배열을 인자로 받아, 하나는 읽기 용도(read end
), 다른 하나는 쓰기 용도(write end
)로 사용됩니다.
#include <stdio.h>
#include <unistd.h>
int main() {
int pipe_fd[2]; // 파일 디스크립터 배열
if (pipe(pipe_fd) == -1) {
perror("pipe"); // 에러 처리
return 1;
}
printf("Read end: %d, Write end: %d\n", pipe_fd[0], pipe_fd[1]);
return 0;
}
데이터 쓰기와 읽기
파이프를 통해 데이터를 교환하려면 write()
로 데이터를 쓰고, read()
로 데이터를 읽습니다. 아래 예제는 부모 프로세스가 자식 프로세스로 메시지를 전송하는 기본적인 시나리오를 보여줍니다.
#include <stdio.h>
#include <unistd.h>
#include <string.h>
int main() {
int pipe_fd[2];
pid_t pid;
char write_msg[] = "Hello from parent!";
char read_msg[100];
if (pipe(pipe_fd) == -1) {
perror("pipe");
return 1;
}
pid = fork();
if (pid == -1) {
perror("fork");
return 1;
}
if (pid == 0) { // 자식 프로세스
close(pipe_fd[1]); // 쓰기 끝 닫기
read(pipe_fd[0], read_msg, sizeof(read_msg));
printf("Child received: %s\n", read_msg);
close(pipe_fd[0]); // 읽기 끝 닫기
} else { // 부모 프로세스
close(pipe_fd[0]); // 읽기 끝 닫기
write(pipe_fd[1], write_msg, strlen(write_msg) + 1);
close(pipe_fd[1]); // 쓰기 끝 닫기
}
return 0;
}
리소스 관리
파이프를 사용할 때는 파일 디스크립터를 적절히 닫아야 리소스 누수를 방지할 수 있습니다. 사용하지 않는 읽기 끝 또는 쓰기 끝을 닫는 것은 필수적인 단계입니다.
유용한 참고 사항
- 파이프는 단방향 통신을 제공합니다. 양방향 통신을 원한다면 두 개의 파이프를 사용하거나 소켓을 고려하세요.
pipe()
호출이 실패하면 보통 시스템의 파일 디스크립터 제한이 초과된 경우일 수 있습니다.- 데이터가 읽히기 전까지 파이프는 쓰기 작업을 차단(block)하여 통신을 동기화합니다.
이 기본적인 사용법은 파이프를 사용한 IPC를 시작하는 데 필요한 기초를 제공합니다.
익명 파이프와 명명된 파이프의 차이점
익명 파이프
익명 파이프는 부모-자식 관계의 프로세스 간에만 사용할 수 있는 파이프입니다. 이 파이프는 생성된 프로세스에서만 유효하며, 데이터 교환이 단방향으로 제한됩니다.
특징
- 생성 및 사용:
pipe()
시스템 호출로 생성됩니다. - 제한된 범위: 동일한 부모 프로세스에서 생성된 자식 프로세스 간 통신에만 사용 가능합니다.
- 일시적 존재: 익명 파이프는 프로세스가 종료되면 함께 소멸됩니다.
예제
#include <stdio.h>
#include <unistd.h>
int main() {
int pipe_fd[2];
if (pipe(pipe_fd) == -1) {
perror("pipe");
return 1;
}
pid_t pid = fork();
if (pid == 0) { // 자식 프로세스
close(pipe_fd[1]); // 쓰기 끝 닫기
char buffer[100];
read(pipe_fd[0], buffer, sizeof(buffer));
printf("Child received: %s\n", buffer);
close(pipe_fd[0]);
} else { // 부모 프로세스
close(pipe_fd[0]); // 읽기 끝 닫기
write(pipe_fd[1], "Hello from parent!", 18);
close(pipe_fd[1]);
}
return 0;
}
명명된 파이프 (FIFO)
명명된 파이프는 파일 시스템에 이름이 등록된 파이프입니다. 서로 다른 부모-자식 관계가 없는 프로세스 간에도 통신이 가능합니다.
특징
- 생성:
mkfifo()
함수를 사용하거나 명령줄에서mkfifo
명령으로 생성합니다. - 이름 기반 통신: 파일 시스템의 경로를 통해 서로 다른 프로세스 간 통신을 지원합니다.
- 지속성: 파일 시스템에서 삭제되지 않는 한 지속적으로 사용 가능합니다.
예제
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/stat.h>
#define FIFO_NAME "myfifo"
int main() {
if (mkfifo(FIFO_NAME, 0666) == -1) {
perror("mkfifo");
return 1;
}
pid_t pid = fork();
if (pid == 0) { // 자식 프로세스
int fd = open(FIFO_NAME, O_RDONLY);
char buffer[100];
read(fd, buffer, sizeof(buffer));
printf("Child received: %s\n", buffer);
close(fd);
} else { // 부모 프로세스
int fd = open(FIFO_NAME, O_WRONLY);
write(fd, "Hello from parent!", 18);
close(fd);
unlink(FIFO_NAME); // 사용 후 FIFO 삭제
}
return 0;
}
익명 파이프와 명명된 파이프 비교
속성 | 익명 파이프 | 명명된 파이프 (FIFO) |
---|---|---|
범위 | 부모-자식 프로세스 간 | 모든 프로세스 간 |
생성 방식 | pipe() | mkfifo() 또는 명령어 |
지속성 | 프로세스 종료 시 삭제 | 파일 시스템에 존재 |
통신 방향 | 단방향 | 기본 단방향, 필요 시 양방향 |
유연성 | 제한적 | 파일 경로로 유연하게 통신 가능 |
익명 파이프는 간단한 IPC에 적합하고, 명명된 파이프는 더 복잡한 시나리오에 적합합니다. 적절한 선택으로 IPC를 효과적으로 구현할 수 있습니다.
파이프 IPC 구현 예제
익명 파이프를 활용한 부모-자식 프로세스 통신
아래 예제는 부모 프로세스에서 자식 프로세스로 메시지를 전송하고, 자식 프로세스가 이를 읽어 출력하는 간단한 구현입니다.
#include <stdio.h>
#include <unistd.h>
#include <string.h>
int main() {
int pipe_fd[2];
pid_t pid;
char write_msg[] = "Hello from parent!";
char read_msg[100];
// 파이프 생성
if (pipe(pipe_fd) == -1) {
perror("pipe");
return 1;
}
pid = fork();
if (pid == -1) {
perror("fork");
return 1;
}
if (pid == 0) { // 자식 프로세스
close(pipe_fd[1]); // 쓰기 끝 닫기
read(pipe_fd[0], read_msg, sizeof(read_msg)); // 파이프에서 데이터 읽기
printf("Child received: %s\n", read_msg);
close(pipe_fd[0]); // 읽기 끝 닫기
} else { // 부모 프로세스
close(pipe_fd[0]); // 읽기 끝 닫기
write(pipe_fd[1], write_msg, strlen(write_msg) + 1); // 파이프에 데이터 쓰기
close(pipe_fd[1]); // 쓰기 끝 닫기
}
return 0;
}
출력 결과
Child received: Hello from parent!
명명된 파이프(FIFO)를 활용한 프로세스 간 통신
명명된 파이프는 파일 시스템을 통해 독립적인 두 프로세스 간의 통신을 제공합니다.
생성 및 사용 방법
mkfifo()
를 사용하여 파일 시스템에 FIFO를 생성합니다.- 한 프로세스는 쓰기 모드(
O_WRONLY
)로 열고, 다른 프로세스는 읽기 모드(O_RDONLY
)로 엽니다.
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/stat.h>
#define FIFO_NAME "myfifo"
int main() {
if (mkfifo(FIFO_NAME, 0666) == -1) {
perror("mkfifo");
return 1;
}
pid_t pid = fork();
if (pid == 0) { // 자식 프로세스
int fd = open(FIFO_NAME, O_RDONLY); // 읽기 모드로 FIFO 열기
char buffer[100];
read(fd, buffer, sizeof(buffer)); // 데이터 읽기
printf("Child received: %s\n", buffer);
close(fd); // FIFO 닫기
} else { // 부모 프로세스
int fd = open(FIFO_NAME, O_WRONLY); // 쓰기 모드로 FIFO 열기
write(fd, "Hello from parent via FIFO!", 27); // 데이터 쓰기
close(fd); // FIFO 닫기
unlink(FIFO_NAME); // FIFO 삭제
}
return 0;
}
출력 결과
Child received: Hello from parent via FIFO!
멀티프로세스 환경에서 파이프 활용
아래는 두 개의 자식 프로세스를 생성하여 데이터를 부모 프로세스로 전달하는 예제입니다.
#include <stdio.h>
#include <unistd.h>
#include <string.h>
int main() {
int pipe_fd[2];
pid_t pid1, pid2;
char read_msg[100];
if (pipe(pipe_fd) == -1) {
perror("pipe");
return 1;
}
pid1 = fork();
if (pid1 == 0) { // 첫 번째 자식 프로세스
close(pipe_fd[0]); // 읽기 끝 닫기
write(pipe_fd[1], "Message from child 1", 21);
close(pipe_fd[1]); // 쓰기 끝 닫기
return 0;
}
pid2 = fork();
if (pid2 == 0) { // 두 번째 자식 프로세스
close(pipe_fd[0]); // 읽기 끝 닫기
write(pipe_fd[1], "Message from child 2", 21);
close(pipe_fd[1]); // 쓰기 끝 닫기
return 0;
}
// 부모 프로세스
close(pipe_fd[1]); // 쓰기 끝 닫기
while (read(pipe_fd[0], read_msg, sizeof(read_msg)) > 0) {
printf("Parent received: %s\n", read_msg);
}
close(pipe_fd[0]); // 읽기 끝 닫기
return 0;
}
출력 결과
Parent received: Message from child 1
Parent received: Message from child 2
이 예제는 멀티프로세스 환경에서 파이프를 활용해 데이터를 교환하는 방법을 보여줍니다. 데이터를 안전하게 교환하려면 프로세스 동기화를 추가로 고려해야 합니다.
파이프 사용 시 발생 가능한 문제
데드락 (Deadlock)
문제: 파이프를 사용할 때 읽기와 쓰기 작업이 서로 차단(blocking)될 수 있습니다. 예를 들어, 읽는 프로세스가 데이터를 기다리는 동안 쓰는 프로세스가 종료되거나, 쓰는 프로세스가 데이터를 다 채울 때까지 읽는 프로세스가 실행되지 않는 경우가 있습니다.
해결 방법:
- 읽기와 쓰기를 비차단(non-blocking) 모드로 설정합니다.
- 충분한 버퍼 크기를 설정하여 데이터가 꽉 차거나 비는 상황을 피합니다.
fcntl(pipe_fd[0], F_SETFL, O_NONBLOCK); // 비차단 모드로 설정
버퍼 오버플로우 (Buffer Overflow)
문제: 파이프는 내부적으로 고정 크기의 버퍼를 사용합니다. 데이터가 버퍼 크기를 초과하면 쓰기 작업이 차단되거나 실패할 수 있습니다.
해결 방법:
- 데이터를 작은 청크로 나누어 전송합니다.
- 버퍼 크기를 고려하여 데이터를 주고받는 속도를 조절합니다.
파이프 읽기 시 EOF (End of File) 처리
문제: 파이프에서 읽을 데이터가 없을 때, 읽기 작업이 차단되거나 예기치 않은 EOF를 반환할 수 있습니다.
해결 방법:
- EOF를 반환하지 않으려면 쓰는 끝을 닫기 전에 모든 데이터를 보냅니다.
read()
함수의 반환 값을 확인하여 EOF 상황을 적절히 처리합니다.
양방향 통신에서의 문제
문제: 파이프는 기본적으로 단방향 통신을 제공합니다. 두 프로세스 간 양방향 통신을 구현하려면 두 개의 파이프를 생성해야 합니다.
해결 방법:
- 두 개의 파이프를 생성하여 각 방향으로 데이터를 주고받습니다.
- 소켓 또는 공유 메모리와 같은 대체 IPC 기술을 고려합니다.
int pipe1[2], pipe2[2];
pipe(pipe1); // 프로세스 A -> 프로세스 B
pipe(pipe2); // 프로세스 B -> 프로세스 A
파이프 닫기와 리소스 누수
문제: 파이프의 읽기 끝 또는 쓰기 끝을 적절히 닫지 않으면 파일 디스크립터가 누적되어 시스템 자원이 소진될 수 있습니다.
해결 방법:
- 데이터를 모두 사용한 후
close()
를 호출하여 파일 디스크립터를 닫습니다. - 예외 상황에서도 리소스를 해제할 수 있도록 코드에 에러 처리를 추가합니다.
프로세스 종료 시 파이프 문제
문제: 쓰는 프로세스가 종료되었지만 읽는 프로세스가 계속 데이터를 기다리는 경우, 읽는 프로세스가 차단되거나 잘못된 데이터를 처리할 수 있습니다.
해결 방법:
- 종료 신호를 전송하거나, 종료 시 프로세스 상태를 확인하여 적절히 처리합니다.
운영 체제 파일 디스크립터 제한
문제: 시스템에서 생성할 수 있는 파일 디스크립터의 수에는 제한이 있습니다. 많은 파이프를 생성할 경우 이 제한에 도달할 수 있습니다.
해결 방법:
- 시스템 제한을 확인하고 필요 시 파일 디스크립터 제한을 늘립니다.
- 불필요한 파이프를 닫아 리소스를 재활용합니다.
ulimit -n 4096 # 파일 디스크립터 제한 늘리기
이 문제들은 파이프 기반 IPC를 구현할 때 자주 발생하지만, 적절한 코드 작성과 시스템 설정으로 대부분 해결할 수 있습니다.
고급 파이프 활용 기술
멀티프로세스 환경에서의 동기화
다수의 프로세스가 동일한 파이프를 공유할 때 동기화 문제가 발생할 수 있습니다. 데이터를 동시에 읽거나 쓰는 작업이 겹칠 경우 데이터 손실이나 충돌이 일어날 수 있습니다.
해결 방법
- 락 메커니즘 사용: 파이프 작업 중 데이터 일관성을 유지하기 위해 세마포어나 뮤텍스를 사용합니다.
- 순차 처리: 프로세스 간 통신 순서를 미리 정의하여 작업을 순차적으로 실행합니다.
예제: 세마포어를 사용한 동기화
#include <stdio.h>
#include <unistd.h>
#include <semaphore.h>
#include <fcntl.h>
sem_t *sem;
int main() {
int pipe_fd[2];
pipe(pipe_fd);
sem = sem_open("/sync_sem", O_CREAT, 0644, 1); // 세마포어 생성
if (fork() == 0) { // 자식 프로세스
sem_wait(sem); // 세마포어 락 획득
write(pipe_fd[1], "Child writes\n", 13);
sem_post(sem); // 세마포어 락 해제
close(pipe_fd[1]);
} else { // 부모 프로세스
char buffer[100];
sem_wait(sem); // 세마포어 락 획득
read(pipe_fd[0], buffer, sizeof(buffer));
printf("Parent reads: %s", buffer);
sem_post(sem); // 세마포어 락 해제
close(pipe_fd[0]);
sem_close(sem); // 세마포어 닫기
sem_unlink("/sync_sem"); // 세마포어 삭제
}
return 0;
}
비차단(non-blocking) 파이프
기본적으로 파이프는 읽기나 쓰기 작업이 완료될 때까지 차단(blocking)됩니다. 비차단 모드는 작업을 즉시 반환하며, 데이터가 준비되지 않았을 경우 EAGAIN
에러를 반환합니다.
비차단 모드 활성화
#include <fcntl.h>
fcntl(pipe_fd[0], F_SETFL, O_NONBLOCK);
사용 예제
char buffer[100];
if (read(pipe_fd[0], buffer, sizeof(buffer)) == -1) {
perror("read");
} else {
printf("Read data: %s\n", buffer);
}
양방향 통신 구현
파이프는 기본적으로 단방향 통신을 제공하지만, 두 개의 파이프를 사용하면 양방향 통신을 구현할 수 있습니다.
예제
#include <stdio.h>
#include <unistd.h>
int main() {
int pipe1[2], pipe2[2];
pipe(pipe1); // 부모 -> 자식
pipe(pipe2); // 자식 -> 부모
if (fork() == 0) { // 자식 프로세스
char msg1[100], msg2[] = "Message from child";
close(pipe1[1]); // 부모 -> 자식 쓰기 닫기
close(pipe2[0]); // 자식 -> 부모 읽기 닫기
read(pipe1[0], msg1, sizeof(msg1));
printf("Child received: %s\n", msg1);
write(pipe2[1], msg2, sizeof(msg2));
close(pipe1[0]);
close(pipe2[1]);
} else { // 부모 프로세스
char msg1[] = "Message from parent", msg2[100];
close(pipe1[0]); // 부모 -> 자식 읽기 닫기
close(pipe2[1]); // 자식 -> 부모 쓰기 닫기
write(pipe1[1], msg1, sizeof(msg1));
read(pipe2[0], msg2, sizeof(msg2));
printf("Parent received: %s\n", msg2);
close(pipe1[1]);
close(pipe2[0]);
}
return 0;
}
파이프와 셀 통합
파이프는 셸 명령어와 통합하여 강력한 기능을 제공합니다. popen()
함수를 사용하면 C 코드에서 셸 명령어의 출력을 읽거나 명령어에 입력을 전달할 수 있습니다.
예제: 셸 명령어 출력 읽기
#include <stdio.h>
#include <stdlib.h>
int main() {
FILE *fp = popen("ls -l", "r"); // 'ls -l' 명령 실행
char buffer[256];
while (fgets(buffer, sizeof(buffer), fp) != NULL) {
printf("%s", buffer);
}
pclose(fp); // 파일 포인터 닫기
return 0;
}
파이프 활용 극대화를 위한 팁
- 표준 입출력 리다이렉션: 파이프를 표준 입출력(STDIN, STDOUT)에 연결하여 데이터를 쉽게 처리할 수 있습니다.
- 복합 IPC 활용: 파이프를 소켓이나 공유 메모리와 조합해 더 복잡한 통신을 구현합니다.
- 시스템 제한 고려: 파일 디스크립터 제한을 초과하지 않도록 주의하며, 적절한 리소스 관리를 수행합니다.
이 고급 기술들은 파이프를 더욱 효과적이고 유연하게 활용할 수 있도록 돕습니다.
연습 문제와 해결 방법
연습 문제 1: 부모-자식 프로세스 간의 단방향 통신 구현
문제: 부모 프로세스에서 “Hello, Child!” 메시지를 보내고, 자식 프로세스가 이를 읽어 출력하는 프로그램을 작성하세요.
해결 방법:
pipe()
로 파이프를 생성합니다.- 부모는 파이프의 쓰기 끝에 데이터를 기록하고, 자식은 읽기 끝에서 데이터를 읽습니다.
- 사용 후 파일 디스크립터를 닫아 리소스를 해제합니다.
#include <stdio.h>
#include <unistd.h>
#include <string.h>
int main() {
int pipe_fd[2];
char msg[] = "Hello, Child!";
char buffer[100];
pipe(pipe_fd);
if (fork() == 0) { // 자식 프로세스
close(pipe_fd[1]); // 쓰기 끝 닫기
read(pipe_fd[0], buffer, sizeof(buffer));
printf("Child received: %s\n", buffer);
close(pipe_fd[0]);
} else { // 부모 프로세스
close(pipe_fd[0]); // 읽기 끝 닫기
write(pipe_fd[1], msg, strlen(msg) + 1);
close(pipe_fd[1]);
}
return 0;
}
연습 문제 2: 명명된 파이프(FIFO)로 양방향 통신 구현
문제: 명명된 파이프를 사용해 부모 프로세스가 메시지를 보내고, 자식 프로세스가 응답하는 프로그램을 작성하세요.
해결 방법:
- 두 개의 명명된 파이프(FIFO)를 생성하여 각각의 통신 방향을 담당합니다.
- 각 프로세스는 한 파이프를 읽기 모드로 열고, 다른 파이프를 쓰기 모드로 엽니다.
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <string.h>
#define FIFO1 "fifo1"
#define FIFO2 "fifo2"
int main() {
mkfifo(FIFO1, 0666); // 부모 -> 자식
mkfifo(FIFO2, 0666); // 자식 -> 부모
if (fork() == 0) { // 자식 프로세스
char buffer[100];
int read_fd = open(FIFO1, O_RDONLY);
int write_fd = open(FIFO2, O_WRONLY);
read(read_fd, buffer, sizeof(buffer));
printf("Child received: %s\n", buffer);
write(write_fd, "Hello, Parent!", 15);
close(read_fd);
close(write_fd);
} else { // 부모 프로세스
char buffer[100];
int write_fd = open(FIFO1, O_WRONLY);
int read_fd = open(FIFO2, O_RDONLY);
write(write_fd, "Hello, Child!", 14);
read(read_fd, buffer, sizeof(buffer));
printf("Parent received: %s\n", buffer);
close(write_fd);
close(read_fd);
unlink(FIFO1);
unlink(FIFO2);
}
return 0;
}
연습 문제 3: 비차단(non-blocking) 모드에서의 읽기
문제: 비차단 모드에서 파이프를 사용해 데이터를 읽으려면 어떻게 구현해야 할까요?
해결 방법:
- 파이프의 읽기 끝을 비차단 모드로 설정합니다.
- 데이터를 읽는 동안 파이프가 비어 있으면 적절한 메시지를 출력하고 계속 실행합니다.
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
int main() {
int pipe_fd[2];
char msg[] = "Hello, Non-blocking!";
char buffer[100];
pipe(pipe_fd);
fcntl(pipe_fd[0], F_SETFL, O_NONBLOCK); // 비차단 모드 설정
if (fork() == 0) { // 자식 프로세스
close(pipe_fd[1]); // 쓰기 끝 닫기
while (read(pipe_fd[0], buffer, sizeof(buffer)) == -1) {
printf("No data available yet...\n");
sleep(1);
}
printf("Child received: %s\n", buffer);
close(pipe_fd[0]);
} else { // 부모 프로세스
close(pipe_fd[0]); // 읽기 끝 닫기
sleep(3); // 지연 후 쓰기
write(pipe_fd[1], msg, strlen(msg) + 1);
close(pipe_fd[1]);
}
return 0;
}
연습 문제 4: 부모-자식 간 대량 데이터 전송
문제: 부모 프로세스에서 대량의 데이터를 생성하고, 자식 프로세스가 이를 모두 읽어 출력하는 프로그램을 작성하세요.
해결 방법:
- 데이터를 일정한 크기의 청크로 나누어 파이프를 통해 전송합니다.
#include <stdio.h>
#include <unistd.h>
#include <string.h>
int main() {
int pipe_fd[2];
char buffer[100];
const char *data = "Large amount of data...";
int len = strlen(data);
pipe(pipe_fd);
if (fork() == 0) { // 자식 프로세스
close(pipe_fd[1]); // 쓰기 끝 닫기
while (read(pipe_fd[0], buffer, sizeof(buffer)) > 0) {
printf("Child received: %s\n", buffer);
}
close(pipe_fd[0]);
} else { // 부모 프로세스
close(pipe_fd[0]); // 읽기 끝 닫기
for (int i = 0; i < len; i += 10) {
write(pipe_fd[1], &data[i], (len - i < 10) ? len - i : 10);
usleep(100000); // 쓰기 지연
}
close(pipe_fd[1]);
}
return 0;
}
이 연습 문제를 통해 파이프를 활용한 다양한 시나리오를 경험하며 실력을 향상할 수 있습니다.
요약
본 기사에서는 C 언어에서 파이프를 활용한 프로세스 간 통신(IPC)에 대해 기본 개념부터 고급 활용 기술까지 다루었습니다. 파이프의 작동 원리, 익명 및 명명된 파이프의 차이점, 실전 예제, 문제 해결 방법, 그리고 연습 문제를 통해 파이프의 실용성을 이해하고 응용할 수 있도록 구성했습니다. 파이프를 통해 안정적이고 효율적인 프로세스 통신을 구현할 수 있으며, 이를 기반으로 더 복잡한 IPC 기술로 확장할 수 있습니다.