개요
마이크로프로세서에서 인터럽트란 마이크로프로세서(CPU)가 프로그램을 실행하고 있을 때, 입출력 하드웨어 등의 장치에 예외상황이 발생하여 처리가 필요할 경우에 마이크로프로세서에게 알려 처리할 수 있도록 하는 것을 말한다. 폴링이 대상을 주기적으로 감시하여 상황이 발생하면 해당처리 루틴을 실행해 처리한다면, 인터럽트는 상대가 마이크로프로세서에게 일을 처리해 달라고 요청하는 수단이다. 따라서 폴링과 대비되는 개념이다.
마이크로프로세서의 다른일과 겹쳐 폴링이 고속의 하드웨어에서 데이터 손실될 여지가 있다면, 인터럽트는 해당 하드웨어가 CPU에게 요청하므로 빨리만 처리된다면 데이터 손실 위험이 작아진다. 폴링은 리얼타임에 문제의 소지가 있다면, 인터럽트는 필요할 때 처리되는 경향이 있어서 폴링보다 유리하다.
마이크로프로세서는 인터럽트를 감지하면 지금 실행중인 기계어 코드를 중단하고 해당 인터럽트를 위한 처리 프로그램으로 점프하여 해당 일을 수행 한다. 인터럽트 처리를 위한 루틴을 인터럽트 서비스 루틴(ISR, Interrupt Service Routine)이라고 한다. 인터럽트는 주로 하드웨어적으로 CPU 코어(CPU-core)에 입력되고, 현재 진행중인 기계어 코드가 종료되면 실행한다. 인터럽트가 접수 되었을 때, 인터럽트를 처리할 것인가는 CPU코어의 특수레지스터에 비트 마스크를 통해 선택적으로 수용한다.
인터럽트가 걸리면 해당 서비스 루틴이 실행되어야 하는데, 현재 진행중인 프로그램이 영향을 받으면 안되므로 우선 ISR에서 레지스터를 스택에 대피하고 해당일을 수행한다. 레지스터 대피는 ISR에서 행하도록 기계어 코드를 구성해야 한다. C로 작성할 경우 일반함수와 차이를 두어 컴파일마다 정의하는 방식이 제공된다. CPU코어 외부에서 인터럽트를 거는 경우가 일반적이지만, CPU 내부에서 실행하면서 걸리는 경우도 있다. 예를 들어 DIV 명령어를 실행할 때 0으로 나누어지거나, 주소 버스에서 할당되지 않는 주소공간을 액세스 한다든지 하는 경우를 예외(exception)라고 말하고 예외처리를 한다.
인터럽트를 소프트웨어적으로 실행하는 방법도 제공한다. 인터럽트 기계어 명령에 의해 실행된다. x86의 경우 INT 명령어가 소프트웨어 인터럽트 명령어이다. 리눅스 커널과 같은 운영 체제에서 응용 프로그램의 저수준 입출력 함수가 실행되면, 해당 실시간 라이브러리 함수에 의해 소프트웨어 인터럽트가 실행된다. 이것을 시스템 콜(system call)이라고 하고, 함수의 기능에 따라 드라이버를 구별하고 드라이버 내의 함수를 지정함과 동시에 데이터를 레지스터를 통해 넘겨준다.
컴퓨터 시스템에서 인터럽트를 거는 원천은 여러개가 존재하는 것이 일반적이다. 따라서 인터럽트의 종류를 구분하는 방법이 필요하다.
인터럽트 서비스루틴 점프방식
인터럽트는 주로 하드웨어적으로 접수되어 실행되는 것이 일반적이다. 따라서 인터럽트가 접수되었을 때 어떤 하드웨어에서 보낸것인지를 CPU코어는 알필요가 있다. 인터럽트가 걸리고 인터럽트 소스가 어디인지를 알기위한 절차가 실행되고, 이때 벡터라는 숫자로 CPU코어에 보내지면 소스를 구별할 수단으로 사용한다.
인터럽트 벡터를 얻었으면 ISR 주소값을 찾는과정이 실행되는데 여기에는 2가지 방식이 있다:
- 정해진 주소값으로 무조건 점프 한다. 따라서 정해진 메모리 위치에 ISR 코드가 존재해야 한다.
- 인터럽트 벡터 테이블에 주소값을 얻어서 점프한다. 따라서 인터럽트가 걸리기 전에 테이블이 완성되어 있어야 한다. RAM을 사용할 경우 ISR 주소값은 변경이 가능한 경우도 있다.
8비트 마이크로프로세서에서 인텔계열(8085,Z80)은 정해진 주소값으로 점프하는 방식을, 모토로라(6809)에서는 인터럽트 벡터 테이블 방식을 사용하였다.
보통 32비트 CPU(x86, 68000)는 인터럽트 벡터 테이블 방식을 사용한다. 정해진 메모리에 해당 벡터의 ISR 주소값을 저장하고 벡터값으로 부터 테이블의 위치를 얻고 다시 ISR 주소값을 읽어 점프한다. 보통 256개의 벡터값을 갖는다. 여기에는 예외처리도 포함한다.
ARM등의 RISC에서는 정해진 주소값으로 점프하는 방식을 사용한다. 특이한 것은 순수한 주소값이 아니라 해당위치에 B 명령어를 넣어 특정 ISR 위치로 점프한다.
인터럽트 처리절차
인터럽트 원천인 하드웨어에서 또는 예외상황이 발생하거나 소프트웨어 인터럽트가 걸리면:
- 현재 진행 중인 기계어 코드를 완료한다.
- Atomic하게 인터럽트를 수행하기 위한 작업을 처리한다. 즉 인터럽트 루틴에 들어가기 전에 걸리는 인터럽트는 Pending된다.
- CPU의 특수레지스터 중, 하이로인터럽트 마스크 비트를 보고 마스크 되면 인터럽트 무시 한다.
- 인터럽트 벡터를 읽고
- ISR 주소값을 얻는다.
- ISR로 점프 한다. 이때 PC(Program Counter, IP) 값은 자동 대피 저장된다.
- 현재 진행중인 프로그램의 레지스터를 대피한다.
- 해당 코드를 실행한다.
- 해당 일을 다 처리하면, 대피시킨 레지스터를 복원한다.
- ISR의 끝에 IRET 명령어에 의해 인터럽트가 해제 된다. 이떄 IRET은 특정한 스택 프레임을 RSP가 가르키도록 요구한다. (즉 돌아갈 정보가 담긴 위치를 RSP가 지정하고 있어야 한다.)
- IRET 명령어가 실행되면, 대피시킨 PC 값을 복원하여 이전 실행 위치로 복원한다.
보통 ISR로 들어가면서 CPU코어의 인터럽트 마스크 비트를 자동 설정하고 시작되기 때문에 다른 인터럽트 처리는 대기 상태로 되는 경우가 일반적이다. 따라서 ISR 내에서 다른 인터럽트를 처리하고 싶을 경우 이 마스크 비트를 해제 해야 한다.
인터럽트 핸들러
마이크로프로세서에 인터럽트가 접수되면, 해당 인터럽트 핸들러의 코드의 위치를 찾고 실행에 옮긴다. 이 전에 실행되던 상태가 없어지면 나중에 복귀 되었을 때 문제가 발생하므로 레지스터와 프로그램 카운터를 보관함으로써 CPU의 상태를 보존한다. 인터럽트가 핸들링이 완료되면 이전의 상태로 복귀된다.
마이크로컨트롤러와 같은 간단한 시스템에서는 바로 RETI와 같은 명령어에 의해 바로 복귀된다. 인터럽트 실행 시, 마이크로프로세서의 레지스터 상태를 스택이나 레지스터 블럭에 대피하고, 인터럽트 핸들러의 기능적 처리를 완료한다. 인터럽트 이전으로 되돌리는 RETI 코드가 실행되기 전에 대피 했던 레지스터를 복귀한다. RETI 명령어에 의해 이제는 프로그램 카운터(PC)를 이전의 주소 위치로 바꿈으로써 복귀가 완료된다. 이러한 마이크로프로세서의 대피 및 복귀는 인터럽트 핸들러의 시작과 끝에 기계어 코드를 삽입해야 한다. 따라서 C언어에서 말하는 함수의 구조와 차이가 있다.
Serial_ISR:
PUSH PSW ; 스택에 상태 레지스터 대피
PUSH ACC ; 스택에 A(Accumulator) 레지스터 대피
JNB RI,output ; RI 레지스터를 확인하여 수신 여부 확인하고, 수신 데이터 없으면 수신 루틴 점프
MOV A, SBUF ; 데이터 버퍼를 A레지스터로 가져오고
MOV inchar, A ; 문자를 변수 inchar에 저장. inchar는 특정 RAM 메모리
CLR RI ; 수신 플래그를 지우고
output: ; 출력 루틴 시작
JNB TI, done ; 현재 데이터 전송 중인지를 확인하여, 전송 중일때는 출력 루틴 점프하여 생략
MOV A, outchar ; 데이터 변수 outchar 내용을 A레지스터로 옮기고
MOV SBUF, A ; 시러얼 전송 버퍼에 써서 하드웨어가 보내기 시작
CLR TI ; 전송 인터럽트 플래그 지우고
done: ; 종료 루틴
POP ACC ; A 레지스터 복귀
POP PSW ; 상태(플래그) 레지스터 복귀
RETI ; 인터럽트 핸들러 종료
8051 시리얼 인터럽트 핸들러를 위한 어셈블러 언어 예 처럼, PUSH/POP 명령어에 의해 마이크로프로세서의 레지스터를 대피하여 기존에 진행되던 루틴에 영향을 미치지 않도록 하는 것이 일반적인 방법이다. 8051의 경우 ACC 뿐만 아니라 각 R0~R7까지의 범용 레지스터의 대피 여부도 함께 고민해야 한다. R 레지스터는 뱅크 구조로 여러개의 쌍이 존재하므로 인터럽트 발생 시 다른 뱅크를 선택적으로 바꾸어 대피 문제를 해결할 수 있다.
어셈블리에서 C언어의 함수처럼 특정 기능을 구현하는 블럭을 실행할 때, CALL 명령어를 사용하는데 이때 복귀 명령어는 RET이다. RET은 복귀 주소를 CALL이 실행될 때 스택에 대피 시킨 주소를 사용하여 CALL 다음 명령어로 진행한다. 인터럽트는 임의의 명령어 실행 중에 접수되면, 이 명령어가 끝나자마자 바로 특정 코드 블럭으로 점프한 것이므로 RET과 다르게 RETI을 두어 복귀 절차를 진행한다.
인터럽트 핸들러는 일반적인 C언어의 함수 들과는 차이가 있다. 따라서 인터럽트 핸들러의 표현은 C언어의 함수와는 다른 표현이 되어야 하므로 각각의 개발도구에서 제공한다. 인터럽트 핸들러의 코딩 방법은 개발 도구에서 제공된 방법을 숙지하여 표현을 해야 한다. 이것은 C/C++언어의 표준이 아니기 때문에 개발도구의 사용설명을 참고하여 코딩해야 한다.
종류
- 슈퍼바이저 호출 인터럽트(Supervisor Call Interrupt) : 사용자가 프로그램에서 SVC 명령을 호출 하였을 경우, I/O 수행, 기억 장치 할당등의 역할을 하는 인터럽트
- 입출력 인터럽트(I/O Interrupt) : 입출력의 종료나, 입출력의 오류에 의해 CPU의 기능이 요청되는 인터럽트
- 외부 인터럽트(External Interrupt) : 오퍼레이터나 타이머에 의해 의도적으로 프로그램이 중단되는 인터럽트
- 재시작 인터럽트(Restart Interrupt) : 오퍼레이터 및 다른 프로세스에 의해 재시작 명령이 도착했을 때 실행되는 인터럽트
- 프로그램 검사 인터럽트(Program Check Interrupt) : 프로그램 실행중 보호된 기억 공간 내에 접근하거나, 불법적인 명령수행과 같은 프로그램의 문제가 발생한 경우 호출되는 인터럽트
- 장치 검사 인터럽트 (Machine check interrupt) : 하드웨어 구조로 인해 발생된다.