'cv'에 해당되는 글 1

  1. [OS/161] #1 Synchronization 2009/12/30

[OS/161] #1 Synchronization

Posted at 2009/12/30 02:25 // in Note/OS // by drDorothy
처음으로 할 일은 ASST1 을 위한 환경을 잡는 일입니다.
먼저 cygwin을 실행시켜서 os161 소스의 위치로 이동 후 ASST1의 환경을 만들어 줍니다.

resize_image

여기 까지 진행했으면 이제 Eclipse의 환경도 수정합니다.

resize_image

사용자 삽입 이미지






















메뉴의 [Project] → [Properties] 를 눌러서 위의 화면을 띄운다음, Build directory 부분의 ASST0을 ASST1로 바꿔줍니다.
그리고 위의 오른쪽 화면처럼 make 부분들을 추가해 줍니다. (방법은 앞선 #0의 설명과 동일합니다.)

● Semaphore 분석

우리의 첫 과제는 os161에 동기화 관련 메서드를 추가하는 것입니다.
kern/include/synch.hkern/thread/synch.c 를 살펴보면, 이미 세마포어는 구현되어 있음을 알 수 있습니다.
이것을 참고하여 LockCV(Condition Variable)를 구현하는것이 목표입니다.

그럼 세마포어를 분석해 보겠습니다.

   25 struct semaphore {

   26     char *name;

   27     volatile int count;

   28 };

   29 

   30 struct semaphore *sem_create(const char *name, int initial_count);

   31 void              P(struct semaphore *);

   32 void              V(struct semaphore *);

   33 void              sem_destroy(struct semaphore *);


위의 코드는 synch.h 에 정의된 세마포어의 선언입니다.
간단히 집고 넘어가면, sem_create( )를 이용하여 세마포어를 생성하고,
P( )를 통해서 공유자원에 접근을 제한(제한된 수의 스레드만 접근)하고,
V( )로 다음 스레드가 공유자원에 접근할 수 있도록 설정합니다.
마지막으로, sem_destroy( )를 사용하여 사용한 메모리를 해제합니다.

당연하지만, volatile int count; 로 선언한 이유는 이 변수가 레지스터에 존재하게 되면,
문맥교환(Context Switching)시 레지스터셋 교환이 일어나면서, 원하지 않은 값이 될 수 있기 때문입니다.

   62 void

   63 P(struct semaphore *sem)

   64 {

   65     int spl;

   66     assert(sem != NULL);

   67 

   68     /*

   69      * May not block in an interrupt handler.

   70      *

   71      * For robustness, always check, even if we can actually

   72      * complete the P without blocking.

   73      */

   74     assert(in_interrupt==0);

   75 

   76     spl = splhigh();

   77     while (sem->count==0) {

   78         thread_sleep(sem);

   79     }

   80     assert(sem->count>0);

   81     sem->count--;

   82     splx(spl);

   83 }


전체적인 코드는 세마포어의 개념이라 건너뛰고, 중요한 부분만 집고 넘어가겠습니다. 이 코드는 synch.c 에 있는 P( ) 연산 입니다.
코드에서 붉은색으로 표시한 부분을 눈여겨 봐야 합니다. 붉은 부분이 하는 역할은 인터럽트를 on/off 시키는 역할을 합니다.

splhigh( )를 통해서 spl값을 최고(15)로 세팅하게 되면, cpu의 인터럽트 플레그를 수정하여 인터럽트 불가 상태로 만들게 됩니다.
이렇게 되면 타이머 인터럽트가 무시되면서, 문맥교환(Context Switching)이 일어나지 않게 되고,
결과적으로 인터럽트 허용 상태가 될때 까지, 현재 스레드만 실행됨을 보장합니다.
다른 스레드가 실행되지 못하게 하여 변수에 접근조차 할 수 없기 때문에, 공유자원 접근을 제한할 수있습니다.
(위의 코드에서 공유되는 변수는 sem->count(접근가능한 스레드의 수) 가 됩니다.)

● Lock 설계

먼저 어떤 형태의 Lock을 구현할지 정해야 합니다. 우리는 Spin Lock 과 Mutex Lock 두가지에 대해서 생각해 보겠습니다.

먼저 Mutex Lock입니다. 핵심은 이미 누군가가 Lock을 사용중이면 sleep 상태로 들어가는 것입니다.
이것은 세마포어를 이용해서 쉽게 구현가능합니다.
사용자 삽입 이미지

Spin Lock 방식은 Lock 변수를 체크해서 다른 스레드가 소유중이면 무시하고, 아무도 사용중이지 않다면 획득하는 방식입니다.
이때, 다른 스레드가 Lock을 소유중이면 thread_yeild( )등을 사용하여, 다른 스레드로 CPU를 넘기면 됩니다.
그리고 다시 자신의 차례가 되었을때 위의 과정을 반복하며 Lock을 획득할 때 까지 진행합니다.

사용자 삽입 이미지
그럼 코드를 통해서 위의 구조를 구현해 보자.

  147 void

  148 lock_acquire(struct lock *lock)

  149 {

  150     int s;

  151     assert(lock != NULL);

  152     assert(in_interrupt == 0);

  153 

  154 restart:

  155     s = splhigh();

  156     if (lock->owner != NULL) {    // Lock을소유한 스레드가 있다면..

  157         splx(s);                  // spl을 복원하고 yeild 수행

  158         thread_yield();           // (yield가 내부적으로 spl을 변경)

  159         goto restart;             // while 대용 goto 루프

  160     }

  161 

  162     assert(lock->owner == NULL);  // 소유주가 없을때만 자신이 획득

  163     lock->owner = curthread;      // 스레드포인터로 소유를 구분

  164 

  165     splx(s);                      // 종료전에 인터럽트 허용모드로 전환

  166 }

  167 

  168 void

  169 lock_release(struct lock *lock)

  170 {

  171     assert(lock_do_i_hold(lock) != 0); // 소유한 스레드만이 풀기 가능

  172     lock->owner = NULL;                // 아무나 획득가능한 상태로 변경

  173 }

  174 

  175 int

  176 lock_do_i_hold(struct lock *lock)

  177 {

  178     return lock->owner == curthread; // Lock 소유주가 자신인지 테스트

  179 }


위와 같은 모양으로 만든 다음 컴파일하여 실행시키면 아래와 같은 결과가 나온다.

resize_image

걸린 시간에 연연해 할 필요 없다. 지금의 sys161 환경이 특수(?)하게 잡혀 있어서 느릴 뿐, 다음 ASST2 이후부터는 빠르게 수행된다.

중요한건 spin lock의 작동 원리이다.
sleep 상태가 아닌, 계속된 체크를 통한 Lock 획득임을 반드시 기억해야 한다.

잘못된 구현 예..


● Condition Variable 설계

다음은 윈도우의 Event / 리눅스의 pthread_cond_xxxxxx( ) 시리즈 들이다.
cv_wait( ) 의 주된 역할은 잠시 Lock을 풀고, sleep 한 다음, 다시 Lock을 획득하는 것이다.
사용자 삽입 이미지
cv_wait( ) 안에서 sleep과 unlock, lock이 동시에 이루어 지기 때문에, 실제 함수를 사용하는 입장에서는 신경쓸 필요가 없다.
내부에 잠깐의 unlock구간을 두어서, 다른 thread가 해당 lock으로 다른 작업을 수행할 틈을 주는것이다.

이것이 왜 필요한 것일까?

공유변수를 사용하는 두 thread (T1, T2)가 있다 가정하자.
그런데 T2는 T1의 계산결과를 가지고 작업을 해야할때 cv_wait를 사용한다면, T1이 신호(cv_signal)를 보낼때 까지 T2가 대기하고,
T1이 cv_signal을 사용하면 T2가 깨어나서 마저 작업을 하는 등으로 동기화를 시킬 수 있을 것이다.
(물론 위의 방법 이외에도 해법이 있으나 위의 방식으로도 가능하다는 뜻이다.)

구현하는 방법에는 여러가지가 있을 것이다.
thread 배열을 유지하여 하나씩 또는 모두 깨우는 방법도 있을 것이고, 세마포어처럼 숫자만 유지하는 방법도 있다.
그럼 두 방식을 모두 살펴보자.

먼저 세마포어처럼 기다리고 있는 스레드의 숫자만 유지하는 방식이다.

synch.h

   98 struct cv {

   99     char *name;

  100     // add what you need here

  101     // (don't forget to mark things volatile as needed)

  102     volatile int sleeper;   // 대기중인 thread의 수

  103     volatile int wakeup;    // 일어날 thread의 수

  104 };


synch.c

  313 struct cv *

  314     cv_create(const char *name)

  315 {

  316     struct cv *cv;

  317 

  318     cv = kmalloc(sizeof(struct cv));

  319     if (cv == NULL) {

  320         return NULL;

  321     }

  322 

  323     cv->name = kstrdup(name);

  324     if (cv->name==NULL) {

  325         kfree(cv);

  326         return NULL;

  327     }

  328 

  329     // add stuff here as needed

  330     cv->sleeper = 0;

  331     cv->wakeup = 0;

  332 

  333     return cv;

  334 }

  335 

  336 void

  337 cv_destroy(struct cv *cv)

  338 {

  339     assert(cv != NULL);

  340 

  341     // add stuff here as needed

  342     assert(cv->sleeper == 0);

  343     assert(cv->wakeup == 0);

  344 

  345     kfree(cv->name);

  346     kfree(cv);

  347 }

  348 

  349 void

  350 cv_wait(struct cv *cv, struct lock *lock)

  351 {

  352     int s = splhigh();

  353 

  354     lock_release(lock);

  355     cv->sleeper++;

  356 

  357 sleep:

  358     thread_sleep(cv);

  359     if (cv->wakeup == 0)    // 같은 주소로 sleep하기 때문에 별도로

  360         goto sleep;         // 실제 깨어날 숫자를 카운트 해야한다.

  361 

  362     cv->wakeup--;

  363     cv->sleeper--;

  364     splx(s);

  365 

  366     lock_acquire(lock);

  367 }

  368 

  369 void

  370 cv_signal(struct cv *cv, struct lock *lock)

  371 {

  372     int s;

  373     assert(lock_do_i_hold(lock) != 0);

  374 

  375     s = splhigh();

  376     cv->wakeup = 1;        // 하나의 thread만 깨움

  377     thread_wakeup(cv);

  378     splx(s);

  379 }

  380 

  381 void

  382 cv_broadcast(struct cv *cv, struct lock *lock)

  383 {

  384     int s;

  385     assert(lock_do_i_hold(lock) != 0);

  386 

  387     s = splhigh();

  388     cv->wakeup = cv->sleeper;    // 모든 thread를 깨움

  389     thread_wakeup(cv);

  390     splx(s);

  391 }

thread_wakeup이 어떤 주소로 잠든 모든 thread를 깨우기 때문에 별도로 thread_wakeup함수를 만들지 않고,
잠들어 있는 숫자를 유지하는 방식을 사용하였다.

Queue를 사용한 방식..


os161은 시뮬레이터를 떠나서 실제 mips 환경에서 작동하기엔 무리가 있다.
os 내부의 구현에 대해서 탐구하는것이 주 목적이라 생각하여 과도하게 성능에 집착하진 않았다.
위에서 구현한 Lock과 wakeup을 유지하는 cv의 코드는 옆에 파일을 첨부하였다.
2009/12/30 02:25 2009/12/30 02:25