18.4.2.2. 데이터 오염 방지: `irqsave()` / `irqrestore()`를 이용한 인터럽트 마스킹(Masking) 및 임계 구역(Critical Section) 보호

18.4.2.2. 데이터 오염 방지: irqsave() / irqrestore()를 이용한 인터럽트 마스킹(Masking) 및 임계 구역(Critical Section) 보호

고주파수 멀티 스레딩(Multi-threading) 문맥과 예측 불가능한 비동기 하드웨어 센서 인터럽트(IRQ)가 격렬하게 난무하는 하드 리얼타임(Hard Real-time) PX4 비행 제어기 코어 환경에서, 전역 메모리 공간을 공유하는 uORB 링 버퍼는 필연적으로 심각한 동시성(Concurrency) 파괴 위협에 노출된다. 만약 두 개 이상의 퍼블리셔(Publisher) 스레드나 인터럽트 핸들러 루틴이 동일한 센서 토픽 버퍼에 동시에 진입하여 물리적 쓰기(Write) 작업을 시도한다면, 타이밍이 겹치는 그 찰나 구조체 메모리가 갈가리 찢어지는 치명적인 데이터 오염(Data Corruption)이 발생할 것이다. 본 절에서는 이러한 생존의 파열을 원천 차단하기 위해 uORB 커널 시스템이 운영체제 하단에서 즐겨 뽑아드는 전가의 보도, 즉 irqsave()irqrestore() 매크로를 활용한 하드웨어 인터럽트 마스킹 레이어 기반의 임계 구역(Critical Section) 방어 아키텍처를 심층 해부한다.

1. 소프트웨어 락(Software Lock)의 구조적 한계와 인터럽트의 무자비함

통상적인 데스크탑 범용 리눅스나 고수준 POSIX 애플리케이션 프로그래밍에서는 동시성 제어를 위해 뮤텍스(Mutex)나 세마포어(Semaphore)라는 소프트웨어 락 메커니즘을 널리 적극 활용한다. 그러나 PX4 비행 제어 코어 생태계, 그중에서도 혈관 역할을 하는 가장 밑바닥 uORB 데이터 파이프라인의 중심에서는 이들을 철저히 이단시하고 배제한다. 그 이유는 뮤텍스가 커널 스케줄러에 의존하기 때문에 본질적으로 “하드웨어 인터럽트 문맥(Interrupt Context ISR)“에서는 절대로 사용될 수 없기 때문이다.

  • 블로킹 타임아웃의 물리적 치명성: 초당 1,000Hz(1ms 주기)로 사정없이 쏟아지는 SPI 드라이버 자이로 센서 하드웨어 인터럽트(ISR) 루틴이, 찰나의 순간 EKF2 스레드가 선점해 쥐고 있는 uORB 링버퍼 뮤텍스를 대기열에서 얌전히 기다리며 블로킹(Blocking)된다면 어떻게 될까? 시스템 메인 클럭이 여지없이 꼬이고 워치독(Watchdog) 타임아웃 하드웨어 타이머가 폭발하여 기체의 로터 출력이 꺼지고 즉시 지상으로 추락(Crash)한다.
  • 우선순위 역전(Priority Inversion)의 딜레마: 가장 높은 생존 우선순위의 비행 코어 태스크가 엉뚱하게 최하위 우선순위 텔레메트리 송신 태스크가 쥐고 놓지 않는 뮤텍스를 하염없이 기다리다, 전체 시스템 병목 버스가 연쇄적으로 마비되는 기형적 낭패 현상을 낳을 위험이 상존한다.

이러한 재난을 완벽히 해결하기 위한 커널의 유일하고도 절대적인 물리적 방어 수단은, 느려터진 운영체제 소프트웨어 스케줄러를 개입시키지 않고 아예 CPU 프로세서 코어 자체의 하드웨어 귀를 일시적으로 100% 틀어막아버리는 인터럽트 마스킹(Interrupt Masking) 전략뿐이다.

2. irqsave()irqrestore(): 찰나의 시간 정지 레지스터 마법

uORB 코어 소스 코드 내부 깊숙한 uORBDeviceNode::write 트랜잭션 영역에서는, 이 거친 세계의 데이터 오염을 완벽히 차단하기 위해 불과 수 마이크로초(us) 단위로 코어의 시간을 완전히 멈추어버리는 아래의 C 마스킹 매크로 구조가 신앙처럼 반드시 수반된다. (현대 PX4에서는 이종 플랫폼 이식성을 위해 px4_enter_critical_section 등의 래퍼 API를 사용하나 그 하부 본질은 NuttX 커널의 irqsave 레지스터 훅과 정확히 동일하다.)

// 임계 구역(Critical Section) 방어 메커니즘의 커널 정석 (마이크로 개념화 코드)
irqstate_t flags = irqsave(); // [1] 무결성 결계의 시작 (인터럽트 봉인)

// =================== [Critical Section 영역] ===================
// 오직 현재 진입에 성공한 단일 스레드 문맥만이 이 물리 공간을 절대적으로 독점한다.
unsigned head = _generation % _queue_size;         // 버퍼 위치 연산
memcpy(&_buffer[head * _o_size], buffer, _o_size); // 완전 무결성 데이터 안전 복사
_generation++;                                     // 세대 카운터 증가 원자적 수학적 보장
// ===============================================================

irqrestore(flags); // [2] 결계의 물리적 해제 (인터럽트 원복)

2.1 단계: irqsave()의 시스템 절대적 선점 거부

irqsave() 구문이 코드 상에서 마침내 호출되는 순간, ARM Cortex-M 마이크로프로세서의 CPSR(Current Program Status Register) 특수 레지스터 내 전역 인터럽트 마스크 비트 플래그가 물리 스위치처럼 강제 비활성화된다. 이는 지금부터 CPU가 시스템 타이머 틱(Systick) 스케줄러 요청이나 타 센서 핀 하드웨어의 무자비한 인터럽트 요청 명령을 일절 듣지 않고 일시 수신 거부(Blind) 처리함을 거만하게 선포하는 것이다.
따라서 이 명령문 이후에 이어지는 C++ 코드는 이 우주에서 가장 강력한 권위의 물리 락(Physical Lock)에 걸리게 되며, 그루터기처럼 절대 중간에 선점(Preemption)당하거나 교차 스텝(Context Interleaving)을 밟혀 잘리지 않고 언제나 처음부터 끝까지 하나의 덩어리 묶음(Atomic Execution)으로 온전히 실행됨을 기계적으로 보장받는다. 또한 함수는 인터럽트를 끄기 직전의 기존 CPU 레지스터 상태 스냅샷을 flags 변수 안에 고스란히 백업(Save) 박제해 둔다.

2.2 단계: irqrestore()의 우아하고 안전한 상태 복구

수 나노초 간의 숨 막히지만 강력했던 임계 구역 링버퍼 memcpy 물리 이식이 완수되면, 곧이어 irqrestore(flags)를 호출하여 백업해 두었던 이전 상태의 레지스터 비트 맵 스냅샷을 코어 레지스터에 다시 부어 넣는다.
단순히 “지금부터 모든 인터럽트를 강제로 다시 무조건 켜라“는 무식한 명령어인 irqenable()을 쓰지 않는 이유는 아키텍처적 배려에 있다. 만약 이 함수 밖의 더 큰 거시적 외부 호출자(Caller) 루틴이 스케줄링 상 원래부터 인터럽트를 이미 끈 채로 이 노드에 진입했을 수 있기 때문이다. 백업된 flags만을 조심스럽게 복원하는 방식은, 코드가 여러 겹으로 중첩 호출(Nested Calling)되는 복잡한 재귀적 환경에서도 시스템의 기존 상태 기계(State Machine)를 해치지 않는 우아한 논리 복구를 보장한다.

3. 시스템 모어 생존을 위한 임베디드 절대 철칙: 짧고 순식간에 끝낼 것

이처럼 irqsave/restore 매크로 쌍은 그 어떤 교착 상태 데드락(Deadlock)의 아찔한 고민이나 위험 없이도 세상에서 가장 완벽하고 가벼운 무결성(Integrity)을 즉시 제공하지만, 그 자체로 지독한 맹독을 품고 시스템을 망칠 수 있는 양날의 검이다.

만약 바보 같은 프로그래머가 저 임계 구역 방어막 안으로 들어가서 지루하고 타임이 긴 printf 콘솔 출력을 때리거나, 무거운 센서 폴링(Polling) 대기 지연 while 루프를 무한정 돈다면 어떻게 될까? 전체 CPU 코어가 그 시간 동안 전역 인터럽트를 일절 받지 못하므로 운영체제 커널 컴포넌트들의 시계가 비틀리고, 하드웨어 I/O 통신 버퍼가 통째로 영구 결손되며 결국 시스템 전체가 데드락 패닉(Kernel Panic)에 빠져 드론은 땅으로 추락하고 만다.

그렇기에 PX4 uORB 아키텍처의 엘리트 코어 설계자들은, 저 신성불가침의 마스킹 구역 내부를 오직 단 한 번의 번개 같은 레지스터 복사 연산(memcpy)과 단순 정수 메모리 증감식(_generation++)이라는 극한적으로 미니멀한 마이크로 인스트럭션 콤보로만 의도적으로 디자인했다. 결과적으로 이 코어는 시스템 시간 정지의 딜레이를 불과 수십 나노초(ns) 수준의 플랑크 길이에 가깝게 깎아냄으로써, 디바이스 시스템 스케줄러 전체 실시간성의 흠집조차 털끝만큼도 허락하지 않는 눈부신 공학적 통제 기지를 발탁해 낸 것이다.