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회 반복 테스트, 백트레이스 수집 |
| 해결 | 인터럽트 금지 범위 설정 / 락 획득 구간 재설계 |
이번 이슈는 스레드 스케줄링과 인터럽트의 상호작용을 깊이 있게 이해할 수 있었던 중요한 트러블슈팅 경험이었습니다. 우연처럼 보이는 커널 패닉의 본질은 결국 "인터럽트와 문맥 전환의 충돌"이었습니다.
커널 구조를 고려하면, 단순히 인터럽트를 막는 것보다 락 설계의 시맨틱 보장과 타이밍 이슈에 대한 고려가 병행되어야 합니다.
'개발' 카테고리의 다른 글
| Pintos Project 3: Anonymous Page 구현 흐름 집중 해부 (1) | 2025.06.12 |
|---|---|
| Pintos VM Project - lazy_load_segment & fork-read 디버그 (0) | 2025.06.05 |
| [Pintos] userprog- exec 개발하면서 겪었던 문제 (0) | 2025.05.27 |
| 궁금증 pintos에서 Condition Variable(condvar)의 동작방식과 존재이유 (0) | 2025.05.25 |
| pintos project 1 threads (0) | 2025.05.25 |



