[임베디드 OS 개발 프로젝트] 13. 동기화

Sijin·2025년 7월 23일

동기화

  • 동기화는 어떤 작업을 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이 생기면 아래와 같이 동작한다
    1. 스레드 A가 sSpintLock이 false라고 확인
    2. 스레드 A가 sSpintLock을 true로 바꾸기 직전에 스케줄러에 의해 스레드 B 실행
    3. 스레드 B도 sSpintLock이 false라고 확인
    4. 스레드 B가 sSpintLock을 true로 바꾸고 critical section에 진입
    5. 다시 제어권을 얻은 스레드 A는 이전에 확인한 결과를 바탕으로 sSpintLock에 true를 쓰고 critical section에 진입
  • 서로 다른 두 스레드가 서로 모두 lock을 획득했다고 착각하고 동시에 critical section에 진입해 데이터 일관성이 깨질 수 있다

0개의 댓글