프로젝트 요구사항
다음 세 가지 프로그램을 작성하여 Linux System V IPC(프로세스 간 통신) 메커니즘을 통한 공유 메모리 접근의 상호 배제를 구현합니다.
- 프로세스 A (생성자): 공유 메모리 및 세마포어를 유일하게 생성하고, 공유 메모리에 초기 문자열을 저장합니다. 세마포어는 이진 세마포어로 초기값 1(뮤텍스 잠금)을 설정합니다. 테스트가 완료될 때까지 블록 상태로 대기한 후 커널 자원을 제거합니다.
- 프로세스 B 및 프로세스 C (소비자): 두 프로그램은 완전히 동일한 코드를 가집니다. 이미 생성된 공유 메모리와 세마포어를 열어, 세마포어를 사용하여 상호 배제를 구현하고 공유 메모리의 내용을 읽습니다.
- 목표: 임계 구역(공유 메모리)에는 동시에 하나의 프로세스만 접근하도록 하여 동시성 경쟁 문제를 방지합니다.
System V IPC 기본 원칙
1. IPC 자원 관리 규칙
shmget / semget호출 시IPC_CREAT | IPC_EXCL플래그 사용: 자원이 존재하지 않으면 새로 생성하고, 이미 존재하면 오류를 반환합니다. 이는 주로 자원을 최초로 생성하는 프로세스 A에 사용됩니다.shmget / semget호출 시 권한만 전달 (예:0644): 이미 존재하는 자원을 엽니다. 자원이 존재하지 않으면 오류를 반환합니다. 이는 기존 자원을 사용하는 프로세스 B/C에 사용됩니다.- 세마포어 초기값을 1로 설정: 이진 세마포어로 동작하여 상호 배제를 구현합니다.
acquire_semaphore(P 연산):semop(semId, {0,-1,0})를 호출하여 자원을 점유합니다.release_semaphore(V 연산):semop(semId, {0,1,0})를 호출하여 자원을 해제합니다.
- 공유 메모리:
shmat함수로 프로세스의 가상 주소 공간에 연결(attach)하고, 사용 후에는shmdt함수로 분리(detach)합니다. 커널 자원(공유 메모리 및 세마포어)은IPC_RMID를 통해 수동으로 제거해야 합니다.
2. 주의해야 할 핵심 사항 (주요 문제 발생 지점)
System V 세마포어는 프로세스가 종료되어도 잠금을 자동으로 해제하지 않습니다!
만약 V 연산이 실패하거나, 프로그램이 비정상적으로 종료되어 V 연산이 호출되지 않으면, 세마포어 값은 0으로 유지됩니다. 이 경우 이후 모든 프로세스의 P 연산은 영구적으로 블록되어 시스템이 멈출 수 있습니다.
전체 구현 코드
프로세스 A (생성자: creator.c)
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <sys/shm.h>
#define SHARED_MEM_KEY_ID 80
#define SEMAPHORE_KEY_ID 20
#define SHARED_MEM_SIZE 256
// 세마포어 초기화에 사용되는 공용체
union semun {
int val;
struct semid_ds *buf;
unsigned short *array;
};
// P 연산: 잠금 (acquire)
void acquire_semaphore(int semId)
{
struct sembuf operation = {0, -1, 0}; // 세마포어 값 1 감소
if (semop(semId, &operation, 1) == -1) {
perror("세마포어 잠금 실패");
}
}
// V 연산: 잠금 해제 (release)
void release_semaphore(int semId)
{
struct sembuf operation = {0, 1, 0}; // 세마포어 값 1 증가
if (semop(semId, &operation, 1) == -1) {
perror("세마포어 해제 실패");
}
}
int main()
{
key_t sharedMemKey = ftok(".", SHARED_MEM_KEY_ID);
key_t semaphoreKey = ftok(".", SEMAPHORE_KEY_ID);
// 1. 공유 메모리 생성 또는 가져오기
int sharedMemId = shmget(sharedMemKey, SHARED_MEM_SIZE, IPC_CREAT | IPC_EXCL | 0644);
if (sharedMemId == -1)
{
if (errno == EEXIST) {
fprintf(stderr, "공유 메모리가 이미 존재합니다. 기존 자원을 가져옵니다.\n");
sharedMemId = shmget(sharedMemKey, SHARED_MEM_SIZE, 0644);
if (sharedMemId == -1) {
perror("기존 공유 메모리 가져오기 실패");
return 1;
}
} else {
fprintf(stderr, "shmget 오류, errno:%d, %s\n", errno, strerror(errno));
return 1;
}
}
printf("【생성자】공유 메모리 생성/가져오기 성공 (ID: %d)\n", sharedMemId);
// 2. 세마포어 생성 또는 가져오기 및 초기화
int semaphoreId = semget(semaphoreKey, 1, IPC_CREAT | IPC_EXCL | 0644);
union semun arg;
arg.val = 1; // 이진 세마포어 초기값 설정
if (semaphoreId == -1)
{
if (errno == EEXIST) {
fprintf(stderr, "세마포어가 이미 존재합니다. 기존 자원을 가져옵니다.\n");
semaphoreId = semget(semaphoreKey, 1, 0644);
if (semaphoreId == -1) {
perror("기존 세마포어 가져오기 실패");
return 1;
}
// 기존 세마포어의 값은 재설정하지 않음!
} else {
fprintf(stderr, "semget 오류, errno:%d, %s\n", errno, strerror(errno));
return 1;
}
}
else
{
// 새로 생성된 세마포어에만 초기값을 설정
if (semctl(semaphoreId, 0, SETVAL, arg) == -1) {
perror("세마포어 초기값 설정 실패");
return 1;
}
printf("【생성자】새 세마포어 생성 및 초기값 1 설정 (ID: %d)\n", semaphoreId);
}
// 3. 공유 메모리 연결
char *sharedBuffer = (char *)shmat(sharedMemId, NULL, 0);
if (sharedBuffer == (void *)-1)
{
perror("공유 메모리 연결 실패");
return 1;
}
// 잠금 후 임계 자원 초기화
acquire_semaphore(semaphoreId);
strncpy(sharedBuffer, "초기화된 공유 데이터", SHARED_MEM_SIZE - 1); // 버퍼 오버플로우 방지
sharedBuffer[SHARED_MEM_SIZE - 1] = '\0'; // 안전을 위한 널 종료
printf("【생성자】공유 메모리 초기화 완료: \"%s\"\n", sharedBuffer);
release_semaphore(semaphoreId);
// 공유 메모리 분리
if (shmdt(sharedBuffer) == -1) {
perror("공유 메모리 분리 실패");
return 1;
}
printf("【생성자】로컬 매핑이 분리되었습니다. 새 터미널에서 './consumer_A' 및 './consumer_B'를 실행하세요.\n");
printf("모든 테스트가 완료되면 엔터 키를 눌러 커널 자원을 제거하세요.\n");
getchar(); // 사용자 입력 대기
// IPC 자원 제거
if (semctl(semaphoreId, 0, IPC_RMID) == -1) {
perror("세마포어 제거 실패");
return 1;
}
if (shmctl(sharedMemId, IPC_RMID, NULL) == -1) {
perror("공유 메모리 제거 실패");
return 1;
}
printf("【생성자】모든 자원 제거 완료. 프로그램 종료.\n");
return 0;
}
프로세스 B / 프로세스 C (소비자: consumer.c, 두 파일 동일)
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <sys/shm.h>
#define SHARED_MEM_KEY_ID 80
#define SEMAPHORE_KEY_ID 20
#define SHARED_MEM_SIZE 256 // Note: SHM_SIZE is needed to calculate key, but not for accessing already existing shm.
// P 연산: 잠금 (acquire)
void acquire_semaphore(int semId)
{
struct sembuf operation = {0, -1, 0};
if (semop(semId, &operation, 1) == -1) {
perror("세마포어 잠금 실패");
}
}
// V 연산: 잠금 해제 (release)
void release_semaphore(int semId)
{
struct sembuf operation = {0, 1, 0};
if (semop(semId, &operation, 1) == -1) {
perror("세마포어 해제 실패");
}
}
int main()
{
key_t sharedMemKey = ftok(".", SHARED_MEM_KEY_ID);
key_t semaphoreKey = ftok(".", SEMAPHORE_KEY_ID);
// 공유 메모리 가져오기
int sharedMemId = shmget(sharedMemKey, 0, 0644); // 크기는 0으로 지정해도 기존 메모리 접근 가능
if (sharedMemId == -1)
{
fprintf(stderr, "shmget 오류: 공유 메모리를 찾을 수 없습니다. errno:%d, %s\n", errno, strerror(errno));
return 1;
}
// 세마포어 가져오기
int semaphoreId = semget(semaphoreKey, 0, 0644); // 세마포어 개수는 0으로 지정해도 기존 세마포어 접근 가능
if (semaphoreId == -1)
{
fprintf(stderr, "semget 오류: 세마포어를 찾을 수 없습니다. errno:%d, %s\n", errno, strerror(errno));
return 1;
}
// 공유 메모리 연결
char *sharedBuffer = (char *)shmat(sharedMemId, NULL, 0);
if (sharedBuffer == (void *)-1)
{
perror("공유 메모리 연결 실패");
return 1;
}
// 임계 구역: 세마포어를 사용하여 상호 배제 접근
acquire_semaphore(semaphoreId);
printf("==== 현재 프로세스가 공유 메모리에 접근합니다 ====\n");
printf("공유 메모리 내용: \"%s\"\n", sharedBuffer);
printf("==== 접근 완료, 잠금 해제 ====\n");
release_semaphore(semaphoreId);
// 공유 메모리 분리
if (shmdt(sharedBuffer) == -1) {
perror("공유 메모리 분리 실패");
return 1;
}
return 0;
}
컴파일 및 표준 실행 절차
1. 컴파일 명령
# 생성자 프로세스 컴파일
gcc creator.c -o creator
# 소비자 프로세스 A 컴파일
gcc consumer.c -o consumer_A
# 소비자 프로세스 B 컴파일
gcc consumer.c -o consumer_B
2. 올바른 실행 순서
- 첫 번째 터미널에서
./creator실행. 프로그램이 사용자 입력 대기 상태가 됩니다. (종료하지 마세요) - 새 터미널(두 번째)에서
./consumer_A실행. 공유 메모리 내용을 정상적으로 읽고 잠금을 해제합니다. - 또 다른 새 터미널(세 번째)에서
./consumer_B실행. 이 또한 공유 메모리 내용을 정상적으로 읽고 잠금을 해제합니다. - 모든 테스트가 완료되면, 첫 번째 터미널로 돌아가 엔터 키를 누르면 공유 메모리 및 세마포어 커널 자원이 자동으로 제거됩니다.
3. 잔여 IPC 자원 확인 및 제거 명령어
# 모든 세마포어 확인 (명령어와 옵션 사이에 공백 필수)
ipcs -s
# 모든 공유 메모리 확인
ipcs -m
# 강제로 멈춘 세마포어 제거 (ID는 'ipcs -s'로 확인된 ID로 대체)
ipcrm -s [세마포어_ID]
# 강제로 멈춘 공유 메모리 제거 (ID는 'ipcs -m'으로 확인된 ID로 대체)
ipcrm -m [공유_메모리_ID]
실제 문제 해결 및 팁 (주요 발생 문제 사례)
문제 1: 잠금 해제 함수에 잘못된 ID 전달로 인한 영구 데드락
잘못된 코드 예시:
release_semaphore(sharedMemId); // sharedMemId는 공유 메모리 ID입니다.
문제 분석: release_semaphore 함수 내부에서 호출되는 semop의 첫 번째 인자는 반드시 세마포어 ID(semaphoreId)여야 합니다. 유효하지 않은 ID를 전달하면 V 연산이 실패하고, 세마포어 값은 영원히 0으로 유지됩니다. 결과적으로 consumer_A 프로그램이 종료되어도 잠금이 해제되지 않아 이후 consumer_B 등이 acquire_semaphore에서 영구적으로 블록됩니다.
해결책:
release_semaphore(semaphoreId); // 올바른 세마포어 ID 전달
문제 2: printf("%s", &buffer[i]) 반복 출력 시 문자열 손상/중복
잘못된 코드 예시:
for(int i = 0; i < strlen(sharedBuffer); i++)
{
printf("%s", &sharedBuffer[i]);
}
원리: %s 형식 지정자는 문자열의 시작 주소를 요구하며, 널 종료 문자(\0)를 만날 때까지 문자를 출력합니다. 위 코드에서는 i=0일 때 전체 문자열을 출력하고, i=1일 때는 두 번째 문자부터 다시 전체 문자열을 출력하는 식으로 반복되어 중복된 내용이 계속 출력됩니다. 또한 \0이 없는 메모리 영역에 접근하여 알 수 없는 문자나 충돌이 발생할 수 있습니다.
두 가지 해결책:
- 전체 문자열 직접 출력 (권장):
printf("내용: %s\n", sharedBuffer); - 문자 단위로 출력:
for(int i = 0; i < strlen(sharedBuffer); i++) { printf("%c", sharedBuffer[i]); } printf("\n"); // 줄바꿈 추가
문제 3: 포인터에 sizeof(pointer)를 사용하여 공유 메모리 크기 계산 실패
잘못된 코드 예시:
char *sharedBuffer = shmat(...);
for(int i = 0; i < sizeof(sharedBuffer); i++)
// ...
원리: sharedBuffer는 char* 타입의 포인터이므로, sizeof(sharedBuffer)는 포인터 자체의 크기(64비트 시스템에서는 대개 8바이트)를 반환합니다. 이 값은 실제 공유 메모리 버퍼의 크기(예: 256바이트)와는 전혀 관련이 없습니다. 따라서 위 루프는 8번만 실행되어 공유 메모리 전체에 접근하지 못합니다.
해결책:
- 전체 메모리 버퍼를 순회해야 한다면, 정의된 상수 크기를 사용합니다:
for(int i=0; i < SHARED_MEM_SIZE; i++) - 유효한 문자열 데이터만 순회해야 한다면,
strlen()을 사용합니다:for(int i=0; i < strlen(sharedBuffer); i++)
문제 4: strlen 함수로 int 배열의 요소 개수 세기 불가능
원리: strlen 함수는 단일 바이트 널 종료 문자(\0)를 문자열의 끝으로 인식하고 그 이전까지의 길이를 반환합니다. 이 함수는 char 타입의 문자열에만 적용될 수 있습니다. int 배열의 숫자 0은 4바이트를 차지하며, strlen은 이를 문자열 종료로 인식하지 못하므로, int 배열의 유효한 요소 수를 세는 데 사용할 수 없습니다. 스택에 할당된 배열의 길이는 sizeof(arr)/sizeof(int)와 같이 계산하며, 포인터의 경우 별도의 길이 정보를 수동으로 관리해야 합니다.
문제 5: 명령어 오타 또는 구문 오류
ipcs-s명령 오류 (command not found): 명령어와 인자 사이에 공백이 필요합니다.ipcs -s./consumer_a실행 시No such file: Linux는 파일 이름을 대소문자를 구분합니다. 컴파일 시 생성된 실행 파일 이름(예:consumer_A)과 정확히 일치시켜야 합니다.ipcs도구 누락: Ubuntu와 같은 시스템에서는sudo apt install ipcs명령으로 설치해야 할 수 있습니다.
문제 6: 생성자 프로세스 A가 대기하지 않아 자원이 조기 파괴되는 문제
만약 프로세스 A에 getchar()와 같은 대기 메커니즘이 없다면, A는 자원 생성 직후 IPC_RMID를 호출하여 공유 메모리와 세마포어를 즉시 파괴합니다. 이 상태에서 프로세스 B/C를 실행하면, 필요한 자원을 찾지 못해 오류가 발생하거나 acquire_semaphore에서 영구적으로 블록될 수 있습니다.
해결책: 프로세스 A에 getchar()를 추가하여 사용자가 수동으로 테스트 완료를 알리고 자원을 해제하도록 합니다.
문제 7: 세마포어는 오직 생성 시에만 초기값을 설정해야 함
잘못된 초기화 로직 (매번 SETVAL 호출):
int semaphoreId = semget(..., IPC_CREAT|IPC_EXCL|0644);
if(semaphoreId == -1) {
// 이미 존재, 가져오기
semaphoreId = semget(..., 0644);
// 이 경우에도 semctl(semaphoreId,0,SETVAL,arg); 호출
} else {
semctl(semaphoreId,0,SETVAL,arg); // 새로 생성 시 값 1로 초기화
}
문제 분석: 만약 프로세스 A가 매번 실행될 때마다 semctl(..., SETVAL, arg)를 호출하여 세마포어 값을 강제로 1로 재설정한다면 문제가 발생합니다. 예를 들어, consumer_A가 잠금을 획득하여 세마포어 값이 0인 상태(임계 구역 접근 중)일 때, A가 다시 실행되어 세마포어 값을 강제로 1로 재설정하면, 여러 프로세스가 동시에 임계 구역에 접근하게 되어 상호 배제가 무효화됩니다.
해결책: IPC_EXCL 플래그를 사용하여 세마포어가 새로 생성될 때(즉, semget이 성공했을 때만) semctl을 통해 초기값을 설정해야 합니다. 기존 세마포어를 가져오는 경우에는 값을 변경하지 않아야 합니다. (현재 제공된 creator.c 코드는 이 원칙을 따르고 있습니다.)
문제 8: 불필요한 헤더 파일 및 오타
- 반복적으로 포함된 헤더 파일 (예:
<sys/ipc.h>중복)이나 사용되지 않는 헤더 파일 (예:<sys/msg.h>,<sys/stat.h>)은 코드의 가독성을 해치고 불필요한 의존성을 만듭니다. - 오류 메시지나 변수명에 오타 (예:
shmgeit대신shmget)는 디버깅을 어렵게 하고 코드의 품질을 떨어뜨립니다.
핵심 지식 요약
- 자원 권한 구분: 프로세스 A는
IPC_CREAT플래그를 사용하여 자원을 생성하고, 프로세스 B/C는 단순히 기존 자원을 엽니다. - 상호 배제 원리: 이진 세마포어를 사용하며, 초기값을 1로 설정하여 하나의 P 연산에 하나의 V 연산이 정확히 대응하도록 합니다.
- 세마포어의 비자동 해제: System V 세마포어는 프로세스 종료 시 잠금을 자동으로 해제하지 않습니다. 잘못된 사용은 영구적인 블록 상태를 초래할 수 있습니다.
- 포인터 크기 문제:
sizeof(포인터)는 포인터 변수 자체의 크기를 반환하며, 가리키는 버퍼의 실제 크기가 아닙니다. strlen의 제한:strlen은 널 종료 문자(\0)로 끝나는char타입의 문자열에만 적용 가능합니다.- System V IPC 자원 수동 관리: 공유 메모리 및 세마포어와 같은 커널 자원은 사용 후 반드시
IPC_RMID를 통해 수동으로 제거해야 합니다.ipcs및ipcrm명령어를 사용하여 관리할 수 있습니다.
확장 질문
- 세마포어 없이 프로세스 B와 C를 동시에 실행하면 어떤 현상이 발생할까요? (힌트: 동시성 문제, 데이터 경쟁)
- 프로세스 B가 공유 메모리에 접근하는 도중 강제로
kill되면 세마포어가 자동으로 해제될까요? (힌트: 수동 정리의 필요성) - 파이프(pipe)나 메시지 큐(message queue)를 사용하여 세마포어와 유사한 상호 배제 기능을 구현할 수 있을까요?