2008년 10월 17일 금요일

POSIX semaphore

POSIX 함수에 대해 글 쓴 지가 너무 오래 전이라 까맣게 잊고 있었다. 오늘은 두서 없이 세마포어라는 놈을 건들여보도록 하자. 일단 세마포어 하면 유명한 예제가 있다. 철학자라는 것들이 가서 포크랑 나이프랑 한 벌 씩 더 가져오면 해결할 수 있는 문제를 굳이 앉아서 남이 썼던 것 더럽게 씻지도 않고 빌려쓰고... 암튼 그런 드러운 문제를 해결하기 위해 세마포어가 탄생했다...라고 하는데 드럽긴 마찬가지고 좀 그렇다. 세마포어 개념은 안드로메다 같은 곳에서 찾아보길 바란다. 그래도 귀찮은 사람은 세마포어를 들어갈 수 있는 사람 수를 한정한 매점(PX?)이라고 생각해도 좋을 것이다. '세마포어 값'은 매장에 들어갈 수 있는 사람 수 여분을 뜻한다.

일단 세마포어도 SysV에서 뛰쳐나온 IPC개념인데, 일전에 알아봤던 것과 비슷한 API를 제공한다. semget, semop, semctl 이 바로 그것인데, 역시나 유명한 녀석이라 자세한 설명은 생략한다. 대충 semget으로 커널에 세마포어 객체 만들어달라고 떼를 쓴 뒤에 semop을 통해 세마포어 값을 증감하고, semctl을 통해 커널에 세마포어 객체 파괴하여 정리하는 역을 한다. 이 역시 IPC이므로 명시적으로 세마포어 객체를 커널에서 제거하지 않으면 리부팅할 때까지는 커널자원을 차지한다. - 설명 끝 -

이번에도 SysV는 별 관심사는 아니고, 세마포어 개념을 거의 그대로 가져온 POSIX API가 있다. 아, 그전에 미리 설명해야할 것이 있는데, 세마포어에도 이름이 있는 녀석(named-)이 있고, 없는 녀석(unnamed-)이 있다. 뭐더게 이름 있고 없는 녀석을 만들었는지는 각자가 판단해야할 것 같지만, 보통 이름이 있는 녀석은 세마포어를 만든 녀석과 그것을 공유하고 싶은 녀석끼리 별 관련 없을 때 사용하고, 이름 없는 녀석은 동일 프로세스 내 쓰레드끼리 또는 부모자식 같은 관계가 있을 경우에만 공유해서 사용하고 싶을 때 사용한다. 이름이 있냐 없냐 따라 생성 및 해체 API가 다르지만, 락을 걸고(값 감소), 푸는(값 증가) API는 동일하다.

먼저 이름이 있는 녀석부터 알아보자. 이름이 있는 녀석은 마치 세마포어 객체를 파일처럼 관리한다. 물론 정확히 파일은 아니므로, read/write/close 함수를 쓸 수 없다. 초기화는 sem_open 함수를 이용하며 형태는 아래와 같다.
sem_t* sem_open(const char* name, int oflag, mode_t mode, unsigned int value);

name은 이름 있는 녀석이므로 당연히 이름이 들어가야하고, 공유메모리처럼 '/'으로 시작하는 파일명을 써주는 것을 추천하는 바이다.

oflag는 열 때 어떤 방식으로 여는 가인데, O_CREAT, O_EXCL 말고는 쓸 일이 거의 없을 것 같다.

mode는 접근제어로 'S_'로 시작하는 매크로를 OR연산으로 묶어서 사용할 수 있다. 이것 역시 공유메모리 그것처럼 사용하는 것이다.

value는 세마포어 초기값을 설정할 때 사용한다.

당연한 이야기이겠지만, mode, value는 oflag에 O_CREAT를 설정해야만 의미가 있으며, man페이지에서 sem_open이 다른 모습으로 같이 소개하는 것도 있다. 뭐 이렇다~하는 정도만 알고 넘어 가자.

제대로 초기화를 끝내면 sem_t 포인터 형으로 세마포어 컨텍스트를 퉤!하고 뱉어준다. 물론 실패하면 NULL과 함께 그에 상응하면 errno를 세팅한다.

이렇게 만들어진 세마포어 객체는 sem_wait, sem_timedwait, sem_trywait 등을 통해 락을 걸고, sem_post를 통해 락을 푼다. 가용 락 개수는 sem_getvalue를 통해 얻을 수 있으며, 이것이 가지는 초기값은 sem_open에서 준 value와 동일하다.

잘 사용하고 더 이상 쓸 필요가 없는 세마포어 객체는 sem_close 함수를 통해 닫을 수 있지만, 없어지진 않는다. 파일과 마찬가지로 생성한 뒤에 닫는다고 파일이 없어지는 것이 아니듯, sem_unlink를 통해 세마포어 객체를 OS에 반환 신청을 해야한다. 물론 파일과 마찬가지로 반환신청했다고 바로 없어지는 것은 아니고, 해당 세마포어를 참조하는 프로세스가 모두 없어져야 비로소 세마포어 객체가 사라진다.

다음은 이름이 없는 세마포어. 이것 역시 이름 있는 녀석과 살짝 다르게 sem_t 객체를 미리 만들어놓고 시작해야한다. sem_t 라는 구조체 내부는 POSIX에서 정의하지 않았으며, 알 필요도 없다. 다만 사용범위에 따라 동일 프로세스 안에 쓰레드끼리 공유하는 객체로 사용하려면, 쓰레드끼리 같이 접근할 수 있는 메모리 영역(예를 들면 전역변수!)에 만들어야하며, 부모자식 프로세스끼리 공유하기 위해서는 semget+semat이나 shm_open+mmap으로 얻은 공유메모리에 만들지 않으면 원하는 결과를 얻을 수 없다. 이게 좀 귀찮다.

아무튼, 이름 없는 세마포어는 sem_open/sem_close 대신 sem_init/sem_destroy를 사용한다. sem_init은 다음과 같다.
int sem_init(sem_t* sem, int pshared, int value);

sem은사용범위에 따라 전역변수 또는 공유메모리에 선언한 변수 포인터이다.

pshared는 프로세스끼리 공유하는지 여부를 알려주는 플래그이며, sem을 전역변수 선언하여 쓰레드끼리만 쓰는 것이라면 '0'을, 그렇지 않고 부모자식 프로세스끼리 공유까지 열어주려면 '0' 이외 값으로 설정한다.

value는 sem_open에서 설명한 value와 동일하다. 세마포어 초기값이다.

논외지만, pshared라는 값을 지정해줘야 하는 까닭은 API 내부 입장에서 sem이 전역변수영역인지 공유메모리영역인지 모르며 또한 프로세스끼리 공유한다고 치면 sem_t 내부에 레퍼런스 카운터 같은 것이 있어 그것을 증감시켜야하기 때문이 아닐까 조심스레 추측해본다. 아님 말고~ (딴따라당스러워~)

sem_destroy를 할 때는 반드시 락을 걸려고 시도하는 쓰레드가 없어야 한다. 또한 sem_destroy를 하지 않으면, 구현방식에 따라 메모리가 좔좔 샐 수도 있다고 하니 주의하도록 하자.

예제도 있으면 참 좋겠지만, 귀찮은 관계로 그냥그냥 넘어가자.


덧글: 살째기 재밌는 사실 하나 덧붙이자면, Linux2.6에서 sem_open하면 shm_open처럼 /dev/shm에 파일이 생긴다.

관련글