기본 콘텐츠로 건너뛰기

괜찮으십니까

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에 파일이 생긴다.

관련글

댓글

이 블로그의 인기 게시물

Bash Array, Map 정리

Bash에서 Array, Map에 대한 정리. (매번 찾기 귀찮) 찾아보진 않았지만, Bash에서 Array든 Map이든 동일하게 Map(C++에서 Unordered Map)으로 동작하는 것 같다. 왜냐하면, Array의 Index가 연속하지 않아도 동작한다. 그저 Key가 0 이상의 정수인 Map이랑 비슷하게 동작한다. 예) 1, 2, 3, 9, 10 Array # 생성 declare -a empty_array declare -a ar=(haha hoho baba "long string haha hoho") # 접근 echo "ar[0]=${ar[0]}" echo "all as array=${ar[@]}" # 큰따옴표 안에서 각 원소를 따로따로 전달한다. echo "all as one=${ar[*]}" # 큰따옴표 안에서 각 원소를 문자열 하나로 합쳐 전달한다. echo "indexes=${!ar[@]}" echo "indexes=${!ar[*]}" echo "length=${#ar[@]}" echo "length=${#ar[*]}" echo "last=${ar[-1]}" echo "last=${ar[@]: -1}" # 콜론 뒤에 빈 칸이 꼭 필요하다. 옛 방식 # 현재 상황 declare -p ar #(출력) declare -a ar=([0]="haha" [1]="hoho" [2]="baba" [3]="long string haha hoho") ar[100]=hello # 인덱스를 건너 뛰어도 동작한다. declare -p ar #(출력) declare -a ar=([0]="haha" [1]="hoho" [2]="baba" [3]=&

SQLite에서 파일 크기 줄이기

간단한 개인 프로젝트를 하고 있는데, SQLite DB파일 크기가 매우 커져서 테이블에 필요 없는 레코드를 날렸다. 그런데 날리고도 파일크기가 그대로라서 여기저기 뒤져보니 VACUUM 커맨드를 사용하란다. 사용법은 매우 간단하다. 그저 "VACUUM;"이라고 날려주면 동작한다. (참조: http://sqlite.org/lang_vacuum.html ) 다만, 동작이 매우 느려서 자주 쓸만한 것은 아니다. 실제로 100MB짜리 파일을 7KB로 줄이는데 수 분이 걸렸다. 소스를 봐야겠지만, DB를 EXPORT한 뒤에, 파일을 지우고 다시 IMPORT하는게 아닐까 하는 의구심이 든다. 매번 하기 귀찮으면 "PRAGMA auto_vacuum=1;"를 하면, 새로운 빈 페이지(DELETE나 DROP TABLE 같은...)가 생길 때마다, VACUUM을 실행한다. 다만, SQLite구조 문제로 테이블을 생성하기 전에 미리 날려야하는 안타까움이 있다. (참조: http://sqlite.org/pragma.html#pragma_auto_vacuum )

std::thread에서 클래스 메소드 호출하기

C++11이 나온지 어언 3년... 그동안 GCC도 많이 발전하고 해서, 귀찮은 pthread_* 함수를 쓰는 것보다, 잘 감싼 std::thread를 써보고 있다. 딱히 커다란 장단점은 찾아볼 수 없지만, 콜백 함수 형태에 구애받지 않는다. C++11이 갖는 꽃별천지 언어특성과 표준라이브러리가 만나 그렇게 할 수 있는 것이지만, 자세한 설명은 생략한다... (응?) 보통 일반 함수를 쓸 경우, 아래와 같이 쓸 수 있다. void cb_func1(void) { /* ... */ } void cb_func2(int i) { /* ... */ } void init(void) { std::thread t1(cb_func1); std::thread t2(cb_func1, 10); t1.join(); t2.join(); } 그러면 메소드를 쓰다면...? 메소드 포인터 다음에 객체 포인터를 넣어서 해결할 수 있다. class MyThread { public: MyThread() { m_th = std::thread(&MyThread::proc, this); m_th.detach(); } private: void proc(void) { /* ... */ } private: std::thread m_th; }; 요로코롬 할 수 있다. 물론 밖에서 호출할 때는 "this" 대신에 해당 객체 포인터를 넘겨야 제대로 동작한다.