본문 바로가기
Computer Science/UNIX & Linux

[UNIX/Linux] ep10-2) 세마포

by 클레어몬트 2024. 11. 13.

https://claremont.tistory.com/entry/%EC%9A%B4%EC%98%81%EC%B2%B4%EC%A0%9C-IPCInter-Process-Communication

 

[운영체제] IPC(Inter-Process Communication)

https://claremont.tistory.com/entry/ep2-%ED%94%84%EB%A1%9C%EC%84%B8%EC%8A%A4process [운영체제] ep2) 프로세스(process)ㅁ프로세스(process): 실행 중인 프로그램 (실행/스케줄링의 단위 및 자료구조)보조기억장치에 저장

claremont.tistory.com

https://claremont.tistory.com/entry/UNIXLinux-ep10-1-%EB%A9%94%EC%8B%9C%EC%A7%80-%ED%81%90-%EA%B3%B5%EC%9C%A0-%EB%A9%94%EB%AA%A8%EB%A6%AC

 

[UNIX/Linux] ep10-1) 메시지 큐, 공유 메모리

https://claremont.tistory.com/entry/%EC%9A%B4%EC%98%81%EC%B2%B4%EC%A0%9C-IPCInter-Process-Communication [운영체제] IPC(Inter-Process Communication)https://claremont.tistory.com/entry/ep2-%ED%94%84%EB%A1%9C%EC%84%B8%EC%8A%A4process [운영체제] ep2)

claremont.tistory.com

 

 

[ep10의 학습목표]

1. UNIX 시스템 V에서 제공하는 IPC 기법

2. 메시지 큐를 이용한 통신 프로그램 작성

3. 공유 메모리를 이용한 통신 프로그램 작성

4. 세마포를 이용한 IPC 기법

 

 

UNIX는 크게 BSD 계열과 시스템 V 계열로 구분할 수가 있다

시스템 V IPC: 시스템 V 계열 UNIX에서 개발한 프로세스 간 통신 방법(IPC)

1. 메시지 큐

2. 공유 메모리

3. 세마포

 

 

IPC 객체:  + ID

시스템 V IPC를 사용하려면 IPC 객체를 생성해야 하는데 이를 위해 공통으로 사용하는 기본 요소가 키와 식별자(ID)이다. IPC 객체는 키를 통해 생성되며, 생성된 객체는 ID를 통해 접근할 수 있다.

(객체를 생성하고 현재 사용 중인 각 IPC의 상태를 확인하고 사용을 마친 객체는 삭제할 수 있도록 관리 명령을 제공)

 

 

<키 생성 방법>

1. IPC_PRIVATE 지정

식별자를 알아야 통신할 수 있으므로 IPC_PRIVATE를 키로 지정해 생성된 식별자를 Server와 Client 모두 알 수 있게 생성

(fork() 함수로 생성된 부모-자식 프로세스 간 통신에서 유용하게 사용할 수 있다)

 

2. ftok() 함수: 경로명과 숫자값을 받아 키를 생성

Server와 Client가 같은 경로명과 숫자값을 지정하면 공통 식별자를 생성할 수 있다

 

[키 생성 주의사항]

 같은 키로 생성된 식별자는 통신에 사용할 수 있음

 미리 정해진 키를 서버와 클라이언트 프로세스가 공유할 수 있게 해야 함

 헤더 파일이나 환경 설정 파일에 키를 저장해 공유할 수 있음, 다만 이 키를 제3의 프로세스가 먼저 사용하고 있으면 안 됨

※ 각자 현재 directory로 수정할 것, key값을 줄 경우 뒤 3자리 정수를 이용할 것

 

ㅇ key_t ftok(const char* pathname, int proj_id): 키 생성하기

- pathname : 파일 시스템에 이미 존재하는 임의의 파일 경로명

- proj_id : 키값을 생성할 때 지정하는 임의의 번호(1~255)

pathname에 지정한 경로명과 proj_id에 지정한 정수값을 조합해 새로운 키를 생성, 이 키는 IPC 객체를 생성할 때 사용

(경로명은 파일 시스템에 존재해야 하며 접근 권한이 있어야 함)

성공하면 생성한 키값을 반환, 실패하면 –1을 반환

 

(IPC 공통 구조체)

struct ipc_perm {
    key_t __key; // 키값
    uid_t uid; // 구조체의 소유자 ID
    gid_t gid; // 구조체의 소유 그룹 ID
    uid_t cuid; // 구조체를 생성한 사용자 ID
    gid_t cgid; // 구조체를 생성한 그룹 ID
    unsigned short mode; // 구조체에 대한 접근 권한
    unsigned short __seq; // 일련번호
};

 

ㅁ시스템 V IPC 정보 검색

ㅇipcs 명령의 기본 형식

 ipcs: 시스템 V IPC의 정보를 검색하고 현재 상태를 확인하는 명령

 ipcs 명령을 실행하는 동안에도 IPC의 상태가 변경될 수 있음

 ipcs 명령은 검색하는 순간의 정확성만 보장

 상태가 변경된 정보를 보려면 ipcs 명령을 다시 수행

ipcs [-ihVmqsaclptu]

 

① 일반 옵션

출력 옵션은 하나만 지정, 여러 개를 지정하면 마지막에 지정한 옵션이 적용

-i id : id로 지정한 특정 요소에 대한 상세 정보를 출력, 이 옵션은 –m, –q, -s 중 하나와 결합해 사용

-h : 도움말을 출력

-V : 버전 정보를 출력

 

② 자원 옵션

-m : 공유 메모리 정보만 검색

-q : 메시지 큐 정보만 검색

-s : 세마포어 정보만 검색

-a : 공유 메모리, 메시지 큐, 세마포어 모두의 정보를 검색, 이 옵션이 기본

ipcs 명령의 다양한 옵션을 적절히 조합해 원하는 정보를 검색할 수 있음

 

ㅇipcmk 명령

ipcmk [options]

 

-M size : size에 지정한 바이트 크기로 공유 메모리를 생성 (KB, MB, GB 단위를 사용)

-Q : 메시지 큐를 생성

-S number : number에 지정한 개수의 요소를 갖는 세마포어를 생성

-p mode : 자원의 접근 권한을 지정, 기본값은 0644

 

ㅇipcrm 명령

ipcrm [options]

-a : 모든 자원을 제거 (이 옵션은 자원을 모두 삭제하므로 주의해서 사용해야 함)

-M shmkey : shmkey로 생성한 공유 메모리의 마지막 연결이 해제된 후 공유 메모리를 제거

-m shmid : shmid로 지정한 공유 메모리를 삭제 (공유 메모리에 대한 마지막 해제 동작 이후에 관련된 메모리 세그먼트가 제거)

-Q msgkey : msgkey로 생성한 메시지 큐를 제거

-q msgid : msgid로 지정한 메시지 큐를 삭제

-S semkey : semkey로 생성한 세마포어를 삭제

-s semid : semid로 지정한 세마포어를 삭제

 

 

 

3. 세마포: 프로세스 사이의 동기를 맞추는 기능을 제공

공유 메모리에 여러 프로세스가 동시에 쓰기를 시도하면 데이터가 손상되는 현상이 발생하게 된다. 따라서 여러 프로세스 사이에서 동작 순서를 지정해줘야 하는데, 세마포가 접근 순서를 정하는 방법을 제공해 준다.

 

 

[세마포어의 기본 동작 구조]

① 세마포어는 중요한 처리 부분으로 들어가기 전에 p() 함수를 실행해 잠금 기능을 수행 (잠금 기능을 수행 중인 동안에는 다른 프로세스가 처리 부분의 코드를 실행할 수 없다)

② 처리를 마치면 다시 v() 함수를 실행해 잠금을 해제

p(sem); /* 잠금 */
중요한 처리 부분(critical section)
v(sem); /* 잠금 해제 */

※ sem은 세마포어 값을 의미

 

✅ p() 함수의 기본 동작 구조

p(sem) {
    while (sem == 0) {
        wait();
    }
        
    sem--; // 세마포 값을 1 감소
}

p() 함수는 sem 값이 0이면 다른 프로세스가 처리 부분을 수행하고 있다는 의미이므로, 값이 1이 될 때까지 기다려야 함

sem 값이 0이 아니면 0으로 만들어 다른 프로세스가 들어오지 못하게 함

 

✅ v() 함수의 기본 동작 구조

v(sem) {
    sem++; // sem 값을 1 증가
    
    if(대기 중인 프로세스 有)
        대기 중인 첫 번째 프로세스를 동작시킨다
}

v() 함수는 sem 값을 1만큼 증가시킴

처리 부분을 수행하려 대기 중인 프로세스가 있으면 첫 번째 대기 프로세스를 동작

 

 

ㅇ int semget(key_t key, int nsems, int semflg): 세마포 생성

- key : IPC_PRIVATE 또는 ftok() 함수로 생성한 키

- nsems : 생성할 세마포 개수 (세마포는 집합 단위로 처리되므로 한 식별자에 여러 세마포가 생성)

- semflg : 세마포 접근 속성 - 플래그, 접근 권한 (msgget() 함수와 마찬가지로 IPC_CREAT와 IPC_EXCL을 사용)

수행에 성공하면 세마포 식별자를 반환

 

세마포 식별자와 관련된 세마포와 데이터 구조체가 새로 생성되는 경우: key와 관련된 다른 세마포 집합이 없고 플래그(semflg)에 IPC_CREAT가 설정되어 있음

 

세마포 정보를 담고 있는 구조체는 semid_ds로, sys/sem.h 파일에 다음과 같이 정의되어 있음

struct semid_ds {
    struct ipc_perm sem_perm; // IPC 공통 구조체(ipc_perm)
    ushort_t sem_nsems; // 세마포 집합의 세마포 개수
    time_t sem_ctime; // 세마포의 접근 권한을 변경한 마지막 시간
    time_t sem_otime; // 세마포 연산을 수행한 마지막 시간
};

 

(새로운 세마포 식별자를 리턴할 때 구조체의 설정)

sem_perm.cuid, sem_perm.uid : 함수를 호출한 프로세스의 유효 사용자 ID로 설정

sem_perm.cgid, sem_perm.gid : 함수를 호출한 프로세스의 유효 그룹 ID로 설정

sem_perm.mode : semflg 값으로 설정

sem_nsems : nsems 값으로 설정, 세마포 집합이 생성되지 않으면 0으로 설정될 수도 있음

sem_otime : 0으로 설정

sem_ctime : 현재 시각으로 설정

 

(세마포에 연관된 값)

semval : 세마포의 값

sempid : 세마포에 최근 접근했던 프로세스의 프로세스 식별번호

semncnt : 세마포의 값이 현재보다 더 큰 값을 갖기를 기다리는 프로세스의 수

semzcnt : 세마포의 값이 0이 되기를 기다리는 프로세스의 수

 

 

semid로 식별되는 세마포 집합에서 semnum으로 지정한 세마포어 cmd로 지정한 제어 기능을 수행

ㅇ int semctl(int semid, int semnum, int cmd, …): 세마포 제어

- sedmid : semget() 함수로 생성한 세마포 식별자

- semnum : 기능을 제어할 세마포 번호

- cmd : 수행할 제어 명령

- : 제어 명령에 따라 필요시 사용할 세마포 공용체의 주소(선택 사항)

cmd에 따라 선택적으로 네 번째 인자가 있을 수 있음

수행에 성공하면 0을, 오류가 발생하면 모든 경우에 –1을 반환

 

(cmd에 지정할 수 있는 값)

IPC_STAT : 현재 세마포어의 정보를 arg.buf로 지정한 메모리에 저장

IPC_SET : 세마포어 정보 중 sem_perm.uid, sem_perm.gid, sem_perm.mode 값을 네 번째 인자로 지정한 값으로 변경 (이 명령은 root 권한이 있거나 유효 사용자 ID일 경우만 사용할 수 있음)

IPC_RMID : semid로 지정한 세마포어와 관련된 데이터 구조체를 제거

 

(구조체 항목)

GETALL : 세마포 집합에 있는 모든 세마포의 semval 값을 arg.array가 가리키는 배열에 저장

GETNCNT : 세마포의 semncnt 값을 읽어옴

GETVAL : 세마포의 semval 값을 읽어옴

SETVAL : 세마포의 semval 값을 arg.val로 설정

GETPID : 세마포의 sempid 값을 읽어옴

GETZCNT : 세마포의 semzcnt 값을 읽어옴

SETALL : 세마포 집합에 있는 모든 세마포의 semval 값을 arg.array가 가리키는 배열값으로 설정

 

semctl( ) 함수의 리턴값은 cmd에 따라 달라진다

cmd가 GETVAL이면 semval 값을, GETPID면 sempid 값을 리턴

cmd가 GETNCNT면 semncnt 값을, GETZCNT면 semzcnt 값을 리턴

 

네 번째 인자는 제어 명령에 따른 선택 사항으로 네 번째 항목이 필요할 경우 다음과 같은 공용체를 사용 (이 공용체는 프로그램에서 명시적으로 선언하고 사용해야 함)

union semun {
    int val;
    struct semid_ds* buf;
    unsigned short* array;
    struct seminfo* __buf; /* 리눅스에서만 사용 */
};

 

 

ㅇ int semop(int semid, struct sembuf* sops, size_t nsops): 세마포 연산

- sedmid : semget() 함수로 생성한 세마포 식별자

- sops : sembuf 구조체의 주소

- nsops : sops가 가리키는 구조체의 크기

semid가 가리키는 세마포에 크기가 nsops인 sembuf 구조체로 지정한 연산을 실행

sembuf 구조체는 sys/sem.h 파일에 정의

struct sembuf {
    unsigned short sem_num; // 세마포 번호
    short sem_op; // 세마포 연산
    short sem_flg; // 연산을 위한 플래그로, IPC_NOWAIT나 SEM_UNDO를 지정
};
// SEM_UNDO: 프로세스가 비정상적으로 갑자기 종료될 때 세마포 동작을 취소시킴

 

세마포 연산은 sembuf 구조체의 sem_op 항목에 지정

sem_op 항목은 semop 함수가 수행할 기능을 정수로 나타냄

 

(semop() 함수의 특징)

if (sem_op < 0) { /* 세마포 잠금 */
    wait until semval >= | sem_op |; // if (sem_flg & IPC_NOWAIT) return -1 immediately
    semval -= | sem_op |;
}
else if (sem_op > 0) { /* 세마포 잠금 해제 */
    semval += sem_op;
}
else {
    wait until semval is 0;
}

 

① sem_op가 음수일 경우: 세마포 잠금 기능을 수행, 이는 공유 자원을 사용하려는 것

semval 값이 sem_op의 절댓값과 같거나 크면 semval 값에서 sem_op의 절댓값을 뺌

semval 값이 sem_op 값보다 작고 sem_flg에 IPC_NOWAIT이 설정되어 있으면 semop() 함수는 즉시 리턴

semval 값이 sem_op 값보다 작은데 sem_flg에 IPC_NOWAIT이 설정되어 있지 않으면 semop() 함수는 semncnt 값을 증가시키고 다음 상황을 기다림

semval 값이 sem_op의 절댓값보다 같거나 커짐 이 경우 semncnt 값은 감소하고 semval 값에서 sem_op의 절댓값을 뺌

시스템에서 semid가 제거, 이 경우 errno가 EIDRM으로 설정되고 -1을 리턴

semop() 함수를 호출한 프로세스가 시그널을 받음, 이 경우 semncnt 값은 감소하고 시그널 처리 함수를 수행

 

② sem_op가 양수일 경우: 세마포 잠금을 해제하고, 사용 중이던 공유 자원을 돌려줌

이 경우 sem_op 값이 semval 값에 더해짐

 

③ sem_op 값이 0인 경우

semval 값이 0이면 semop() 함수는 즉시 리턴

semval 값이 0이 아니고 sem_flg에 IPC_NOWAIT이 설정되어 있으면 semop() 함수는 즉시 리턴

semval 값이 0이 아니고 sem_flg에 IPC_NOWAIT이 설정되어 있지 않으면 semop() 함수는 semzcnt 값을 증가시키고 semval 값이 0이 되기를 기다림

 

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

// 세마포 공용체를 정의
union semun {
    int val;
    struct semid_ds* buf;
    unsigned short* array;
};

int initsem(key_t semkey);
int semlock(int semid);
int semunlock(int semid);
void semhandle();

int main() {
    int a;
    for (a = 0; a < 3; a++) {
        // fork() 함수를 세 번 호출해 자식 프로세스 3개를 생성하고
        // 각 자식 프로세스는 semhandle() 함수를 실행
        // 즉, 자식 프로세스가 모두 semhandle() 함수를 같이 실행
        if (fork() == 0) {
            semhandle();
        }
    }
    
    return 0;
}

int initsem(key_t semkey) {
    union semun semunarg;
    int status = 0, semid;
    
    // initsem() 함수의 인자로 받은 키를 지정해 세마포 식별자를 생성
    semid = semget(semkey, 1, IPC_CREAT | IPC_EXCL | 0600);
    if (semid == -1) {
        if (errno == EEXIST) {
            // 리턴값이 –1이고 errno 값이 EEXIST면 이미 존재하는 세마포 식별자라는 의미이므로 기존 식별자를 읽어옴
            semid = semget(semkey, 1, 0);
        }
    }
    else {
        // semctl() 함수를 사용해 세마포 값을 1로 초기화
        // 즉, 잠금이 해제되어 있는 초기 상태
        semunarg.val = 1;
        status = semctl(semid, 0, SETVAL, semunarg);
    }

    if (semid == -1 || status == -1) {
        perror("initsem");
        return (-1);
    }
    
    return semid;
}

// semlock() 함수는 sem_op 값을 -1로 설정해 공유 자원을 얻고 잠금 기능을 수행
int semlock(int semid) {
    // initsem() 함수에서 세마포 값의 초깃값을 1로 설정했으므로 
    // 처음 semlock() 함수를 실행하는 프로세스는 세마포 값에서 1을 빼면 0이 되어 잠금 기능을 수행
    struct sembuf buf;

    buf.sem_num = 0;
    buf.sem_op = -1;
    buf.sem_flg = SEM_UNDO; // sem_flg를 SEM_UNDO로 설정해 문제가 발생하면 동작을 취소할 수 있게 함
    if (semop(semid, &buf, 1) == -1) {
        perror("semlock failed");
        exit(1);
    }

    return 0;
}

// semunlock() 함수는 sem_op 값을 1로 설정해 사용 중인 공유 자원의 잠금 기능을 해제하고 되돌려주려 함
// 처음 semunlock() 함수를 호출하면 세마포 값이 0이므로 여기에 1을 더하면 세마포 값이 양수가 되어 잠금을 해제
int semunlock(int semid) {
    struct sembuf buf;

    buf.sem_num = 0;
    buf.sem_op = 1;
    buf.sem_flg = SEM_UNDO; // sem_flg를 SEM_UNDO로 설정해 문제가 발생하면 동작을 취소할 수 있게 함
    if (semop(semid, &buf, 1) == -1) {
        perror("semunlock failed");
        exit(1);
    }

    return 0;
}

// semhandle() 함수는 세마포를 생성해 잠금을 수행하고 작업한 후 다시 잠금을 해제
void semhandle() {
    int semid;
    pid_t pid = getpid();

    if ((semid = initsem(1)) < 0) {
        exit(1);
    }

    // 여기에 있는 printf() 문이 실제 수행 부분
    // 이 예제에서는 간단한 출력문이지만 실제 상황에서는 중요한 처리 부분이 위치!
    semlock(semid);
    printf("Lock : Process %d\n", (int)pid);
    printf("** Lock Mode : Critical Section\n");
    sleep(1);
    printf("Unlock : Process %d\n", (int)pid);
    semunlock(semid);

    exit(0);
}

 

(1) semhandle()에서 semlock()과 semunlock()을 제거해 세마포 기능을 사용하지 않고 실행할 경우

Lock이 모두 출력된 후 Unlock이 출력된 것을 알 수 있음

 

이는 한 프로세스가 출력문을 실행하는 동안 다른 프로세스도 이 부분을 실행했기 때문

 

(2) semhandle()에서 semlock()과 semunlock()을 넣어 세마포 기능을 되살리고 실행할 경우

자식 프로세스들이 한 번에 한 프로세스만 잠금 부분에 들어가는 것을 확인

 

"세마포는 이렇게 프로세스 사이에서 동기를 맞출 수 있도록 도와준다"

 

 

(ipcs 명령으로 세마포의 정보를 확인하고 ipcrm 명령(ipcrm –s #)으로 삭제)

 

 

 

 

 

 

 

참고 및 출처: 시스템 프로그래밍 리눅스&유닉스(이종원)