본문 바로가기

Job Notes/Linux & Android

[펌] 리눅스에서의 세마포어(1)

제공 : 한빛 네트워크
저자 : Vikram Shukla
역자 : 주재경
원문 : Semaphores in Linux 

멀티쓰레드로 된 응용프로그램이 나날이 상용 프로그램의 한 부분을 이루고 있다. 멀티쓰레드를 사용하지 않은 상용 프로그램이 사용된다는 것은 상상도 할 수 없다. 응용프로그램은 시스템이나 프로그램의 성능향상 측면에서 멀티쓰레드를 반드시 사용해야 한다. 그러나 인생에서 가장 아름다운 것은 그만한 대가 없이는 얻을 수 없는 법이다. 응용프로그램에 멀티쓰레드 기능을 사용하고자 한다면 여기에는 deadlock, 경쟁조건(race condition), 쓰레드의 비정상 동작 등 몇 가지 이슈 사항이 뒤따른다. 이 문제를 해결하기 위해 OS는 멀티쓰레드로 이루어진 다중 프로세스 문제를 해결하는데 사용하는 mutex, 세마포어, 시그널, barriere 등과 같은 기능을 제공한다. 이 글은 이 기능 중의 하나인 세마포어에 대해서 논의하며 이에 대해 세부적인 고찰을 제공한다. 

세마포어 개요 

세마포어는 리소스의 상태를 나타내는 간단한 카운터로 생각할 수 있다. 이 카운터는 보호되는 변수이며 사용자가 바로 접근할 수 없다. 이 변수에 대한 방어벽은 다름 아닌 커널이 제공한다. 세마포어 변수의 사용법은 간단하다. 카운터가 0보다 큰 경우 리소스를 사용할 수 있으며 카운터가 0이거나 혹은 이 보다 작은 경우 리소스는 다른 곳에서 사용하고 있는 중이다. 이 간단한 매커니즘이 멀티쓰레드와 멀티프로세스 기반의 응용프로그램 동기화에 도움을 준다. 세마포어는 Edsger Dijkstr가 고안했으며 동기화의 목적으로 오늘날 OS에서도 여전히 사용되고 있다. 이와 똑같은 매커니즘을 응용프로그램 개발자들 또한 사용하고 있으며 이중 가장 중요한 측면 중 하나가 프로세스간 통신이다. 

세마포어는 공유하고 있는 자원의 수에 따라 binary 혹은 counting 세마포어가 될 수 있다. 하나의 공유자원이 사용되고 있는 경우 동기화의 목적으로는 단지 하나의 세마포어만 있으면 된다. 이 경우 세마포어를 binary 세마포어라 한다. 공유되는 자원의 수가 하나 이상인 다른 모든 경우에 다중 세마포어를 사용해야 하며 이를 counting 세마포어라 한다. 

세마포어는 기본적으로 두 가지 동작을 구현한다. 하나는 세마포어 변수를 wait하는 동작이고 다른 하나는 세마포어 변수에 시그널을 보내는 동작이다. 세마포어는 단지 하나의 카운터이며 다음의 알고리즘이 이러한 2가지 동작을 설명한다. 

가정 :
S는 세마포어 변수를 나타낸다.
W(s)는 세마포를 wait하고 있음을 나타낸다.
P(s)는 세마포어를 사용할 수 있음을 나타낸다. 

알고리즘 :
W(s) 
 while  (s<=0)    {
   //do  nothing 
}
s=s-1;
P(s)
s=s+1;
위 알고리즘으로부터 세마포어를 기다리는 것은 세마포어 카운터를 1만큼 감소시키는 것 이외 아무것도 아님을 쉽게 알 수 있다. 세마포어를 사용하는 것은 이와 정확히 반대로 세마포어 카운터를 1만큼 증가시킨다. 

세마포어 기능을 구현하기 위해 리눅스 커널이 내부적으로 사용하고 있는 구조와 함수를 살펴보자. 세마포어는 아래의 2가지 구조를 내부적으로 사용한다.
struct semaphore
{ 
 atomic_t count;
 int sleepers;
 wait_queue_head_t  wait;
}
struct rw_semaphore
{
 _s32 activity
 spinlock_t wait_lock;
 struct list_head  wait_list;
}
이 구조는 최근 커널에서 수정이 진행 중이며 아래에서 보는 것처럼 추가된 멤버 변수를 포함하고 있다.
struct rw_semaphore {
   signed  long count;
   #define RWSEM_UNLOCKED_VALUE 0x00000000
   #define RWSEM_ACTIVE_BIAS 0x00000001
   #define RWSEM_ACTIVE_MASK 0x0000ffff
   #define RWSEM_WAITING_BIAS (-0x00010000)
   #define RWSEM_ACTIVE_READ_BIAS RWSEM_ACTIVE_BIAS
   #define RWSEM_ACTIVE_WRITE_BIAS (RWSEM_WAITING_BIAS + RWSEM_ACTIVE_BIAS) spinlock_t wait_lock;
    struct  list_head wait_list;
   #if RWSEM_DEBUG
    int debug;
   #endif
}
커널 레벨에서 세마포어 기능을 구현한 기본함수는 /asm/semaphore.h와 /asm/rwsem.h에서 찾을 수 있다.
  • __down(struct semaphore *): 이 함수는 세마포어가 0보다 큰값을 가지는지를 조사한다. 만약 그렇다면 세마포어를 1만큼 감소시키고 리턴한다. 그렇지 않다면 슬립상태로 진입하며 후에 다시 시도한다.
  • __up(struct semaphore *): 이 함수는 세마포어를 1만큼 증가시켜 세마포어를 wait하는 프로세스를 wake시킨다.
  • __down_trylock(struct semaphore *): 이 함수는 세마포어가 사용가능한지를 조사하며 그렇지 않다면 리턴한다. 그러므로 이 함수는 non-blocking 함수로 분류된다.
  • __down_interruptible(struct semaphore *): 이 함수의 동작은 __down과 거의 비슷하지만 시그널로 인터럽트를 걸 수 있다는 면에서 차이를 지닌다. 시그널로 인터럽트가 걸린 경우 -EINTR이 리턴된다. __down버젼은 실행 중 시그널을 블록킹한다.
  • __down_read(읽기동작 동안 세마포어 잠금),__down_write(쓰기동작 동안 세마포어 잠금), __up_read(읽기동작 후 세마포어 릴리즈), __up_write(쓰기동작 후 세마포어 릴리즈)와 같은 함수는 접근 금지된(protected) 리소스에 대해 한번 이상의 읽기동작을 허용하지만 업데이트를 위해서는 단지 한번의 쓰기 동작만 허용한다.
세마포어 실사용 현황 

현재 UNIX 환경에서는 2가지 유형의 세마포어가 있다: System V와 POSIX가 그것이다. 일반적으로 이전 유닉스 기반의 시스템은 System V를 사용하고 현재의 리눅스 기반의 시스템은 POSIX 버전을 사용한다. 그러나 세마포어에 대한 일반적인 동작이나 기술이 버전에 따라 틀린 것은 아니다. 이 2가지 서로 다른 버전의 세마포어 인터페이스를 살펴보고 이들이 어떻게 동작하는지를 보자. 

System V 세마포어 

System V 세마포어의 인터페이스와 사용법은 불필요하게 복잡하다. 예를 들어 생성된 세마포어는 단순한 하나의 카운터 값이 아니라 세마포어 카운터 값들의 집합이다. 생성된 세마포어 객체는 고유한 세마포어 ID를 가진 집합에서 0부터 n까지의 세마포어로 구성된다. 이 역할을 하는 함수는
int semget(key__t key, int nsems, int semflag);
key :
세마포어를 구분하기 위해 사용됨
nsems :
집합에서 필요한 세마포어의 수
semflg :
세마포어가 어떻게 생성되었는지를 가리킨다. 아래 유형가운데 하나이다.
  • IPC_CREAT : key가 존재하지 않는 경우 새로운 세마포어를 생성한다.
  • IPC_EXCL : key가 존재한다면 함수는 실행은 실패한다.
key_t형의 key는 유저나 프로그래머 혹은 ftok()호출로 제공된 유의미한 값을 가질 수 있다. 그러나 System V 세마포어는 IPC_PRIVATE로 구분되는 다른 key값을 제공한다. 이 key가 사용된 경우 semget()을 호출할 때 마다 세마포어 ID로 구분할 수 있는 새로운 집합의 세마포어를 생성한다. 아래의 코드는 semget 함수를 호출할 때 마다 새로운 세마포어가 어떻게 생성되는지를 보여준다.
{
 int semid;
 semid=semget(IPC_PRIVATE,  1,IPC_CREAT);
 if  (semid<0) 
 {
  perror("Semaphore creation failed  Reason:");
 }
}
[주의] 실제로 IPC_PRIVAE는 ((__key_t)0)으로 정의된다. 이 부분은 ipc.h에서 찾을 수 있다. 

System V에서는 세마포어가 생성되면 생성된 세마포어는 초기화 되어야 한다. 세마포어의 생성과 초기화는 atomic 동작이 아니며 초기화는 유저에 의해 분리되어 수행 되어야 한다. 이 역할을 하는 함수는
int semc시 (int semid, int semnum, int 층, ...);
semid :
세마포어 구분자
semnum :
semid로 구분되는 집합내에서 n번째 세마포어
cmd :
이것은 세마포어에 대해 실행되는 동작의 유형을 정의한다. 초기화 목적으로 사용된 플래그는 SETVAL이다. 다른 값들에 대해서는 man 페이지를 참조하라. 

cmd가 요구하는 것에 따라 4번째 인자는 생략될 수 있다--union semun-- 이것은 선택적인(option) 인자이다. 이 구조를 사용하기 위해서 사용자는 명시적으로 아래처럼 정의해야 한다.
union semun {
  int val;
  struct semid_ds *buf;
  ushort *array;
} arg;
아래의 코드는 세마포어 semid로 구분되는 집합의 첫 번째 세마포어를 1로 설정한다.
{
 semun init;
 init.val = 1;
   int i = semctl(semid, 1, SETVAL, init);
}
세마포어가 생성되고 초기화 되자마자 사용자는 이 세마포어 집합에 대해 동작을 수행할 수 있는 준비가 되어 있다. 다시 한번 강조하지만 세마포어 locking이나 unlocking같은 동작을 수행하는데 사용되는 인터페이스는 직접적이지 않다. 세마포어에 대해 원하는 동작을 수행 할 함수를 호출하기 전에 일정량의 명령어를 수행해야 한다.
int semop(int semid, struct sembuf *sops, size_t nsops);
semid :
세마포어 구분자
sops :
세마포어 구조체에 대해 유저가 정의한 배열에 대한 포인터. 문서는 이 구조체가 확장되지 말 것을 권고한다. 구조체는 다음과 같다.
struct sembuf{
	ushort sem_num; /*집합내에서 세마포어를 구분한다 */
	short  sem_op; /* +, -, 0의 값을 가질 수 있다 */
	short  sem_flag; /*IPC_NOWAIT, SEM_UNDO값을 가질 수 있다 */
};
nsops : 사용자가 전달하는 sembuf의 수를 결정한다. nsops인자는 동작이 한 번에 한 묶음의 세마포어에 대해 수행될 필요가 있는 경우에 제공된다. 

[주의] SEM_UNDO가 설정되어 있는 경우 커널은 프로그램이 종료될 때 이전 모든 동작의 영향을 반대로 하여 세마포어 값을 리셋한다. 

구조체 sembuf의 멤버 sem_op는 세마포어를 lock하고 unlock하는 중요한 변수이다. 세마포어에 수행되는 동작의 유형을 결정하는 이 멤버는 아래의 값을 가질 수 있다. 

sem_op == 0 : 세마포어값이 0으로 될 때까지 기다린다.
sem_op > 0 :sem_op의 절대값 만큼 세마포어 값을 증가시킨다.
sem_op < 0 : sem_op의 절대값 만큼 세마포어 값을 감소시킨다. 

양의 값은 세마포어를 릴리즈 하는 것에 대응하고 음의 값은 세마포어를 locking하는 것에 대응하는 것을 위 sem_op값으로부터 알 수 있다. 

[주의] 세마포어 값은 결코 0보다 작은 값을 가질 수 없다. 

세마포어를 생성하고 초기화 하고 세마포어에 대해 동작을 한 다음 가장 중요한 것은 세마포어와 메모리를 깨끗이(cleanup)하는 것이다. 이 동작은 cleanup 핸들러나 소멸자 정상적인 종료 혹은 프로그램의 비정상적인 종료 시에 이루어져야 한다. 이를 수행하지 않으면 세마포어가 System V기반의 구현에서는 시스템 전체적으로 정의되어 있기 때문에 응용프로그램이 임의의 지점에서 동작해야 할 세마포어를 찾지 못하게 된다. 

아래의 코드는 semid로 구분되는 세마포어를 깨끗이(cleanup)한다.
{
   int ret =  semctl(semid,0,IPC_RMID); //집합에서 0번째 세마포어를 제거
}
IPC_RMID플래그는 semid로 구분되는 세마포어에 대해 수행될 필요가 있는 동작 유형을 나타낸다. 아래의 동작 예가 이 개념을 사용하고 있다.
<u>File: sysvsem_demo.c</u>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <stdio.h>

//세마포어 초기화를 위해 유저가 정의한 semun을 생성한다.

void *Thread1(void *arg)
{
  int semid;
  semid = (int)arg;

  //세마포어에 동작을 수행하기위해 sembuf 정의를 위해 가장 먼저 필요
   struct sembuf op1,op2;

  //0번째 세마포어에 대한 동작
  op1.sem_num = 0; //0번째 세마포어를 나타냄
  op1.sem_op = -1; //lock을 위해 세마포어 카운트 감소시킴
  op1.sem_flg = 0; //세마포어에 대해 lock을 얻을때 까지 기다림

  //1번째 세마포어에 대한 동작
  op2.sem_num = 1; //0번째 세마포어를 나타냄
  op2.sem_op = -1; //lock을 위해 세마포어 카운트 감소시킴
  op2.sem_flg = 0; //세마포어에 대해 lock을 얻을때 까지 기다림

  //0번째 세마포어 locking
  if (semop(semid,&op1,1) == -1)
    {
      perror("Thread1:semop failure Reason:");
      exit(-1);
    }
  else
    fprintf(stderr,"Thread1:Successfully locked 0th semaphoren");
  //1번째 세마포어 locking
  if (semop(semid,&op2,1) == -1)
    {
      perror("Thread1:semop failure Reason:");
      exit(-1);
    }
  else
    fprintf(stderr,"Thread1:Successfully locked 1th semaphoren");

  //0번째 세마포어 릴리즈
  op1.sem_num = 0; //0번째 세마포어를 나타냄
  op1.sem_op = 1; //lock을 위해 세마포어 카운트 감소시킴
  op1.sem_flg = 0; //세마포어에 대해 lock을 얻을때 까지 기다림

  if (semop(semid,&op1,1) == -1)
    {
      perror("Thread1:semop failure Reason:");
      exit(-1);
    }
  else
    fprintf(stderr,"Thread1:Successfully unlocked 0th semaphoren");

  //1번째 세마포어 릴리즈
  op2.sem_num = 1; //0번째 세마포어를 나타냄
  op2.sem_op = 1; //lock을 위해 세마포어 카운트 감소시킴
  op2.sem_flg = 0; //세마포어에 대해 lock을 얻을때 까지 기다림

  if (semop(semid,&op2,1) == -1)
    {
      perror("Thread1:semop failure Reason:");
      exit(-1);
    }
  else
    fprintf(stderr,"Thread1:Successfully unlocked 1th semaphoren");
}

void *Thread2(void *arg)
{
  int semid;
  semid = (int)arg;

  //in order to perform the operations on semaphore
  // first need to define the sembuf object
  struct sembuf op1,op2;

  //operation for 0th semaphore
  op1.sem_num = 0; //0번째 세마포어를 나타냄
  op1.sem_op = -1; //lock을 위해 세마포어 카운트 감소시킴
  op1.sem_flg = 0; //세마포어에 대해 lock을 얻을때 까지 기다림

  //operation for 1th semaphore
  op2.sem_num = 1; //0번째 세마포어를 나타냄
  op2.sem_op = -1; //lock을 위해 세마포어 카운트 감소시킴
  op2.sem_flg = 0; //세마포어에 대해 lock을 얻을때 까지 기다림

  //0번째 세마포어 locking
  if (semop(semid,&op1,1) == -1)
    {
      perror("Reason:");
      exit(-1);
    }
  else
    fprintf(stderr,"Thread2:Successfully locked 0th semaphoren");

  //1번째 세마포어 locking
  if (semop(semid,&op2,1) == -1)
    {
      perror("Reason:");
      exit(-1);
    }
  else
    fprintf(stderr,"Thread2:Successfully locked 1th semaphoren");

  //0번째 세마포어 릴리즈

  op1.sem_num = 0; //0번째 세마포어를 나타냄
  op1.sem_op = 1; //lock을 위해 세마포어 카운트 감소시킴
  op1.sem_flg = 0; //세마포어에 대해 lock을 얻을때 까지 기다림

  if (semop(semid,&op1,1) == -1)
    {
      perror("Reason:");
      exit(-1);
    }
  else
    fprintf(stderr,"Thread2:Successfully unlocked 0th semaphoren");

  //1번째 세마포어 릴리즈
  op2.sem_num = 1; //0번째 세마포어를 나타냄
  op2.sem_op = 1; //lock을 위해 세마포어 카운트 감소시킴
  op2.sem_flg = 0; //세마포어에 대해 lock을 얻을때 까지 기다림

  if (semop(semid,&op2,1) == -1)
    {
      perror("Reason:");
      exit(-1);
    }
  else
    fprintf(stderr,"Thread2:Successfully unlocked 1th semaphoren");

}

int main()
{
  pthread_t tid1,tid2;
  int semid;

  //세마포어 초기화를 위해 유저가 정의한 semun 생성

  typedef union semun
  {
    int val;
    struct semid_ds *buf;
    ushort * array;
  }semun_t;

  semun_t arg;
  semun_t arg1;

  //하나의 집합에 2개의 세마포어를 가진 세마포어 객체 생성
  //0번째, 1번째 세마포어
  semid = semget(IPC_PRIVATE,2,0666|IPC_CREAT);
  if(semid<0)
    {
      perror("semget failed Reason:");
      exit(-1);
    }

  //1로 설정하여 0번째 세마포어 초기화
  arg.val = 1;
  if ( semctl(semid,0,SETVAL,arg)<0 )
    {
      perror("semctl failure Reason:");
      exit(-1);
    }
  //1로 설정하여 1번째 세마포어 초기화
  arg1.val = 1;
  if( semctl(semid,1,SETVAL,arg1)<0 )
    {
      perror("semctl failure Reason: ");
      exit(-1);
    }

  //이 세마포어에 대해 동작하는 2개의 쓰레드 생성
  if(pthread_create(&tid1, NULL,Thread1, semid))
    {
      printf("n ERROR creating thread 1");
      exit(1);
    }
  if(pthread_create(&tid2, NULL,Thread2, semid) )
    {
      printf("n ERROR creating thread 2");
      exit(1);
    }
  //쓰레드 동작이 끝나기를 기다림
  pthread_join(tid1, NULL);
  pthread_join(tid2, NULL);

  //once done clear the semaphore set
  if (semctl(semid, 1, IPC_RMID ) == -1 )
    {
      perror("semctl failure while clearing Reason:");
      exit(-1);
    }
  //메인 쓰레드 종료
  pthread_exit(NULL);
  return 0;
}
[주의] System V 세마포어는 유저에 의해 제어되므로 세마포어에 대한 허가권이 개발자에게 문제일 수 있다. 이 정보를 유지하는 구조체는 struct semid_ds이다.
struct semid_ds
{
  struct  ipc_perm sem_per; /* operation's permission structure */
  struct  sem * sem_base; /* pointer to first sem in a set */
  ushort sem_nsems; /* number of sem in a set */
  time_t sem_otime; /*time of last semop */
  time_t sem_ctime; /* time of last change */
}
시스템의 모든 세마포어에 대해 커널은 sys/ipc.h에 정의된 정보에 관한 구조체를 유지한다. 위에서 설명한 세마포의 좀 더 상세한 설정을 위해 IPC_STAT플래그가 사용되고 구조체 semid_ds에 대한 포인터가 전달된다. 허가권을 얻고 허가권을 설정하는 멤버 변수는 sem_per이다. 간단한 예제를 살펴보자.
#include <pthread.h>
#include <stdio.h>
#include <semaphore.h>
#include <sys/sem.h>

int main()
{
  int semid;
  struct semid_ds status;

  semid = semget(( key_t )0x20,10,IPC_CREAT|0666);
  if(semid == -1)
    {
      perror("sem creation failed:Reason");
      exit(0);
    }
  //허가권을 가져옴
  semctl(semid,0,IPC_STAT,&status);
  printf("owners uid is %un",status.sem_perm.uid);
  printf("group uid is %un",status.sem_perm.gid);
  printf("Access mode is %cn",status.sem_perm.mode);

  //허가권을 설정함 
  status.sem_perm.uid = 102;
  status.sem_perm.gid = 102;
  status.sem_perm.mode = 0444;
  semctl(semid,0,IPC_SET,&status);

  return 1;
}
[주의] 새로운 허가권 집합이 처음 허가권의 부분 집합인 경우에만 허가권 설정이 허용된다. 

역자 주재경님은 현재 (주)세기미래기술에 근무하고 있으며 리눅스, 네트워크, 운영체제 및 멀티미디어 코덱에 관심을 가지고 있습니다.
* e-mail : jkjoo@segifuture.com