ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 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
Designed by Tistory.