[TIL/크래프톤 정글] DAY 61

배재준·2025년 5월 9일

크래프톤 정글 - TIL

목록 보기
54/93
post-thumbnail

2025.05.09

TIL(TODAY I LEARN)


  • 오늘한 내용 : OS - 동기화(Synchronization)

  • WEEK 09 : 정글 끝까지(PintOS) - Threads


Synchronization

동기화(synchronization)는 다수의 스레드나 프로세스가 동시에 하나의 자원(메모리, 파일, I/O 장치 등)에 접근할 때 발생할 수 있는 경쟁 상태(race condition)와 데이터 손상을 방지하기 위해, 접근 순서상호 배제를 보장하는 기법

기법

1. 인터럽트 비활성화(Disabling Interrupts)

  • 가장 원시적이면서도 강력한 동기화 수단
  • CPU가 외부 인터럽트 신호에 전혀 반응하지 않도록 잠시 차단함으로써, 스레드 선점(preemption) 자체를 물리적으로 불가능하게 만든다.
    • 핀토스는 “선점형 커널”
  • 커널 모드에서는 명시적으로 인터럽트를 끄고 켜야 한다.
  • include/threads/interrupt.h에 선언
    함수설명
    enum intr_level intr_disable(void);현재 인터럽트 상태를 저장한 뒤, 인터럽트를 꺼버린다.
    enum intr_level intr_enable(void);현재 상태를 저장한 뒤, 인터럽트를 켠다.
    enum intr_level intr_set_level(enum intr_level level);INTR_OFF 또는 INTR_ON으로 상태를 설정하고, 이전 상태를 반환한다.
    enum intr_level intr_get_level(void);현재 인터럽트 활성화 여부(INTR_ON/INTR_OFF)를 반환한다.

2. 세마포어(semaphores)

  • 음이 아닌 정수(counter)와 이를 원자적으로 조작하는 두 개의 연산자(P, V)로 구성
    • 내부 값(count)이 곧 “허용된 동시 접근(또는 발생 가능한 이벤트 횟수)”을 나타낸다.

    • count가 0이면 더 이상 자원 접근(또는 이벤트 기다림)이 불가능하며, 대기 중인 스레드는 블록(block)된다.

      연산이름동작
      Pdowncount > 0이 될 때까지 대기(블록) → count–
      Vupcount++ → 만약 블록된 스레드가 있으면 하나를 깨워(unblock) 다시 준비 상태로 전환
    • P(↓)

      1. count가 0이라면 스레드를 대기 큐(waiters)에 넣고 블록한다.
      2. count가 1 이상이 되면 바로 count–를 수행하고, 임계 구역(공유 자원)으로 진입한다.
    • V(↑)

      1. count++를 수행한다.
      2. 대기 큐에 블록된 스레드가 하나라도 있다면, 가장 오래 기다린(또는 우선순위가 높은) 스레드를 깨워준다.
  • 코드 예
    struct semaphore sema;
    
    void threadA(void) {
      sema_down(&sema);  // B가 up 해줄 때까지 블록
      // B 완료 후 수행할 작업…
    }
    
    void threadB(void) {
      // 작업 수행…
      sema_up(&sema);    // A를 깨움
    }
    
    int main(void) {
      sema_init(&sema, 0);
      thread_create("A", PRI_DEFAULT, threadA, NULL);
      thread_create("B", PRI_DEFAULT, threadB, NULL);
    }
    
    초기값(value)처음 sema_down() 동작count 변화차이점
    0sema_down()을 호출한 스레드는 바로 블록된다. (카운터가 0이기 때문)변화 없음 (대기)“B가 sema_up()을 호출해야만 A가 깨어난다.”
    1sema_down()을 호출한 스레드는 블록 없이 즉시 진입한다. (카운터가 1이므로)count가 0으로 감소“락(lock)처럼, 한 스레드만 진입시키고 다음엔 대기시킨다.”
    >1sema_down()을 호출한 만큼(예: value=2→두 번) 대기 없이 진입한다.호출 횟수만큼 count가 감소“최대 n개 스레드(자원)까지 동시 접근을 허용한다.”
  • include/threads/synch.h에 선언
    함수설명
    void sema_init(struct semaphore *s, unsigned value);세마포어 객체 svalue로 초기화
    void sema_down(struct semaphore *s);P 연산 수행. 값이 0이면 블록 후 대기, 양수가 되면 즉시 count–
    bool sema_try_down(struct semaphore *s);블록 없이 즉시 P 연산 시도, 성공 시 true/실패 시 false 반환
    void sema_up(struct semaphore *s);V 연산 수행. count++ 후 대기 스레드 하나를 깨움
  • 연산 도중 중단되면 안 되므로, 내부적으로는 인터럽트 비활성화(intr_disable()/intr_enable() )로 원자성(atomicity)을 보장
  • lib/kernel/list.c의 연결리스트를 이용해 스레드 대기열 유지

3. 락(Lock) == 뮤텍스(Mutex)

  • 락(lock)은 초기값이 1인 세마포어와 유사
  • 제약을 더하여 사용성을 높임
    • 소유자(owner) 개념: 락을 획득한 스레드만이 락을 해제할 수 있다.
    • 비재귀성(non-recursive): 이미 획득한 락을 다시 획득하려 하면 오류가 발생한다.
  • include/threads/synch.h 에 선언
    함수설명
    lock_init(struct lock *lock);락을 초기화한다.
    lock_acquire(struct lock *lock);락을 획득할 때까지 블록.
    bool lock_try_acquire(struct lock *lock);즉시 락 획득 시도, 실패 시 false 반환.
    lock_release(struct lock *lock);락 소유권을 해제.
    bool lock_held_by_current_thread(const struct lock *lock);현재 스레드가 락을 소유했는지 확인.
  • 상호 배제(mutex)가 필요한 임계 구역 진입/탈출에서 주로 사용한다.
  • 소유자 제약 덕분에 세마포어보다 버그 예방이 쉽다.

4. 모니터(Monitors)

  • 모니터는 자체 과 하나 이상의 조건 변수, 그리고 보호할 데이터를 하나의 추상적 단위로 묶은 동기화 구조
  • 스레드는 보호된 데이터에 접근하기 전 모니터 락을 획득해야 하며, 작업 후에는 락을 해제한다.
  • DeadLock과 busywaiting 방지 가능
  • 동작 방식
    • 락 획득 시도
      • lock_acquire() 호출 → 락이 이미 잡혀 있으면 락 대기열에 들어가 블록
      • 락이 풀리면 대기열에서 빠져나와 락을 획득하고 임계 영역에 진입
    • 조건 검사 & 대기
      • 락 내부에서 “지금 처리할 수 없는 상태”라 판단되면
        1. cond_wait() 호출
        2. 자동으로 락 해제 → 조건 변수 대기열에 들어가 블록
    • 조건 신호
      • 다른 스레드가 cond_signal() 또는 cond_broadcast() 호출 → 조건 변수 대기열 중 하나(또는 전부)를 깨움
      • 깨운 스레드는 준비 큐로 이동
      • 나중에 스케줄러가 이 스레드를 선택하면, cond_wait() 함수가 복귀하며 다시 락을 획득한 뒤 임계 영역에 진입
    • 락 해제
      • lock_release() 호출 → 락을 반납
      • 락 대기열에 있던 스레드 중 하나를 thread_unblock() 으로 준비 큐에 올려 스케줄러가 실행하도록 함
  • include/threads/synch.h 선언
함수설명
모니터 락
void lock_init(struct lock *lk);락 객체를 초기화한다.
void lock_acquire(struct lock *lk);락을 획득할 때까지 블록(block)한다.
bool lock_try_acquire(struct lock *lk);블록 없이 즉시 락 획득을 시도, 성공 시 true, 실패 시 false.
void lock_release(struct lock *lk);락 소유자(owner)가 락을 해제한다.
조건 변수
void cond_init(struct condition *cv);조건 변수 객체를 초기화한다.
void cond_wait(struct condition *cv, struct lock *lk);모니터 락을 해제하고 대기, 신호 수신 시 락을 재획득한다.
void cond_signal(struct condition *cv, struct lock *lk);대기 중인 스레드 하나를 깨운다.
void cond_broadcast(struct condition *cv, struct lock *lk);대기 중인 모든 스레드를 깨운다.

5. 최적화 배리어(Optimization Barrier)

  • 컴파일러 최적화가 변수 접근 순서를 변경하거나 제거해, 의도와 다른 실행이 될 수 있음. 최적화 배리어를 사용해 이러한 재배열을 방지함.
  • 예시
    • 타이머 틱(ticks) 대기 루프에서 while (ticks == start) barrier();로 무한 루프 최적화 방지.
    • 단순 루프가 제거되지 않도록 while (loops-- > 0) barrier();를 사용.
  • 매크로 :include/threads/synch.h에 정의된 barrier() 매크로가 최적화 배리어 역할을 한다.

0개의 댓글