Computer Science
탄탄한 기반 실력을 위한
전공과 이론 지식 모음
Today I Learned!
배웠으면 기록을 해야지
TIL 사진
Flutter 사진
Flutter로 모바일까지
거꾸로캠퍼스 코딩랩 Flutter 앱개발 강사
스파르타코딩클럽 즉문즉답 튜터
카카오테크캠퍼스 3기 학습코치
프로필 사진
박성민
임베디드 세계에
발을 들인 박치기 공룡
임베디드 사진
EMBEDDED SYSTEM
임베디드 SW와 HW, 이론부터 실전까지
ALGORITHM
알고리즘 해결 전략 기록
🎓
중앙대학교 소프트웨어학부
텔레칩스 차량용 임베디드 스쿨 3기
애플 개발자 아카데미 1기
깃허브 사진
GitHub
프로젝트 모아보기
Instagram
인스타그램 사진

Embedded System/Embedded Linux

[Embedded Linux] 프로세스간 통신 IPC - System V 기반의 IPC

sm_amoled 2025. 11. 29. 16:07

일반적으로 프로세스 끼리는 서로 독립된 메모리 공간을 사용한다. 따라서 프로세스끼리는 서로의 영역을 침범할 수 없다.

ㅤㅤ

프로그램을 설계하다보면 여러 프로세스가 서로에게 정보나 신호를 주고받아야하는 상황이 온다. A 에서 처리한 결과가 B 에게 전달되어야 한다거나, 혹은 A와 B가 동시에 작업을 시작하거나 작업의 우선순위가 있을 수 있다. 이러한 상황에서 프로세스 간의 통신 (IPC, Inter-Process Communication)이 필요하다.

ㅤㅤ

가장 기본이 되는 파일을 이용한 IPC(파이프, FIFO, Socket)가 궁금하다면 이 글을 읽어보자.

System V 의 IPC

UNIX가 실제 산업 환경에서 쓰이기 시작하면서, 여러가지 요구사항이 추가되기 시작했다. 이 니즈를 충족시키기 위해 새로운 IPC 방식이 제시되었다.

  • 다중 프로세스의 공유자원 접근
  • 트랜젝션 처리 → 메시지 유실을 막고 순서를 보장
  • 프로세스가 죽어도 통신 채널은 유지

ㅤㅤ

기존의 Pipe 에서는 파일 디스크립터를 통해 연결되기 때문에 상속을 해야만 동일한 파일 디스크립터를 물려받아 접근할 수 있었음.
→ 그래서 이후에 FIFO 를 추가하면서 파일 디스크립터 대신에 식별자 Key를 사용해서 파이프를 찾도록 개선 (그래서 named pipe라고 부른다) … 아무 프로세스나 Key만 알면 접근 가능
그럼에도 여전히 위 문제들이 있어서 System V 가 등장하였다.

ㅤㅤ

2-1 메시지 큐

메시지 큐는 커널이 관리하는 메시지 저장소로, 프로세스들은 여기에서 메시지를 넣고 빼면서 서로 통신한다.

특징

  • 메시지 단위 통신: 바이트 스트림이 아닌, 구조화된 메시지 단위로 송수신
  • 메시지 타입 지원: 각 메시지에 타입(long 값)을 부여하여 선택적 수신 가능
  • 커널 영속성: 프로세스가 종료되어도 명시적으로 삭제하기 전까지 큐가 유지됨. (
  • 비동기 통신: sender와 receiver가 동시에 실행될 필요 없음

ㅤㅤ

장점

  • 보낸 메시지 단위 그대로 수신 (파싱 불필요)
  • 타입 기반 선택적 수신: 여러 종류의 메시지를 하나의 큐로 관리 가능
  • 우선순위 구현 용이: 타입 값을 우선순위로 활용 가능

ㅤㅤ

단점

  • 크기 제한: 시스템별 메시지/큐 크기 제한 존재
  • 수동 정리 필요: 프로세스 종료 후에도 큐가 남아있어 명시적 삭제 필요
  • 인터페이스가 좀 불편

ㅤㅤ

어디에 쓰나?

  • 서로 다른 타입의 메시지를 하나의 채널로 주고받을 때
  • 메시지 경계가 명확해야 할 때 (프로토콜 패킷 등)
  • 비동기적으로 메시지를 쌓아두고 처리해야 할 때

ㅤㅤ

메시지큐 실습

핵심적인 API 들은 아래와 같다.

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>

// 고유 키 생성
key_t ftok(const char *pathname, int proj_id);  
// 큐 생성/접근
int msgget(key_t key, int msgflg);        
// 전송     
int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
// 수신  
ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);
// 제어/삭제
int msgctl(int msqid, int cmd, struct msqid_ds *buf);  

ㅤㅤ

이 메시지큐를 사용하기 위해서, 메시지는 아래의 구조를 따라야한다. 크게 제약은 없고, 첫 번째 칸에 long mtype이 들어있으면 된다.

struct msgbuf {
    long mtype;     // 메시지 타입 (반드시 첫 번째, 양수)
    char mtext[N];  // 실제 데이터 (가변 크기)
};

ㅤㅤ

실습을 위해, Sender와 Receiver 가 하나의 메시지큐를 이용해 데이터를 주고 받는 코드로 실습을 진행하였다.

  • sender는 메시지 큐를 만들고 메시지를 2개 넣는다. 이때 두 메시지는 서로 다른 우선순위(type 값)를 가진다.
  • receiver는 메시지 큐에 메시지가 있으면 받아와 출력한다. 이때 우선순위가 높은 메시지부터 먼저 출력한다. 모든 메시지를 출력하고 나면 메시지 큐를 해제한다. 만약 메시지가 없으면 에러를 출력한다.

ㅤㅤ

아래에 있는 실습 코드를 실행해보면, 아래와 같은 결과를 확인할 수 있다.

  1. receiver는 처음에 메시지 큐가 없는 것을 보고 에러를 출력
  2. sender가 메시지를 큐에 삽입
  3. receiver가드
  4. receiver가 다시 메시지 큐에 접근하지만 큐가 없어서 에러를 출력

ㅤㅤ

실습 코드

// sender.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>

#define MSG_SIZE 64

struct msgbuf {
    long mtype;
    char mtext[MSG_SIZE];
};

int main(void)
{
    key_t key;
    int msqid;
    struct msgbuf msg;

    /* 키 생성: 현재 디렉토리 + 프로젝트 ID */
    // 여기에서 Sender와 Receiver가 동일한 방식으로 
    key = ftok(".", 'A');
    if (key == -1) {
        perror("ftok");
        exit(1);
    }

    /* 메시지 큐 생성 (없으면 생성, 권한 0644) */
    msqid = msgget(key, IPC_CREAT | 0644);
    if (msqid == -1) {
        perror("msgget");
        exit(1);
    }

    printf("Message Queue ID: %d\n", msqid);

    /* 타입 1 메시지 전송 */
    msg.mtype = 1;
    strcpy(msg.mtext, "Hello from sender (type 1)");
    if (msgsnd(msqid, &msg, strlen(msg.mtext) + 1, 0) == -1) {
        perror("msgsnd");
        exit(1);
    }
    printf("Sent: [type=%ld] %s\n", msg.mtype, msg.mtext);

    /* 타입 2 메시지 전송 */
    msg.mtype = 2;
    strcpy(msg.mtext, "Urgent message (type 2)");
    if (msgsnd(msqid, &msg, strlen(msg.mtext) + 1, 0) == -1) {
        perror("msgsnd");
        exit(1);
    }
    printf("Sent: [type=%ld] %s\n", msg.mtype, msg.mtext);

    printf("Messages sent. Run receiver to read.\n");

    return 0;
}
// receiver.c
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>

#define MSG_SIZE 64

struct msgbuf {
    long mtype;
    char mtext[MSG_SIZE];
};

int main(void)
{
    key_t key;
    int msqid;
    struct msgbuf msg;
    ssize_t ret;

    key = ftok(".", 'A');
    if (key == -1) {
        perror("ftok");
        exit(1);
    }

    /* 기존 큐에 접근 */
    msqid = msgget(key, 0644);
    if (msqid == -1) {
        perror("msgget");
        exit(1);
    }

    printf("Message Queue ID: %d\n", msqid);

    /* 타입 2 메시지 먼저 수신 (선택적 수신) */
    ret = msgrcv(msqid, &msg, MSG_SIZE, 2, 0);
    if (ret == -1) {
        perror("msgrcv");
        exit(1);
    }
    printf("Received: [type=%ld] %s\n", msg.mtype, msg.mtext);

    /* 타입 1 메시지 수신 */
    ret = msgrcv(msqid, &msg, MSG_SIZE, 1, 0);
    if (ret == -1) {
        perror("msgrcv");
        exit(1);
    }
    printf("Received: [type=%ld] %s\n", msg.mtype, msg.mtext);

    /* 큐 삭제 */
    if (msgctl(msqid, IPC_RMID, NULL) == -1) {
        perror("msgctl");
        exit(1);
    }
    printf("Message queue removed.\n");

    return 0;
}

ㅤㅤ

2-2 세마포어

세마포어는 카운터 기반의 동기화 도구로, 여러 프로세스가 공유 자원에 안전하게 접근할 수 있도록 상호 배제를 얻거나 동기화 신호를 전달하는 목적으로 사용된다.

ㅤㅤ

특징

  • 하나의 ID로 여러 세마포어를 배열처럼 관리할 수 있다 (세마포어 SET)
  • 세마포어를 획득하고 반환하는 과정이 커널 수준에서 원자적으로 수행되어, 방해받지 않는다.
  • 프로세스가 종료된 이후에도 세마포어가 유지된다.
  • 프로세스가 비정상적으로 종료된 경우에 자동으로 복구되는 Undo 기능을 제공한다.

ㅤㅤ

장점

  • 프로세스 간의 동기화 가능
  • 카운팅 세마포어를 통한 제한된 수의 리소스 관리 가능
  • 여러 세마포어를 한 번에 조작할 수도 있다.

ㅤㅤ

단점

  • 단일 세마포어를 사용하고 싶어도 무조건 set으로 써야함
  • 세마포어의 제어를 위해서는 커널 호출이 필요하여, 성능 상 오버헤드가 있다.

ㅤㅤ

어디에 쓰나?

  • 여러 프로세스가 파일이나 공유 메모리같은 공유 자원에 접근해야 할 때
  • 제한된 리소스의 수를 관리할 필요가 있을 때
  • 프로세스간의 실행 순서를 제어할 때 (← 이때는 동기화를 위한 세마포어)

ㅤㅤ

세마포어 실습

세마포어의 핵심적인 API 들은 아래와 같다.

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>

// 세마포어 집합 생성/접근
// 단일 세마포어를 만들더라도 nsems를 1개로 지정하는 세마포어 set을 생성해주어야 한다.
int semget(key_t key, int nsems, int semflg); 
// P/V 연산 
int semop(int semid, struct sembuf *sops, size_t nsops);  
// 제어/초기화/삭제
int semctl(int semid, int semnum, int cmd, ...);  

그리고 세마포어의 구조체 내부는 아래와 같이 생겼다.

struct sembuf {
    unsigned short sem_num;  // 세마포어 집합 내 인덱스
    short          sem_op;   // 연산: -1(P), +1(V), 0(wait for zero)
    short          sem_flg;  // 플래그: IPC_NOWAIT, SEM_UNDO
};

// semctl용 union (일부 시스템에서 직접 정의 필요)
union semun {
    int              val;    // SETVAL용
    struct semid_ds *buf;    // IPC_STAT, IPC_SET용
    unsigned short  *array;  // GETALL, SETALL용
};

ㅤㅤ

실습을 위한 코드는 아래처럼 구성해주었다. 인자로 Semaphore를 이용하거나 하지 않도록 하여, 결과의 차이를 확인할 수 있도록 하였다.

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>

#define SHARED_FILE "shared_data.txt"

/* 일부 시스템에서 semun이 정의되지 않음 */
union semun {
    int              val;
    struct semid_ds *buf;
    unsigned short  *array;
};

/* P 연산 (세마포어 획득) */
static int sem_wait(int semid)
{
    struct sembuf op = {
        .sem_num = 0,
        .sem_op  = -1,
        .sem_flg = SEM_UNDO
    };
    return semop(semid, &op, 1);
}

/* V 연산 (세마포어 해제) */
static int sem_signal(int semid)
{
    struct sembuf op = {
        .sem_num = 0,
        .sem_op  = +1,
        .sem_flg = SEM_UNDO
    };
    return semop(semid, &op, 1);
}

int main(int argc, char *argv[])
{
    key_t key;
    int semid = -1;
    union semun arg;
    FILE *fp;
    char id;
    int use_sem;
    int i;

    if (argc < 3) {
        fprintf(stderr, "Usage: %s <writer_id (A, B, ...)> <use_semaphore (0 or 1)>\n", argv[0]);
        fprintf(stderr, "  0: No semaphore (race condition)\n");
        fprintf(stderr, "  1: Use semaphore (mutual exclusion)\n");
        exit(1);
    }

    id = argv[1][0];
    use_sem = atoi(argv[2]);

    printf("[%c] Semaphore: %s\n", id, use_sem ? "ENABLED" : "DISABLED");

    if (use_sem) {
        key = ftok(".", 'S');
        if (key == -1) {
            perror("ftok");
            exit(1);
        }

        semid = semget(key, 1, IPC_CREAT | IPC_EXCL | 0644);
        if (semid == -1) {
            semid = semget(key, 1, 0644);
            if (semid == -1) {
                perror("semget");
                exit(1);
            }
            printf("[%c] Attached to existing semaphore (id=%d)\n", id, semid);
        } else {
            arg.val = 1;
            if (semctl(semid, 0, SETVAL, arg) == -1) {
                perror("semctl SETVAL");
                exit(1);
            }
            printf("[%c] Created new semaphore (id=%d), initialized to 1\n", id, semid);
        }
    }

    for (i = 0; i < 5; i++) {
        if (use_sem) {
            printf("[%c] Waiting for semaphore...\n", id);
            if (sem_wait(semid) == -1) {
                perror("sem_wait");
                exit(1);
            }
            printf("[%c] === ENTERED critical section ===\n", id);
        } else {
            printf("[%c] Entering WITHOUT semaphore...\n", id);
        }

        fp = fopen(SHARED_FILE, "a");
        if (fp == NULL) {
            perror("fopen");
            if (use_sem) {
                sem_signal(semid);
            }
            exit(1);
        }

        fprintf(fp, "[%c] Line %d - start ... ", id, i + 1);
        fflush(fp);
        usleep(50000);
        fprintf(fp, "middle ... ");
        fflush(fp);
        usleep(50000);
        fprintf(fp, "end\n");
        fflush(fp);

        fclose(fp);

        if (use_sem) {
            printf("[%c] === LEAVING critical section ===\n", id);
            if (sem_signal(semid) == -1) {
                perror("sem_signal");
                exit(1);
            }
        } else {
            printf("[%c] Exiting WITHOUT semaphore...\n", id);
        }

        usleep(10000);
    }

    printf("[%c] Done.\n", id);

    return 0;
}

ㅤㅤ

먼저, 세마포어가 없는 상황을 살펴보면, A와 B가 서로의 Enter / Exit에 상관없이 자원을 사용하는 코드에 접근하고 있다. 이러한 상황에서 공유자원의 Race Condition으로 인한 문제가 발생할 수 있다.

pi07@pi07:~/Documents/TC1127/Vsema $ ./writer A 0 &
pi07@pi07:~/Documents/TC1127/Vsema $ ./writer B 0 &
[1] 2674
[A] Semaphore: DISABLED
[A] Entering WITHOUT semaphore...
[A] Exiting WITHOUT semaphore...
[A] Entering WITHOUT semaphore...
[A] Exiting WITHOUT semaphore...
[A] Entering WITHOUT semaphore...
[B] Semaphore: DISABLED
[B] Entering WITHOUT semaphore...
[A] Exiting WITHOUT semaphore...
[A] Entering WITHOUT semaphore...
[B] Exiting WITHOUT semaphore...
[B] Entering WITHOUT semaphore...
[A] Exiting WITHOUT semaphore...
[A] Entering WITHOUT semaphore...
[B] Exiting WITHOUT semaphore...
[B] Entering WITHOUT semaphore...
[A] Exiting WITHOUT semaphore...
[A] Done.
[B] Exiting WITHOUT semaphore...
[B] Entering WITHOUT semaphore...
[B] Exiting WITHOUT semaphore...
[B] Entering WITHOUT semaphore...
[B] Exiting WITHOUT semaphore...
[B] Done.

ㅤㅤ

A와 B가 둘 다 세마포어를 사용하도록 설정해준 결과를 확인해보면, 서로의 Task가 Critical Section을 LEAVE 한 이후에 ENTER 하는 것을 확인할 수 있다. 이를 통해 공유 자원에 한 번에 하나의 프로세스만 접근하여 사용하도록 처리할 수 있다.

pi07@pi07:~/Documents/TC1127/Vsema $ ./writer A 1 &
pi07@pi07:~/Documents/TC1127/Vsema $ ./writer B 1 
[1] 1744
[A] Semaphore: ENABLED
[A] Attached to existing semaphore (id=0)
[A] Waiting for semaphore...
[A] === ENTERED critical section ===
[A] === LEAVING critical section ===
[A] Waiting for semaphore...
[A] === ENTERED critical section ===
[A] === LEAVING critical section ===
[A] Waiting for semaphore...
[A] === ENTERED critical section ===
[B] Semaphore: ENABLED
[B] Attached to existing semaphore (id=0)
[B] Waiting for semaphore...
[A] === LEAVING critical section ===
[B] === ENTERED critical section ===
[A] Waiting for semaphore...
[B] === LEAVING critical section ===
[A] === ENTERED critical section ===
[B] Waiting for semaphore...
[A] === LEAVING critical section ===
[B] === ENTERED critical section ===
[A] Waiting for semaphore...
[B] === LEAVING critical section ===
[A] === ENTERED critical section ===
[B] Waiting for semaphore...
[A] === LEAVING critical section ===
[B] === ENTERED critical section ===
[A] Done.
[B] === LEAVING critical section ===
[B] Waiting for semaphore...
[B] === ENTERED critical section ===
[B] === LEAVING critical section ===
[B] Waiting for semaphore...
[B] === ENTERED critical section ===
[B] === LEAVING critical section ===
[B] Done.

ㅤㅤ

유용한 명령어

혹시 현재 시스템에 등록되어있는 세마포어의 목록을 확인하거나 제어하고 싶다면, 아래 명령어들을 터미널에 입력해볼 수 있다.

# 현재 시스템의 세마포어 목록 확인
ipcs -s

# 특정 세마포어 삭제
ipcrm -s <semid>

# 세마포어 시스템 제한 확인
cat /proc/sys/kernel/sem
# 출력: SEMMSL SEMMNS SEMOPM SEMMNI
# (집합당 최대 수, 시스템 전체 최대 수, 연산당 최대 수, 집합 최대 수)

ㅤㅤ

2-3 공유 메모리

공유 메모리는 여러 프로세스가 동일한 물리 메모리 영역을 자신의 가상 주소 공간에 매핑하여 직접 접근하는 방식이다. 커널을 거치지 않고 메모리를 직접 읽고 쓰기에, 가장 빠른 IPC 이다. (기존의 방식에서는 파일 시스템 또는 커널의 버퍼 공간을 거쳐 처리되었다)

ㅤㅤ

특징

  • 커널 버퍼 복사 없아 바로 메모리에 접근하기 때문에 가장 빠른 IPC이다.
  • 대용량의 데이터를 공유하기에 좋다.
  • 별도의 동기화 메커니즘을 적용해야하며, 명시적으로 삭제하기 전까지는 유지된다.

ㅤㅤ

장점

  • 데이터 복사 오버헤드가 없어 성능이 좋다.
  • MB, GB 단위의 데이터를 공유하기에도 좋다.
  • 원하는 위치에 즉시 접근할 수 있다. (연결리스트처럼 순회가 필요하지 않음)
  • 데이터 구조가 복잡하더라도 그대로 공유할 수 있다.

ㅤㅤ

단점

  • 같은 기기 내에서만 사용할 수 있다. (네트워크 환경에서는 사용 불가)
  • 메모리 문제가 발생했을 때 추적하기가 어렵다.

ㅤㅤ

언제 쓰냐면?

  • 대용량 데이터를 빠르게 공유해야 할 때
  • 여러 프로세스가 동일 데이터 구조를 실시간으로 참조할 때
  • 성능이 중요한 프로세스 간 통신 (영상 처리, 센서 데이터 등)

ㅤㅤ

공유 메모리 실습

주요 API는 다음과 같다.

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>

// 공유 메모리 생성/접근
int shmget(key_t key, size_t size, int shmflg);  
// 프로세스에 연결(attach)
void *shmat(int shmid, const void *shmaddr, int shmflg);
// 프로세스에서 분리(detach)  
int shmdt(const void *shmaddr);  
// 제어/삭제
int shmctl(int shmid, int cmd, struct shmid_ds *buf);

// 실행 흐름
shmget() → shmid 획득
    ↓
shmat()  → 가상 주소 획득 (void *)
    ↓
직접 읽기/쓰기 (포인터 사용)
    ↓
shmdt()  → 분리
    ↓
shmctl(IPC_RMID) → 삭제 (마지막 프로세스가)  

ㅤㅤ

실습을 위해서 A와 B가 동일한 공유 메모리에 접근하면서 데이터를 읽고 쓰는 과정에서 세마포어를 쓰지 않은 상황을 가정하였다. A와 B 모두 총 100회 공유 메모리의 값을 +1 하는 Read-Modify-Write를 수행하였다.

pi07@pi07:~/Documents/TC1127/Vshared $ ./shm_writer A 0 &
pi07@pi07:~/Documents/TC1127/Vshared $ ./shm_writer B 0 &
[1] 3013
[A] Semaphore: DISABLED
[A] Shared memory id: 12
[A] Shared memory attached at: 0x7f9133b000
[A] Read counter: 0
[A] Wrote counter: 1
[A] Read counter: 1
[A] Wrote counter: 2
[A] Read counter: 2
[A] Wrote counter: 3
...
[B] Read counter: 139
[B] Wrote counter: 140
[B] Read counter: 140
[B] Wrote counter: 141
[B] Final counter: 141
[B] Final message: Last writer: B, count: 141
[B] Done.

공유 메모리 자체는 그 자체로 자원에 대한 접근을 통제하지 않기 때문에 200이 아닌 141이 최종 결과로 나왔다. (버그가 아니라 기능입니다)

ㅤㅤ
ㅤㅤ

shm_writer.c

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

#define SHM_SIZE 1024

/* 공유할 데이터 구조체 */
struct shared_data {
    int counter;
    char message[256];
};

union semun {
    int              val;
    struct semid_ds *buf;
    unsigned short  *array;
};

static int sem_wait(int semid)
{
    struct sembuf op = { .sem_num = 0, .sem_op = -1, .sem_flg = SEM_UNDO };
    return semop(semid, &op, 1);
}

static int sem_signal(int semid)
{
    struct sembuf op = { .sem_num = 0, .sem_op = +1, .sem_flg = SEM_UNDO };
    return semop(semid, &op, 1);
}

int main(int argc, char *argv[])
{
    key_t shm_key, sem_key;
    int shmid, semid = -1;
    struct shared_data *shared;
    union semun arg;
    char id;
    int use_sem;
    int i;

    if (argc < 3) {
        fprintf(stderr, "Usage: %s <writer_id (A, B, ...)> <use_semaphore (0 or 1)>\n", argv[0]);
        exit(1);
    }

    id = argv[1][0];
    use_sem = atoi(argv[2]);

    printf("[%c] Semaphore: %s\n", id, use_sem ? "ENABLED" : "DISABLED");

    /* 공유 메모리 생성/접근 */
    shm_key = ftok(".", 'M');
    if (shm_key == -1) {
        perror("ftok shm");
        exit(1);
    }

    shmid = shmget(shm_key, SHM_SIZE, IPC_CREAT | 0644);
    if (shmid == -1) {
        perror("shmget");
        exit(1);
    }
    printf("[%c] Shared memory id: %d\n", id, shmid);

    /* 공유 메모리 연결 */
    shared = (struct shared_data *)shmat(shmid, NULL, 0);
    if (shared == (void *)-1) {
        perror("shmat");
        exit(1);
    }
    printf("[%c] Shared memory attached at: %p\n", id, (void *)shared);

    /* 세마포어 설정 */
    if (use_sem) {
        sem_key = ftok(".", 'S');
        if (sem_key == -1) {
            perror("ftok sem");
            exit(1);
        }

        semid = semget(sem_key, 1, IPC_CREAT | IPC_EXCL | 0644);
        if (semid == -1) {
            semid = semget(sem_key, 1, 0644);
            if (semid == -1) {
                perror("semget");
                exit(1);
            }
        } else {
            arg.val = 1;
            semctl(semid, 0, SETVAL, arg);
            printf("[%c] Semaphore created (id=%d)\n", id, semid);
        }
    }

    /* 카운터 10회 증가 */
    for (i = 0; i < 100; i++) {
        int temp;

        if (use_sem) {
            sem_wait(semid);
        }

        /* 임계 영역: read-modify-write */
        temp = shared->counter;
        printf("[%c] Read counter: %d\n", id, temp);
        usleep(10000);  /* 10ms 지연 (race condition 유발) */
        temp++;
        shared->counter = temp;
        snprintf(shared->message, sizeof(shared->message),
                 "Last writer: %c, count: %d", id, temp);
        printf("[%c] Wrote counter: %d\n", id, temp);

        if (use_sem) {
            sem_signal(semid);
        }

        usleep(5000);  /* 5ms */
    }

    printf("[%c] Final counter: %d\n", id, shared->counter);
    printf("[%c] Final message: %s\n", id, shared->message);

    /* 분리 (삭제는 하지 않음 - reader가 확인) */
    if (shmdt(shared) == -1) {
        perror("shmdt");
        exit(1);
    }

    printf("[%c] Done.\n", id);

    return 0;
}

shm_reader.c

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

#define SHM_SIZE 1024

struct shared_data {
    int counter;
    char message[256];
};

int main(void)
{
    key_t shm_key, sem_key;
    int shmid, semid;
    struct shared_data *shared;

    /* 공유 메모리 접근 */
    shm_key = ftok(".", 'M');
    if (shm_key == -1) {
        perror("ftok shm");
        exit(1);
    }

    shmid = shmget(shm_key, SHM_SIZE, 0644);
    if (shmid == -1) {
        perror("shmget - shared memory not found");
        exit(1);
    }

    shared = (struct shared_data *)shmat(shmid, NULL, SHM_RDONLY);
    if (shared == (void *)-1) {
        perror("shmat");
        exit(1);
    }

    /* 결과 출력 */
    printf("=== Shared Memory Contents ===\n");
    printf("Counter: %d\n", shared->counter);
    printf("Message: %s\n", shared->message);
    printf("==============================\n");

    /* 분리 */
    shmdt(shared);

    /* 공유 메모리 삭제 */
    if (shmctl(shmid, IPC_RMID, NULL) == -1) {
        perror("shmctl IPC_RMID");
    } else {
        printf("Shared memory removed.\n");
    }

    /* 세마포어 삭제 */
    sem_key = ftok(".", 'S');
    if (sem_key != -1) {
        semid = semget(sem_key, 1, 0644);
        if (semid != -1) {
            if (shmctl(semid, IPC_RMID, NULL) != -1) {
                printf("Semaphore removed.\n");
            }
        }
    }

    return 0;
}

shm_init.c

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

#define SHM_SIZE 1024

struct shared_data {
    int counter;
    char message[256];
};

int main(void)
{
    key_t key;
    int shmid;
    struct shared_data *shared;

    key = ftok(".", 'M');
    if (key == -1) {
        perror("ftok");
        exit(1);
    }

    shmid = shmget(key, SHM_SIZE, IPC_CREAT | 0644);
    if (shmid == -1) {
        perror("shmget");
        exit(1);
    }

    shared = (struct shared_data *)shmat(shmid, NULL, 0);
    if (shared == (void *)-1) {
        perror("shmat");
        exit(1);
    }

    /* 초기화 */
    shared->counter = 0;
    strcpy(shared->message, "Initialized");

    printf("Shared memory initialized.\n");
    printf("  shmid: %d\n", shmid);
    printf("  counter: %d\n", shared->counter);

    shmdt(shared);

    return 0;
}

ㅤㅤ

공유 메모리의 시스템콜

이 과정에서 공유 메모리의 장점이 “시스템콜이 적다” 이기 때문에, 시스템콜도 한 번 분석을 해보았다. 시스템콜 추적을 살펴보면, 여기에서는 공유 자원을 할당받고 해제하기 위한 시스템콜은 총 3회를 볼 수 있었지만, 공유자원에 접근하기 위해 커널에게 시스템콜을 요청하는 경우는 없었다.

pi07@pi07:~/Documents/TC1127/Vshared $ strace -c ./shm_writer A 0
% time     seconds  usecs/call     calls    errors syscall
------ ----------- ----------- --------- --------- ----------------
 47.43    0.006611          33       200           clock_nanosleep
 45.97    0.006407          31       206           write          // printf를 위한 호출
  1.35    0.000188          31         6           mmap
  0.95    0.000133          33         4           mprotect
  0.80    0.000111          37         3           munmap
  0.50    0.000069          34         2           openat
  0.47    0.000066          22         3           brk
  0.42    0.000059          59         1           shmdt          // 공유메모리 해제
  0.39    0.000055          18         3           fstat
  0.34    0.000048          48         1           shmat          // 프로세스 주소공간에 매팽
  0.22    0.000030          30         1         1 faccessat
  0.22    0.000030          30         1           newfstatat
  0.19    0.000026          13         2           close
  0.17    0.000023          23         1           read
  0.14    0.000020          20         1           getrandom
  0.10    0.000014          14         1           shmget         // 공유메모리 획득
  0.10    0.000014          14         1           prlimit64
  0.09    0.000012          12         1           set_tid_address
  0.08    0.000011          11         1           rseq
  0.07    0.000010          10         1           set_robust_list
  0.00    0.000000           0         1           execve
------ ----------- ----------- --------- --------- ----------------
100.00    0.013937          31       441         1 total

ㅤㅤ

비교를 위해 가져온 세마포어 (위 실습) 의 시스템콜 추적이다. 아래에서 살펴보면 5번 자원에 접근하고 나오면서 semtimedop 이라는 시스템콜을 10번 호출한 것을 볼 수 있다. 공유메모리가 아니라면 공유자원에 접근하기 위해서는 커널을 거쳐야하고, 이때 시스템콜이 필연적으로 발생하면서 오버헤드로 인해 수행 시간이 늘어나게 된다. (적절한 예시는 아니지만) 많은 데이터를 주고받아야 하는 상황이라 시스템 콜로 인한 오버헤드 성능저하가 걱정된다면 공유 메모리를 사용하자.

pi07@pi07:~/Documents/TC1127/Vsema $ strace -c ./writer A 1
[A] Semaphore: ENABLED
[A] Attached to existing semaphore (id=0)
[A] Waiting for semaphore...
[A] === ENTERED critical section ===
[A] === LEAVING critical section ===
[A] Waiting for semaphore...
[A] === ENTERED critical section ===
[A] === LEAVING critical section ===
[A] Waiting for semaphore...
[A] === ENTERED critical section ===
[A] === LEAVING critical section ===
[A] Waiting for semaphore...
[A] === ENTERED critical section ===
[A] === LEAVING critical section ===
[A] Waiting for semaphore...
[A] === ENTERED critical section ===
[A] === LEAVING critical section ===
[A] Done.
% time     seconds  usecs/call     calls    errors syscall
------ ----------- ----------- --------- --------- ----------------
  0.00    0.000000           0         1         1 faccessat
  0.00    0.000000           0         7           openat
  0.00    0.000000           0         7           close
  0.00    0.000000           0         5           lseek
  0.00    0.000000           0         1           read
  0.00    0.000000           0        33           write
  0.00    0.000000           0         1           newfstatat
  0.00    0.000000           0         8           fstat
  0.00    0.000000           0         1           set_tid_address
  0.00    0.000000           0         1           set_robust_list
  0.00    0.000000           0        15           clock_nanosleep
  0.00    0.000000           0         2         1 semget          // 세마포어 획득
  0.00    0.000000           0        10           semtimedop      // 세마포어 접근 (P/V)
  0.00    0.000000           0         3           brk
  0.00    0.000000           0         3           munmap
  0.00    0.000000           0         1           execve
  0.00    0.000000           0         6           mmap
  0.00    0.000000           0         4           mprotect
  0.00    0.000000           0         1           prlimit64
  0.00    0.000000           0         1           getrandom
  0.00    0.000000           0         1           rseq
------ ----------- ----------- --------- --------- ----------------
100.00    0.000000           0       112         2 total

ㅤㅤ

320x100