본문 바로가기
ATOMIC - Critical Section Mutex Semaphore

식당 System에 대해서 다시 한번 살펴 보는 시간을 갖도록 하겠사옵니다. waiter에게 식당의 온도도 확인하는 일을 시켜 본다고 생각해 보시지요. 식당의 온도 System은 자동으로 온도를 맞춰 주는 최신식 온도 System이 설치되어 있고요. 일단은 1초에 한번씩 interrupt를 발생시켜서, 온도를 두 번 읽는 ISR을 만든다고 예를 들어 보시죠. 한번은 문가의 온도계를, 한번은 실내의 온도계를 읽어서 저장한다고 했을 때, waiter에게 주어진 일은 문가의 온도와, 실내의 온도가 차이가 나면 수리공한테 전화를 하라고 시켜 놓는다고 한다면,
 
평소에는 거의 두 개의 온도가 차이가 없어야 하겠지요.
 
int curr_temp[2];
 
void read_temperature_isr(void)
{
      curr_temp [0] = read_temp_door();  /* 문가의 온도계를 읽어 옴 */
      curr_temp [1] = read_temp_indoor();  /* 실내의 온도계를 읽어 옴 */
}
 
요렇게 isr에서 curr_temp array에 각각 읽은 data을 넣어두고요, waiter에게는 계속~ 이 Data를 확인해서, 두 개 값이 틀린 경우에만, 수리공에게 전화를 한다고 한다면,
 
 
void waiter_task()
{
     int temp_door;
     int temp_indoor;
    
     waiter_task_init();   
                                   
     while (1)
     {
            temp_door = curr_temp [0];  /* ISR의 Data */
            temp_indoor = curr_temp [1];  /* ISR의 Data */
 
            if (temp_door != temp_indoor)
               {
                     call_repair();  /* 수리공에게 전화를! */
                }
      } /* while (1) */
}
 
 
요런 식으로 일을 시키면 되겠지요. 별로 문제가 없어 보이긴 하는데요, 혹시 문제점을 찾으셨는지요? 문제는 waiter_task()의 아래 위치에서 interrupt가 발생했을 때가 문제 입니다요. 
 
            temp_door = curr_temp [0];
            → 요기에서 curr_temerature_isr()이 발생! 한다면?
            temp_indoor = curr_temp [1];
 
뭐, 그래도 별로 문제는 없어 보이지요. 헌데, 만약에, 문가의 온도가 원래 30도였었는데,
온도가 31도로 올라간 경우에 평소 같으면 temp_door나, temp_indoor 모두 30도를 가르 키고 있겠지요.
하지만, ISR이 저 중간에 발생한 경우에는 어떻게 될까요? 이전 ISR에서 curr_temp[0]와 curr_temp[1]을 모두 30도로 맞춰 두었었는데, 31로 변하는 순간에 문제의 위치에서 ISR이 발생했다면, curr_temp[0]와 curr_temp[1]이 31도로 바뀔 테죠. temp_door는 이전의 30도를 가지고 있을 것이고, temp_indoor는 31도를 가리키고 있을 것이지요. 이러면 문제가 없는데도, waiter는 수리공에게 전화를 때릴 겁니다.
 
수리공이 전화를 받고 와보면, 문제가 없어 보이지요. 하지만, 수리공이 돌아가면 이런 문제가 또 발생하는 거에요. 몇 번 이런 일이 반복되면 수리공이 짜증을 내겠죠! 이런 문제를 일컬어 공유데이터 문제라고 하는데 ISR과 일반 code가 Data를 공유하게 되면 이런 문제가 발생해요. 이런 문제를 해결하려면 어떻게 하면 좋을까요? 아~주 간단한 방법으로 해결 할 수 있습니다요. waiter_task()에서 data를 읽어올 때 Interrupt가 안 걸리도록 Interrupt_lock을 거는 거에요. 우리 이거 ARM 제어의 구현에서 구현해 본 적이 있죠. ARM의 CPSR의 Status를 바꾸어서, Interrupt가 걸리지 않도록 하는 방법 말이죠.
 
 
void waiter_task()
{
     int temp_door;
     int temp_indoor;
    
     waiter_task_init();   
                                   
     while (1)
     {
            Interrupt_Lock();
            temp_door = curr_temp [0];
            temp_indoor = curr_temp [1];
            Interrupt_Free();
 
            if (temp_door != temp_indoor)
               {
                     call_repair();  /* 수리공에게 전화를! */
                }
      } /* while (1) */
}
 
이런 식으로 Data를 만질 때 Interrupt가 걸리지 않도록 하는 Code를 삽입하면, 문제가 발생하지 않겠지요. 이런 Interrupt가 결려서는 안 되는 Code의 영역을 Atomic 영역이라고 부르고요, 이런 ISR과 일반 Code사이의 공유 Data가 있을 때는 반드시! Atomic 영역을 만들어 놓고 사용해야 해요.
 
이런 Atomic 영역을 다른 말로는 Critical Section이라고 부르고요, 이런 Critical Section에 진입하게 되면, ISR이나 다른 Task들도 이 Critical Section의 수행이 끝나기 전에는 Critical Section을 사용할 수가 없어요. 용어가 좀 헷갈리겠지만, 이런 식으로 말할 수 있겠죠. 이 Critical Section에는 Atomic이 제대로 되고 있는 건가? 라고 말할 수 있겠네요. 이런 Critical Section을 동기화라는 용어와 같이 많이 사용되는데, 동기화라는 건 참으로 난감한 표현이 아닐 수 없겠습니다. 뒤에도 나오겠지만, Hardware Level에서의 Synchronous와 Asynchronous의 표현이 또 있긴 한데 여기서의 Synchronization의 의미는 도대체 뭔지 참.내.
 
순서를 정한다는 말과 일맥 상통한다고 보면 되는데, Data를 동시에 접근할 수 없게 만들고, 해당 Data를 만지는데 순서를 잘 정해서 만질 수 있도록 해준다는 의미라고 이해 하는 것이 가장~ 적당할 것 같네요. 뭐, Semaphore라든가, Mutex라든가 하는 말이 엄청 많이 쓰이는데, 이 녀석들도 모두 이런 문제를 해결하기 위해서 사용하는데, 이런 Atomic한 Critical Section을 어떻게 운용할 것이냐 하는 것에 따라 Semaphore나 Mutex 뭐 이런 걸로 나눠지는 거죠. 음하하. 지금은 ISR과 Task간의 Data 공유 문제로서, Critical Section에 Interrupt Lock을 걸어서 공유 Data가 아무렇게나 만져지는 문제를 해결했지만, RTOS는 Kernel service로 Task 끼리 발생하는  이런 문제를 쉽게 해결할 수 있게 하기 위해서 Semaphore나, Mutex등을 제공하는 거지요. Semaphore나 Mutex를 이용해서 공통으로 사용하는 Resource를 사용할 때 여러 개의 Task가 동시에 Critical Section에 진입하지 못하게 하고요, Critical Section을 사용하려는 Task들에게 일종의 순번을 쥐어 주는 거지요. 먼저 선점한 녀석이 사용을 다 하고 나면, 그 다음에 쓰려고 한 녀석이 Critical Section에 진입하고, 뭐 그런 식으로 동시에 Resource에 Access하지 못하게 하는 것들이에요.
 
Semaphore는 철도의 신호기를 말하고요, Mutex는 Mutual Exclusion의 약자이고요, Mutex는 Semaphore의 변종이라는 것만 알아두세요. RTOS의 종류에 따라 그 구현도 다르게 되어 있으니까, 그 핵심만 따지고 넘어가는 게 좋을 듯 하네요. 일단, Semaphore는 철도의 신호기에서 따온 만큼 그 이름의 유래를 살펴 보는 게 좋겠지요. 어느 역에서 철로가 하나밖에 없는 구간이 있으면, 여러 개의 철로가 하나밖에 없는 구간을 공유하고 있다면, 기차가 지나갈 때, 신호기를 켜 놓으면 기차가 못 지나 가게 하고, 신호기를 꺼 놓으면 기차가 지나갈 수 있게 해 놓는다면 하나밖에 없는 구간에서 사고가 일어나지 않겠죠. 이걸 Idea로 Semaphore를 구현하게 되는데요, 이런 신호기처럼 Kernel Service로 두 개의 함수를 보통 제공해요. 이름은 천차만별일 테니까, 제 마음대로 이름을 정한다면..
 
ObtainSemaphore()와 ReleaseSemaphore()라고 이름 지으면, read_temperature_task()와 waiter_task()를 아래와 같이 다시 구현할 수 있겠네요.
 
 
void read_temperature_task(void)
{
     while (1)
     {
         task_sleep (1000);      /* 1초에 한번 씩 다시 자기 자신을 호출해서 실행 함 */    
            ObtainSemaphore();
           curr_temp [0] = read_temp_door();  /* 문가의 온도계를 읽어 옴 */
           curr_temp [1] = read_temp_indoor();  /* 실내의 온도계를 읽어 옴 */
            ReleaseSemaphore();
     }
}
 
 
void waiter_task(void)
{
     int temp_door;
     int temp_indoor;
    
     waiter_task_init();   
                                   
     while (1)
     {
         task_sleep (1500); /* 1.5초에 한번씩 다시 자기 자신을 호출해서 실행 함 */
            ObtainSemaphore();
            temp_door = curr_temp [0];
            temp_indoor = curr_temp [1];
            ReleaseSemaphore();
 
            if (temp_door != temp_indoor)
               {
                     call_repair();  /* 수리공에게 전화를! */
                }
      } /* while (1) */
}
 
한 Task에서 ObtainSemaphore()가 호출되면 그 다음에 다른 Task에서 btainSemphore()를 만나게 되면 Semaphore를 얻어오지 못하니까, 더 이상 Task를 진행하지 않고, 기다리게 되고, ReleaseSemaphore()가 호출될 때 까지 무작정 기다리게 되는 형식이에요. 그러니까, 예제에서는 둘 중에 하나만 ObtainSemaphore()를 호출하게 되면 나머지 한 Task에서 ObtainSemaphore()가 불리는 순간에 다른 녀석이 Semaphore()를 Obtain해 간 상태이므로, 기다리게 되는 거죠. ReleaseSemaphore()를 호출하는 순간에 Obtain하려고 기다리던 녀석이 치고 들어가는 거에요. 이런 걸 Binary Semaphore라고 해요. 한번에 한 녀석만 Semaphore를 획득할 수 있어서 그렇고요. 이런걸 Mutex라고 부르는 거에요. Lock과 Unlock 두 가지만 있는 거에요. Mutex는 원래 Semaphore를 획득한 Task에서 Release를 꼭 해줘야 하는 특징이 있어요. 이런 Semaphore를 사용하는 System에서 여러 개의 Task가 동시에 Semaphore Obtain을 하려고 기다리고 있다고 했을 때 Semaphore를 Release한 순간 어떤 녀석이 Semaphore를 가져갈 수 있을 것인가는 OS마다 틀린 데, 어떤 System에서는 가장 오래 기다린 녀석이 가져가거나, 어떤 System에서는 가장 Priority가 높은 녀석이 가져간다든가 하는 여러 가지 전략이 있으니까, 사용하시는 RTOS는 어떤 전략을 쓰는지 확인하시는 게 신상에 좋을 거에요. ㅋ. 그러면, 원래 Semaphore는 한개 이상을 획득할 수 있는 건가요! 그렇습니다. Binary Semaphore는 Mutex라고 부르지만 일반적인 Semaphore는 Obtain을 여러 개의 Task가 동시에 할 수 있고, 그 개수가 한정되어 있는 경우도 있고, 아닌 경우도 있고 그래요. System을 구성하기에 따라서 여러 개의 Task가 한꺼번에 Semaphore를 Obtain 가능하게 하는 건데요. 글쎄요, 이거 좀 불안해서 이런 거 잘 안 쓰고 기냥~ Mutex로 자알~ 구성해 본 답니다요. 이거 외에, Spinlock이라는 것도 있는데요, CPU가 여러 개 일 때 여러 개의 CPU가 한 개의 Resource에 접근할 때 사용하는 거에요. Spinlock은 여러 개의 CPU가 여러 개의 Kernel로 동작을 하게 되면, Task Level로 기다리게 한다던가 하는 게, 좀 어려우니까, 한 개의 CPU가 Resource를 Spinlock 걸어 놓고 사용하고 있으면, 나중에 사용하려고 했던 CPU는 Spin (뺑~ 돌면서) 하면서 Spinlock이 해제될 때 까지 기다리는 거에요. 이런 경우에 Spinlock 걸어 놓은 상태에서는 나머지 CPU는 놀게 되는 거죠.
 
 
아.. 정말 이런 류의 얘기는 어려울 수 밖에 없는데, Concept만 잘 잡고 가셔도, 어디 가서 꿀리지(?)는 않으니까, 다행이라고 생각하시고 더 자세한 내용은 구글링이나 네이버질 또는 여러가지 OS 책자에서 지식 습득을 하시는 게, 훨씬 나으리라는 생각을 가지고 이만 접습니다. 채찍질은 하지 말아주세요. 네네.

Linked at 친절한 임베디드 시스템 개발자.. at 2009/10/01 12:45

... ⓛ ATOMIC - Critical Section, Mutex, Semaphore ... more

Commented by kyle at 2009/08/25 10:24
어떤OS는 Binary SEM과 Mutex SEM을 명확이 구분하는데요. Mutex SEM은 한정된 resource를 여럿 task가 공유할때 data corruption을 방지하기 위해서 사용합니다. 위에서 들어주신 예제가 mutex SEM 에 해당되고요. 보통 SEM을 가진 TASK 만 다른 TASK에 SEM을 give 할수 있습니다.

그와 달리 Binary SEM 은 Mutual exclusion 용이 아니라 TASK간 실행시점을 얄려주기 위해서 많이 사용합니다. 대표적인 예로는 A라는 TASK가 B/C/D 라는 TASK를 동시에 unblock 시켜주기위해서 사용할수 있지요. 즉 A가 SEM give하는 순간 B/C/D 세개을 TASK가 동시에 sem Take가 가능합니다. 세개의 TASK을 동시 start해주었지요. ( mutex sem이 1 대 1ㅣ로 주고 받는것과는 차이가 있지요 )

아무튼 그런 embedded OS도 있어요.
( 올려주시는 히언님글은 너무 감사히 꼬박꼬박 읽고 있습니다. 좋을글 계속 올려주세요)
Commented by 히언 at 2009/08/26 20:29
아하하~ 넹 그렇지요~ 이런 감사한 코멘트 저야말로 영광입니다~
※ 이 포스트는 더 이상 덧글을 남길 수 없습니다.
친절한 임베디드 시스템 개발자 되기 강좌 글 전체 리스트 (링크) -



댓글





친절한 임베디드 개발자 되기 강좌 글 전체 리스트 (링크) -