본문 바로가기
3_ 매콤한 컴퓨터세상

무결성, 쓰레드, 뮤텍스, 세마포어 에 관한 간단한 정리

by 준환이형님_ 2012. 8. 18.


들을때마다 가물가물 해서 좋은 내용들을 찾아 포스팅합니다. 저는 BD관련 공부를 하고 있어요.



출처 : 잇힝국대통령(sungeuns) http://cafe.naver.com/cmenia/2627,

   블링블링 (cabsoft88) http://blog.naver.com/cabsoft88?Redirect=Log&logNo=90115377264,

   네이버지식백과



■ 무결성(integrity)


데이터 및 네트워크 보안에 있어서 정보가 인가된 사람에 의해서 만이 접근 또는 변경 가능하다는 확실성. 무결성 대책은 네트워크 단말기와 서버의 물리적 환경 통제, 데이터 접근 억제 등의 엄격한 인가 관행을 유지하는 것이다. 한편, 데이터 무결성은 열, 먼지, 전기적 서지(surge)와 같은 환경적 해이에 의해 위협받을 수 있는데, 데이터 무결성의 물리적 환경 대책은 네트워크 관리자만의 서버 접근, 케이블 혹은 콘넥터와 같은 전송선의 외부 접촉 방지 대책, 전력선 서지, 정전기 방전, 자력으로부터 하드웨어와 저장 장치들을 보호하는 것을 말하며, 데이터 무결성의 네트워크 관리 대책은 현재의 인가 수준을 모든 사람에게 유지시키고, 시스템 관리 절차, 관리 항목, 유지보수 사항을 문서화하며, 정전과 서버 장애, 바이러스 공격과 같은 불시의 재난에 대한 대비책을 세우는 것을 말한다. 



■  뮤텍스와 세마포어


이 글은 Niclas Winquist씨가 2005년에 쓴 화장실에 비유한 글을 번역하였습니다.


뮤텍스 : 뮤텍스는 화장실에 들어가기 위한 열쇠로 비유할 수 있습니다. 즉 화장실에 들어갈 수 있는 열쇠를 한 사람이 갖고 있다면, 한 번에 열쇠를 갖고 있는 그 한 사람만이 들어갈 수 있습니다. 화장실에 열쇠를 갖고 있는 사람이 들어가 볼일을 다 본 후에는 줄을 서서 기다리고 있는(대기열-큐) 다음 사람에게 열쇠를 주게 됩니다.  

 

공식적인 정의(심비안 개발자 라이브러리에서 발췌) : 뮤텍스는 한 번에 하나의 쓰레드만이 실행되도록 하는 재 입장할 수 있는 코드 섹션에 직렬화된  접근이 가능하게 할 때 사용됩니다. 뮤텍스 객체는 제어되는 섹션에 하나의 쓰레드만을 허용하기 때문에 해당 섹션에 접근하려는 다른 쓰레드들을 강제적으로 막음으로써 첫 번째 쓰레드가 해당 섹션을 빠져나올 때까지 기다리도록 합니다.

 

뮤텍스는 값이 1인 세마포어입니다.

 

세마포어세마포어는 빈 화장실 열쇠의 갯수라고 보면 됩니다. 즉, 네 개의 화장실에 자물쇠와 열쇠가 있다고 한다면 세마포어는 열쇠의 갯수를 계산하고 시작할 때 4의 값을 갖습니다. 이 때는 이용할 수 있는 화장실 수가 동등하게 됩니다. 이제 화장실에 사람이 들어갈 때마다 숫자는 줄어들게 됩니다. 4개의 화장실에 사람들이 모두 들어가게 되면 남은 열쇠가 없게 되기 때문에 세마포어 카운트가 0이 됩니다. 이제 다시 한 사람이 화장실에서 볼일을 다 보고 나온다면 세마포어의 카운트는 1이 증가됩니다. 따라서 열쇠 하나가 사용가능하기 때문에 줄을 서서 기다리고 있는 다음 사람이 화장실에 입장할 수 있게 됩니다.

 

공식적인 정의(심비안 개발자 라이브러리에서 발췌): 세마포어는 공유 리소스에 접근할 수 있는 최대 허용치만큼 동시에 사용자 접근을 할 수 있게 합니다. 쓰레드들은 리소스 접근을 요청할 수 있고 세마포어에서는 카운트가 하나씩 줄어들게 되며 리소스 사용을 마쳤다는 신호를 보내면 세마포어 카운트가 하나 늘어나게 됩니다.



■  쓰레드와 뮤텍스, 세마포어


예전에 스터디 때 뮤텍스, 세마포어에 관해 공부했는데, 자세하게 들어가지 않았었는데, 책을 읽다가 내용이 좋아서 정리해봅니다.

일단 쓰레드가 무엇인지 알아봅시다^^

서버-클라이언트 통신을 할 때, 서버는 하나 클라이언트는 여러개의 구조를 취하게 됩니다. 요때 나오는 모델이 멀티프로세스, 멀티플렉싱, 멀티쓰레드 등이 있습니다. 일단 멀티프로세스는 말그대로 서버에서  fork()를 사용하여 프로세스를 여러개 복제하여각각이 클라이언트와 통신하게 하는방식입니다. 근데 문제는 프로세스를 여러개 생성하고 돌리는 과정은 부하가 많이 걸리는 작업입니다.  따라서 클라이언트 수가 많아지만 서버는 매우 힘겨워질 수밖에 없습니다. 이렇다보니 나온것이 멀티쓰레드인데요, 쓰레드는 '경량화된 프로세스'라고 할 수 있습니다. 프로세스보다 가볍고 프로세스의 기능을 대신할수 있지요. 물론 프로세스보다는 기능이 적지만요.

프로세스와 쓰레드의 큰 차이점을 보자면, 프로세스는 fork()하여 만든 자식 프로세스와 부모 프로세스가 각각 독립적인 메모리 공간(data, heap, stack 등)을 가지지만, 쓰레드는 다른 메모리 공간은 공유하며 stack만 독립적으로 가집니다. 이런 차이점으로 쓰레드를 프로세스보다 가볍게 만들어주는데요, 왜냐면 CPU는 한번에 하나의 연산처리밖에 못하므로 context switching을 하는데, 이때 프로세스는 메모리 공간이 독립적이어서 data,heap,stack을 올렸다 내렸다 하지만, 쓰레드는 stack만 올렸다 내렸다 하면 되기 때문입니다. 이외에도 여러 이유때문에 쓰레드가 가벼우며, 생성시간도 쓰레드가 프로세스보다 20~100배정도 빨리 생성된다고 합니다. 이런 이유로 쓰레드를 사용하게 됩니다. 하지만, 쓰레드는 stack외 메모리공간을 공유하므로, 프로그래밍할때 주의를 기울여야 합니다.

또한 프로세스가 운영체제가 바라보는 일의 단위라고 한다면 쓰레드는 프로세스가 바라보는 일의단위라고 할수있기 때문에, 프로세스가 없다면 쓰레드도 존재하지 않습니다.

 

그럼 멀티쓰레드에서 문제점을 알아봅시다. 멀티쓰레드를 사용할경우, 두 개의 쓰레드에 의해 동시에 실행되면 안 되는 영역이 존재하는데, 이것을 critical section(임계영역)이라고 합니다. 쓰레드의 내부적인 문제 때문에 생기는건데요, 이것은 컴퓨터의 내부 연산처리구조를 이해하고 있어야 알수있습니다.

 

간단한 예를 들어서 컴퓨터가 덧셈하는 원리를 살펴봅시다.(그림은 못그려서 못넣었네요 ㅠㅠ)

----------------------------------------------------

int x=10;

int y=20;

y += x;

----------------------------------------------------

- 요런 간단한 코드는 결과가 y=30이 될것입니다.

이 연산을 컴퓨터 내부적으로 살펴보자면, 일단 메인메모리에 x=10, y=20 이 저장됩니다. 다음 덧셈을 위하여 y=20이 임시 메모리 영역에 저장됩니다. 이 임시 메모리 영역에서 x변수가 지니고있는 10의 값을 가지고 덧셈을 실행합니다. 그럼 임시메모리 영역에 있는값이 30이 됩니다. 이 값을 다시 y변수의 메모리 영역에 대입해주면 연산은 끝이나며 결과적으로 메인메모리에는 x=10 , y=30 이라는 값이 저장됩니다.

주의할점은 y의 영역에서 연산이 바로 이뤄지는게 아니고, 연산을 위해 덧셈을 하는 다른 메모리 영역을 이용하여 그 결과만을 y영역에 대입한다는 겁니다. 이 연산방식이 멀티쓰레드에 적용될때 문제가 있을 수 있습니다.

 

그 이유를 살펴보도록 합시다. 다음과 같은 코드가 있습니다.

-----------------------------------------------------

int i=10; //전역변수

i += 10; //쓰레드에 의해 두번실행

-----------------------------------------------------

- 여기서 A,B라는 두개의 쓰레드가 모두 동시에 위의 코드를 실행한다고 합시다.

- 일반적인 상식으로는   i+=10;     <-  요게 두번 실행되므로, 결과는 i=30이 나와야 하죠. 

- 아까 쓰레드는 stack을 제외한 영역을 공유한다고 했으므로 전역변수인 i는 data영역이므로 모든 쓰레드에 의해 접근가능합니다.

- 프로세스는 context switching을 통해 할당시간을 나누는데, 쓰레드도 마찬가지므로, 사람이 볼때 동시에 실행이지만 CPU는 한순간에 하나의 쓰레드만 실행합니다.

일단 A쓰레드는 메인메모리에 i=10이라는 값을 저장한 후, 임시 메모리에 i=10이라는 값을 옮기고, 10을 더하여 임시메모리에 있는 값을 20으로 만드는데, 여기까지는 문제가없죠. 그런다음 20을 메인메모리의 i값에 저장하려고 하는데, 이 옮겨놓는 과정이 끝나기 전에 A쓰레드가 할당받은 시간이 끝나 B쓰레드가 동작합니다. context switching이 일어난겁니다. 그러므로 A쓰레드는 다시 할당받기 전까지 다른 메모리 공간으로 옮겨집니다. 그리고 B쓰레드가 실행되기 시작합니다. B쓰레드는 아까 A와 같은일을 반복합니다. 즉 임시메모리에 10을 저장하여 10을 더하여 20을만든후, 메인메모리에 i값에 20을 써주는 거죠. 이제 메인메모리에는 i=20이라는 값이 써있습니다. 이제 context switching으로 잠시 중단되었던 A쓰레드가 마지막 작업인 임시메모리 저장된 값을 메인메모리에 쓰는 짓을 합니다. 아까 A쓰레드가 임시메모리에 20을 만들었으므로, A는 20이라는 값을 또 쓰는겁니다.  그러면 결과는 i=20이 됩니다.

 

* 왜 이런 문제가 발생했을까요? context switching을 연산 단위로 하지 않고 시간 단위로 나누었기 때문입니다.

또한 이것은 쓰레드에 의해 공유된 메모리 구조에서 시작된 문제입니다. 

덧셈을 예로들었지만 곱셈 나눗셈 등등 도 마찬가지입니다. 즉 결론을 살펴보면 critical section(임계영역) 은 동시에 접근 가능한 메모리 자체를 말하는게 아니라 그러한 메모리(전역변수)에 접근해서 연산하는 코드 영역을 말하는 것입니다.

 

* 최근의 컴퓨터들은 성능이 매우 좋아져서 이것이 문제가 크지 않을 수 있겠지만, 임베디드 쪽은 성능이 매우 낮은 cpu도 많으므로 이것은 중요한 문제가 됩니다. 또한 이것을 생각하지 않는다면 프로그램이 에러를 내포하고 있는것이므로 완벽하지가 않습니다~!!

 

* 그렇다면 어떻게 문제를 해결해야 할까요? synchronization(동기화)매커니즘을 통하여 문제를 해결하면 됩니다.

 

동기화는 두가지 측면에서 생각해볼수 있습니다.

1. 공유된 메모리에 둘이상의 쓰레드가 동시에 접근할때 생기는 문제점해결 -> 하나의 쓰레드가 공유된메모리에 접근하는 동안 다른 쓰레들이 접근하지 못하도록 막아주면 됩니다.

2. 쓰레드의 실행순서를 control -> 예를들어 A,B쓰레드가 있을때, A는 데이터를 가져다놓는쓰레드, B는 데이터를 가져가는 쓰레드라고 할때, 반드시 A쓰레드가 실행 된 후 B쓰레드가 실행되도록 하여 실행순서를 제어하는 방법을 사용하면 됩니다.

 

리눅스기반에서 쓰레드 동기화하는데 있어서 일반적 사용하는것이 바로바로바로바로!!!! 뮤텍스, 세마포어 입니다 ㅎㅎ

 

뮤텍스는 mutual exclusion을 줄여 mutex라고 부르는건데 말그대로 입니다. 저번에 스터디때 예로든것처럼 화장실을 예로들어보면, 화장실이 열려있으면 들어가서 문고리를 걸어잠그면되고, 만일 화장실을 사용하려는데 문고리가 잠겼으면 나올때까지 기다리다가, 나오면 화장실에 들어가서 문고리를 걸어잠그고 일을보는것과 같습니다. 요기서 화장실은 임계영역, 문고리는 뮤텍스라고 볼 수 있습니다. 즉 뮤텍스가 쓰레드들이 임계영역에 들어갈때 잠그고, 나갈때 풀어주도록 해주는겁니다.

 

pthread_mutex_t 타입의 데이터변수를 뮤텍스라고 표현합니다.

 

1 #include<stdio.h>

  2 #include<unistd.h>
  3 #include<stdlib.h>
  4 #include<pthread.h> // 쓰레드,뮤텍스관련
  5
  6 void * thread_increment(void * arg);
  7 char thread1[] = "A thread";
  8 char thread2[] = "B thread";
  9
 10 pthread_mutex_t mutx;
 11 //전역변수 number선언
 12 int number=0;
 13
 14 int main(int argc, char **argv){
 15     pthread_t t1,t2;
 16     void *thread_result;
 17     int state;
 18
 19     //뮤텍스를 초기화 합니다.
 20     state = pthread_mutex_init(&mutx,NULL);
 21     if(state) puts("Error mutex initialization");
 22
 23     //2개의 쓰레드를 생성합니다.
 24     pthread_create(&t1,NULL,thread_increment,&thread1);
 25     pthread_create(&t2,NULL,thread_increment,&thread2);
 26
 27     //프로세스가 쓰레드 종료후 종료되도록 하는것
 28     pthread_join(t1,&thread_result);
 29     pthread_join(t2,&thread_result);
 30
 31     printf("최종 number : %d\n",number);
 32     pthread_mutex_destroy(&mutx); //뮤텍스 종료함수입니다.
 33     return 0;
 34 }
 35
 36 //쓰레드에 의해 실행되는 루틴
 37 void *thread_increment(void *arg){
 38     int i;
 39     for(i=0;i<5;i++){ //여기 for문을 critical section이라고 할수있다!
 40         pthread_mutex_lock(&mutx);//임계영역 들어가기전 뮤텍스를 걸어줌
 41         sleep(1); // 없어도됨
 42         number++;
 43         printf("실행 : %s, number : %d\n",(char *)arg,number);
 44         pthread_mutex_unlock(&mutx);//임계영역 나가며 뮤텍스 풀어줌
 45     }
 46 }


 

컴파일 옵션은 다음과같이-D_REENTRANT -lpthread 를 쓰레드를 사용할땐 추가해야합니다.

[root@localhost net_study]# gcc -D_REENTRANT -lpthread -o mutex mutex.c


 

 

세마포어는 뮤텍스와 비슷한데, sem_t타입의 변수를 세마포어라고 합니다. 여기서는 데이터에 접근하는 쓰레드의 순서를 control하는 방법에 초점을 맞춰봅시다. 일단 0과 1만을 사용하는 바이너리 세마포어에 대해서만 알아봅시다.

여기서 볼 바이너리 세마포어는 0보다 작은값이 될 수 없는 특징을 가지고 있습니다. (즉, -1같은값이 불가능)

 

예를 들어봅시다. 

변수 하나가 존재하는데 접근하려는 쓰레드는 A,B두개입니다. A는 데이터를 채우는 작업을 하고, B는 A에의해 채워진 데이터를 가져가는 작업을 합니다. 따라서 반드시 실행순서는  A와 B가 한번씩 교대로 실행을 해야 합니다. 세마포어 초기값이 0입니다. 이때 A가 데이터를 저장시킨후 세마포어값을 증가시킵니다(세마포어는 1이됩니다.) 이때 B가 세마포어값을 감소시키고 데이터를 가져갑니다.(세마포어는 0이됩니다.) 그러므로 실행순서에있어서 동기화가 이루어졌습니다.

 

이때 순서가 바뀌어 B가 먼저 접근하게 되었다고 칩시다. 처음에 세마포어값은 0입니다.  B쓰레드가 와서 세마포어값을 하나 감소시킨후 데이터 가져가려고합니다. B가와서 세마포어값 감소시키려고 하는데 현재 세마포어값은 0입니다. 따라서 B는 대기상태로 들어가버립니다. A가 와서 데이터를 가져다놓고 세마포어값을 1로증가시키기 전까지 말입니다. 세마포어값이 1이 되면 A는 세마포어를 0으로 감소시킨후 대기상태에서 빠져나와 데이터를 가져갑니다.

결과적으로 B쓰레드가 먼저 데이터영역에 접근하는 일이 발생하지 않아 동기화가 이루어졋습니다.

 

다음은 세마포어를 이용한 예제입니다. 여기서 A쓰레드는 number를 1로 B,C스레드는 number를 0으로 만드는데, number가 1이면 데이터가 있다. 0이면 데이터가 없다고 가정한 것입니다. 즉, A쓰레드는 데이터를 가져다놓고, B,C쓰레드는 데이터를 가져간다고 가정합니다.

 

1 #include<stdio.h>

  2 #include<unistd.h>
  3 #include<stdlib.h>
  4 #include<pthread.h>
  5 #include<semaphore.h>
  6
  7 void * thread_snd(void *arg);
  8 void * thread_rcv(void *arg);
  9
 10 sem_t bin_sem; //세마포어~~
 11 int number = 0; //전역변수, 모든쓰레드 접근가능
 12
 13 //세 개의 쓰레드
 14 //여기서 A쓰레드는 number를 1로만들고, B,C는 number를 0으로 만듭니다.
 15 char thread1[] = "A thread";
 16 char thread2[] = "B thread";
 17 char thread3[] = "C thread";
 18
 19 int main(int argc, char **argv)
 20 {
 21     pthread_t t1,t2,t3;
 22     void * thread_result;
 23     int state;
 24
 25     state = sem_init(&bin_sem,0,0); //초기화, 처음에 0으로설정
 26     if(state != 0) puts("Error semaphore initialization");
 27
 28     //쓰레드를 생성합니다.
 29     pthread_create(&t1,NULL,thread_snd,&thread1);
 30     pthread_create(&t2,NULL,thread_rcv,&thread2);
 31     pthread_create(&t3,NULL,thread_rcv,&thread3);
 32
 33     //프로세스가 먼저 종료되어 쓰레드가 중간에 종료되지않도록합니다.
 34     pthread_join(t1,&thread_result);
 35     pthread_join(t2,&thread_result);
 36     pthread_join(t3,&thread_result);
 37
 38     printf("최종 number : %d \n",number);
 39     sem_destroy(&bin_sem); //세마포어 소멸
 40     return 0;
 41 }
 42
 43 void * thread_snd(void * arg){
 44     int i;
 45     for(i=0;i<4;i++){
 46         while(number != 0) //만일 number가 0이 아니라면
 47             sleep(1); // number=0이될때까지 기다립니다.
 48         number++;
 49         printf("실행 : %s, number : %d\n",(char *)arg, number);
 50         sem_post(&bin_sem); //세마포어를 1로 만드는것!!
 51     }
 52 }
 53
 54 void * thread_rcv(void * arg){
 55     int i;
 56     for(i=0;i<2;i++){
 57         sem_wait(&bin_sem); //세마포어를 0으로 만드는것!!
 58         number--;
 59         printf("실행 : %s, number : %d\n",(char *)arg, number);
 60     }
 61 }

 



 

여기서 sleep을 사용했는데, 효율적인 방법은 아닙니다. 이를 개선시키려면 세마포어를 두개 사용할수도 있습니다ㅋ 

 

* 참고 

<열혈강의 TCP/IP 소켓프로그래밍, 윤성우 저, 프리렉>