동기화
- 동기화는 어떤 작업을 atomic operation으로 만들어 주는 것을 의미한다
- 해당 작업을 원자처럼 쪼갤 수 없는 것을 의미한다
- 해당 작업이 끝날 때 까지 컨텍스트 스위칭, 멀티 코어에서는 다른 코어가 끼어드는 일이 일어나지 않는 것과 같다
- atomic한 작업을 critical section이라고도 한다
- 동기화는 세마포어, 뮤텍스, 스핀락으로 구현할 수 있다
세마포어
- 세마포어는 Test, Release 두개 함수로 구현할 수 있다
- Test 함수는 critical section에 진입, 즉 세마포어를 잠글 수 있는지 확인하는 함수이다
- Release 함수는 세마포어의 잠금을 푸는 역할을 한다
# kernel/synch.c
#define DEF_SEM_MAX 8
static int32_t sSemMax;
static int32_t sSem;
void Kernel_sem_init(int32_t max)
{
sSemMax = (max <= 0) ? DEF_SEM_MAX : max;
sSemMax = (max >= DEF_SEM_MAX) ? DEF_SEM_MAX : max;
sSem = sSemMax;
}
bool Kernel_sem_test(void)
{
if (sSem <= 0)
{
return false;
}
sSem--;
return true;
}
void Kernel_sem_release(void)
{
sSem++;
if (sSem >= sSemMax)
{
sSem = sSemMax;
}
}
- sSem 변수는 현재 critical section에 들어올 수 있는 컨텍스트 수를 의미한다
- init에서 max가 1로 들어오면 critical section에 한 개의 컨텍스트만 들어올 수 있음을 의미한다
- 세마포어도 커널 API로 구현해보자
# kernel/Kernel.c
void Kernel_lock_sem(void)
{
while (!Kernel_sem_test())
{
Kernel_yield();
}
}
void Kernel_unlock_sem(void)
{
Kernel_sem_release();
}
- 세마포어가 lock된 상태에서는 yield 함수를 호출해 스케줄링한다
- 이렇게 해야 해당 세마포어를 점유하고 있는 다른 태스크가 실행되어 잠금을 풀어줄 수 있다
뮤텍스
- 뮤텍스는 바이너리 세마포어의 일종이다
- 뮤텍스에는 소유의 개념이 있다
- 세마포어에는 소유 개념이 없으므로 누가 잠근 세마포어이든 누구나 잠금을 풀 수 있다
- 뮤텍스는 뮤텍스를 잠근 태스크만 그 잠금을 풀 수 있다
- 즉, 뮤텍스는 소유의 개념이 생긴 바이너리 세마포어로 볼 수 있다
typedef struct KernelMutex_t
{
uint32_t owner;
bool lock;
} KernelMutex_t;
void Kernel_mutex_init(void);
bool Kernel_mutex_lock(uint32_t owner);
bool Kernel_mutex_unlock(uint32_t owner);
- mutex 구조체에는 owner와 lock이 선언되어 있다
- mutex lock, unlock에서도 owner를 받는다
static KernelMutex_t sMutex;
void Kernel_mutex_init(void)
{
sMutex.owner = 0;
sMutex.lock = false;
}
bool Kernel_mutex_lock(uint32_t owner)
{
if (sMutex.lock)
{
return false;
}
sMutex.owner = owner;
sMutex.lock = true;
return true;
}
bool Kernel_mutex_unlock(uint32_t owner)
{
if (owner == sMutex.owner)
{
sMutex.lock = false;
return true;
}
return false;
}
- mutex의 lock은 true가 잠긴 상태를 의미한다
- lock 함수에서도 기존 뮤텍스가 이미 lock인 경우 false를 리턴한다
- unlock에서는 onwer를 검사하고 잠금을 해제한다
- mutex도 커널 API로 만들어보자
void Kernel_lock_mutex(void)
{
while (true)
{
uint32_t current_task_id = Kernel_task_get_current_task_id();
if (false == Kernel_mutex_lock(current_task_id))
{
Kernel_yield();
}
else
{
break;
}
}
}
void Kernel_unlock_mutex(void)
{
uint32_t current_task_id = Kernel_task_get_current_task_id();
if (false == Kernel_mutex_unlock(current_task_id))
{
Kernel_yield();
}
}
- mutex 커널 API에서는 현재 task ID를 뮤텍스 API에 알려주는 역할을 포함하고 있다
- 현재 task id를 받아오는 Kernel_task_get_current_task_id 함수는 아래와 같이 구현할 수 있다
uint32_t Kernel_task_get_current_task_id(void)
{
return sCurrent_tcb_index;
}
스핀락
- 스핀락은 busy waiting 개념의 critical section 보호 기능이다
- busy waiting은 CPU를 점유한 상태에서 락이 풀리는 것을 대기한다는 의미이다
- 세마포어, 뮤텍스의 경우 접근이 막힌경우 yield를 호출해 다음 태스크를 스케줄링한 것과 다르다
- busy waiting 방식은 싱글 코어 환경에서는 사용할 수 없다
- 스핀락 구현은 busy waiting 자체가 완전히 atomic 해야 하므로 배타적 메모리 연산을 지원하는 어셈블리로 구현해야 한다
- 여러 스레드의 Race Condition을 방지하기 위함이다
- sSpinLock 값을 읽으면서 동시에 업데이트를 atomic하게 하는 어셈블리 명령어가 필요하다
- 우리는 C 언어 의사 코드로 구현해보자
static bool sSpintLock = false;
void spin_lock(void)
{
while (sSpintLock);
sSpintLock = true;
}
void spin_unlock(void)
{
sSpintLock = false;
}
- 대기할 때 스케줄러를 호출하지 않고 while loop로 CPU를 점유한 채 기다린다
[참고] Race Condition
- 둘 이상의 스레드, 프로세스가 공유된 자원에 동시에 접근해 조작할 때, 그 실행 순서에 따라 결과가 달라지는 현상
- 즉, 어떤 스레드가 먼저 실행되느냐에 따라 프로그램 결과가 예측 불가능하게 바뀌는 버그의 일종이다
- Race Condition은 읽기-수정-쓰기가 atomic하게 이루어지지 않아서 생기는 문제이다
- SpinLock의 예시로 Race condition이 생기면 아래와 같이 동작한다
- 스레드 A가 sSpintLock이 false라고 확인
- 스레드 A가 sSpintLock을 true로 바꾸기 직전에 스케줄러에 의해 스레드 B 실행
- 스레드 B도 sSpintLock이 false라고 확인
- 스레드 B가 sSpintLock을 true로 바꾸고 critical section에 진입
- 다시 제어권을 얻은 스레드 A는 이전에 확인한 결과를 바탕으로 sSpintLock에 true를 쓰고 critical section에 진입
- 서로 다른 두 스레드가 서로 모두 lock을 획득했다고 착각하고 동시에 critical section에 진입해 데이터 일관성이 깨질 수 있다