[Pintos] 타이머 인터럽트 때문에 랜덤 커널 패닉이 터진 이유와 해결 방법

2025. 5. 29. 14:31개발

 랜덤 커널 패닉? 타이머 인터럽트와 palloc의 충돌 트러블슈팅 기록

1. 문제 발생

Pintos의 Project 2에서 시스템 콜을 여러 번 실행하거나, 로드 테스트를 반복 수행하면 간헐적으로 커널 패닉이 발생했습니다. 재현이 불가능할 정도로 비결정적이며 불안정한 오류였습니다.

2. 증거 수집 (백트레이스 로그)

GDB를 활용하여 커널 패닉 시점의 백트레이스를 수집했습니다.

  • lock_acquire() 내부에서 ASSERT(!intr_context())가 터짐
  • palloc_get_multiple() 내부에서 발생
  • intr_handler → intr_entry → thread_yield → thread_current → debug_panic의 백트레이스 흐름이 반복됨

3. 원인 추측: 왜 랜덤하게 터질까?

백트레이스의 공통점을 통해 다음과 같은 사실을 파악했습니다:

  • 모든 패닉은 lock_acquire() 내부의 ASSERT(!intr_context())에서 발생
  • 즉, 인터럽트 핸들러 안에서 락을 획득하려고 했다는 의미

왜 인터럽트 핸들러가 락을 잡으려고 하지?

파악 결과, palloc_get_multiple() 함수는 내부에서 lock_acquire()를 사용하며, 이는 페이지 프레임 할당 시 발생합니다. 문제는 이 함수가 시스템콜 핸들링 도중 실행되기도 하고, 타이머 인터럽트가 발생하는 그 순간에도 실행될 수 있다는 점입니다.

정리하면:

  • 현재 스레드가 palloc_get_multiple()을 수행 중
  • 그 찰나에 타이머 인터럽트가 발생함
  • 타이머 인터럽트가 thread_tick()를 호출
  • 그 안에서 thread_yield()가 실행 → 문맥 전환 시도
  • 스케줄러가 새로운 스레드를 깨우기 위해 lock_acquire() 호출
  • 결국 인터럽트 핸들러 내부에서 락 획득 시도로 이어짐 → ASSERT 실패

4. 검증 실험

랜덤성의 원인을 타이머 인터럽트라고 의심하고, 다음 실험을 반복했습니다.

  • 특정 테스트 케이스를 20회 이상 반복 수행
  • 패닉 발생 시점과 백트레이스를 수집
  • 모두 lock_acquire()에서 동일한 원인으로 터짐

→ 타이머 인터럽트 도중 커널 락 진입 시도로 인한 패닉임을 확신

5. 해결

방법 1: palloc 전체에 인터럽트 비활성화

void *palloc_get_multiple (...) {
    enum intr_level old_level = intr_disable();
    ...
    lock_acquire(...);
    ...
    lock_release(...);
    ...
    intr_set_level(old_level);
}

단점: 불필요하게 long critical section을 만들 수 있음.

방법 2: sema_up()thread_yield() 위치를 수정

기존에는 다음과 같이 thread_yield()가 인터럽트 컨텍스트 안에서 호출될 가능성이 있었습니다.

void sema_up (...) {
    old_level = intr_disable();
    ...
    if (is_thread_init && !intr_context()) {
        thread_yield();  // 이 부분에서 터짐!
    }
    intr_set_level(old_level);
}

thread_yield()는 문맥 전환을 발생시키기 때문에 인터럽트 컨텍스트 내에서는 절대 호출되면 안 됩니다. 그러나 위 코드는 "인터럽트 컨텍스트가 아닐 때만" 호출하도록 조건을 넣었지만, interrupt가 disable 되어 있는 상태에서 context switch가 불가능함을 간과했습니다.

 구조 변경: yield를 intr_set_level 이후로 옮김

void sema_up (...) {
    old_level = intr_disable();
    ...
    bool yield_required = false;
    if (!intr_context()) yield_required = true;
    intr_set_level(old_level);
    if (yield_required) thread_yield();  // 컨텍스트 전환은 여기서 안전하게!
}

이제 스레드 양보는 인터럽트가 다시 허용된 안전한 지점에서 발생하므로, 더 이상 인터럽트 컨텍스트 내에서 커널 패닉이 발생하지 않습니다.

결론

  • 커널 내부에서 thread_yield() 호출 위치는 매우 민감함
  • 인터럽트가 발생한 상태에서 문맥 전환이 일어나면 예기치 않은 커널 패닉 유발
  • thread_yield는 반드시 인터럽트 컨텍스트 외부, 인터럽트가 enable 상태에서만 호출
문제 타이머 인터럽트가 락 획득 로직을 preempt 하면서 ASSERT 터짐
원인 락이 필요한 palloc 도중 인터럽트 발생 → thread_yield()lock_acquire()
특징 100% 재현 불가, 실행 환경마다 랜덤한 패닉 발생
검증 방법 20회 반복 테스트, 백트레이스 수집
해결 인터럽트 금지 범위 설정 / 락 획득 구간 재설계

이번 이슈는 스레드 스케줄링과 인터럽트의 상호작용을 깊이 있게 이해할 수 있었던 중요한 트러블슈팅 경험이었습니다. 우연처럼 보이는 커널 패닉의 본질은 결국 "인터럽트와 문맥 전환의 충돌"이었습니다.

커널 구조를 고려하면, 단순히 인터럽트를 막는 것보다 락 설계의 시맨틱 보장타이밍 이슈에 대한 고려가 병행되어야 합니다.