QNX RTOS: 4-9. Synchronization - Mutexes

2025. 11. 26. 21:41운영체제/QNX

Mutex는 "Mutual Exclusion"의 약자로, 한 번에 오직 하나의 스레드만 특정 코드 영역(Critical Section)이나 데이터에 접근할 수 있게 해주는 잠금 장치임.

1. 기본 사용법과 소유권 (Ownership)

  • 초기화: pthread_mutex_init()
  • 잠금: pthread_mutex_lock()
    • 이미 잠겨있으면 대기(Block).
    • 내가 잠근 걸 또 잠그려고 하면 에러(EDEADLK).
  • 해제: pthread_mutex_unlock()
    • 핵심 규칙: 잠근 놈이 풀어야 한다 (Ownership). Thread A가 잠근 걸 Thread B가 풀 수 없음. 이게 세마포어와의 결정적 차이임.
  • 파괴: pthread_mutex_destroy()
    • 누군가 잠그고 있는데 파괴하면 안 됨. (단, 내가 잠그고 내가 파괴하는 건 QNX에서 허용됨)

 

  • 자동 초기화: pthread_mutex_init()을 호출할 적절한 위치가 없을 경우, 뮤텍스 선언 시  PTHREAD_MUTEX_INITIALIZER를 할당하여 첫 사용 시 자동 초기화되도록 할 수 있음
  • 프로세스 간 공유 (Process Sharing): 프로세스 간 공유 메모리의 데이터를 보호하기 위해 뮤텍스를 공유 메모리에 저장하여 사용할 수 있음.
    • 이 경우, 뮤텍스 속성에 PTHREAD_PROCESS_SHARE 플래그를 설정해야 커널이 필요한 추적 데이터와 유효성 검사를 설정함.
    • 프로세스 공유 뮤텍스는 커널 측 객체 할당이 필요하므로 초기화 시 실패할 가능성이 있어 반환 값을 확인해야 함.
  • 뮤텍스의 한계와 주요 위험
    • 운영체제는 프로세스 간 메모리 보호를 강제하지만, 뮤텍스는 그렇지 않음.
    • 뮤텍스는 프로그래머가 접근 규칙을 유지해야 하는 협력적 도구이며, 운영체제는 뮤텍스와 데이터 간의 연관성을 알지 못함.
    • 규칙을 따르지 않아 발생하는 데이터 손상 오류는 간헐적으로 발생하며 재현하기 매우 어려움.

 

 

2. Mutex가 해결하는 문제: Race Condition

영상에서는 buf_alloc() (버퍼 할당 함수) 예제를 듦.

  • 상황: Free List(연결 리스트)에서 빈 버퍼를 찾아 할당해줌.

 

  • 문제: 두 스레드가 동시에 buf_alloc을 호출하면?
    • 둘 다 같은 "첫 번째 빈 버퍼"를 발견함.
    • 둘 다 그 버퍼를 가져가서 자기 데이터를 씀.

 

  • 결과: 데이터 오염(Corruption) 발생.

 

  • 해결: 리스트를 탐색하고 수정하는 구간 앞뒤를 Mutex로 감싸서(Lock -> Work -> Unlock), 한 명씩만 작업하도록 강제함.

 

3. 심화 이슈 I: 데드락 (Deadlock)

두 개 이상의 Mutex를 사용할 때 발생하는 교착 상태임.

  • 시나리오:
    • Thread 1: Lock(A) 성공 -> Lock(B) 대기
    • Thread 2: Lock(B) 성공 -> Lock(A) 대기
    • 결과: 서로가 가진 걸 내놓으라고 영원히 기다림.
  • OS의 한계: OS는 데드락을 자동으로 감지하거나 풀어주지 않음 (비용이 너무 큼).
  • 해결책 (Design Level): Locking Hierarchy (순서 정하기)
    • "무조건 A를 먼저 잠그고, 그 다음에 B를 잠근다"라는 규칙을 정함.
    • Thread 2가 B를 먼저 잠그려고 하면 "규칙 위반"이므로 코드를 수정해야 함. (B를 잠그려면 A부터 잠그고 와야 함)

4. 심화 이슈 II: 우선순위 역전 (Priority Inversion) & 상속 (Inheritance)

RTOS에서 가장 유명한 문제인 "화성 탐사선 패스파인더 사건"의 원인임.

4.1. 우선순위 역전이란?

  • 상황:
    • Low Priority (L): Mutex 잠금.
    • Medium Priority (M): L을 선점(Preempt)하고 신나게 실행 중.
    • High Priority (H): L이 잡고 있는 Mutex가 필요해서 대기 중.
  • 문제: H는 L이 Mutex를 놔줘야 실행되는데, L은 M한테 CPU를 뺏겨서 실행을 못 함. 결과적으로 가장 중요한 H가 중간급인 M 때문에 무한 대기하는 하극상이 발생함.

 

4.2. 해결책: Priority Inheritance (QNX 기본 동작)

  • QNX 커널은 H가 Mutex 대기에 들어가는 순간, 해당 Mutex를 잡고 있는 L의 우선순위를 H만큼 일시적으로 승격(Boost) 시켜줌.
  • 효과: L이 M보다 우선순위가 높아지므로, CPU를 할당받아 빨리 작업을 마치고 Mutex를 내놓게 됨.
  • 복귀: L이 unlock 하는 순간, 원래의 낮은 우선순위로 돌아감. H는 즉시 락을 얻고 실행됨.

5. 성능 최적화: Kernel Call 최소화

QNX의 Mutex는 성능을 위해 하이브리드 방식을 씀.

  • Fast Path (User Space):
    • pthread_mutex_lock 호출 시, 아무도 안 잡고 있으면 커널까지 안 가고 원자적 연산(Atomic Compare & Exchange)만으로 깃발 꽂고 바로 리턴함. 매우 빠름.
  • Slow Path (Kernel Space):
    • 이미 누가 잡고 있어서 대기(Block)해야 하거나, 복잡한 설정이 있으면 그때서야 커널(SyncMutexLock)을 호출함.

교훈: Mutex를 짧게 잡고 빨리 놔줄수록, Fast Path를 탈 확률이 높아져 시스템 전체 성능이 좋아짐.


💡 정리 및 인사이트

  1. 소유권: Mutex는 잠근 스레드만이 풀 수 있음. 이 특징 덕분에 Priority Inheritance가 가능함. (누굴 승격시켜야 할지 아니까)
  2. 데드락 방지: 여러 락을 쓸 땐 무조건 순서(Hierarchy)를 지켜라.
  3. 임계 영역 최소화: 락 구간은 짧을수록 좋음. 길어지면 병렬성의 이점이 사라지고 우선순위 역전 가능성만 높아짐.