전산학/운영체제

[OS] 프로세스 간 통신 방법 (Interprocess Communication, IPC)

lindeblad 2024. 10. 30. 00:51

프로세스는 실행 중인 프로그램을 의미하고, 운영체제는 여러 프로세스를 운영하는 방식(스케줄링, 외부 입출력 장치와 연결 등)을 제공한다.

 

하나의 프로세스 내부적으로 작업이 끝나는 경우도 있지만, 실제로는 여러 프로세스가 상호작용하는 일이 많이 일어난다. 편의상 서로 다른 프로세스들을 P1, P2, ...로 나타내었다.

  • 웹 서버: 웹 서버(P1)가 클라이언트(P2)의 요청을 받아 처리한 후 클라이언트(P2)에게 결과물을 전송한다.
  • 분산 파일 시스템: 여러 서버(P1, P2, P3, ...)가 네트워크를 통해 하나의 파일 시스템을 공유한다.
  • 자동차 제어 시스템: 자동차의 다양한 센서(P1, P2, P3, ...)와 제어 장치(Q1, Q2, Q3, ...)들이 데이터를 주고받아 차량을 제어한다.

하지만, 프로세스는 하나의 프로그램 실행을 관리하는 단위이기 때문에, 메모리나 CPU 상태, I/O 상태 등이 서로 간섭할 수 없게 운영체제에서 보호한다. 그래서 기본적으로는 프로세스 간 통신이 이루어질 수 없지만, 이 기능을 위해 운영체제에서 몇 가지 방법을 제공한다:

  • 공유 메모리
  • 파이프
  • 메시지 큐
  • 메모리 맵
  • 소켓
  • RPC (Remote Procedure Call)

하나씩 알아보도록 하자.

공유 메모리

스레드가 메모리를 공유하는 것처럼, 프로세스에서도 메모리의 공유가 가능하다. 결국 두 프로세스의 가상 메모리 매핑에서 공유하고 싶은 페이지에 같은 논리적 메모리 주소를 매핑해주면 된다.

  • (빠르다) 공유 메모리 할당을 요청할 때만 약간의 시스템 콜로 인한 오버헤드가 발생하고, 추가적으로 공유된 자원의 접근에서 오버헤드가 발생하지 않는다.
  • 프로세스 간 읽기, 쓰기가 모두 가능하다.
  • 대량의 정보를 다수의 프로세스가 공유할 수 있다.

다음은 부모-자식 프로세스 간 공유 메모리를 사용하는 예제 코드이다. shmget으로 SHM_KEY라는 key 값으로 공유 메모리 영역을 만든 후, shmat으로 공유 메모리를 프로세스의 주소 공간에 배정하는 것을 볼 수 있다.

 

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <unistd.h>

#define SHM_SIZE 1024  // Define the size of the shared memory segment
#define SHM_KEY 1234

int main() {
    key_t key;
    int shmid;
    char *data;

    // Create the shared memory segment
    shmid = shmget(SHM_KEY, SHM_SIZE, 0666 | IPC_CREAT);

    // Attach the shared memory segment to the parent process's address space
    data = (char*) shmat(shmid, NULL, 0);

    // Fork a child process
    pid_t pid = fork();

    if (pid == 0) {  // Child process
        // Attach the shared memory segment to the child process's address space
        data = (char*) shmat(shmid, NULL, 0);
        if (data == (char*)(-1)) {
            perror("shmat");
            exit(1);
        }

        // Read data from the shared memory
        printf("Child process read: %s\n", data);
        
        // Detach the shared memory segment
        shmdt(data);
    } else {  // Parent process
        // Write data to the shared memory
        strncpy(data, "Hello from parent process!", SHM_SIZE);
        
        // Wait for the child process to complete
        wait(NULL);
        
        // Detach the shared memory segment
        shmdt(data);

        // Destroy the shared memory segment
        shmctl(shmid, IPC_RMID, NULL);
    }

    return 0;
}

파이프

커널 메모리의 일부에 통신을 위한 메모리 공간을 생성한다. 파이프는 공통적으로 다음과 같은 특징이 있다.

  • 읽기/쓰기: 하나의 프로세스는 데이터를 쓰기만 하고, 다른 하나는 데이터를 읽기만 할 수 있다.
  • 쓰는 상황: 두 가지 방향의 읽기/쓰기를 원한다면 파이프를 두 개 쓰면서 낭비가 심해지기 때문에 좋지 않다. 공급자와 소비자가 명확히 정해진 경우 쓰기 좋다.
  • 구현 방식: 커널 메모리에 선입선출 큐의 형태로 동작하는 버퍼가 배정된다.

파이프의 종류에는 두 가지 종류가 있다.

  • 익명 파이프: 부모-자식 또는 형제 프로세스 간 통신에 사용되고, 외부에 프로세스에서 접근할 수 없는 기본적인 형태
  • 네임드 파이프: 임의의 두 프로세스들 사이의 통신에 사용한다.

익명 파이프가 부모-자식 프로세스 간 통신에 사용되는 예제 코드이다:

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>

int main() {
    int pipe_fd[2];
    pid_t pid;
    char buffer[30];

    // Create the pipe
    if (pipe(pipe_fd) == -1) {
        perror("pipe");
        exit(EXIT_FAILURE);
    }

    // Fork the process
    pid = fork();
    if (pid == -1) {
        perror("fork");
        exit(EXIT_FAILURE);
    }

    if (pid == 0) {  // Child process
        close(pipe_fd[1]);  // Close the write end
        read(pipe_fd[0], buffer, sizeof(buffer));  // Read from the pipe
        printf("Child received: %s\n", buffer);
        close(pipe_fd[0]);  // Close the read end
    } else {  // Parent process
        close(pipe_fd[0]);  // Close the read end
        char message[] = "Hello from parent!";
        write(pipe_fd[1], message, sizeof(message));  // Write to the pipe
        close(pipe_fd[1]);  // Close the write end
        wait(NULL);  // Wait for child process to finish
    }

    return 0;
}

 

네임드 파이프의 경우에는, 해당 파이프의 이름을 가진 파일을 통해 프로세스 간 정보가 공유된다. 다음은 네임드 파이프의 예시인데, PIPE_NAME으로 실제 파일시스템에 정보를 저장하는 것을 확인할 수 있다. mkfifo 콜을 통해 파이프를 만드는데, FIFO(First In, First Out)라는 이름은 파이프의 마치 큐와 같은 선입선출 순서를 의미한다.

 

Writer:

#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <unistd.h>

#define PIPE_NAME "/tmp/my_named_pipe"

int main() {
    int fd;
    char *message = "Hello from the writer process!";
    
    // Create the named pipe
    mkfifo(PIPE_NAME, 0666);

    // Open the pipe for writing
    fd = open(PIPE_NAME, O_WRONLY);
    write(fd, message, sizeof(message));
    close(fd);

    return 0;
}

 

Reader:

#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <unistd.h>

#define PIPE_NAME "/tmp/my_named_pipe"

int main() {
    int fd;
    char buffer[256];

    // Open the pipe for reading
    fd = open(PIPE_NAME, O_RDONLY);
    read(fd, buffer, sizeof(buffer));
    printf("Reader received: %s\n", buffer);
    close(fd);

    // Remove the named pipe
    unlink(PIPE_NAME);

    return 0;
}

 

메시지 큐

익명 파이프와 유사하게, 커널 메모리에 선입선출 큐의 형태로 동작하는 버퍼를 배정한다. 하지만 몇 가지 점에서 차이가 있는데,

  • 구조: 파이프는 byte 단위의 FIFO로 구현되는 반면, 메시지 큐는 message라는 더 큰 단위의 FIFO로 동작한다.
  • 기능: 메시지의 우선순위나 메시지 타입 기반 수신과 같은 고급 기능이 제공된다.

익명 파이프를 더 쓰기 좋도록 확장한 개념으로 이해해도 좋다.

메모리 맵

디스크의 파일을 매개로 메모리가 공유되는 방식이다. 디스크에 있는 파일이 프로세스 메모리의 특정 영역에 배정된다.

  • 프로세스의 기본 요소: 프로세스를 실행할 때 언제나 실행 프로그램의 명령을 메모리에 로드해야 하는데, 이게 메모리 맵을 통해서 이루어진다.
  • 느리다. IPC로 활용하기 위해서는 디스크에 write-back이 일어나야 다른 프로세스에 쓰기 정보가 공유된다.

보통 다음과 같은 패턴으로 활용한다:

int fd = open("file.txt", O_RDWR);
char *map = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);

 

다른 상황에서도 자주 쓰이는 open 콜로 파일시스템에 있는 파일을 접근할 수 있는 file descriptor를 배정받고, mmap 콜을 통해 파일 내용에 해당하는 메모리 영역이 할당되어 매핑이 시작되는 메모리 영역의 주소가 반환된다. (mmap의 세부 설명은 mmap(2) - Linux manual page를 보면 알 수 있다)

 

소켓과 RPC는 복잡한 관계로 기본적인 개념만 정리했다:

소켓

다른 머신에 존재하는 프로세스와도 통신할 수 있는 유일한 네트워킹의 방식이다.

  • 데이터 교환을 통해 양쪽 PC에서 임의의 포트를 정하고 해당 포트 간의 대화로 데이터를 주고 받는다.
  • 양방향 통신이 가능하다.
  • 서버(bind, listen, accept)와 클라이언트(connect) 콜을 활용한다.

(서로 다른 PC가 어떻게 서로 통신할 수 있는지는 네트워크 쪽의 내용을 살펴보아야 한다)

RPC (Remote Procedure Call)

  • 다른 주소 공간에서 함수나 프로시저를 실행할 수 있는 기술
  • 다른 PC에 저장된 데이터를 마치 내 PC에 존재하는 것처럼 착각을 일으키는 스텁(stub)을 통해 이루어진다.

동기화 (Syncronization)

IPC 통신에서 프로세스 간 데이터를 동기화하고 보호하기 위해서 세마포어와 뮤텍스를 활용한다. 둘 다 커널의 메모리에서 관리되고 semget, sem_open 등의 콜로 접근할 수 있다.

 

정리

  Shared
Memory
PIPE Named
PIPE
Message
Queue
Memory
Map
Socket
방향 양방향 단방향 단방향 단방향 양방향 양방향
대상 다른 프로세스 간 부모-자식, 형제 프로세스 간 다른 프로세스 간 다른 프로세스 간 다른 프로세스 간 다른 시스템 간
공유 매개체 커널 메모리 커널 메모리 커널 메모리 커널 메모리 커널 메모리, 파일 소켓
통신 단위 바이트 스트림 스트림 구조체 페이지 스트림

 

 

*제가 이해한 내용을 바탕으로 적은 것이라 문서에 오류가 있을 수 있습니다. 지적은 언제나 환영입니다.