실행 도중에 다시 호출되어도 안전한 함수(reentrant function). 인터럽트가 있는 환경에서 필수 개념이다.
어떤 상황을 말하는가
메인 루프가 f() 를 실행하는 중
↓
인터럽트 발생 → ISR 이 같은 f() 를 호출
↓
ISR 의 f() 가 끝나고 메인의 f() 로 돌아옴
↓
메인의 f() 가 올바르게 이어지는가? ← 이것이 재진입성
깨지는 예
static char buf[32]; // ★ 공유 상태
char *format(int n) {
sprintf(buf, "%d", n);
return buf;
}
메인: format(100) 실행 → buf = "100"
↓ 인터럽트!
ISR: format(999) 실행 → buf = "999"
↓ 복귀
메인: buf 를 읽는다 → "999" ✗ 조용히 잘못된 값
크래시가 나지 않고 값만 틀리기 때문에 원인을 찾기가 매우 어렵다.
재진입 함수의 조건
- ① static / 전역 변수를 수정하지 않는다
- ② 자기 자신의 주소를 반환하지 않는다 (호출자마다 다른 결과여야)
- ③ 하드웨어 자원을 배타적으로 점유하지 않는다
- ④ 재진입하지 않는 함수를 호출하지 않는다 ← 전염된다
④가 놓치기 쉽다. 내가 아무리 잘 만들어도 안에서 malloc이나
printf를 부르면 재진입성이 깨진다.
표준 라이브러리 — 대부분 재진입하지 않는다
| 함수 | 왜 |
|---|---|
strtok | 내부 static 포인터로 위치를 기억 |
malloc / free | 전역 힙 자료구조를 수정 |
printf | 전역 스트림 버퍼 |
rand | 내부 시드 상태 |
asctime, ctime | static 버퍼 반환 |
errno | 전역 변수(스레드 로컬로 구현되기도) |
ISR 안에서 printf를 부르면 안 되는 이유가 여기에 있다
(느린 것도 이유지만 재진입성이 더 근본적이다).
재진입 버전이 별도로 제공되기도 한다 — strtok_r, rand_r.
newlib은 -DREENTRANT 계열 설정으로 재진입 변형을 제공한다.
재진입성과 스레드 안전성 — 다르다
자주 혼동되지만 같은 개념이 아니다.
int counter = 0;
pthread_mutex_t m;
void increment(void) {
pthread_mutex_lock(&m);
counter++;
pthread_mutex_unlock(&m);
}
| 스레드 안전? | 재진입? | |
|---|---|---|
| 위 함수 | ○ (락으로 보호) | ✗ |
왜 재진입하지 않나 — ISR이 락을 잡은 상태에서 같은 함수에 재진입하면 자기가 잡은 락을 자기가 기다린다 → 데드락. 락은 다른 스레드의 개입을 막지만, 같은 실행 흐름의 재진입은 못 막는다.
스레드 안전 : 여러 스레드가 동시에 불러도 안전 (락으로 달성 가능)
- 재진입 — : 하나의 실행 흐름이 중첩해 불러도 안전 (락으로 달성 불가)
재진입 ⊂ 스레드 안전 (인 경우가 많지만 위처럼 예외가 있다)
이 구분을 설명할 수 있으면 면접에서 확실히 좋은 인상을 준다.
어떻게 만드나
// ✗ 재진입 불가
char *format(int n) { static char buf[32]; … return buf; }
// ✓ 호출자가 버퍼를 제공한다 ← 임베디드의 표준 패턴
void format(int n, char *buf, size_t len) { snprintf(buf, len, "%d", n); }
"상태를 함수 밖으로 빼서 호출자에게 맡긴다" 는 것이 핵심이다. 지역 변수(스택)만 쓰면 호출마다 별도 공간이 생기므로 자동으로 재진입 가능하다.
실무 원칙
✅ ISR에서 부르는 함수는 재진입 가능해야 한다 ✅ 공유 데이터는 임계 구역으로 보호하고, 그 구간을 최대한 짧게 ✅ ISR은 플래그만 세우고 실제 처리는 메인 루프로 (deferred 처리) ❌ ISR에서 printf, malloc, 긴 라이브러리 호출 금지
가장 안전한 설계는 "ISR과 메인이 같은 함수를 공유하지 않는 것" 이다. 공유가 불가피하면 재진입 가능하게 만들거나 인터럽트를 차단한다.