리눅스 프로세스 간 통신(IPC) 이해

1. 파이프

파이프는 프로세스 간 통신(IPC) 메커니즘으로, 프로세스들 사이에서 데이터를 전달하는 데 사용됩니다. 본질적으로 운영체제 커널이 관리하는 메모리 버퍼이며, 파일 디스크립터를 통해 데이터를 읽고 씁니다. 파이프의 핵심은 메모리 버퍼이지만, 운영체제는 파이프를 파일처럼 `read` 및 `write`와 같은 파일 작업 인터페이스로 다룰 수 있도록 구현합니다. 따라서 파이프는 특수한 파일이지만, 데이터를 영구적으로 저장하는 용도는 아닙니다.

1) 명명 파이프와 익명 파이프

  • 익명 파이프(Anonymous Pipe): 친연 관계에 있는 프로세스(예: 부모-자식 프로세스) 사이에서만 사용 가능한 통신 메커니즘입니다.
    • 단일 부모 프로세스와 그 자식 프로세스 간의 통신에 사용됩니다.
    • 데이터 흐름은 관련 프로세스 간에만 전달되며, 파일 시스템을 거치지 않습니다.
    • `pipe` 시스템 호출: 익명 파이프를 생성합니다.
      int pipe(int pipe_descriptors[2]);
      int fd_pipe[2];
      pipe(fd_pipe);
      // fd_pipe[0]은 읽기용 파일 디스크립터, fd_pipe[1]은 쓰기용 파일 디스크립터입니다.
      
      • `pipe_descriptors`: 두 개의 파일 디스크립터가 포함된 배열로, `pipe_descriptors[0]`은 읽기 끝(read end), `pipe_descriptors[1]`은 쓰기 끝(write end)을 나타냅니다.
      • 반환 값: `0`은 성공을, `-1`은 오류를 나타내며 `errno`를 설정합니다.
  • 명명 파이프(Named Pipe): 파일 시스템을 통해 통신 인터페이스를 제공하는 영구적인 파이프로, 친연 관계가 없는 프로세스 간 통신을 지원합니다.
    • `mkfifo` 시스템 호출: 명명 파이프 파일을 생성합니다.
      mkfifo [파일이름]
      mkfifo my_fifo_pipe // 'my_fifo_pipe'라는 명명 파이프 생성
      int mkfifo(const char *path, mode_t mode);
      mkfifo("my_fifo_pipe", 0644); // 'my_fifo_pipe'라는 파이프 생성 (권한 0644)
      • `path`: 생성할 명명 파이프의 경로명입니다.
      • `mode`: 생성될 명명 파이프의 접근 권한을 지정합니다 (파일 권한과 동일, 예: `0644`는 소유자 읽기/쓰기, 그룹 및 기타 사용자 읽기 전용).
      • 반환 값: `0`은 성공을, `-1`은 오류를 나타내며 `errno`를 설정합니다.
    • `unlink` 시스템 호출: 파일 또는 디렉토리 링크를 삭제하는 데 사용됩니다. 명명 파이프를 삭제할 때도 사용합니다.
      unlink [파일이름]
      unlink my_fifo_pipe // 'my_fifo_pipe'라는 파이프 삭제
      int unlink(const char* path);
      unlink("my_fifo_pipe"); // 'my_fifo_pipe'라는 파이프 삭제
      • `path`: 삭제할 파일 또는 명명 파이프의 경로명입니다.
      • 반환 값: `0`은 성공을, `-1`은 오류를 나타내며 `errno`를 설정합니다.

2) 파이프의 특징

  • 순차적 읽기/쓰기: 파이프 데이터는 바이트 스트림 형태로 쓰기 끝에서 흘러들어와 읽기 끝에서 흘러나옵니다. 순차적으로만 읽고 쓸 수 있으며, 임의 접근을 지원하지 않습니다. 파이프는 파일 포인터 개념이 없으며 `seek` 작업을 지원하지 않습니다.
  • 일회성 전달: 데이터는 파이프를 통해 한 프로세스에서 다른 프로세스로 전달되며, 읽고 나면 사라지므로 다시 읽을 수 없습니다. 이는 파이프가 임시적이며 저장 능력을 갖지 않음을 의미합니다.
  • 반이중 통신(Half-Duplex): 파이프는 기본적으로 반이중이며, 데이터는 한 방향(쓰기 끝에서 읽기 끝으로)으로만 전달될 수 있습니다. 양방향 통신이 필요한 경우, 송신 및 수신을 위한 두 개의 파이프를 생성해야 합니다.
  • 파일 디스크립터 기반: 파이프의 읽기 끝과 쓰기 끝은 각각 두 개의 파일 디스크립터에 해당하며, 이들은 관련 프로세스들 사이에서 공유됩니다. 파이프 생성 시 파일 디스크립터 배열이 반환됩니다. 프로세스가 종료되면 파이프도 자연스럽게 해제됩니다.
  • 제한된 용량: 파이프의 메모리 버퍼 크기는 제한되어 있습니다 (일반적으로 운영체제에서 4KB 또는 64KB로 설정). 쓰기 끝에서 버퍼 용량을 초과하는 데이터를 쓰면, 읽기 끝에서 일부 데이터를 읽을 때까지 쓰기 작업은 블록(block)됩니다. 파이프를 읽을 때 파이프 버퍼에 데이터가 없으면, 데이터가 쓰여질 때까지 읽기 프로세스는 블록됩니다.
  • 바이트 스트림 지향: 데이터는 파이프에 바이트 단위로 순차적으로 쓰여지고, 읽기 끝에서는 동일한 순서로 읽힙니다. 전송되는 데이터에는 명확한 경계 표시가 없으며, 읽기/쓰기 작업은 내용 분리보다는 바이트 수만 압니다. 파이프는 데이터의 특정 형식(예: 텍스트, 이진, 구조화된 데이터)에 상관없이 바이트 전송만 담당합니다. 특정 형식이나 구분자가 필요한 경우, 프로그램이 직접 정의하고 파싱해야 합니다. 바이트 스트림 지향은 파이프가 기록된 데이터의 순서대로만 읽을 수 있으며, 파일처럼 임의 접근(예: 오프셋 지정 읽기)을 지원하지 않음을 의미합니다.

부모-자식 프로세스가 협력할 때, 동기화 및 상호 배제 메커니즘을 통해 자원 접근을 조정하여 파이프 파일의 데이터 전송이 안전하고 신뢰할 수 있도록 보장합니다.

`PIPE_BUF`: 파이프와 관련된 중요한 상수로, 파이프 쓰기 작업이 원자적으로 완료될 수 있는 최대 데이터 양을 정의합니다.

  • 쓰기 바이트 수가 `PIPE_BUF` 이하인 경우, 시스템은 전체 쓰기 작업이 다른 프로세스의 쓰기 작업에 의해 중단되지 않도록 보장합니다.
  • 쓰기 바이트 수가 `PIPE_BUF`를 초과하는 경우, 시스템은 여러 개의 쓰기 작업으로 분할할 수 있으며, 이 때 다른 프로세스의 쓰기 작업이 중간에 삽입될 수 있습니다.

파이프의 4가지 상황:

  • 읽기/쓰기 끝이 정상일 때, 파이프가 비어 있으면 읽기 끝은 블록됩니다.
  • 읽기/쓰기 끝이 정상일 때, 파이프가 가득 차 있으면 쓰기 끝은 블록됩니다.
  • 읽기 끝이 정상이고 쓰기 끝이 닫히면, 읽기 끝은 `0`을 읽어 파일의 끝에 도달했음을 알리며 블록되지 않습니다.
  • 쓰기 끝이 닫히고 읽기 끝이 정상일 때, 운영체제는 `13-SIGPIPE` 신호를 통해 쓰기 중인 프로세스를 종료시킵니다.

3) 익명 파이프와 명명 파이프 비교

특성 익명 파이프 명명 파이프
생성 방식 `pipe()` 시스템 호출 사용 `mkfifo()` 또는 `mknod()` 시스템 호출 사용
통신 범위 친연 관계에 있는 프로세스(부모-자식 등)에만 한정 친연 관계가 없는 프로세스 간 통신 지원
존재 시간 프로세스 종료 후 자동으로 소멸 파일 시스템에 파일 형태로 존재하며, 수동으로 삭제해야 함
접근 방식 파일 디스크립터 기반으로 접근 파일 경로를 통해 접근
용량 제한 운영체제에서 설정하는 버퍼 크기 (예: 4KB/64KB) 운영체제에서 설정하는 버퍼 크기
양방향 통신 두 개의 익명 파이프 생성 필요 두 개의 명명 파이프 생성 필요

2. 공유 메모리

System V 공유 메모리는 여러 프로세스가 하나의 메모리 영역을 공유할 수 있도록 허용하는 프로세스 간 통신(IPC) 메커니즘입니다. 이 메커니즘을 통해 프로세스 간 데이터 복사 오버헤드를 피함으로써 효율적으로 데이터를 교환할 수 있습니다.

1) 공유 메모리 시스템 호출

  • `ftok()`: IPC(공유 메모리, 세마포어, 메시지 큐 등)에서 사용되는 고유 식별자를 생성하는 함수입니다.
    key_t ftok(const char *pathname, int proj_id);
    key_t ipc_key = ftok("/tmp/my_shm_file", 123); // '/tmp/my_shm_file' 파일을 기반으로 키 생성
    • `pathname`: 실제 파일 시스템에 존재하는 파일의 경로명입니다. 보통 `/tmp`나 프로그램 디렉토리 내의 파일을 선택합니다.
    • `proj_id`: 동일한 경로 내에서 생성된 식별자가 여전히 고유하도록 보장하는 사용자 정의 값입니다.
    • 반환 값: `key_t` 타입의 고유 식별자(일반적으로 정수 타입)를 반환하며, 실패 시 `-1`을 반환하고 `errno`를 설정합니다.
  • `shmget()`: 공유 메모리 세그먼트를 생성하거나 기존 세그먼트의 ID를 가져옵니다.
    int shmget(key_t key, size_t size, int shmflg);
    // 1024바이트 크기의 공유 메모리 세그먼트를 생성하고, 권한은 0666으로 설정
    int shared_mem_id = shmget(ipc_key, 1024, IPC_CREAT | 0666);
    
    • `key`: 공유 메모리를 얻을 때 사용되는 고유 식별자입니다. 생성 시 `key`는 이미 존재해야 합니다.
    • `size`: 공유 메모리 세그먼트의 크기(바이트 단위)입니다.
    • `shmflg`: 권한 플래그로, 파일 권한과 유사합니다.
      • `IPC_CREAT`: 지정된 `key`에 해당하는 IPC 객체(공유 메모리 세그먼트, 메시지 큐 또는 세마포어)가 존재하지 않으면 새로 생성합니다. 이미 존재하면 기존 공유 메모리 세그먼트의 식별자를 반환합니다.
      • `IPC_EXCL`: `IPC_CREAT`와 함께 사용되며(예: `IPC_CREAT | IPC_EXCL`), 요청된 공유 메모리가 새로 생성되는 것임을 보장합니다. 지정된 `key`에 해당하는 공유 메모리 세그먼트가 이미 존재하면 오류를 반환하고 `errno`를 `EEXIST`로 설정합니다.
      • 권한 설정: `0666`, `0664` 등 (리눅스 파일 권한과 동일).
    • 반환 값: 공유 메모리 세그먼트의 고유 식별자(`shmid`)를 반환하며, 실패 시 `-1`을 반환합니다.

    1. 왜 운영체제가 직접 `key`를 생성하지 않을까요?

    • 운영체제가 무작위로 `key`를 생성한다면, 여러 프로세스가 이 `key`를 공유하기 위해 추가적인 메커니즘이 필요하여 복잡성이 증가합니다.
    • 프로그램 재시작 시, 이전에 생성된 IPC 리소스에 접근하는 방법을 보장하기 어렵습니다. `ftok` 메커니즘은 동일한 경로와 `proj_id`를 기반으로 동일한 `key`를 얻을 수 있도록 보장합니다.
    • 시스템이 생성한 `key`는 시간이나 사용 조건에 따라 충돌할 수 있지만, `ftok` 방식은 개발자가 경로와 `proj_id`를 기반으로 고유성을 수동으로 제어할 수 있게 합니다.

    2. `key`와 `shmid`의 관계

    • 두 프로그램이 하나의 공유 메모리 세그먼트를 얻고 싶을 때, 해당 공유 데이터 세그먼트의 `shmid`가 얼마인지 알 필요가 없습니다. 두 프로그램은 파일명과 지정된 `proj_id`를 약속하고, `ftok`를 사용하여 해당하는 고유 식별자(key)를 생성합니다. 이 `key`를 운영체제에 넘겨주면, 운영체제는 공유 메모리 세그먼트를 통합적으로 관리하며 `shmid`를 반환하고, 이후에는 이 `shmid`를 사용하여 작업을 수행합니다.
    • `key`는 운영체제 내에서 고유성을 식별합니다.
    • `shmid`는 프로세스 내에서 자원의 고유성을 나타냅니다.
    • 간단히 기억하세요: `key`는 프로그래머가 사용하고, `shmid`는 시스템이 사용합니다!
  • `shmat()`: 공유 메모리를 프로세스 주소 공간에 연결합니다.
    void* shmat(int shmid, const void* shmaddr, int shmflg);
    // 공유 메모리 세그먼트를 주소 공간에 연결
    char* shm_buffer = (char*) shmat(shared_mem_id, NULL, 0);
    • `shmid`: `shmget()`이 반환한 공유 메모리 세그먼트 식별자입니다.
    • `shmaddr`: 공유 메모리가 연결될 주소를 지정합니다. 일반적으로 `NULL`을 전달합니다.
    • `shmflg`: 연결 플래그로, 일반적으로 `0`입니다.
    • 반환 값: 연결된 프로세스 공간의 가상 주소입니다.
  • `shmdt()`: 공유 메모리 세그먼트를 현재 프로세스에서 분리합니다.
    int shmdt(const void* shmaddr);
    // 공유 메모리 세그먼트 분리
    shmdt(shm_buffer);
    • `shmaddr`: `shmat()`이 반환한 공유 메모리 주소입니다.
    • 반환 값: 성공 시 `0`, 실패 시 `-1`을 반환합니다.
  • `shmctl()`: 공유 메모리를 제어합니다.
    int shmctl(int shmid, int cmd, struct shmid_ds* buf);
    // 공유 메모리 세그먼트 삭제
    shmctl(shared_mem_id, IPC_RMID, NULL);
    • `shmid`: `shmget()`이 반환한 공유 메모리 세그먼트 식별자입니다.
    • `cmd`: 제어 명령입니다.
      • `IPC_RMID`: 공유 메모리 세그먼트를 삭제합니다.
      • `IPC_STAT`: `shmid_ds` 구조체의 데이터를 공유 메모리의 현재 연관 값으로 설정합니다.
      • `IPC_SET`: 프로세스가 충분한 권한을 가지고 있다면, 공유 메모리의 현재 연관 값을 `shmid_ds` 데이터 구조체에 주어진 값으로 설정합니다.
    • `buf`: 제어 정보를 전달하는 데 사용되는 구조체 포인터입니다. 삭제 시에는 `NULL`이 될 수 있습니다.
    • 반환 값: 명령에 따라 반환 값이 다르며, 실패 시 `-1`을 반환합니다.

1. 공유 메모리 생성 및 쓰기:

#include 
#include 
#include 
#include 
#include  // for exit

int main() {
    key_t ipc_key = ftok("my_shm_data", 123); // 고유 키 생성 (ftok 파일이 실제 존재해야 함)
    if (ipc_key == -1) {
        perror("ftok failed");
        exit(EXIT_FAILURE);
    }

    // 공유 메모리 세그먼트 생성 (1024바이트)
    int shared_mem_id = shmget(ipc_key, 1024, 0666 | IPC_CREAT);
    if (shared_mem_id == -1) {
        perror("shmget failed");
        exit(EXIT_FAILURE);
    }

    // 공유 메모리 세그먼트를 프로세스 주소 공간에 연결
    char* shm_buffer = (char*) shmat(shared_mem_id, NULL, 0);
    if (shm_buffer == (char*)-1) {
        perror("shmat failed");
        exit(EXIT_FAILURE);
    }

    printf("공유 메모리에 데이터 쓰기: ");
    fgets(shm_buffer, 1024, stdin); // 공유 메모리에 데이터 쓰기
    printf("메모리에 쓰여진 데이터: %s\n", shm_buffer);

    // 공유 메모리 세그먼트 분리
    if (shmdt(shm_buffer) == -1) {
        perror("shmdt failed");
        exit(EXIT_FAILURE);
    }

    // 중요: 이 프로세스에서 공유 메모리를 삭제하지 않고,
    // 읽기 프로세스에서 삭제하는 것이 일반적입니다.
    // shmctl(shared_mem_id, IPC_RMID, NULL); 

    return 0;
}

2. 공유 메모리 읽기:

#include 
#include 
#include 
#include  // for exit

int main() {
    key_t ipc_key = ftok("my_shm_data", 123); // 동일한 고유 키 생성
    if (ipc_key == -1) {
        perror("ftok failed");
        exit(EXIT_FAILURE);
    }

    // 공유 메모리 세그먼트 가져오기
    int shared_mem_id = shmget(ipc_key, 1024, 0666);
    if (shared_mem_id == -1) {
        perror("shmget failed");
        exit(EXIT_FAILURE);
    }

    // 공유 메모리 세그먼트를 프로세스 주소 공간에 연결
    char* shm_buffer = (char*) shmat(shared_mem_id, NULL, 0);
    if (shm_buffer == (char*)-1) {
        perror("shmat failed");
        exit(EXIT_FAILURE);
    }

    printf("메모리에서 읽은 데이터: %s\n", shm_buffer);

    // 공유 메모리 세그먼트 분리
    if (shmdt(shm_buffer) == -1) {
        perror("shmdt failed");
        exit(EXIT_FAILURE);
    }

    // 공유 메모리 세그먼트 삭제 (모든 프로세스가 분리된 후 물리 메모리 해제)
    if (shmctl(shared_mem_id, IPC_RMID, NULL) == -1) {
        perror("shmctl IPC_RMID failed");
        exit(EXIT_FAILURE);
    }
    printf("공유 메모리 세그먼트가 삭제되었습니다.\n");

    return 0;
}

2) 공유 메모리 원리

  • 공유 메모리 생성
    • `shmget()`을 호출하여 공유 메모리 세그먼트를 생성합니다.
    • 운영체제는 공유 메모리 세그먼트에 물리 메모리를 할당하고 고유 식별자(`shmid`)를 생성합니다.
    • 시스템은 공유 메모리 테이블(일반적으로 커널 데이터 구조의 일부)을 유지하여 공유 메모리의 메타데이터(예: 크기, 권한, 참조 카운트 등)를 기록합니다.

    1. 메타데이터 저장: 커널은 `shmid_ds` 구조체를 사용하여 공유 메모리 세그먼트의 메타 정보를 저장합니다.

    struct shmid_ds {
        struct ipc_perm shm_perm;   // 권한 정보, key 포함
        size_t shm_segsz;           // 메모리 세그먼트 크기
        time_t shm_atime;           // 마지막 연결 시간
        time_t shm_dtime;           // 마지막 분리 시간
        time_t shm_ctime;           // 생성 시간
        pid_t shm_cpid;             // 공유 메모리를 생성한 프로세스 ID
        pid_t shm_lpid;             // 마지막으로 공유 메모리에 접근한 프로세스 ID
        shmatt_t shm_nattch;        // 현재 이 공유 메모리 세그먼트에 연결된 프로세스 수 (shmat() 호출 수)
        // ... 기타 필드
    };
    

    2. `ipcs`: 생성된 공유 메모리 확인

    ipcs [옵션]
    ipcs -m // 공유 메모리 상태만 표시
    • `-m`: 공유 메모리 상태만 표시합니다.
    • `-q`: 메시지 큐 상태만 표시합니다.
    • `-s`: 세마포어 상태만 표시합니다.
  • 메모리 매핑
    • `shmat()` 호출 시, 운영체제는 공유 메모리 세그먼트의 물리 페이지를 호출 프로세스의 가상 주소 공간에 매핑합니다.
    • 운영체제는 페이지 테이블을 수정하여 프로세스의 가상 주소가 공유 메모리의 물리 주소를 가리키도록 합니다.
    • 반환된 가상 주소는 프로세스가 일반 변수를 다루듯이 직접 접근할 수 있습니다.
  • 다중 프로세스 접근
    • 서로 다른 프로세스가 `shmat()`을 호출하면, 이들의 가상 주소 공간은 모두 동일한 물리 메모리 영역을 가리키게 됩니다.
    • 물리 메모리를 수정함으로써 데이터는 해당 공유 메모리 세그먼트에 연결된 모든 프로세스에 의해 접근 가능해집니다.
  • 분리 및 정리
    • 공유 메모리 세그먼트 분리: `shmdt()`를 호출하여 공유 메모리를 프로세스의 가상 주소 공간에서 매핑 해제합니다. 물리 메모리는 참조 카운트가 0보다 큰 한 계속 존재합니다.
    • 공유 메모리 세그먼트 삭제: `shmctl()`을 `IPC_RMID` 명령과 함께 호출하여 공유 메모리 세그먼트를 삭제합니다. 모든 프로세스가 분리된 후에야 운영체제는 관련 물리 메모리를 해제합니다.

    1. `ipcrm`: 특정 공유 메모리 세그먼트 삭제

    ipcrm -m [shmid]

3) 공유 메모리의 특징

공유 메모리의 주요 특징은 효율적이고 직접적이며 낮은 오버헤드로 프로세스 간 데이터를 공유하는 것이지만, 동시에 프로세스 간 동기화 및 보안 문제를 야기하므로 데이터 충돌과 불일치를 피하기 위한 적절한 동기화 메커니즘이 필요합니다.

  • 효율적인 통신 방식
    • 공유 메모리는 데이터를 커널을 통해 복사할 필요가 없으므로 프로세스 간 통신 중 가장 빠른 방법입니다. 데이터가 공유 메모리 영역에서 직접 공유되어 프로세스 간 데이터 복사 오버헤드를 피합니다.
    • 프로세스는 공유 메모리 내의 데이터를 직접 읽고 쓸 수 있으며, 데이터 전송을 위한 컨텍스트 스위칭 횟수를 줄입니다.
  • 메모리 영역 공유
    • 여러 프로세스가 동일한 물리 메모리 블록을 각자의 가상 주소 공간에 매핑할 수 있습니다. 이를 통해 공유된 메모리 영역에 접근하여 데이터를 교환할 수 있습니다.
    • 각 프로세스는 공유 메모리의 매핑된 사본을 가지며, 일반 메모리에 접근하는 것처럼 읽고 쓸 수 있습니다.
  • 동기화 메커니즘 필요
    • 여러 프로세스가 메모리 내의 데이터를 공유할 수 있지만, 이는 경쟁 조건(race condition)데이터 불일치(공유 메모리에 아무 데이터도 쓰지 않아도 읽을 수 있음)와 같은 문제를 야기할 수 있습니다.
    • 따라서 공유 메모리를 사용할 때는 일반적으로 세마포어, 뮤텍스(상호 배제 잠금), 또는 조건 변수와 같은 추가적인 동기화 메커니즘을 사용하여 공유 메모리에 접근하는 프로세스 간의 조정을 보장하고, 동시에 동일한 데이터를 수정하여 발생할 수 있는 불일치를 방지해야 합니다.
  • 메모리 보호 및 접근 권한
    • 공유 메모리 영역의 접근 권한은 운영체제의 제어 메커니즘을 통해 관리될 수 있습니다. 서로 다른 프로세스는 다른 접근 권한(예: 읽기/쓰기, 읽기 전용, 쓰기 전용)을 가질 수 있습니다.
    • 운영체제는 프로세스가 공유 메모리에 접근할 때 경계를 넘어 접근하거나 다른 프로세스의 데이터를 손상시키지 않도록 보장합니다.
  • 생명 주기 관리
    • 공유 메모리의 생명 주기는 일반적으로 운영체제에 의해 관리됩니다. 공유 메모리 세그먼트가 생성되면, 운영체제는 물리 메모리 영역을 할당하고 여러 프로세스가 이 메모리를 매핑하고 사용할 수 있도록 허용합니다. 프로세스는 `shmdt`(매핑 해제) 작업을 통해 공유 메모리를 분리할 수 있지만, 공유 메모리 자체는 프로세스가 종료된 후 자동으로 파괴되지 않으며, 일반적으로 `shmctl` 또는 유사한 함수를 통해 삭제해야 합니다.
    • 공유 메모리 세그먼트의 생명 주기는 `shmctl`을 호출한 프로세스에 의해 제어됩니다 (즉, 생성한 주체가 삭제를 책임집니다).

3. 세마포어

세마포어(Semaphore)는 프로세스 간 통신의 중요한 메커니즘 중 하나로, 동기화 및 상호 배제 문제를 해결하는 데 사용됩니다. 이는 다중 스레드 프로그래밍과 다중 프로세스 프로그래밍에서 공유 자원에 대한 접근을 제어하는 데 널리 활용됩니다.

1) 세마포어 시스템 호출

  • `semget()`: 세마포어 집합을 생성하거나 기존 세마포어 집합의 ID를 가져옵니다.
    int semget(key_t key, int num_sems, int semflg);
    // 하나의 세마포어를 생성하고, 권한은 0666으로 설정
    int semaphore_id = semget(IPC_PRIVATE, 1, IPC_CREAT | 0666);
    
    • `key`: 세마포어를 얻을 때 사용되는 고유 식별자입니다. 생성 시 `key`는 이미 존재해야 합니다. `IPC_PRIVATE`는 키를 특정 파일과 연결하지 않고, 해당 프로세스에서만 유효한 세마포어 집합을 생성할 때 사용됩니다.
    • `num_sems`: 세마포어 집합 내의 세마포어 개수입니다.
    • `semflg`: 권한 플래그로, 파일 권한과 유사합니다.
      • `IPC_CREAT`: 지정된 `key`에 해당하는 IPC 객체(공유 메모리 세그먼트, 메시지 큐 또는 세마포어)가 존재하지 않으면 새로 생성합니다. 이미 존재하면 기존 세마포어 집합의 식별자를 반환합니다.
      • `IPC_EXCL`: `IPC_CREAT`와 함께 사용되며(예: `IPC_CREAT | IPC_EXCL`), 요청된 세마포어가 새로 생성되는 것임을 보장합니다. 지정된 `key`에 해당하는 세마포어 집합이 이미 존재하면 오류를 반환하고 `errno`를 `EEXIST`로 설정합니다.
      • 권한 설정: `0666`, `0664` 등.
    • 반환 값: 세마포어 집합의 고유 식별자(`semid`)를 반환하며, 실패 시 `-1`을 반환합니다.
  • `semctl()`: 세마포어에 대한 제어 작업을 수행합니다. 초기화, 값 가져오기, 삭제 등이 포함됩니다.
    int semctl(int semid, int semnum, int cmd, ...);
    // 첫 번째 세마포어 (인덱스 0)의 값을 1로 초기화
    semctl(semaphore_id, 0, SETVAL, 1);
    
    // 세마포어 집합 삭제
    semctl(semaphore_id, 0, IPC_RMID);
    
    • `semid`: 세마포어 집합의 식별자입니다.
    • `semnum`: 세마포어의 인덱스(0부터 시작)입니다.
    • `cmd`: 제어 명령입니다.
      • `SETVAL`: 세마포어의 값을 설정합니다.
      • `GETVAL`: 세마포어의 값을 가져옵니다.
      • `IPC_RMID`: 세마포어 집합을 삭제합니다.
    • 선택적 매개변수 `arg`: `cmd`에 따라 `int` 또는 `union semun` 타입의 값일 수 있습니다.
    • 반환 값: 명령에 따라 반환 값이 다르며, 실패 시 `-1`을 반환합니다.
  • `semop()`: 세마포어에 대한 P 연산(wait) 또는 V 연산(signal)과 같은 작업을 실행합니다.
    int semop(int semid, struct sembuf *semaphore_ops, size_t num_ops);
    struct sembuf wait_op = {0, -1, 0};  // P 연산 (세마포어 값 1 감소)
    struct sembuf signal_op = {0, 1, 0};   // V 연산 (세마포어 값 1 증가)
    
    // P 연산 실행 (자원 획득 시도)
    semop(semaphore_id, &wait_op, 1);
    // 임계 영역 코드 (Critical Section)
    printf("임계 영역 진입\n");
    
    // V 연산 실행 (자원 반납)
    printf("임계 영역 종료\n");
    semop(semaphore_id, &signal_op, 1);
    
    • `semid`: 세마포어 집합의 식별자입니다.
    • `semaphore_ops`: `struct sembuf` 타입의 작업 배열입니다.
      // 구조체 정의
      struct sembuf {
          unsigned short sem_num; // 세마포어 인덱스 (0부터 시작)
          short sem_op;           // 작업 값 (-1: P 연산, +1: V 연산)
          short sem_flg;          // 작업 플래그 (예: SEM_UNDO - 프로세스 종료 시 자동 원복)
      };
      
    • `num_ops`: 작업의 개수입니다.
    • 반환 값: 성공 시 `0`, 실패 시 `-1`을 반환합니다.

프로세스 통신의 본질은 서로 다른 프로세스가 동일한 자원을 볼 수 있게 하는 것입니다.

태그: linux ipc Pipes Shared Memory Semaphores

8월 6일 01:46에 게시됨