이로또

임베디드 리눅스 드라이버의 이해 7편: 세마포어 경쟁과 커널 동기화 메커니즘 본문

임베디드

임베디드 리눅스 드라이버의 이해 7편: 세마포어 경쟁과 커널 동기화 메커니즘

이로또 2025. 6. 4. 20:38

이 글은 리눅스 커널에서 발생하는 Race Condition 문제와 이를 해결하기 위한 동기화 기법들(세마포어, 뮤텍스, completion, spinlock, seqlock 등)을 정리한 글입니다. 각각의 메커니즘이 어떻게 동작하고, 어떤 상황에 적합하며, 실제 커널 코드에서는 어떻게 사용하는지를 예제와 함께 설명합니다. 공유 자원 보호를 위한 커널 스레드 동기화 실습 예제도 포함됩니다.

목차

  1. Race Condition과 Critical Section
  2. 동기화 방식 구분: Blocking vs Busy Waiting
  3. 세마포어 (Semaphore)
  4. 뮤텍스 (Mutex)
  5. 커널 스레드와 공유 자원 보호 실습 예제
  6. Completion: 작업 완료 대기
  7. Spinlock: 빠른 임계 구간 보호
  8. Seqlock: 읽기 성능 최우선 락

1. Race Condition과 Critical Section

  • Race Condition: 두 개 이상의 쓰레드가 동시에 공유 자원에 접근하여 결과가 예측 불가능해지는 상황
  • Critical Section: Race Condition이 발생할 수 있는 코드 영역

2. 동기화 방식 구분

방식 도구 설명
Blocking (잠들기) semaphore, mutex, rw_semaphore, completion context switch 발생 가능, CPU 자원 절약
Busy Waiting (불야성) spinlock, seqlock 짧고 빠른 작업에 적합, 루프를 돌며 기다림

3. 세마포어 (Semaphore)

  • 여러 개의 자원에 대한 동시 접근 제어
  • P 연산: 값을 1 감소시키고, 자원을 점유함 -> 즉, P 연산은 "나 지금 이 자원 써도 돼?" 라고 묻는 과정
함수 설명
void down(struct semaphore *sem) 무조건 기다림. signal 무시
int down_interruptible(struct semaphore *sem) 기다리다가 signal 오면 빠져나옴. → 리턴값 체크 필요
int down_trylock(struct semaphore *sem) 지금 당장 못 얻으면 실패하고 리턴 (대기 없음)
  • V 연산: 값을 1 증가시키고, 자원을 반납
void up(struct semaphore *sem);
  • sema_init()로 초기화
struct semaphore sem;
sema_init(&sem, 1);
down(&sem); // 자원 획득
// 공유 자원 사용
up(&sem);   // 자원 반납

4. 뮤텍스 (Mutex)

  • 항상 1개의 자원만 보호
  • mutex_init(), mutex_lock(), mutex_unlock() 사용
  • 잠근 스레드만 해제 가능 → 더 안전함
struct mutex my_lock;
mutex_init(&my_lock);
mutex_lock(&my_lock);
// 공유 자원 사용
mutex_unlock(&my_lock);

세마포어 vs 뮤텍스 비교

항목 세마포어 뮤텍스
0 이상 (N개 자원) 항상 Binary
해제 권한 누구나 잠근 스레드만
사용 용도 다중 접근 자원 단일 자원

5. 커널 스레드 실습 예제 (mutex 사용)

  • 두 개의 커널 스레드가 공유 변수 acme_global_variable을 증가시킴
  • mutex_lock()으로 임계 구간 보호

주요 흐름 요약

구성요소  설명
mutex 공유 자원 보호
kthread_run() 커널 스레드 생성
mutex_lock() / mutex_unlock() 자원 접근 시 동기화

6. Completion: 작업 완료 대기

  • "다른 스레드가 어떤 작업을 끝낼 때까지 기다리는" 용도로 사용
  • wait_for_completion()으로 대기
  • complete()로 완료 통지
DECLARE_COMPLETION(my_comp);
wait_for_completion(&my_comp);
complete(&my_comp);

completion vs mutex/semaphore

항목  completion mutex semaphore
목적 작업 완료 대기 자원 접근 동기화
대기 일회성 반복적 사용

7. Spinlock

  • 짧은 시간 동안만 보호하는 임계 구간에 적합
  • 짧은 시간 동안만 잠글 자원을 보호할 때 사용하는 락(Lock)
  • 락이 안 풀려도 "기다리지 않고 계속 확인(루프)" 하며 도는 형태
  • 일반적인 mutex는 잠들기 때문에 context switch(문맥 전환) 발생 → 이건 시간/자원 소모가 큼
  • 스핀락은 잠들지 않고 루프를 돌면서 기다림 락이 곧 풀릴 것 같을 때 훨씬 빠르고 효율적
  • spin_lock_irqsave() / spin_unlock_irqrestore() 사용
  • context switch 없이 busy-wait
spinlock_t my_lock;
unsigned long flags;
spin_lock_irqsave(&my_lock, flags);
// critical section
spin_unlock_irqrestore(&my_lock, flags);

8. Seqlock

  • 읽기-쓰기 동기화(read-write synchronization)를 위한 락
  • Reader가 Writer보다 많을 때 유리  → writer를 좀 더 우대
  • Reader는 락 없이 읽고, 중간에 Writer가 끼면 재시도

사용 예

unsigned int seq;
do {
    seq = read_seqbegin(&mylock);
    // 데이터 읽기
} while (read_seqretry(&mylock, seq));

Writer는 seqlock을 이렇게 사용한다

데이터를 쓰는 쪽(Writer)은 임계 구간에 들어가기 전에 lock을 걸고,

다 쓰고 나올 때 lock을 해제한다.

이 과정에서 seqlock 내부의 "시퀀스 번호"가 바뀐다.


Writer가 lock을 걸 때 조건

  • 다른 Writer가 없다면 → lock 획득 성공 → 쓰기 가능
  • 다른 Writer가 이미 쓰고 있다면 → lock 못 잡고 기다림
  • Reader가 있는 건 상관없음 → Reader는 그냥 읽고 있기 때문에 Writer가 무시하고 진입해도 됨

시퀀스 값의 의미

  • Writer가 임계 구간에 진입하면, seq 값을 홀수로 바꿈 → "지금 쓰는 중이다" (seq값이 홀수면 다른 writer가 있다는 소리다)
  • Writer가 빠져나오면, seq 값을 짝수로 바꿈 → "쓰기 끝났다"

즉, 이 숫자가 홀수냐 짝수냐만 보고도 Reader는 "지금 누가 쓰고 있는가?" 를 알 수 있다.

특징 요약

항목 설명
Reader 락 없음 빠름, 재시도 필요
Writer 시퀀스 번호 갱신, write_seqlock() / write_sequnlock() 사용

이 글을 통해 리눅스 커널에서 다양한 동기화 메커니즘이 어떤 상황에서 쓰이는지, 각 방식의 차이점과 특징을 실습 중심으로 이해할 수 있습니다.