세마포어(Semaphores)를 비록 IPC설비중의 하나로 분류하긴 했지만, 다른 파이프, 메시지큐, FIFO 등과는 좀다르다. 다른 IPC 설비들이 대부분 프로세스간 메시지 전송을 그 목적으로 하는데 반해서 세마포어는 프로세스간 데이타를 동기화 하고 보호하는데 그목적이 있다.
프로세스간 메시지 전송을 하거나, 혹은 공유메모리를 통해서 특정 데이타를 공유하게 될경우 발생하는 문제가, 공유된 자원에 여러개의 프로세스가 동시에 접근을 하면 안되며, 단지 한번에 하나의 프로세스만 접근 가능하도록 만들어줘야 할것이다. 이것은 쓰레드에서 메시지간 동기화를 위해서 mutex 를 사용하는것과 같은 이유이다.
하나의 데이타에 여러개의 프로세스가 관여할때 어떤 문제점이 발생할수 있는지 간단한 예를 들어보도록 하겠다.
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이면 자원을 사용할수 있을때까지 기다리고, 0보다 더크면(1이라고 가정하자) 자원에 접근해서 세마포어 값을 1 감소 시켜서, 세마포어 값을 0으로 만들어서, 다른 프로세스가 자원에 접근할수 없도록 하고, 자원의 사용이 끝나면 세마포어 값을 다시 1증가시켜서 다른 프로세스가 자원을 사용할수 있도록 만들어주면 된다.
만약 세마포어 값을 검사했는데 세마포어 값이 0이라면 사용할수 있게 될때까지 (1이 될때까지) 기다리면 (block) 될것이다.
세마포어의 사용은 위의 작동원리를 그대로 적용한다. 즉 1. 세마포어로 제어할 자원을 설정한다.
2. 해당 자원을 사용하기전에 세마포어 값을 확인한다.
3. 세마포어 값이 0보다 크면 자원을 사용하고, 세마포어 값을 1 감소 시킨다.
4. 세마포어 값이 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);
|
세마포어는 그 특성상 원자화된 연산을 필요로 한다. 이러한 원자화된 연산은 유저레벨의 함수에서는 제공하기가 힘들므로, 세마포어 정보는 커널에서 전용 구조체를 이용해서 관리하게 된다. 다음은 커널에서 세모포어 정보를 유지하기 위해서 관리하는 구조체인 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(2)를 제공한다. 첫번째 아규먼트는 세마포어의 유일한 키값을 위해서 사용하는 int 형의 키값이다. 우리는 이 key 값을 이용해서 유일한 세마포어를 생성하거나 접근할수 있게 된다. 새로 생성되거나 기존의 세마포어에 접근하거나 하는것은 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)라는 도구를 이용하면 된다.
[root@localhost test]# ipcs -s
------ Semaphore Arrays --------
key semid owner perms nsems status
0x00003039 0 root 666 1
|
0x00003039 은 key 12345 의 16진수 표현이다. 퍼미션은 666으로 되어 있고 semget 를 통해서 제대로 설정되어 있음을 알수 있다.
세마포어를 이용해서 접근제어 하기
이제 semget 을 통해서 세마포어를 만들거나 접근했으니, 이제 실제로 세마포어상태를 검사해서 접근제어를 해보도록하자.
이것은 semop를 통해서 이루어진다.
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 은 세마포어 배열을 다룰 경우 사용되며, 보통은 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);
}
// 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 를 실행시키는데, 최초에는 생성자를 만들어야 하므로 아규먼트를 주어서 실행시키고, 그다음에 실행시킬때는 소비자가 되므로 아규먼트 없이 실행하도록 하자. 다음은 테스트 방법이다.
[root@coco test]# ./sem_test 1&
[1] 3473
36
[root@coco test]# ./sem_test
|
위 코드를 실행해보면 ./sem_test 1 이 세마포어자원을 돌려주기 전까지 ./sem_test 가 해당영역에서(세마포어 요청하는 부분) 블럭되어 있음을 알수 있고, 충돌없이 count가 잘되는것을 볼수 있을것이다.
세마포어는 커널에서 관리하는데 세마포어를 사용하는 프로세스가 없다고 하더라도 semctl 을 이용해서 제거하지 않는한은 커널에 남아있게 된다. 세마포어 정보를 제거하기 위해서는 semctl 연산을 하든지, 컴퓨터를 리붓 시커거나,
ipcrm(8)이란 도구를 사용해서 제거시켜 줘야 한다.
결론
세마포어는 fcntl 과 매우 비슷한 일을 수행하는데, 좀더 세밀한 조정이 가능하다라는 장점을 가지고 있지만 간단한 일을 하기엔 지나치게 복잡한 면이 없잖아 있다. 이럴경우에는 세마포어 대신 fcntl 을 사용하자.
0