본문 바로가기

Job Notes/Linux & Android

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

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

[이전 기사 보기]
리눅스에서의 세마포어(1) 

POSIX 세마포어 

System V 세마포어에 대한 잠재적인 학습곡선이 POSIX 세마포어에 비해 훨씬 높다. 당신이 이 부분을 본 후 이전 장에서 당신이 배운 것과 비교해 보면 더욱 쉽게 이해 될 것이다. 

POSIX는 세마포어에 대한 동작, 생성, 초기화에 대한 사용법이 간단하다. 프로세스간 통신을 제어하는 훨씬 효과적인 방법을 제공한다. POSIX는 2가지 종류 named 세마포어와 unnamed 세마포어를 가지고 있다. 

Named 세마포어 

man 페이지를 보면 named 세마포어는 System V 세마포어처럼 이름으로 구분된다는 것을 알 수 있을 것이다. System V와 같은 세마포어는 시스템 전체에 걸쳐 영향을 미치며 임의의 순간에 활성화 될 수 있는 세마포어 수가 제한된다. 

named 세마포어는 아래의 함수 호출로 생성 된다 :
sem_t *sem_open(const char *name,  int oflag, mode_t mode , int value);
name
서로 구분되는 세마포어의 이름 
oflag
세마포어 생성을 위해서 O_CREAT로 설정된다(이미 세마포어가 존재하는 경우 함수 호출을 실패로 하는 경우 O_EXCL로 설정한다) 
mode_t
새로운 세마포어에 대한 허가권을 제어한다. 
value
세마포어의 초기 값을 결정한다. 

한 번의 호출로 세마포어를 생성, 초기화, 허가권을 설정하며 이는 System V 세마포어 동작과는 아주 다르다. 이것이 본질적으로 더욱 분명하고 atomic하다. System V 세마포어 가 int형(open()함수가 리턴하는 fd와 비슷하게)으로 스스로를 구분한다는 면에서 또 다른 차이점이 있으며 반면에 sem_open함수는 POSIX 세마포어의 구분자로 동작하는 sem_t를 리턴한다. 

이제부터 세마포어에 대한 동작이 이루어진다. 세마포어를 locking하는 방법은:
int sem_wait(sem_t *sem);
이 함수 호출은 세마포어 카운트가 0보다 큰 경우 세마포어를 lock한다. 세마포어를 locking한 후 카운트는 1만큼 감소한다. 세마포어 카운트가 0인 경우 함수 호출은 block된다. 

세마포어를 unlocking하는 방법은
int sem_post(sem_t *sem);
이 함수 호출은 세마포어 카운트를 1만큼 증가 시키고 리턴한다. 

일단 세마포어 사용을 완료했으면 이 사용을 해제(destroy)하는 것이 중요하다. named 세마포어에 대한 모든 참조는 종료 전이나 종료 핸들러가 sem_unlink()를 호출하는 범위 내에서 세마포어를 시스템에서 제거한 다음에 sem_close() 함수를 호출하여 종료한다. 주목해야 할 것은, 프로세스나 쓰레드가 세마포어를 참조한다면 sem_unlink() 함수는 영향을 끼치지 못한다는 점이다. 

Unnamed 세마포어 

man 페이지에 따르면 unnamed 세마포어는 다중쓰레드(쓰레드 공유 세마포어)나 프로세스(프로세스 공유 세마포어)가 공유하는 메모리 영역에 위치하고 있다. 쓰레드 공유 세마포어 프로세스의 쓰레드만이 공유하는 영역(예를 들면 전역변수)에 위치한다. 프로세스 공유 세마포어는 서로 다른 프로세스가 공유 할 수 있는 영역(예를 들어 공유메모리와 같은)에 위치한다. Unnamed 세마포어는 쓰레드들 간 그리고 관련 프로세스들 간의 동기화를 제공한다. 

Unnamed 세마포어는 sem_open 호출이 필요하지 않다. 대신에 아래의 2가지 호출이 필요하다.
{
  sem_t semid;
  int sem_init(sem_t *sem, int pshared, unsigned  value);
}
pshared
이 인자는 임의의 프로세스의 쓰레들 사이에 혹은 프로세스들 사이에 세마포어가 공유되는지 어떤지를 표시한다. 만약 pshared 값이 0이라면 세마포어는 프로세스의 쓰레드들 간에 공유된다. Pshared가 0이 아니면 세마포어는 프로세스 간에 공유된다. 
value
초기화될 때 세마포어가 가지는 값 
세마포어가 초기화 되자마자 프로그래머는 sem_t 타입의 세마포어를 사용할 수 있다. 세마포어에 대한 lock과 unlock 동작은 앞서 본 바와 같이 sem_wait(sem_t *sem)와 sem_post(sem_t *sem)을 통해 이루어 진다. 

이 글의 마지막 장은 POSIX 세마포어를 사용하여 개발된 간단한 worker-consumer 문제를 설명한다. 

System V 세마포어와 POSIX 세마포어 

System V 와 POSIX 세마포어 사이에는 많은 차이점이 있다.
  • System V와 POSIX 세마포어에서 한 가지 확연한 차이점은 System V에서는 세마포어 카운터의 증가 혹은 감소량을 사용자가 제어 할 수 있지만 POSIX에서는 증가 감소가 1만큼 이루어 진다.
  • POSIX 세마포어는 세마포어 허가권 조작을 허용하지 않지만 System V 세마포어는 최초 허가권의 부분 집합에 대해 세마포어 허가권을 변경할 수 있다.
  • POSIX 세마포어의 생성과 기화는 유저의 관점에서 최소 단위 동작(atomic)이다.
  • 사용면에서 System V 세마포어는 좀 어색한 면이 있고 반면에 POSIX 세마포어는 직접적이다.
  • unnmaed 세마포어를 사용하는 POSIX 세마포어의 범위는 System V 세마포어의 범위보다 훨씬 더 크다. 사용자/클라이언트 시나리오에서 각 사용자가 서버에 대한 인스턴스를 가지고 있는 경우 POSIX 세마포어를 사용하는 것이 더 좋다.
  • 세마포어를 생성할 때 System V 세마포어는 세마포어 배열을 생성한다. 반면에 POSIX 세마포어는 단지 하나만 생성한다. 이를 이유로 인해 세마포어 생성(메모리 측면에서)은 POSIX 세마포어와 비교했을 때 System V 세마포어의 비용이 더 들어간다.
  • POSIX 세마포어의 성능이 System V 기반의 세마포어 보다 더 좋다고 말했다.
  • POSIX 세마포어는 시스템 전체에 걸친 세마포어 보다는 프로세스 전체에 걸친 매커니즘을 제공한다. 그래서 개발자가 세마포어를 close하는 동작을 잊는다면 세마포어 cleanup은 프로세스 종료 시점에서 이루어진다. 간단히 말해 POSIX 세마포어는 일관되지 않은 매커니즘을 제공한다.
세마포어 유틸리티 이해 

동기화 매커니즘에 대한 세마포어의 잇점은 같은 리소스에 접근 하고자 하는 2개의 관련된 혹은 관련되지 않은 프로세스를 동기화 하기 위해 사용될 수 있다는 점이다. 

related 프로세스 

새로운 프로세스가 이미 있는 프로세스 내에서 생성되면 이 프로세스를 related 프로세스라 한다. 아래의 예제는 관련 프로세스가 어떻게 동기화 되는지를 보여준다.
#include <semaphore.h>
#include <stdio.h>
#include <errno.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/mman.h>

int main(int argc, char **argv)
{
  int fd, i,count=0,nloop=10,zero=0,*ptr;
  sem_t mutex;

  //파일을 열고 메모리와 매핑한다.

  fd = open("log.txt",O_RDWR|O_CREAT,S_IRWXU);
  write(fd,&zero,sizeof(int));
  ptr = mmap(NULL,sizeof(int),PROT_READ |PROT_WRITE,MAP_SHARED,fd,0);
  close(fd);

  /* 세마포어를 생성화고 초기화 한다. */
  if( sem_init(&mutex,1,1) < 0)
    {
      perror("semaphore initilization");
      exit(0);
    }
  if (fork() == 0) { /* child process*/
    for (i = 0; i < nloop; i++) {
      sem_wait(&mutex);
      printf("child: %dn", (*ptr)++);
      sem_post(&mutex);
    }
    exit(0);
  }
  /* 부모 프로세스로 돌아간다. */
  for (i = 0; i < nloop; i++) {
    sem_wait(&mutex);
    printf("parent: %dn", (*ptr)++);
    sem_post(&mutex);
  }
  exit(0);
}
이 예제에서 related 프로세스는 동기화된 메모리를 엑세스한다. 

Unrelated 프로세스 

2개 프로세스가 서로에 대해 전혀 알려져 있지 않고 이들 사이에 어떤 관련성도 없을 때 이를 관련이 없다(unrelated)라고 얘기한다. 예를 들면 2개의 서로 다른 프로그램의 인스턴스는 관련없는(unrelated) 프로세스이다. 이런 프로그램이 공유 자원을 접근하는 경우 세마포어가 이들 접근의 동기화를 위해 사용될 수 있다. 아래의 소스 코드는 이를 설명한다.
<u>File1: server.c </u>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <stdio.h>
#include <semaphore.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

#define SHMSZ 27
char SEM_NAME[]= "vik";

int main()
{
  char ch;
  int shmid;
  key_t key;
  char *shm,*s;
  sem_t *mutex;

  //name the shared memory segment
  key = 1000;

  //세마포어 생성과 초기화
  mutex = sem_open(SEM_NAME,O_CREAT,0644,1);
  if(mutex == SEM_FAILED)
    {
      perror("unable to create semaphore");
      sem_unlink(SEM_NAME);
      exit(-1);
    }

  //이 key로 공유메모리 생성
  shmid = shmget(key,SHMSZ,IPC_CREAT|0666);
  if(shmid<0)
    {
      perror("failure in shmget");
      exit(-1);
    }

  //이 조각을 가상 메모리에 덧붙인다.
  shm = shmat(shmid,NULL,0);

  //메모리에 쓰기 시작
  s = shm;
  for(ch='A';ch<='Z';ch++)
    {
      sem_wait(mutex);
      *s++ = ch;
      sem_post(mutex);
    }

  //아래 루프는 이진 세마포어로 대체 가능하다
  while(*shm != '*')
    {
      sleep(1);
    }
  sem_close(mutex);
  sem_unlink(SEM_NAME);
  shmctl(shmid, IPC_RMID, 0);
  _exit(0);
}
<u>File 2: client.c</u>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <stdio.h>
#include <semaphore.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

#define SHMSZ 27
char SEM_NAME[]= "vik";

int main()
{
  char ch;
  int shmid;
  key_t key;
  char *shm,*s;
  sem_t *mutex;

  //name the shared memory segment
  key = 1000;

  //세마포어 생성 초기화
  mutex = sem_open(SEM_NAME,0,0644,0);
  if(mutex == SEM_FAILED)
    {
      perror("reader:unable to execute semaphore");
      sem_close(mutex);
      exit(-1);
    }

  //이 key를 사용하여 공유메모리 생성
  shmid = shmget(key,SHMSZ,0666);
  if(shmid<0)
    {
      perror("reader:failure in shmget");
      exit(-1);
    }

  //이 조각을 가상 메모리에 덧 붙인다.
  shm = shmat(shmid,NULL,0);

  //start reading
  s = shm;
  for(s=shm;*s!=NULL;s++)
    {
      sem_wait(mutex);
      putchar(*s);
      sem_post(mutex);
    }

  //다른 세마포어로 대체가능
  *shm = '*';
  sem_close(mutex);
  shmctl(shmid, IPC_RMID, 0);
  exit(0);
}
위의 코드(클라이언트/서버)는 완전히 서로 다른 프로세스 사이에서 세마포어가 어떻게 사용될 수 있는지를 설명한다. 

위의 프로그램에 덧붙여 말하자면 세마포어는 리소스 접근을 위해 서로 협조하여 사용될 수 있다. 세마포어는 뮤텍스가 아님에 유의하라. 뮤텍스는 리소스에 순차적인 접근을 허용한다. 반면에 세마포어는 순차적인 접근에 덧붙여 병렬적으로 리소스를 접근하는데 사용된다. 예를 들어 n명의 사용자가 접근 가능한 리소스 R을 생각해 보자. 뮤텍스를 사용하는 경우 리소스를 lock 그리고 unlock하기 위해 뮤텍스 m이 필요하다. 그러므로 리소스 R을 사용하기 위해 한 번에 단지 한 명의 사용자만이 가능하다. 반면에 세마포어는 n명의 사용자가 리소스 R을 동기적으로 접근하는 것을 가능하게 한다. 가장 일반적인 예가 Toilet 예제이다. 

세마포어의 다른 이점은 개발자가 메모리 내에 맵핑 될 수 있는 혹은 실행 할 수 있는 실행 객체의 실행횟수를 제한할 필요가 있는 상황에 있다. 간단한 예를 보자.
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <stdio.h>
#include <errno.h>

#define KEY 0x100

typedef union semun
{
  int val;
  struct semid_ds *st;
  ushort * array;
}semun_t;
int main()
{
  int semid,count;
  struct sembuf op;

  semid = semget((key_t)KEY,10,0666|IPC_CREAT);
  if(semid==-1)
    {
      perror("error in creating semaphore, Reason:");
      exit(-1);
    }
  count = semctl(semid,0,GETVAL);
  if(count>2)
    {
      printf("Cannot execute Process anymoren");
      _exit(1);
    }
  //세마포어를 얻고 다음 단계로 진행한다.
  op.sem_num = 0; //0번째 세마포어를 표시
  op.sem_op = 1; //락을 위해 세마포어 카운터 감소시킴
  op.sem_flg = 0; //락을 얻기까지 기다림
  if( semop(semid,&op,1)==-1)
    {
      perror("semop failed : Reason");
      if(errno==EAGAIN)
    printf("Max allowed process exceededn");
    }
  //실질적인 시작은 여기서 이루어진다.
  sleep(10);
  return 1;
}
세마포어와 뮤텍스의 차이점 

위의 소스를 보면 꽤 분명한 차이점이 나타난다. 세마포어와 뮤텍스 사이의 또 다른 확연한 차이점을 가지고 여기서 차이점에 대해 다시 한번 설명하고자 한다.
  1. 세마포어는 뮤텍스가 될 수 있지만 뮤텍스는 세마포어가 될 수 없다. 이는 간단히 말해 이진 세마포어가 뮤텍스로 사용될 수 있음을 의미하며 뮤텍스는 결코 세마포어의 기능을 보여줄 수는 없다.
  2. 세마포어와 뮤텍스(적어도 최근 커널에서는) 모두는 본래 비 재귀적이다.
  3. 세마포는 소유할 수 없는 반면 뮤텍스는 소유 가능하며 소유주가 이에 대한 책임을 진다. 이는 디버깅 관점에서 중요한 차이점이다.
  4. 뮤텍스의 경우에 뮤텍스를 소유하고 있는 쓰레드가 이 뮤텍스를 해제해야 한다. 그러나 세마포어의 경우 이러한 조건이 필요치 않다. 임의의 다른 쓰레드가 함수 sem_post()를 사용하여 세마포어를 해제하기 위한 신호를 보낼 수 있다.
  5. 정의에 따라 뮤텍스는 하나 이상의 쓰레드에 의해 동시에 실행되지 않도록 재진입코드 부분에 대한 접근을 시리얼화 하기 위해 사용한다.
  6. 개발자에게 중요한 또 다른 차이점은 세마포어는 시스템 범위에 걸쳐 있고 파일시스템상의 파일 형태로 존재한다는 점이다. 뮤텍스는 프로세스 범위를 가지며 프로세스가 종료될 때 자동으로 clean up된다.
  7. 세마포어의 본래 목적은 쓰레드 뿐만 아니라 관련 혹은 비 관련 프로세스를 동기화 하는데 있다. 뮤텍스는 쓰레드간의 동기화에만 사용되며 최대 관련(related)프로세스(최근 커널의 pthread구현은 뮤텍스가 related 프로세스간에도 사용 가능하게 한다.)
  8. 커널 문서에 따르면 뮤텍스가 세마포어에 비해 더 가볍다. 이것이 의미하는 바는 세마포어를 사용하는 프로그램이 뮤텍스를 사용하는 프로그램에 비해 좀더 많은 메모리를 사용함을 의미한다.
  9. 사용면에서 봤을 때 뮤텍스가 세마포어에 비해 좀더 간단한 사용법을 갖는다.
생산자 소비자 문제 

생산자 소비자 문제는 세마포어의 중요성을 알려주는 아주 오래된 문제이다. 전통적인 생산자 소비자 문제를 살펴보고 이의 간단한 해법도 살펴보자. 얘기할 시나리오는 그리 복잡하지 않다. 

2개의 프로세스가 있다: 생산자와 소비자. 생산자는 정보를 데이터 영역에 집어넣는다. 반면에 소비자는 같은 영역의 정보를 소비한다. 생산자가 데이터 영역에 정보를 입력하기 위해서는 충분한 공간이 있어야만 한다. 생산자의 역할은 데이터 영역에 데이터를 입력하는 것이다. 이와 비슷하게 소비자의 역할은 데이터 영역의 정보를 제거하는 것이다. 간단히 말해 생산자는 데이터 영역에서 소비자의 영역 확보에 따라 더 많은 정보를 넣을 수 있다, 반면에 소비자는 생산자가 데이터 영역에 데이터를 넣는 것에 따라 정보를 제거할 수 있다. 

이 시나리오를 개발하기 위해 생산자와 소비자가 통신 할 수 있는 매커니즘이 필요하다. 그래서 생산자 소비자는 언제 데이터 영역에서 데이터를 읽고 쓰기를 해야 안전한지를 안다. 이를 하기 위해 사용되는 매커니즘이 세마포어 이다. 

아래의 코드에서 데이터 영역은 buffer[BUFF_SIZE]로 정의되고 버퍼 크기는 #define BUFF_SIZE 4로 정의된다. 생산자 소비자 모두 이 데이터 영역에 접근한다. 데이터 영역의 크기는 4로 제한된다. 시그널을 위해서는 POSIX 세마포어가 사용되고 있다.
#include <pthread.h>
#include <stdio.h>
#include <semaphore.h>

#define BUFF_SIZE 4
#define FULL 0
#define EMPTY 0
char buffer[BUFF_SIZE];
int nextIn = 0;
int nextOut = 0;

sem_t empty_sem_mutex; //생산자 세마포어
sem_t full_sem_mutex; //소비자 세마포어

void Put(char item)
{
  int value;
  sem_wait(∅_sem_mutex); //버퍼를 채우기 위한 뮤텍스 획득

  buffer[nextIn] = item;
  nextIn = (nextIn + 1) % BUFF_SIZE;
  printf("Producing %c ...nextIn %d..Ascii=%dn",item,nextIn,item);
  if(nextIn==FULL)
    {
      sem_post(&full_sem_mutex);
      sleep(1);
    }
  sem_post(∅_sem_mutex);

}

void * Producer()
{
  int i;
  for(i = 0; i < 10; i++)
    {
      Put((char)('A'+ i % 26));
    }
}

void Get()
{
  int item;

  sem_wait(&full_sem_mutex); //버퍼에서 소비할 뮤텍스 획득

  item = buffer[nextOut];
  nextOut = (nextOut + 1) % BUFF_SIZE;
  printf("t...Consuming %c ...nextOut %d..Ascii=%dn",item,nextOut,item);
  if(nextOut==EMPTY) //its empty
    {
      sleep(1);
    }

  sem_post(&full_sem_mutex);
}

void * Consumer()
{
  int i;
  for(i = 0; i < 10; i++)
    {
      Get();
    }
}

int main()
{
  pthread_t ptid,ctid;
  //세마포어 초기화

  sem_init(∅_sem_mutex,0,1);
  sem_init(&full_sem_mutex,0,0);

  //생산자 소비자 쓰레드 생성

  if(pthread_create(&ptid, NULL,Producer, NULL))
    {
      printf("n ERROR creating thread 1");
      exit(1);
    }

  if(pthread_create(&ctid, NULL,Consumer, NULL))
    {
      printf("n ERROR creating thread 2");
      exit(1);
    }

  if(pthread_join(ptid, NULL)) /* 생산자가 끝나기를 기다림*/
    {
      printf("n ERROR joining thread");
      exit(1);
    }

  if(pthread_join(ctid, NULL)) /* 소비자가 끝나기를 기다림*/
    {
      printf("n ERROR joining thread");
      exit(1);
    }

  sem_destroy(∅_sem_mutex);
  sem_destroy(&full_sem_mutex);

  //main 쓰레드 종료

  pthread_exit(NULL);
  return 1;
}
결론 

세마포어와 뮤텍스의 차이점 뿐만 아니라 세마포어의 다양한 종류에 대해서도 살펴보았다. 개발자가 뮤텍스를 사용할지 세마포어를 사용할지를 결정하고자 할 때 그리고 System V와 POSIX 세마포어 사이의 전환에 이 글은 도움을 준다. 위 예제에서 사용된 API에 대한 좀더 상세한 설명은 관련 man 페이지를 참조하라. 

참조 

http://www.dcs.ed.ac.uk/home/adamd/essays/ex1.html 
http://www.die.net/doc/linux/man/man7/sem_overview.7.html 
http://www.csc.villanova.edu/~mdamian/threads/posixsem.html 
http://www.cim.mcgill.ca/~franco/OpSys-304-427/lecture-notes/node31.html 
리눅스 맨 페이지 
리눅스 커널 문서 

저자 Vikram shukla는 7년반 이상의 객체 기반 언어를 사용한 개발에 대한 경험을 가지고 있으며 현재 뉴저지의 Logic Planet에서 컨설턴트로 일하고 있다. 

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