세마포어(Semaphores)를 비록 IPC(:12)설비중의 하나로 분류하긴 했지만, 다른 파이프, 메시지큐(:12), FIFO(:12)등과는 좀다르다. 다른 IPC 설비들이 대부분 프로세스간 메시지 전송을 그 목적으로 하는데 반해서 세마포어는 프로세스간 데이타를 동기화 하고 보호하는데 그목적이 있다. POSIX(:12) 세마포어와 System V 세마포어 모두를 다룰 계획이다.
프로세스간 메시지 전송을 하거나, 혹은 공유메모리(:12)를 통해서 특정 데이타를 공유하게 될경우 발생하는 문제가, 공유된 자원에 여러개의 프로세스가 동시에 접근을 하면 안되며, 단지 한번에 하나의 프로세스만 접근 가능하도록 만들어줘야 할것이다. 이것은 쓰레드에서 메시지간 동기화를 위해서 mutex(:12) 를 사용하는것과 같은 이유이다.
하나의 데이타에 여러개의 프로세스가 관여할때 어떤 문제점이 발생할수 있는지 간단한 예를 들어보도록 하겠다.
int count=100;
A 프로세스가 count를 읽어온다. 100
B 프로세스가 count를 읽어온다. 100
B 프로세스가 count를 1 증가 시킨다. 101
A 프로세스가 count를 1 증가 시킨다. 101
count는 공유자원(공유메모리 같은)이며 A와 B 프로그램이 여기에 대한 작업을 한다. A가 1을 증가 시키고 B가 1을 증가시키므로 최종 count 값은 102 가 되어야 할것이다. 그러나 A 가 작업을 마치기 전에 B가 작업을 하게 됨으로 엉뚱한 결과를 보여주게 되었다. 위의 문제를 해결하기 위해서는 count 에 A가 접근할때 B프로세스가 접근하지못하도록 block 시키고, A가 모든 작업을 마쳤을때 B프로세스가 작업을 할수 있도록 block 를 해제 시키면 될것이다.
우리는 세마포어를 이용해서 이러한 작업을 할수 있다. 한마디로 줄여서 세마포어는 "여러개의 프로세스에 의해서 공유된는 자원의 접근제어를 위한 도구" 이다.
세마포어의 작동원리
세마포어는 상호 배제알고리즘으로 임계 영역을 만들어서 자원을 보호한다. 작동원리는 매우 간단하다. 차단을 원하는 자원에대해서 세마포어를 생성하면 해당자원을 가리키는 세마포어 값이 할당된다. 이 세마포어값을 검사해서 임계영역에 접근할 수 있는지를 결정하게 된다.
세마포어 값이 0이면 이 자원에 접근할수 없으며, 0보다 큰 정수면 해당 정수의 크기만큼의 프로세스가 자원에 접근할수 있다라는 뜻이 된다. 그러므로 우리는 접근제어를 해야하는 자원에 접근하기 전에 세마포어 값을 검사해서 값이 0이면 자원을 사용할수 있을때까지 기다리고, 0보다 더 크면(1이라고 가정하자) 자원에 접근하게 된다. 자원에 접근하면 세마포어 값을 1 감소해서 세마포어 값을 0으로 만들고 다른 프로세스가 자원에 접근할수 없도록 한다. 자원의 사용이 끝나면 세마포어 값을 다시 1증가시켜서 다른 프로세스가 자원을 사용할수 있도록 만들어주면 된다.
이렇게 보호되어야 하는 자원과 연산을 포함한 영역을 임계 영역 (Critical Section)이라고 한다. 세마포어는 임계 영역에 진입하기 위한 키이다. 공용 사물함을 사용하기 위한 키의 작동방식을 생각하면 된다.
세마포어의 이론적인 원리는 원자화문서를 참고하자.
System V 세마포어
세마포어의 사용
세마포어의 사용은 위의 작동원리를 그대로 적용한다. 즉
임계 영역을 설정한다.
임계 영역에 진입하기전에 세마포어 값을 확인한다.
세마포어 값이 0보다 크면 세마포어를 가져온다. 세마포어를 가져왔으니 (커널의 입장에서)세마포어가 1감소 한다.
세마포어 값이 0이면 값이 0보다 커질때까지 block 되며, 0보다 커지게 되면 2번 부터 시작하게 된다.
이렇게 해서 임계 영역에 하나의 프로세스만 존재하도록 제어할 수 있다.
위의 작업을 위해서 Unix 는 다음과 같은 관련함수들을 제공한다.
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
int semget(key_t key, int nsems, int semflg);
int semop (int semid, struct sembuf *sops, unsigned nsops);
int semctl(int semid, int semnum, int cmd, union semun arg);
세마포어의 관리
세마포어는 그 특성상 원자화(:12)된 연산을 필요로 한다. 원자화된 연산은 유저레벨의 함수에서는 제공하기가 힘들므로, 세마포어 정보는 커널에서 전용 구조체를 이용해서 관리하게 된다. 다음은 커널에서 세모포어 정보를 유지하기 위해서 관리하는 구조체인 semid_ds 구조체의 모습이다.
semid_ds는 /usr/include/bits/sem.h 에 선언되어 있다. (이것은 리눅스에서의 경우로 Unix 버젼에 따라서 위치와 멤버변수에 약간씩 차이가 있을수 있다)
struct semid_ds
{
struct ipc_perm sem_perm;
__time_t sem_otime;
unsigned long int __unused1;
__time_t sem_ctime;
unsigned long int __unused2;
unsigned long int sem_nsems;
unsigned long int __unused3;
unsigned long int __unused4;
};
sem_perm 은 세마포어에 대한 퍼미션으로 일반 파일퍼미션과 마찬가지의 기능을 제공한다. 즉 현재 세마포어 구조체에 접근할수 있는 사용자권한을 설정한다. sem_nsems 는 생성할수 있는 세마포어의 크기이다. sem_otime 은 마지막으로 세마포어관련 작업을 한 시간(semop 함수를 이용)이며, sem_ctim 은 마지막으로 구조체 정보가 바뀐 시간이다.
semget 을 이용해서 세마포어를 만들자.
세마포어의 생성혹은 기존에 만들어져 있는 세마포어에 접근하기 위해서 유닉스에서 는 semget(2)를 제공한다. 첫번째 매개변수는 세마포어의 유일함을 보장하기 위해서 사용하는 키값이다. 우리는 이 키값으로 유일한 세마포어를 생성하거나 접근할 수 있다. 새로 생성되거나 기존의 세마포어에 접근하거나 하는것은 semflg 를 통해서 제어할수 있다. 다음은 semflg에서 사용할 수 있는 값이다.
IPC_CREAT
만약 커널에 해당 key 값으로 존재하는 세마포어가 없다면, 새로 생성 한다.
IPC_EXCL
IPC_CREAT와 함께 사용하며, 해당 key 값으로 세마포어가 이미 존재한다면 실패값을 리턴한다.
semflg 를 통해서 세마포어에 대한 퍼미션을 지정할수도 있다. 퍼미션 지정은 보통의 파일에 대해서 유저/그룹/other 에 대해서 지정하는것과 같다.
만약 IPC_CREAT 만 사용할경우 해당 key 값으로 존재하는 세마포어가 없다면, 새로 생성하고, 이미 존재한다면 존재하는 세마포어의 id 를 넘겨준다. IPC_EXCL을 사용하면 key 값으로 존재하는 세마포어가 없을경우 새로 생성되고, 이미 존재한다면 존재하는 id 값을 돌려주지 않고 실패값(-1)을 되돌려주고, errno 를 설정한다.
nsems 은 세마포어 셋 즉 배열의 크기다. 이값은 최초 세마포어를 생성하는 생성자의 경우에 크기가 필요하다(보통 1). 그외에 세마포어에 접근해서 사용하는 소비자의 경우에는 세마포어를 만들지 않고 단지 접근만 할뿐임으로 크기는 0이 된다.
이상의 내용을 정리하면 semget 은 아래와 같이 사용할수 있을것이다.
만약 최초 생성이라면
sem_num = 1;
그렇지 않고 만들어진 세마포어에 접근하는 것이라면
sem_num = 0;
sem_id = semget(12345, sem_num, IPC_CREAT|0660)) == -1)
{
perror("semget error : ");
return -1;
}
semget 은 성공할경우 int 형의 세마포어 식별자를 되돌려주며, 모든 세마포어에 대한 접근은 이 세마포어 실별자를 사용한다.
위의 코드는 key 12345 를 이용해서 세마포어를 생성하며 퍼미션은 0660으로 설정된다. 세마포어의 크기는 1로 잡혀 있다(대부분의 경우 1).
만약 기존에 key 12345 로 이미 만들어진 세마포어가 있다면 새로 생성하지 않고 기존의 세마포어에 접근할수 있는 세마포어 식별자를 되돌려주게 되고, 커널은 semget 를 통해 넘어온 정보를 이용해서 semid_ds 구조체를 세팅한다.
예제: semget.c
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
int main()
{
int semid;
semid = semget((key_t)12345, 1, 0666 | IPC_CREAT);
}
이제 위의 코드를 컴파일해서 실행시키고 나서 실제로 세마포어 정보가 어떻게 바뀌였는지 확인해 보도록 하자. 커널에서 관리되는 ipc 정보를 알아보기 위해서는 ipcs(8)라는 도구를 이용하면 된다.
0x00003039 은 key 12345 의 16진수 표현이다. 퍼미션은 666으로 되어 있고 semget 를 통해서 제대로 설정되어 있음을 알수 있다.
세마포어를 이용해서 접근제어 하기
이제 semget을 통해서 세마포어를 새로 만들고 얻었으니, 이제 실제로 세마포어상태를 검사해서 접근제어를 해보도록하자. 접근제어는 세마포어를 얻거나 되돌려 주는 방식으로 이루어진다. semop함수로 이러한 일들을 할 수 있다.
int semop(int semid, struct sembuf *sops, unsigned nsops);
semop의 첫번째 semid 는 semget 을 통해서 얻은 세마포어 식별자이다. 2번째 아규먼트는 struct sembuf 로써, 어떤 연산을 이루어지게 할런지 결정하기 위해서 사용된다. 구조체의 내용은 다음과 같으며, sys/sem.h 에 선언되어 있다.
struct sembuf
{
short sem_num; // 세마포어의수
short sem_op; // 세마포어 연산지정
short sem_flg; // 연산옵션(flag)
}
sem_num 멤버는 세마포어의 수로 여러개의 세마포어를 사용하지 않는다면(즉 배열이 아닐경우) 0을 사용한다. 배열의 인덱스 사이즈라고 생각하면 될것이다. 보통의 경우 하나의 세마포어를 지정해서 사용하므로 0 이 될것이다.
sem_op 를 이용해서 실질적으로 세마포어 연산을 하게 되며, 이것을 이용해서 세마포어 값을 증가시키거나 감소 시킬수 잇다. sem_op 값이 양수일 경우는 자원을 다 썼으니, 세마포어 값을 증가시키겠다는 뜻이며, 음수일 경우에는 세마포어를 사용할것을 요청한다라는 뜻이다.
음수일 경우 세마포어값이 충분하다면 세마포어를 사용할수 있으며, 커널은 세마포어의 값을 음수의 크기의 절대값만큼을 세마포어에서 빼준다. 만약 세마포어의 값이 충분하지 않다면 세번째 아규먼트인 sem_flg 에 따라서 행동이 결정되는데, sem_flg 가 IPC_NOWAIT로 명시되어 있다면, 해당영역에서 기다리지 않고(none block) 바로 에러코드를 리턴한다. 그렇지 않다면 세마포어를 획득할수 있을때까지 block 되게 된다.
sem_flg 는 IPC_NOWAIT 와 SEM_UNDO 2개의 설정할수 있는 값을가지고 있다. IPC_NOWAIT 는 none block 모드 지정을 위해서 사용되며, SEM_UNDO 는 프로세스가 세마포어를 돌려주지 않고 종료해버릴경우 커널에서 알아서 세마포어 값을 조정(증가)
할수 있도록 만들어 준다.
설명이 아마 애매모호한면이 있을것이다. 간단한 상황을 예로 들어서 설명해 보겠다.
현재 세마포어 값이 1 이라고 가정하자.
이때 A 프로세스가 semop 를 통해서 세마포어에 접근을 시도한다.
A는 접근을 위해서 sem_op 에 -1 을 세팅한다. 즉 세마포어 자원을 1 만큼 사용하겠다라는
뜻이다.
현재 준비된 세마포어 값은 1로 즉시 사용할수 있으므로,
A는 자원을 사용하게 되며, 커널은 세마포어 값을 1 만큼 감소시킨다.
이때 B 라는 프로세스가 세마포어 자원을 1 만큼 사용하겠다라고 요청을 한다.
그러나 지금 세마포어 값은 0 이므로 B는 지금당장 세마포어 를 사용할수 없으며,
기다리거나, 에러값을 리턴 받아야 한다(IPC_NOWAIT).
B는 자원 사용가능할때까지 기다리기로 결정을 했다.
잠수후 A는 모든 작업을 다마쳤다.
이제 세마포어를 되돌려줘야 한다. sem_op 에 1 을 세팅하면,
커널은 세마포어 값을 1증가시키게 된다.
드디어 기다리던 B가 세마포어 자원을 사용할수 있는 때가 도래했다.
이제 세마포어 값은 1이 므로 B는 세마포어를 획득하게 된다.
커널은 세마포어 값을 1 감소 시킨다.
B는 원하는 작업을 한다.
...
...
세마포어 조작
semctl 이란 함수를 이용해서 우리는 세마포어를 조정할수 있다.
semctl 은 semid_ds 구조체를 변경함으로써 세마포어의 특성을 조정한다.
첫번째 아규먼트인 semid 는 세마포어 식별자다. semnum 은 세마포어 배열에서 몇 번째 세마포어를 사용할지를 선택하기 위해서 사용한다. 세마포어의 크기가 1이라면 0이 된다. (배열은 0번 부터 시작하기 때문) cmd 는 세마포어 조작명령어 셋으로 다음과 같은 조작명령어들을 가지고 있다. 아래는 그중 중요하다고 생각되는 것들만을 설명하였다. 더 자세한 내용은 semctl 에 대한 man 페이지를 참고하기 바란다.
IPC_STAT
세마포어 상태값을 얻어오기 위해 사용되며, 상태값은 arg 에 저장된다.
IPC_RMID
세마포어 를 삭제하기 위해서 사용한다.
IPC_SET
semid_ds 의 ipc_perm 정보를 변경함으로써 세마포어에 대한 권한을 변경한다.
예제
지금까지 익혔던 내용을 토대로 간단한 예제프로그램을 만들어보겠다. 예제의 상황은 하나의 파일에 2개의 프로세스가 동시에 접근하고자 하는데에서 발생한다. 파일에는 count 숫자가 들어 있으며, 프로세스는 파일을 열어서 count 숫자를 읽어들이고, 여기에 1을 더해서 다시 저장하는 작업을한다. 이것을 세마포어를 통해서 제어하지 않으면 위에서 설명한 문제가 발생할것이다.
위의 문제를 해결하기 위해서는 파일을 열기전에 세마포어를 설정해서 한번에 하나의 프로세스만 접근가능하도록 하면 될것이다. 모든 파일작업을 마치게 되면, 세마포어 자원을 돌려줌으로써, 비로서 다른 프로세스가 접근가능하게 만들어야 한다.
예제: sem_test
#include <sys/types.h>
#include <sys/sem.h>
#include <sys/ipc.h>
#include <stdio.h>
#include <unistd.h>
#define SEMKEY 2345
union semun
{
int val;
struct semid_ds *buf;
unsigned short int *array;
};
static int semid;
int main(int argc, char **argv)
{
FILE* fp;
char buf[11];
char count[11];
union semun sem_union;
// open 과 close 를 위한 sembuf 구조체를 정의한다.
struct sembuf mysem_open = {0, -1, SEM_UNDO}; // 세마포어 얻기
struct sembuf mysem_close = {0, 1, SEM_UNDO}; // 세마포어 돌려주기
int sem_num;
memset(buf, 0x00, 11);
memset(count, 0x00, 11);
// 아규먼트가 있으면 생성자
// 그렇지 않으면 소비자이다.
if (argc > 1)
sem_num = 1;
else
sem_num = 0;
// 세마포설정을 한다.
semid = semget((key_t)234, sem_num, 0660|IPC_CREAT);
if (semid == -1)
{
perror("semget error ");
exit(0);
}
// 세마포어 초기화
sem_union.val = 1;
if ( -1 == semctl( semid, 0, SETVAL, sem_union))
{
printf( "semctl()-SETVAL 실행 오류\n");
return -1;
}
// counter.txt 파일을 열기 위해서 세마포어검사를한다.
if(semop(semid, &mysem_open, 1) == -1)
{
perror("semop error ");
exit(0);
}
if ((fp = fopen("counter.txt", "r+")) == NULL)
{
perror("fopen error ");
exit(0);
}
// 파일의 내용을 읽은후 파일을 처음으로 되돌린다.
fgets(buf, 11, fp);
rewind(fp);
// 개행문자를 제거한다.
buf[strlen(buf) - 1] = 0x00;
sprintf(count, "%d\n", atoi(buf) + 1);
printf("%s", count);
// 10초를 잠들고 난후 count 를 파일에 쓴다.
sleep(10);
fputs(count,fp);
fclose(fp);
// 모든 작업을 마쳤다면 세마포어 자원을 되될려준다
semop(semid, &mysem_close, 1);
return 1;
}
코드는 매우 간단하지만, 세마포어에 대한 기본적인 이해를 충분히 할수 있을만한 코드이다. 생성자와 소비자의 분리는 프로그램에 넘겨지는 아규먼트를 이용했다. 모든 작업을 마치면 테스트를 위해서 10초를 기다린후에 세마포어를 돌려주도록 코딩되어 있다.
우선 count 를 저장할 파일 counter.txt 를 만들고 여기에는 1을 저장해 놓는다.
그다음 ./sem_test 를 실행시키는데, 최초에는 생성자를 만들어야 하므로 아규먼트를 주어서 실행시키고, 그다음에 실행시킬때는 소비자가 되므로 아규먼트 없이 실행하도록 하자. 다음은 테스트 방법이다.
위 코드를 실행해보면 ./sem_test 1 이 세마포어자원을 돌려주기 전까지 ./sem_test 가 해당영역에서(세마포어 요청하는 부분) 블럭되어 있음을 알수 있고, 충돌없이 count가 잘되는것을 볼수 있을것이다.
세마포어는 커널에서 관리하는데 세마포어를 사용하는 프로세스가 없다고 하더라도 semctl 을 이용해서 제거하지 않는한은 커널에 남아있게 된다. 세마포어 정보를 제거하기 위해서는 semctl 연산을 하든지, 컴퓨터를 리붓 시커거나, ipcrm(8)이란 도구를 사용해서 제거시켜 줘야 한다.
POSIX 세마포어
POSIX 규격을 따르는 새로운 세마포어 인터페이스로 전통적인 System V 인터페이스에 비해서 좀 더 명확하다. 또한 세마포어 집합이란 개념이 없는데, 덕분에 이해하기가 수월해진 것 같다.
POSIX 세마포어 함수를 사용하기 위해서는 -lrt로 리얼타임 라이브러리를 링크해야 한다.
# gcc -lrt sem_test sem_test.c
세마포어 만들기
세마포어는 "익명 세마포어 (unnamed-)"와 "이름 있는 세마포어 (named-)"가 있다.
익명 세마포어는 sem_init로 만든다.
#include <semaphore.h>
int sem_init(sem_t *sem, int pshared, unsigned int value);
sem : 초기화할 세마포어 객체
pshared : 0이 아니면 프로세스들 간에 세마포어를 공유한다. 0이면 프로세스 내부에서만 사용한다.
value : 세마포어 초기 값
사용 예
sem_init(&sem_name, 0, 10);
이름 있는 세마포어는 sem_open으로 만든다.
sem_t* sem_open(const char* name, int oflag, mode_t mode, unsigned int value);
이름 있는 세마포어는 파일로 만들어지기 때문에, 파일 이름과 권한설정이 필요하다. 세마포어 파일은 /dev/shm에 만들어진다. 그러므로 /dev/shm을 마운트 시켜줘야 한다.
sem_wait함수는 만약 세마포어 값이 0보다 크면 프로세스는 세마포어를 얻고 세마포어를 감소하고 즉시 반환한다. 세마포어값이 0이라면 세마포어가 0보다 더 커지거나 시그널이 발생할 때까지 대기한다.
sem_trywait는 즉시 세마포어를 감소시키고 반환하는 것을 제외하고는 sem_wait와 같다.
timeout시간동안의 제한시간을 가진다는 것을 제외하고는 sem_wait함수와 같다. stimespec구조체를 이용해서 초+나노초 수준에서 제한시간을 지정할 수 있다.
fcntl()이용한 잠금으로도 세마포어와 비슷한 일을 수행할 수 있는데, fcntl보다 좀더 세밀한 조정이 가능하다라는 장점을 가지고 있다. 세밀한 만큼, 간단한 일을 하기에는 지나치게 복잡한 면이 있으니, 상황에 따라서 적절하게 선택하면 된다.
fcntl의 잠금을 이용한 자원접근 제어는 fcntl을 이용한 파일/레코드 잠금 문서를 참고하기 바란다.
Contents
세마포어란 무엇인가
세마포어의 작동원리
System V 세마포어
세마포어의 사용
세마포어의 관리
semget 을 이용해서 세마포어를 만들자.
세마포어를 이용해서 접근제어 하기
세마포어 조작
예제
POSIX 세마포어
세마포어 만들기
세마포어 얻기 (기다리기)
세마포어 정보 가져오기
세마포어 되돌려주기
세마포어 삭제
POSIX 익명 세마포어 예제
이름있는 세마포어 예제
결론
Recent Posts
Archive Posts
Tags