-
IPC - pipe운영체제 2025. 4. 26. 19:48
IPC (Interprocess Communication)
프로세스끼리 정보를 주고받는 방법이다.
IPC를 구현하는 여러가지 방법
- pipe : 한 프로세스의 write() 데이터를 다른 프로세스에서 read()로 읽는 방법
- socket : 네트워크 or 로컬에서 데이터 통신하는 방법 (ex 서버-클라이언트)
- shared memory : 한 메모리 영역을 둘 이상의 프로세스가 함께 사용하는 방법 (ex .so 라이브러리)
- signal : 프로세스 간 이벤트 알림 사용, 비동기적으로 동작해 프로세스의 흐름에 개입하는 방법 (ex 부모가 kill(pid, SIGTERM) 호출시 자식 강제종료됨)
- return values : 자식 프로세스 종료시 부모에게 결과값리턴하는 방법 (ex 정상종료시 exit(0)호출, 비정상종료시 exit(-1)호출)
- environment variables(환경변수) : 부모가 자식에게 초기설정을 넘겨주는 방법 (ex 부모는 setenv()로 환경변수 설정, 자식은 getenv()로 환경변수 읽기)
IPC 종류별 프로세스 관계 지원 여부
부모-자식간 프로세스만 통신가능한 방식도 있고, 부모-자식 뿐만 아니라 형제, 별개 프로세스까지 통신 가능한 방식도 있다.
Pipe
IPC를 위한 메커니즘 중 하나로, 한 프로세스의 write() 데이터를 다른 프로세스에서 read()로 읽는 방법이다.
pipe는 file descriptor(=fd)를 통해 접근하는 방식이다. 결국 파일 시스템과 구조가 유사하다.
file descriptor (fd)란?
- 운영 체제에서 open(), pipe(), socket() 같은 함수 호출할 때, 만들어진 새 파일을 식별하기 위한 정수값이다.
- 기본적으로, 아래 0,1,2 값은 예약된 번호이다.
- 0: 표준 입력 (stdin)
- 1: 표준 출력 (stdout)
- 2: 표준 오류 (stderr)
pipe 사용해보기
pipe()함수 호출시 읽기 전용fd와 쓰기전용 fd값이 생성된다.
아래 예시는 단순 pipe()함수를 호출하는 코드다.
#include <stdio.h> #include <unistd.h> int main() { int pipefd[2]; // step1. 파이프 생성 & fd 할당 if (pipe(pipefd) < 0) { perror("pipe"); return 1; } // fd 값 확인 printf("Pipe created: Read fd = %d, Write fd = %d\n", pipefd[0], pipefd[1]); return 0; }
실행결과
pipefd[0]의 fd는 3으로, pipefd[1]의 쓰기 전용 fd는 4로 할당되었다.
이를 통해 pipe() 호출시 읽기 전용 fd, 쓰기 전용 fd가 각각 할당됨을 알 수 있다.
위 코드에 몇줄 추가하였다.
pipe를 통해 한 프로세스 안에서 데이터를 쓰고 읽는 코드다.
#include <stdio.h> #include <unistd.h> int main() { int pipefd[2]; int read_val; int write_val = 42; // step1. 파이프 생성 & fd 할당 if (pipe(pipefd) < 0) { perror("pipe"); return 1; } // fd 값 확인 printf("Pipe created: Read fd = %d, Write fd = %d\n", pipefd[0], pipefd[1]); // step2. 파이프에 값 쓰기 (write_val을 pipefd[1]에 써야 함) write(pipefd[1], &write_val, sizeof(write_val)); // step3. 파이프에서 값 읽기 (read_val을 pipefd[0]에서 읽어야 함) read(pipefd[0], &read_val, sizeof(read_val)); // 읽은 값 출력 printf("%d\n", read_val); return 0; }
실행결과
이를 통해 pipe에 write 후 read 하면 write한 값을 그대로 받을 수 있음을 알 수 있다.
pipe 동작 방식
위 예시 코드를 기반으로 pipe의 세부 동작 방식을 설명하겠다.
step0.
사실 pipe는 커널에서 관리하는 버퍼다.
이 버퍼는 fd를 통해 접근할 수 있다.
pipe() 호출시 버퍼가 새로 생성된다. 즉 각각의 프로세스마다 독립적인 버퍼가 생성된다.
step1. pipe() 호출
아래 코드가 실행되면 pipe가 생성되고 read fd와 write fd가 할당된다고 했었다.
// 파이프 생성 & fd 할당 if (pipe(pipefd) < 0) { perror("pipe"); return 1; }
이때 할당된 read fd와 write fd는 할당된 버퍼를 가리킨다.
즉 pipedfd[0]는 읽기 포인터, pipefd[1]는 쓰기 포인터라고 할 수 있다.
step2. write()
write()를 실행하여 42를 pipe에 저장한다고 했었다.
// 파이프에 값 쓰기 write(pipefd[1], &write_val, sizeof(write_val));
이때 버퍼에 42를 채우게 되고 write fd는 마지막으로 채운 주소 값을 가리킨다.
step3. read()
read()를 실행하여 pipe에서 42를 읽는다고 했었다.
// 파이프에서 값 읽기 read(pipefd[0], &read_val, sizeof(read_val));
이때 버퍼에 있던 42를 꺼낸다. read fd는 꺼낸 후 마지막 주소값을 가리킨다.
이를 통해 pipe는 queue 방식으로 데이터를 처리함을 알 수 있다.
pipe 활용 : 부모-자식 프로세스 통신
지금까지는 한 프로세스 내에서만 pipe를 사용함을 보였다. 더 나아가 부모-자식 프로세스 내에서 pipe를 사용해보자.
fork()를 사용하여 자식을 생성하고, 부모에서 pipe에 write 후 자식 프로세스에서 read하면 된다. (그 반대도 가능하다)
즉 pipe를 통해 부모 프로세스와 자식 프로세스 간 통신이 가능하다.
해당 코드는 자식에서 부모로 데이터를 전달하는 예시다.
자식이 버퍼에 "Hello"를 write하면 부모가 버퍼값을 read한다.
#include <stdio.h> #include <unistd.h> #include <string.h> #include <sys/types.h> int main() { int pipefd[2]; pid_t pid; char buf[100]; if (pipe(pipefd) == -1) { perror("pipe"); return 1; } pid = fork(); // 자식 프로세스 생성 if (pid == 0) { // 자식 프로세스 write(pipefd[1], "Hello", 6); } else { // 부모 프로세스 read(pipefd[0], buf, sizeof(buf)); puts(buf); } return 0; }
deadlock 문제 & 해결
pipe의 기본 동작은 blocking이다.
즉, read()시 버퍼에 값이 없으면 대기하고, write()시 버퍼가 가득 차면 대기한다.
하지만 아래의 경우 데드락이 발생할 수 있다.
pipe(pipefd); pid = fork(); if (pid == 0) { // 자식 프로세스 write(pipefd[1], big_data, 10000); // 1. 버퍼 꽉 차서 대기 read(pipefd[0], small_buf, 10); // 3. 영영 실행 못함 } else { // 2. 부모는 아무것도 안 함 (read 안 함) wait(NULL); }
1. 자식 프로세스가 write()로 버퍼를 가득 채운다
2. 부모 프로세스는 아무것도 안한다
3. 자식 프로세스는 무한으로 기다려야 한다 -> 데드락!
해결 방법1. Non-blocking 사용하기
애초에 대기하지 않도록 설정하는 방법이다.
read(), write() 호출시 대기하지 않고 즉시 리턴하도록 한다.
만약 버퍼에 아무것도 없거나, 가득 찬 상태면 -1을 리턴한다.
이를 구현한 것이 아래 함수다.
#include <fcntl.h> fcntl(pipe[0], F_SETFL, O_NONBLOCK); // 읽기용 pipe, 값 없으면 -1 리턴 fcntl(pipe[1], F_SETFL, O_NONBLOCK); // 쓰기용 pipe, 버퍼 꽉차면 -1리턴
사용 예시
if (pipe(pipefd) == -1) { perror("pipe"); exit(EXIT_FAILURE); } fcntl(pipefd[0], F_SETFL, O_NONBLOCK); //non-blocking 설정 fcntl(pipefd[1], F_SETFL, O_NONBLOCK); //non-blocking 설정 pid = fork(); // 자식 프로세스 생성 if (pid == 0) { // 자식 프로세스 write(pipefd[1], "Hello", 6); } else { // 부모 프로세스 read(pipefd[0], buf, sizeof(buf)); puts(buf); }
해결 방법2. 한 프로세스 당 read/write 중 하나만 사용하기
아래와 같이 한쪽은 read 전용, 한쪽은 write 전용으로 관리하는 방법이다.
만약 이렇게 관리한다면 pipe는 단방향 통신만 가능하므로 양방향 통신하려면 pipe를 두개 사용해야 한다.
int pipefd[2]; char buf[100]; pipe(pipefd) if (fork() == 0) { // 자식 프로세스 close(pipefd[0]); // read 닫기 write(pipefd[1], "Hello", 6); close(pipefd[1]); // write 닫기 } else { // 부모 프로세스 close(pipefd[1]); // write 닫기 read(pipefd[0], buf, sizeof(buf)); puts(buf); close(pipefd[0]); // read 닫기 }
'운영체제' 카테고리의 다른 글
virtual memory(가상 메모리) / COW와 mmap() (0) 2025.05.10 file descriptor (fd) (0) 2025.05.03 library interpositioning (0) 2025.04.18 [OS] unix I/O와 standard I/O 함수 (0) 2025.04.12 [OS] 접근 권한, UID/GID (0) 2025.04.04