본문 바로가기

CS/시스템 프로그래밍

[Linux] Semaphore

1. 들어가기 전에

 

다음의 코드를 한번 같이 작성해보자.

 

shminit.c

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

int main()
{
        key_t key;
        int shmid;
        int* shmaddr;

        shmid = shmget(key, sizeof(int), IPC_CREAT | 0666);  // key로 shmid 얻어옴
        shmaddr = shmat(shmid, NULL, 0); // 2번째 매개변수 - 이 근처로 할당, 3번째 매개변수는 flag
        *shmaddr = 0;  // 0으로 공유 메모리에 저장된 값을 초기화

        printf("*shmaddr = %d\n", *shmaddr);
        return 0;
}

 

shmsum.c

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

int main()
{
        key_t key;
        int shmid;
        int* shmaddr;

        shmid = shmget(key, sizeof(int), IPC_CREAT | 0666);
        shmaddr = shmat(shmid, NULL, 0);

        for(int i = 0; i < 10; i++)
        {
                for(int j = 0; j < 10000000; j++)
                {
                        (*shmaddr)++;  // shmaddr에 저장된 값을 증가시킴, 총 1억번
                }
        }

        printf("*shmaddr = %d\n", *shmaddr);
        return 0;
}

shminit.c 로 shared memory에 저장된 정수값을 0으로 초기화할 수 있다.

그리고 shmsum.c를 컴파일하여 실행하면 정수 1억이 출력되는 것을 확인할 수 있다.

 

그런데 위의 shmsum.c를 다음과 같이 수정시킨 다음

터미널을 하나 더 열어 shmsum.c 를 컴파일한 shmsum 파일 두개가 함께 실행되게 해보자.

(꼭 동시가 아니라도 짧은 간격에 두개를 실행하여 두 프로세스가 같은 shared memory를 참조하게 하면 된다.)

 

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

int main()
{
        key_t key = 1234;
        int shmid;
        int* shmaddr;

        shmid = shmget(key, sizeof(int), IPC_CREAT | 0666);
        shmaddr = shmat(shmid, NULL, 0); // 2번쨰 - 이 근처로 할당, 3번째 flag

        for(int i = 0; i < 10; i++)
        {
                usleep(200000);
                for(int j = 0; j < 10000000; j++)
                {
                        (*shmaddr)++;
                }
        }

        printf("*shmaddr = %d\n", *shmaddr);
        return 0;
}

(코드에서 달라진 부분은 usleep뿐이다. usleep(x)는 x마이크로초간 sleep한다는 의미)

 

두 프로세스가 정상적으로 작동되었다면 나중에 종료된 프로세스에서 2억이 출력되어야 하지만

이런 식으로 이상한 값이 출력되는 것을 볼 수 있다.

이런 일이 일어나는 이유에 대해서 알아보자.

 

 

이 때 x=10일 때 Process1이 값을 가져가고 Process2도 값을 가져갔다고 가정 (load)

그리고 Process1이 x = 10을 증가시켜 (add) 메모리에 x = 11을 저장 (store)

Process2도 마찬가지로 x = 10을 증가시켜 메모리에 x = 11을 저장

그러면 x++ 가 2번 실행되었음에도 x = 11이 메모리에 저장되게 된다.

 

즉, P1 load - P1 add - P1 store - P2 load - P2 add - P2 store 와 같이 load - add - store가 순서대로 수행된다면 문제가 없으나

위와 같이 순서가 뒤바뀌어 한 프로세스의 load - add - store가 수행되는 도중 값을 load해버리면 똑같은 값이 두번 load되어 load는 두번 수행되었으나 값은 +1이 되어버린다.

 

그럼 프로그램이 정상적으로 수행되려면 즉, 값이 2억이 출력되려면 어떻게 해야할까?

 

여러가지 방법이 있겠지만 그 중 하나는 프로세스 하나가 공유메모리에 접근 중일 때 다른 프로세스의 접근을 막는 것이다.

그리고 이를 위하여 우리는 지금부터 semaphore(세마포어)라는 것을 배울 것이다.

 

 

1. Semaphore(세마포어)의 개념

 

세마포어는 정수형 변수이다.

그럼 정수형 변수로 어떻게 여러개의 프로세스의 접근을 막을까?

 

공유메모리에 n개의 프로세스까지 동시에 접근하게 하고 싶다고 가정하자.

이 때 semaphore의 값을 n으로 두고 어떤 프로세스가 접근할 때마다 -1을 한다고 가정하자.

그럼 n개의 프로세스가 접근하면 값이 0이 된다.

이 때부터 즉, n개 이상의 프로세스가 공유메모리에 접근하여 세마포어의 값이 0이하일 때부터 다른 프로세스의 접근을 막는 것이다.

 

그리고 접근했던 프로세스가 떠날 때 세마포어의 값을 1증가시킨다면?

만약 세마포어의 값이 0이었다가 1개의 프로세스가 떠났다면 값이 1이 된다.

이 때는 다시 1개의 프로세스들이 더 접근이 가능한 상태이다.

즉, 세마포어의 값이 0이하일 때는 프로세스의 접근을 막았다가 다시 0보다 크면 프로세스의 공유메모리에의 접근을 허용하면 최대 n개의 프로세스가 공유메모리에 동시에 접근 가능하도록 제어할 수 있다.

 

정리하면 세마포어의 현재값은 현재 작업수행을 시작할 수 있는 프로세스의 여분 개수이고

 

1. 세마포어의 값을 최대 동시 수행 가능하게 하고 싶은 프로세스의 개수 n으로 초기화

2, 프로세스가 작업 수행을 시작하면 세마포어의 값을 -1, 작업 수행을 끝내면 +1

3. 세마포어의 값이 0이하면 새로운 프로세스의 작업 수행을 막고 0 초과이면 허용

 

이렇게 하면 최대 n개의 프로세스가 같은 작업을 동시에 수행 가능하도록 제어할 수있다.

 

 

2. 세마포어를 사용하는 데 필요한 함수 (POSIX Semaphore 기준)

 

개념을 알았으니 이제 사용하는데 필요한 함수를 알아야 한다.

 

일단 당연히 처음에 초기화를 해주어야 한다.

그 때 사용하는 것이 sem_open 함수이다.

 

ex.

sem_t* sem = sem_open("testsem", O_CREAT, 0666, 1);

이 때 첫번째 매개변수는 세마포어의 이름이다.

O_CREAT, 0666을 두번째, 세번째 매개변수로 주면 해당하는 이름의 세마포어가 없으면 세마포어를 0666권한으로 생성, 있다면 open한다.

네번째 매개변수의 값으로 세마포어를 초기화한다.

 

이 때 이미 1로 초기화되어있는 세마포어를 다시 sem_open하되 네번째 매개변수의 값을 10로 준다고 가정해보자.

그럼 세마포어의 값이 변할까? 변하지 않을까?

 

정답은 '변하지 않는다.' 이더.

이미 존재하는 세마포어를 open하는 경우 저장되어 있는 값을 이용하고 sem_open으로 값을 변경시킬 수는 없다.

즉, sem_open의 네번째 매개변수는 완전히 초기화의 용도인 것이다.

 

 

semaphore는 정수형 변수라고 하였다.

그러나 세마포어는 +, - 연산자로 값을 변화시키는 것이 불가능하다.

 

대신 sem_wait()과 sem_post()함수를 통해 이 값을 제어할 수 있다.

 

ex.

sem_t* sem = sem_open("testsem", O_CREAT, 0666, 1);

sem_wait(sem);
 //work
sem_post(sem);

sem_wait과 sem_post 함수의 매개변수로는 sem_t* 즉, 세마포어 구조체의 포인터 변수가 들어간다.

 

sem_wait 함수는 프로세스가 어떤 작업을 수행하기 시작하면서 sem_t* 변수를 이용하여 세마포어의 값을 -1시키는 함수이다.

(이 때 세마포어의 값이 0이하 이면 0 초과의 값이 될 때까지 wait을 하므로 이름이 sem_wait이다.)

 

sem_post 함수는 프로세스가 어떤 작업을 종료하면서 sem_t* 변수를 이용하여 세마포어의 값을 +1시키는 함수이다.

(작업을 종료했다고 post한다는 의미)

 

 

3. 예제 코드 및 실행 결과

 

(1) seminit.c

 

#include <stdio.h>
#include <semaphore.h>
#include <sys/types.h>
#include <fcntl.h>

int main()
{
        sem_t* sem;  // 세마포어의 포인터를 받아오는 용도
        int svalue;  // 세마포어의 값을 확인하는데 사용할 용도
        int ival;  // 이 값으로 세마포어를 초기화할 것

        printf("Enter an integer: ");
        scanf("%d", &ival);
        sem = sem_open("testsem", O_CREAT, 0666, ival);
        sem_getvalue(sem, &svalue);  // 세마포어의 값을 가져오기 위한 코드

        printf("svalue = %d\n", svalue);
}

그냥 gcc를 이용하면 컴파일이 안될 것이다.

pthread library를 이용해야만 정상적으로 컴파일이 되므로 -lpthread 옵션을 추가해주

(library pthread ->lpthread)

 

sem_getvalue(sem, &svalue)를 해주면 세마포어의 값이 svalue에 저장된다.

 

위의 코드를 실행시키기 전

/dev/shm 폴더를 보자. (ls /dev/shm 으로 확인가능)

아무것도 없는 것을 확인할 수 있다.

 

그리고 위의 코드를 컴파일한 프로그램을 실행하여 10을 입력하면

 

이렇게 svalue가 10인 것을 확인할 수 있다.

 

이 때 ls /dev/shm으로 다시 한번 폴더를 확인해보면

 

이렇게 testsem 파일이 생성된 것을 확인할 수 있다.

 

다시 한번 실행하여 이번에는 20을 입력해보자.

 

이번에는 값이 변경되지 않은 것을 알 수 있다.

이를 통해 sem_open으로 세마포어의 주소값을 받아오긴 하였으나, sem_open으로는 값의 초기화만 가능하고 값의 변경은 불가능한 것을 확인할 수 있다.

 

그럼 다른 값으로 초기화된 testsem 파일을 만들고 싶다면?

rm /dev/shm/sem.testsem 으로 삭제를 하고 다시 sem_open함수를 이용하여 test_sem을 만들면 된다.

 

(2) s_wait.c and s_post.c

다음의 코드들을 작성해보자.

 

s_wait.c

#include <stdio.h>
#include <semaphore.h>
#include <sys/types.h>
#include <fcntl.h>

int main()
{
        sem_t* sem;
        int svalue;

        sem = sem_open("testsem", O_CREAT, 0666, 1);
        sem_wait(sem);

        printf("Wait is Done\n");

        sem_getvalue(sem, &svalue);
        printf("svalue = %d\n", svalue);
        
        return 0;
}

 

s_post.c

#include <stdio.h>
#include <semaphore.h>
#include <sys/types.h>
#include <fcntl.h>

int main()
{
        sem_t* sem;
        int svalue;

        sem = sem_open("testsem", O_CREAT, 0666, 1);

        sem_post(sem);
        printf("Post is Done\n");

        sem_getvalue(sem, &svalue);
        printf("svalue = %d\n", svalue);

        return 0;
}

 

gcc s_wait.c -o s_wait -lpthread
gcc s_post.c -o s_post -lpthread

를 했다고 가정하고 설명해보겠다.

 

아래의 작업을 수행하기 전에 우선 기존의 semaphore를 삭제해주자.

(그래야 초기값이 1인 testsem이 새로 생긴다.)

 

우선 s_wait을 한번 실행해보자. (./s_wait)

 

 

그럼 이렇게 semaphore의 value가 0이 되는 것을 볼 수 있다.

그런데 이 때 s_wait을 한번 더 실행해보면 출력이 되지 않는 것을 볼 수 있다.

현재 semaphore의 value가 0이하라서 semaphore의 value가 양수가 될 때까지 processrk wait하고 있는 것이다.

 

이 때 터미널 창을 하나 더 열어서 s_post를 실행해보자. (./s_post)

 

그럼 이렇게 출력되며 svalue = 1이 출력되고 다시 원래 터미널로 돌아가면

이렇게 wait하고 있던 process가 wait을 멈추고 다시 코드를 실행하면서 svalue = 0이 되는 것을 볼 수 있다.

이 때 s_post를 연달아 세번 실행해보자.

 

그럼 이렇게 semaphore의 value가 3이 되는 것을 확인할 수 있고

연달아서 s_wait을 두번 실행해도 이번에는 바로바로 출력이 되는 것을 확인할 수 있다.

 

(3)s_work.c

 

#include <stdio.h>
#include <fcntl.h>
#include <semaphore.h>
#include <sys/types.h>
#include <time.h>
#include <stdlib.h>

int main()
{
        sem_t* sem;
        int work_t, i;

        srand(time(NULL));
        sem = sem_open("testsem", O_CREAT, 0666, 1);

        for(i = 0; i < 10; i++)
        {
                printf("Trying to get a semaphore: \n");
                sem_wait(sem);

                work_t = rand() % 6 + 1;

                printf("%dth repeat is ", i);
                printf("Working for %d sec.\n", work_t);
                sleep(work_t);

                printf("                Done\n\n");
                sem_post(sem);

                sleep(1);
        }

        return 0;


}

위의 코드를 작성한 후

gcc s_work.c -o s_work -lpthread 로 s_work,c 를 컴파일해주자.

 

그리고 s_work 두개를 동시에 실행해보도록 하자.

 

그러면 두개의 프로세스가 번갈아가면서 작업을 수행하는 것을 볼 수 있다.

(하나는 i th repeat is working for ~sec. 문장이 출력되는 한편 하나는 Trying to get a semaphore에서 멈추어있음)

 

즉, 한 프로세스가 작업을 수행하는 동안은 다른 프로세스는 작업을 수행하지 못하다가 s_post(sem)코드를 수행한 후

sleep하는 동안 다른 프로세스가 semaphore의 값이 1초과가 되었으므로 wait()을 마치고 작업 수행을 시작하는 것을 반복하는 것이다.

 

세개를 수행해도 셋 중에 한 프로세스만 작업을 수행할 것이고 네개를 수행해도 넷 중에 한 프로세스만 작업을 수행할 것이다.

(단, 먼저 기다리고 있던 프로세스가 먼저 작업을 수행하는 것은 보장하지 않는다.)

 

그리고 또다시  rm /dev/shm/sem.testsem 으로 세마포어를 삭제해준 뒤

위의 코드에서 sem_open의 네번째 매개변수만 2로 바꾸어주고 세개의 프로세스를 동시에 실행시켜보자.

 

그럼 세개 중 2개의 프로세스만 작업을 수행하는 것을 확인할 수 있다.

즉, 세마포어의 초기값이 2개이면 최대 2개의 프로세스가 동시에 작업을 수행하도록 제어할 수 있다는 것을 볼 수 있고

따라서 세마포어의 초기값을 통해 동시에 작업을 수행할 수 있는 프로세스의 개수를 제어할 수 있다는 것을 알 수 있다.

 

4, 세마포어의 다른 용도

 

세마포어의 초기값을 통해 프로세스의 작업 순서를 보장해줄 수도 있다.

 

세마포어의 초기값이 0이었다고 가정해보자.

 

 

이와 같은 두 프로세스가 있으면

B코드는 세마코어의 값이 s_post에 의해 증가되기 전에는 0이하 이므로 s_wait에 의해 멈춰있게 된다.

즉, A코드가 먼저 모두 수행이 되고 나서 B코드가 수행됨이 보장되게 된다.