인터럽트 핸들러

**인터럽트 핸들러**(interrupt handler) 또는 **인터럽트 서비스 루틴**(interrupt Service Routine, ISR)은 특정 인터럽트 조건과 관련된 특별한 코드 블록이다. 인터럽트 핸들러는 하드웨어 인터럽트, 소프트웨어 인터럽트 명령어 또는 소프트웨어 예외에 의해 시작되며, 장치 드라이버를 구현하거나 시스템 호출과 같은 보호 모드 간의 전환에 사용된다. 인터럽트 핸들러의 전통적인 형태는 하드웨어 인터럽트 핸들러이다. 하드웨어 인터럽트는 전기적 조건 또는 디지털 논리에 구현된 저수준 프로토콜에서 발생하며, 일반적으로 하드 코딩된 인터럽트 벡터 테이블을 통해 비동기적으로 일반 실행 스트림으로 디스패치되며 (인터럽트 마스킹 수준이 허용하는 한), 종종 별도의 스택을 사용하고, 인터럽트 핸들러 실행 기간 동안 자동으로 다른 실행 컨텍스트(권한 수준)로 진입한다. 일반적으로 하드웨어 인터럽트 및 해당 핸들러는 프로세서가 실행 중인 현재 코드의 인터럽트가 필요한 고우선순위 조건을 처리하는 데 사용된다.publisher = The Linux Documentation Projectpublisher = O'Reilly Media 나중에 소프트웨어가 소프트웨어 인터럽트(동기 인터럽트의 한 형태)를 통해 동일한 메커니즘을 트리거할 수 있는 것이 편리하다는 것이 발견되었다. 하드웨어 수준에서 하드 코딩된 인터럽트 디스패치 테이블을 사용하는 대신, 소프트웨어 인터럽트는 종종 운영체제 수준에서 콜백 함수의 한 형태로 구현된다. 인터럽트 핸들러는 다양한 기능을 가지고 있으며, 이는 인터럽트를 트리거한 요인과 인터럽트 핸들러가 작업을 완료하는 속도에 따라 달라진다. 예를 들어, 컴퓨터 자판의 키를 누르거나 마우스를 움직이면, 키 또는 마우스의 위치를 읽고 관련 정보를 컴퓨터 메모리에 복사하는 인터럽트 핸들러를 호출하는 인터럽트가 트리거된다. 인터럽트 핸들러는 이벤트 핸들러의 저수준 대응물이다. 그러나 인터럽트 핸들러는 비정상적인 실행 컨텍스트, 시간 및 공간에 대한 많은 엄격한 제약 조건, 그리고 본질적으로 비동기적인 특성으로 인해 표준 방식(재현 가능한 테스트 사례가 일반적으로 존재하지 않음)으로 디버깅하기가 매우 어렵기 때문에 하드웨어 인터럽트 계층에서 작업하는 소프트웨어 엔지니어에게는 시스템 프로그래밍의 중요한 하위 집합인 전문 기술이 요구된다.
# 인터럽트 플래그
다른 이벤트 핸들러와 달리, 인터럽트 핸들러는 핵심 기능의 일부로 인터럽트 플래그를 적절한 값으로 설정해야 한다. 중첩 인터럽트를 지원하는 CPU에서도 핸들러는 종종 CPU 하드웨어 작업에 의해 모든 인터럽트가 전역적으로 마스크된 상태로 도달한다. 이 아키텍처에서 인터럽트 핸들러는 일반적으로 필요한 최소한의 컨텍스트를 저장하고, 가장 먼저 글로벌 인터럽트 비활성화 플래그를 재설정하여 더 높은 우선순위의 인터럽트가 현재 핸들러를 인터럽트할 수 있도록 한다. 또한 인터럽트 핸들러는 현재 인터럽트가 핸들러 종료 시 즉시 반복되어 무한 루프가 발생하는 것을 방지하기 위해 어떤 방법(종종 주변 장치 레지스터의 어떤 종류의 플래그 비트를 토글)으로 현재 인터럽트 소스를 진정시키는 것이 중요하다. 모든 상황에서 인터럽트 시스템이 정확히 올바른 상태로 인터럽트 핸들러를 종료하는 것은 때때로 힘들고 까다로운 작업이며, 이를 잘못 처리하면 시스템을 완전히 중단시키는 심각한 버그의 원인이 된다. 이러한 버그는 때때로 간헐적으로 발생하며, 잘못 처리된 엣지 케이스는 몇 주 또는 몇 달 동안 연속 작동해도 발생하지 않을 수 있다. 인터럽트 핸들러의 공식적인 유효성 검사는 매우 어렵고, 테스트는 일반적으로 가장 빈번한 실패 모드만 식별하므로, 인터럽트 핸들러의 미묘하고 간헐적인 버그는 종종 최종 고객에게 전달된다.
# 실행 컨텍스트
현대 운영체제에서, 하드웨어 인터럽트 핸들러는 진입 시 실행 컨텍스트가 미묘하다. 성능상의 이유로 핸들러는 일반적으로 실행 중인 프로세스의 메모리 및 실행 컨텍스트에서 시작되며, 이는 특별한 연결이 없다(인터럽트는 본질적으로 실행 컨텍스트를 빼앗는 것이며, 프로세스 시간 계산은 종종 인터럽트 처리 시간을 인터럽트된 프로세스에 할당한다). 그러나 인터럽트된 프로세스와 달리 인터럽트는 일반적으로 하드 코딩된 CPU 메커니즘에 의해 하드웨어 리소스에 직접 액세스할 수 있을 만큼 높은 권한 수준으로 승격된다.
# 스택 공간 고려 사항
저수준 마이크로컨트롤러에서는 칩에 보호 모드 및 메모리 관리 장치(MMU)가 없을 수 있다. 이 칩들에서 인터럽트 핸들러의 실행 컨텍스트는 기본적으로 인터럽트된 프로그램과 동일하며, 이 프로그램은 일반적으로 고정된 작은 스택에서 실행된다(저가형에서는 전통적으로 메모리 리소스가 극히 부족했다). 중첩 인터럽트가 종종 제공되어 스택 사용을 악화시킨다. 이 프로그래밍 작업에서 인터럽트 핸들러의 주요 제약은 최악의 경우에 사용 가능한 스택을 초과하지 않는 것이며, 이는 프로그래머가 구현된 모든 인터럽트 핸들러 및 응용 프로그램 작업의 스택 공간 요구 사항에 대해 전역적으로 추론해야 함을 요구한다. 할당된 스택 공간이 초과되면(스택 오버플로라고 알려진 조건) 이 클래스의 칩에서는 하드웨어적으로 일반적으로 감지되지 않는다. 스택이 다른 쓰기 가능한 메모리 영역으로 초과되면 핸들러는 일반적으로 예상대로 작동하지만, 핸들러의 메모리 손상이라는 부작용 때문에 응용 프로그램은 나중에(때로는 훨씬 나중에) 실패할 것이다. 스택이 쓰기 불가능(또는 보호된) 메모리 영역으로 초과되면 실패는 일반적으로 핸들러 자체 내에서 발생한다(일반적으로 나중에 디버깅하기 쉬운 경우). 쓰기 가능한 경우, 감시 스택 가드(sentinel stack guard)를 구현할 수 있다. 이는 유효한 스택 끝 바로 너머에 고정된 값으로, 덮어쓸 수 있지만 시스템이 올바르게 작동하면 절대 덮어쓰지 않는다. 어떤 종류의 감시 메커니즘으로 스택 가드의 손상을 정기적으로 관찰하는 것이 일반적이다. 이는 스택 오버플로 조건의 대부분을 문제가 되는 작업에 가까운 시점에서 포착한다. 다중작업 시스템에서는 각 실행 스레드에 자체 스택이 있다. 인터럽트용 특수 시스템 스택이 제공되지 않으면 인터럽트는 인터럽트되는 실행 스레드에서 스택 공간을 소비한다. 이러한 설계는 일반적으로 MMU를 포함하고 있으며, 사용자 스택은 일반적으로 스택 오버플로가 MMU에 의해 포착되도록 구성된다. 이는 시스템 오류(디버깅용)로 처리되거나, 사용 가능한 공간을 확장하기 위해 메모리를 재매핑하는 방식으로 처리된다. 이 수준의 마이크로컨트롤러에서는 메모리 리소스가 훨씬 덜 제한적이므로 스택에 넉넉한 안전 마진을 할당할 수 있다. 높은 스레드 수를 지원하는 시스템에서는 하드웨어 인터럽트 메커니즘이 스택을 특수 시스템 스택으로 전환하는 것이 더 좋다. 그러면 어떤 스레드 스택도 최악의 중첩 인터럽트 사용을 고려할 필요가 없다. 1978년에 출시된 8비트 모토로라 6809와 같은 작은 CPU는 별도의 시스템 및 사용자 스택 포인터를 제공했다.
# 시간 및 동시성 제약
여러 가지 이유로 인터럽트 핸들러는 가능한 한 간결하게 실행되는 것이 매우 바람직하며, 하드웨어 인터럽트가 잠재적으로 차단될 수 있는 시스템 호출을 호출하는 것은 매우 권장되지 않거나 금지된다. 다중 실행 코어 시스템에서는 재진입성 고려 사항도 중요하다. 시스템이 하드웨어 DMA를 제공하는 경우, 단일 CPU 코어만 있어도 병행성 문제가 발생할 수 있다. (중간급 마이크로컨트롤러가 보호 수준과 MMU가 부족하지만 여러 채널을 가진 DMA 엔진을 제공하는 것은 드문 일이 아니다. 이 시나리오에서는 많은 인터럽트가 일반적으로 DMA 엔진 자체에 의해 트리거되며, 관련 인터럽트 핸들러는 주의 깊게 작동해야 한다.) 하드웨어 인터럽트 핸들러를 전반부와 후반부 요소로 나누는 현대적인 관행이 발전했다. 전반부(또는 1단계)는 실행 중인 프로세스의 컨텍스트에서 초기 인터럽트를 수신하고, 하드웨어를 덜 긴급한 상태로 복원하기 위한 최소한의 작업(예: 가득 찬 수신 버퍼 비우기)을 수행한 다음, 후반부(또는 2단계)를 적절한 스케줄링 우선순위로 가까운 시일 내에 실행되도록 표시한다. 일단 호출되면 후반부는 더 적은 제약으로 자체 프로세스 컨텍스트에서 작동하며 핸들러의 논리적 작업(예: 새로 수신된 데이터를 운영체제 데이터 큐로 전달)을 완료한다.
# 현대 운영체제에서의 분할된 핸들러
여러 운영체제(리눅스, 유닉스, macOS, 마이크로소프트 윈도우, Z/OS, DESQview 및 과거에 사용된 일부 다른 운영체제)에서 인터럽트 핸들러는 **1단계 인터럽트 핸들러**(**FLIH**)와 **2단계 인터럽트 핸들러**(**SLIH**)의 두 부분으로 나뉜다. FLIH는 하드 인터럽트 핸들러 또는 고속 인터럽트 핸들러로도 알려져 있으며, SLIH는 저속/소프트 인터럽트 핸들러 또는 윈도우의 지연 프로시저 호출로도 알려져 있다. FLIH는 최소한 인터럽트 루틴과 유사한 플랫폼별 인터럽트 처리를 구현한다. 인터럽트에 응답하여 문맥 교환이 발생하고, 인터럽트 코드가 로드되어 실행된다. FLIH의 작업은 인터럽트를 신속하게 처리하거나, 인터럽트 시에만 사용 가능한 플랫폼별 중요 정보를 기록하고, 더 오랜 시간 동안 지속되는 인터럽트 처리를 위해 SLIH의 실행을 스케줄링하는 것이다. FLIH는 프로세스 실행에 지터를 발생시킨다. FLIH는 또한 인터럽트를 마스킹한다. 지터를 줄이는 것은 실시간 운영체제에 가장 중요하다. 특정 코드의 실행이 합의된 시간 내에 완료된다는 보장을 유지해야 하기 때문이다. 지터를 줄이고 마스킹된 인터럽트로 인한 데이터 손실 가능성을 줄이기 위해 프로그래머는 FLIH의 실행 시간을 최소화하고 가능한 한 많은 부분을 SLIH로 옮기려고 노력한다. 최신 컴퓨터의 속도를 고려할 때, FLIH는 모든 장치 및 플랫폼 종속 처리를 구현하고, 플랫폼 독립적인 장기 처리에는 SLIH를 사용할 수 있다. 하드웨어를 처리하는 FLIH는 일반적으로 실행을 완료할 때까지 관련 인터럽트를 마스킹(또는 상황에 따라 계속 마스킹)한다. 완료하기 전에 관련 인터럽트 마스크를 해제하는 (흔치 않은) FLIH를 재진입 인터럽트 핸들러라고 한다. 재진입 인터럽트 핸들러는 동일한 인터럽트 벡터에 의한 다중 선점으로 인해 스택 오버플로를 유발할 수 있으므로 일반적으로 피한다. 우선순위 인터럽트 시스템에서 FLIH는 또한 동일하거나 낮은 우선순위의 다른 인터럽트를 (잠시) 마스킹한다. SLIH는 장시간 인터럽트 처리 작업을 프로세스와 유사하게 완료한다. SLIH는 각 핸들러에 대해 전용 커널 스레드를 가지거나, 커널 워커 스레드 풀에 의해 실행된다. 이러한 스레드는 프로세서 시간이 가용할 때까지 운영체제의 실행 큐에 머물면서 인터럽트 처리를 수행한다. SLIH는 실행 시간이 길 수 있으므로 일반적으로 스레드 및 프로세스와 유사하게 스케줄링된다. 리눅스에서 FLIH는 상위 절반(upper half)이라고 불리며, SLIH는 하위 절반(lower half) 또는 바텀 절반(bottom half)이라고 불린다. 이는 다른 유닉스 계열 시스템에서 둘 다 바텀 절반의 일부인 것과는 다르다.
# x86 인터럽트 핸들러
* 대개 16-bit(실제 주소 모드라고도 함)에서 실행되며, 인터럽트 명령어는 Windows NT 미만 버전에서 실행되어야 한다. Windows NT부터는 에뮬레이터 모드로 실행되어 하드웨어에 직접 접근할 수 없으며 Windows Vista부터는 제3자 에뮬레이터가 없으면 실행이 불가능하다. * 어셈블리어로 이루어진 코드에서 (MASM 기준으로) int (번호)를 사용한 호출이 가능하다. 예를 들면 다음처럼 쓸 수 있다. .data welcome_message BYTE 'Welcome! interrupt call 21h.', 0dh, 0ah .code mov ah, 40h mov bx, 1 mov cx, lengthof welcome_message mov dl, offset welcome_message int 21h ; << 인터럽트 호출 Interrupt call. * 00000~003FF 까지가 CPU가 인터럽트를 처리할 때 사용하는 메모리 주소이다. (32비트 주소 테이블, 인터럽트 벡터 테이블) 만약 다음 코드가 있다면, mov ah, 2 mov dl, 'P' int 21h ; << 1 push al '<< 1' 표시를 한 줄에서는 인터럽트 벡터 테이블에서 명령어를 호출한다. sti ;.....COMMANDS..... IRET ; << 2 '<< 2' 표시를 한 줄에서 다시 돌아와 '<< 1' 표시를 한곳으로 와서 push al이 수행된다.