세마포어와 더불어 더 많은 정보와 예제를 담은 리눅스 교재를 배포했습니다. 아래의 페이지에서 리눅스 교재를 받아가세요.

https://reakwon.tistory.com/233

 

리눅스 프로그래밍 note 배포

티스토리에 리눅스에 관한 내용을 두서없이 여지껏 포스팅했었데요. 저도 제 포스팅을 찾기가 어렵기도 하고 티스토리에서 코드삽입을 하게 되면 이게 일자로 쭉 쓰여져있는 x같은 현상이 생겨

reakwon.tistory.com

 

세마포어(Semaphore)란

세마포어(Semaphore)는 사전적 의미는 수기 신호라고 합니다. 다익스트라라는 학자가 고안해낸 이 기법은 두 개 이상의 프로세스가 동시에 공유 메모리와 같은 공유 자원을 접근할때 동기화를 걸어주는 것을 목표로 합니다.

 

다음과 같은 상황을 보도록 합시다.

 

프로세스 A, 프로세스 B는 int a=10이라는 자원을 공유합니다. 사용자는 두 프로세스에서 a를 1씩 증가시키는 작업을 시켰고 사용자는 a가 12라는 값이 될 것을 예상하고 있습니다.

a=10
1) 프로세스 A는 a의 값을 읽어온다. a=10
2) 프로세스 B는 a의 값을 읽어온다. a=10
3) 프로세스 A는 a의 값을 증가시킨다. a=11
4) 프로세스 B는 a의 값을 증가시킨다. a=11
a=11 

이렇게 사용자가 예상했던 것과는 다른 예기치못한 결과를 초래합니다. 이런 결과가 된 이유는 무엇일까요?

우선 a는 두 프로세스에서 접근이 가능한 공유자원입니다. 이것을 동시에 임계영역(Critical section)에 진입하여 공유자원에 접근할때 문제가 발생한게 그 원인이지요. 이처럼 한번에 여러 프로세스가 접근하여 데이터를 동시에 변경하는 것을 막기 위한 장치가 바로 세마포어라고합니다.

임계 영역(critical section)
공유자원에 접근할 수 있는 영역을 말하게 됩니다. 이런 임계 영역은 반드시 보호되어야하는 구간으로 보호 메카니즘으로는 세마포어와 뮤텍스 등이 있습니다. 

세마포어는 P연산, V연산으로 이루어져있으며 지금부터 P,V연산이 무엇인지 아주 간단하게 정의하면 아래와 같습니다.

P : S를 1 감소
V : S를 1 증가

 

이 연산을 이용해 임계영역에 어떻게 동기적으로 진입할 수 있을까요?

우선 S를 1로 생각해보고 프로세스는 S가 1일때만 임계영역으로 진입할 수 있다고 보겠습니다. 그렇다면 S가 0이면 진입하지 못하겠군요. 그렇다면 아래와 같이 임계영역에 접근할 수 있겠네요.

초기 S=1
P(S) (S가 1감소되어 0)
//critical section start
// ...공유 자원을 사용할 수 있는 영역
//critical section end
V(S) (S가 1증가되어 1)

P(S)를 수행하면 S가 1이 감소되어 0이지요. 만약 다른 프로세스가 이 임계영역을 보고 S가 0이면 대기합니다. 그러니까 P연산에는 S가 0이라면 대기하는 코드가 들어가겠네요. 만일 0이 아니라면, 즉 1보다 크다면 S를 1감소시킨 후에 임계영역으로 들어갈 수 있게됩니다.P(S)는 어떻게 구현되어있을까요?

P(S){
    while(S==0){
         //wait
    }
    S--;
}

 

그렇다면 V(S)는 어떻게 구현되어있을까요?

V(S)는 S를 하나 증가시킨다고 했지요? 그렇게함으로써 P(S)에서 기다리고 있는 프로세스가 S가 1이 되는 순간 진입할 수 있게 해주거든요. 대략  아래와 같은 코드로 구현되어있겠네요.

V(S){
    S++;
}

** 오해하지 마세요! 세마포어는 실제 완전히 저렇게 생기지 않습니다. 단순한 이해를 위해서 저런 가상의 코드를 구현해놓은 것입니다. 

 

우리는 여기서 S에 주목할 필요가 있습니다. 만일 S가 1이라면 임계영역에 들어갈 수 있는 프로세스는 하나가 되죠. 그렇다면 S가 2이면 임계영역에 들어갈 수 있는 프로세스는 2개가 된다는 의미겠네요.

 

이렇게 Mutex와 Semephore가 차이가 생겨나게 됩니다. 

Mutex Semaphore
lock, unlock의 상태만 존재하는 일종의 binary semaphore 여러개의 프로세스가 동시에 공유자원에 접근할 수도 있음

 

 

Semapore 시스템콜

다음의 함수를 사용하기에 앞서 우리가 필요한 헤더파일은 다음과 같이 3개입니다.

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

 

1. semget : semid라는 세마포어 식별자를 얻는데 쓰이는 시스템콜입니다. 세마포어는 집합이라 같은 집합에 속하여 index로 구분됩니다. 보통은 한 집합에 1개의 세마포어를 사용합니다.

int semget(key_t key, int nsems, int semflg);

- key : 세마포어를 식별하는 키입니다.

- nsems :  세마포어 자원의 갯수를 의미합니다.

- semflg : 세마포어 동작옵션인데요. IPC_CREAT과 IPC_EXCL 두개가 존재합니다. 

IPC_CREAT: 새로운 세마포어를 만듭니다. 

IPC_EXCL : IPC_CREAT과 같이 사용하는데, 이미 세마포어가 존재할 경우 Error를 반환합니다.

 

호출 성공시 semid라는 세마포어 식별자를 반환합니다.

 

2. semctl : 세마포어를 제어할 수 있는 시스템 콜입니다.

int semctl(int semid, int semnum, int cmd, ...);

- semid: 세마포어의 식별자입니다. 이는 위의 semget으로부터 나온 id값입니다.

- semnum : semaphore 집합에서 표현되는 일종의 인덱스입니다.

- cmd : 세마포어를 제어할 수 있는 command인데요. 이것에 따라 semctl이 3개의 인자를 갖느냐, 4개의 인자를 갖느냐가 결정됩니다. 

- union semun : cmd에 의해 4번째 인자가 쓰일때 여러분이 작성하는 프로그램에서는 아래의 union을 정의해주어야합니다. 

 union semun {
        int  val;    /* Value for SETVAL */
        struct semid_ds *buf;    /* Buffer for IPC_STAT, IPC_SET */
        unsigned short  *array;  /* Array for GETALL, SETALL */
        struct seminfo  *__buf;  /* Buffer for IPC_INFO
                                           (Linux-specific) */
  };

 

- struct semid_ds : semun에 멤버 semid_ds라는 구조체는 <sym/sem.h>에 아래와 같이 정의되어있습니다.

struct semid_ds {
       struct ipc_perm sem_perm;  /* Ownership and permissions */
       time_t sem_otime; /* Last semop time */
       time_t sem_ctime; /* Last change time */
       unsigned long   sem_nsems; /* No. of semaphores in set */
};

 

3. semop : 세마포어의 값을 증가, 감소시킴으로써 크리티컬 섹션 전 후에 사용됩니다.

int semop(int semid, struct sembuf *spos, size_t nsops);

 

- semid : 역시 semget에서 받은 semid를 사용합니다.

- spos : sembuf라는 구조체 포인터네요. sembuf는 어떻게 생겼을까요?

unsigned short sem_num;  /* semaphore number */
 short sem_op;   /* semaphore operation */
 short sem_flg;  /* operation flags */

 

3가지 필드로 구성되어있으며 sem_num은 세마포어 번호, sem_op는 증감값이며 이는 원자적으로 처리됩니다. sem_flg는 옵션입니다.

원자적연산 : 원자는 물체를 더 이상 쪼갤수 없는 단위입니다. 컴퓨터 연산의 원자적이라함은 더 이상 쪼개지지 않는 연산을 의미하여 한 사이클에 동작하는 연산을 의미합니다. a++이라는 연산은 메모리에 1) a의 값을 읽어들이고, 2)a의 값을 1개 증가시키고, 3) a를 다시 메모리에 저장하는 과정을 거치는데 이때 1), 2), 3) 각각이 원자적 연산이며 a++자체는 원자적 연산이 되지 않습니다. 만일 1), 2), 3)의 과정에서 어떠한 방해도 받지 않고 수행할 수 있다면 1)2)3)은 a++은 원자적 연산이라고 할 수 있습니다.

  

이제 시스템콜은 적당히 본것같고 예제를 보도록 합시다.

 

우선 세마포어가 적용되지 않은 쓰레드 2개를 돌리는 예제입니다.

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

int sharedVal=0;
int semid;

void s_wait(){
        struct sembuf buf;
        buf.sem_num=0;
        buf.sem_op=-1;
        buf.sem_flg=SEM_UNDO;

        if(semop(semid,&buf,1) == -1){
                printf("semop() fail\n");
                exit(0);
        }
}

void s_quit(){
        struct sembuf buf;
        buf.sem_num=0;
        buf.sem_op=1;
        buf.sem_flg=SEM_UNDO;

        if(semop(semid,&buf,1) == -1){
                printf("semop() fail\n");
                exit(0);
        }
}

void *thread1_func(void *arg){
        int i;
        for(i=0;i<1000000;i++){
                //s_wait();
                sharedVal++;
                //s_quit();
        }
}


void *thread2_func(void *arg){

        int i;
        for(i=0;i<1000000;i++){
                //s_wait();
                sharedVal++;
                //s_quit();
        }
}

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

int main(int argc, char *argv[]){
        pthread_t thread1;
        pthread_t thread2;
        union semun su;

        if((semid = semget(IPC_PRIVATE, 1, IPC_CREAT|0660)) == -1){
                printf("semet() fail \n");
                exit(0);
        }

        su.val=1;
        if(semctl(semid,0,SETVAL, su) == -1){
                printf("semctl fail\n");
                exit(0);
        }

        pthread_create(&thread1,NULL,thread1_func,NULL);
        pthread_create(&thread2,NULL,thread2_func,NULL);
        pthread_join(thread1,NULL);
        pthread_join(thread2,NULL);

        printf("shared val:%d\n",sharedVal);
        if(semctl(semid, 0, IPC_RMID, su) == -1){
                printf("semctl() fail\n");
                exit(0);
        }
        return 0;
}

 

 

이 코드를 컴파일하려면 gcc [파일명] -lpthread로 컴파일 하시기바랍니다.

저의 컴퓨터에서 실행한 결과 매번 다른 값을 보이고 있습니다. (머신마다 다르니 여러번 시도하거나 for loop을 더 많이 돌려보세요.) 우리가 예상하는 값은 가장 아래의 값입니다.

# ./a.out
shared val:1967612
# ./a.out
shared val:1109699
# ./a.out
shared val:2000000

 

반대로 주석을 풀어 세마포어를 적용시켜보도록 합시다.

저의 컴퓨터에서 실행한 결과 세마포어가 실행하는 시간이 있어 오버헤드가 증가하였으나 우리가 예상하는 값을 얻어낼 수가 있습니다.

# ./a.out
shared val:2000000
# ./a.out
shared val:2000000
# ./a.out
shared val:2000000
# ./a.out
shared val:2000000
# ./a.out
shared val:2000000
# ./a.out
shared val:2000000
# ./a.out
shared val:2000000

 

예제를 조금 더 살펴보면 다음과 같습니다.

 if((semid = semget(IPC_PRIVATE, 1, IPC_CREAT|0660)) == -1){
        printf("semet() fail \n");
        exit(0);
 }

setget을 통해서 semid를 가져오는데, IPC_PRIVATE를 통해서 1개의 키를 생성하고 있네요. 

 

이후 semum을 통해서 semid를 갖는 세마포어를 제어합니다. 우선 semum의 val을 1로 셋팅합니다. 위의 P(S), V(S) 연산 기억나시나요? 이건 S의 초기값을 설멍하는 것과 같습니다.

이제 semid의 0번 인덱스의 값을 셋팅(SETVAL)하는데, su로 셋팅합니다. 

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

int main(int argc, char *argv[]){
		//..
        union semun su;
		//..
       
        su.val=1;
        if(semctl(semid,0,SETVAL, su) == -1){
                printf("semctl fail\n");
                exit(0);
        }
        //..
 }

 

다음의 s_waits_quit은 임계영역에 들어가기전 기다리는 동작과 나오는 동작을 구현한 함수입니다. 

 

s_wait부터 봅시다. 자, sembuf에 있는 멤버를 들여다보면 semid라는 집합의 (buf.sem_num)0번 인덱스의 동작(buf.sem_op)을 -1로 규정하고 있습니다. 현재 세마포어의 값에 -1연산을 하라는 의미입니다. -2면 2를 빼는거겠죠.

SEM_UNDO는 프로그램이 종료될때 자동적으로 세마포어가 되돌려지는 옵션이라고 하네요. 잘 모르겠습니다 이건.

위의 P연산과 비슷한 역할을 하는 함수가 되겠죠.

 

s_quit은 반대로 sem_id의 0번 인덱스에 1을 더하라는 거겠네요. 그렇다면 위의 V연산과 비슷한 역할을 하는 함수가 됩니다.

void s_wait(){
        struct sembuf buf;
        buf.sem_num=0;
        buf.sem_op=-1;
        buf.sem_flg=SEM_UNDO;

        if(semop(semid,&buf,1) == -1){
                printf("semop() fail\n");
                exit(0);
        }
}

void s_quit(){
        struct sembuf buf;
        buf.sem_num=0;
        buf.sem_op=1;
        buf.sem_flg=SEM_UNDO;

        if(semop(semid,&buf,1) == -1){
                printf("semop() fail\n");
                exit(0);
        }
}

 

마지막으로 쓰레드가 모두 종료되고 프로그램이 종료되기 전에 semctl로 semid를 파기시킵니다. 

if(semctl(semid, 0, IPC_RMID, su) == -1){
         printf("semctl() fail\n");
         exit(0);
}

여기까지 세마포어의 개념과 간단한 예제를 보았습니다. 자세한것은 저도 잘 모르는 입장이라 공부하면서 더 보완을 해야겠네요.

 

읽어주셔서 감사합니다.

 

 

반응형
블로그 이미지

REAKWON

와나진짜

,