인터럽트
인터럽트는 돌던 일을 멈추고 다른 일부터 처리하게 만드는 신호입니다. 신호가 오면 프로세서는 지금 하던 흐름을 멈추고 미리 정해 둔 처리 루틴으로 건너뜁니다. 처리가 끝나면 멈췄던 자리로 돌아옵니다.
상세
책을 읽다 초인종이 울리면 읽던 쪽을 접어 두고 문을 엽니다. 볼일이 끝나면 접어 둔 쪽을 펴고 읽던 자리부터 다시 읽습니다.
인터럽트는 돌던 흐름을 정해진 자리에서 멈추는 구조입니다. 무엇 때문에 멈췄는지에 따라 미리 정해 둔 처리 루틴으로 제어를 옮깁니다. 처리가 끝나면 멈춘 자리로 되돌아옵니다. 멈춤과 갈아탐과 되돌아옴, 이 셋이 본체입니다.
신호가 어디서 오느냐로 갈립니다. 하나는 밖에서 올라오는 것입니다. 장치가 제 할 일을 마쳤을 때 프로세서에 알리는 쪽입니다. 다른 하나는 실행 중인 명령 자신이 일으키는 것입니다. 명령이 그대로는 계속할 수 없는 상태를 만났거나, 프로그램이 일부러 더 높은 권한 쪽에 일을 넘길 때 생깁니다. 앞쪽은 지금 돌던 코드와 상관없이 들이닥치고, 뒤쪽은 어느 명령에서 생겼는지가 정해져 있습니다.
막을 수 있는지로도 갈립니다. 대부분의 신호는 잠시 받지 않겠다고 걸어 둘 수 있습니다. 걸어 둔 동안 신호는 사라지지 않고 밀린 채로 기다립니다. 반면 걸어 둬도 뚫고 들어오는 신호가 있습니다. 여기서는 갈린다는 것만 짚고, 실제로 어떻게 걸고 푸는지는 아래 동작에서 봅니다.
배경
프로세서는 장치보다 훨씬 빠릅니다. 디스크에서 한 덩이를 읽어 오는 동안 프로세서는 명령을 수없이 실행할 수 있습니다. 그런데 장치가 언제 다 됐는지 프로세서가 알 길이 없으면 방법은 하나뿐입니다. 다 됐냐고 계속 물어보는 것입니다.
물어보는 동안 프로세서는 다른 일을 못 합니다. 물어보는 간격을 늘리면 그 사이에 다른 일을 할 수 있지만, 장치가 다 된 것을 알아채는 시점이 뒤로 밀립니다. 자주 물으면 프로세서 시간을 물어보는 데 쓰고, 드물게 물으면 반응이 늦습니다. 이렇게 계속 물어보며 기다리는 방식을 폴링이라고 부릅니다.
그래서 방향을 뒤집습니다. 묻는 쪽이 기다리는 대신 다 된 쪽이 알리게 합니다. 알림이 오기 전까지 프로세서는 다른 일을 합니다. 알림이 오면 하던 일을 끊고 그 알림부터 처리합니다. 하던 일을 끊는다는 뜻의 말이 그대로 이름이 됐습니다.
동작
신호가 올라온 순간부터 원래 흐름으로 돌아올 때까지 순서가 있습니다. 아키텍처마다 레지스터 이름과 절차 이름은 달라도 뼈대는 같습니다.
flowchart TD
A[신호 올라옴] --> B{받을 수 있나}
B -->|막혀 있다| C[밀린 채로 기다림]
B -->|받는다| D[멈춘 자리 저장]
D --> E[원인 번호로 갈 곳 찾음]
E --> F[처리 루틴 실행]
F --> G[저장해 둔 자리로 복귀]
신호는 올라온다고 바로 처리되지 않습니다. 받을 수 있는 상태인지부터 봅니다. RISC-V 명세는 올라와 기다리는 신호를 표시하는 레지스터와 어느 신호를 받을지 켜 두는 레지스터를 따로 둡니다. 기다리는 신호가 있다는 표시는 그 자리가 쓰기 가능할 때 0을 써서 지울 수 있습니다. 표시 자리가 읽기 전용 0으로 고정돼 있으면 그 신호는 결코 올라올 수 없다는 뜻입니다. 받겠다고 켜는 쪽 레지스터도 같습니다. 올라올 수 있는 신호라면 그 자리는 쓸 수 있어야 합니다. 쓸 수 없는 자리는 읽기 전용 0입니다.
받기로 하면 돌아올 자리부터 적어 둡니다. RISC-V 명세는 트랩이 걸리는 순간, 끊긴 명령이나 예외를 만난 명령의 주소가 전용 레지스터에 적힌다고 정합니다.
그 다음은 어디로 갈지 찾는 단계입니다. 무엇이 멈추게 했는지를 가리키는 번호가 따로 적힙니다. 그 레지스터에는 인터럽트 때문인지 아닌지를 가리키는 비트도 같이 있습니다. 명세는 값의 부호 하나로 인터럽트와 나머지 트랩을 가를 수 있다고 적습니다. 값을 왼쪽으로 밀면 그 비트가 떨어져 나갑니다. 남은 번호는 트랩 벡터 표의 색인 크기에 맞게 조정됩니다.
갈 곳을 정하는 방식은 두 가지입니다. 하나는 원인을 가리지 않고 언제나 같은 기준 주소로 뛰는 방식입니다. 다른 하나는 동기 예외만 기준 주소로 보내는 방식입니다. 이때 인터럽트는 기준 주소에서 원인 번호에 4를 곱한 만큼 떨어진 자리로 갑니다.
처리 루틴은 그 자리에서 불립니다. Linux 커널 공식 문서는 인터럽트마다 디스크립터 구조를 하나씩 둔다고 적습니다. 그 구조에는 상태 정보와 흐름 처리 함수 포인터가 들어 있습니다. 인터럽트가 걸릴 때마다 낮은 수준의 아키텍처 코드가 그 디스크립터의 처리 함수를 불러 범용 인터럽트 코드로 들어옵니다. 인터럽트는 부호 없는 정수 값으로 지목됩니다. 그 값이 디스크립터 배열에서 해당 구조를 고릅니다.
처리가 끝나면 복귀 명령으로 원래 자리로 돌아옵니다. RISC-V 명세의 복귀 명령은 특권 모드를 트랩 이전 값으로 되돌립니다. 인터럽트를 받을지 말지를 정하던 설정도 이전 값으로 되살립니다.
처리 도중에 다른 신호가 올라올 수 있습니다. Zephyr 공식 문서는 커널이 인터럽트 중첩을 지원한다고 적습니다. 우선순위가 더 높은 인터럽트가 올라오면 실행 중이던 처리 루틴이 중간에 선점됩니다. RISC-V 명세도 머신 모드로 가는 인터럽트가 더 낮은 특권 모드로 가는 인터럽트보다 앞선다고 정합니다.
반대로 흐름을 지키려고 신호를 막아 두기도 합니다. Zephyr 공식 문서는 스레드가 인터럽트 잠금으로 시스템의 모든 인터럽트 처리를 잠시 막을 수 있다고 적습니다. 특정 신호 하나만 꺼 두는 것도 됩니다. 다만 잠금을 걸면 관측되는 인터럽트 지연이 늘어날 수 있습니다. 지연 제약이 까다로운 쓰임에서는 높은 지연이 허용되지 않을 수 있다고 같은 문서가 적습니다. 그래서 인터럽트 잠금으로 막을 수 없는 우선순위에서 도는 신호를 따로 둡니다. 그것을 제로 지연 인터럽트라고 부릅니다.
예시
Linux 커널의 처리 루틴 등록
드라이버는 인터럽트 라인 하나에 처리 함수를 붙여 달라고 커널에 요청합니다. Linux 커널 공식 문서가 적은 시그니처는 이렇습니다.
int request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags,
const char *name, void *dev);
irq 는 할당받을 인터럽트 라인입니다. handler 는 그 인터럽트가 발생했을 때 불릴 함수입니다.
스레드 인터럽트에서는 이것이 1차 핸들러가 됩니다. 널을 주면 기본 1차 핸들러가 설치됩니다. flags 는
처리 플래그, name 은 이 인터럽트를 일으키는 장치의 이름, dev 는 핸들러에 그대로 전달되는
쿠키입니다. 같은 문서는 상위 드라이버 API(Application Programming Interface, 응용 프로그램
인터페이스)로 request_irq() 말고도 request_threaded_irq() · free_irq() · disable_irq() ·
enable_irq() · synchronize_irq() 를 함께 나열합니다.
RISC-V 의 타이머 인터럽트 진입 주소
RISC-V 명세는 트랩 벡터의 기준 주소와 방식을 한 레지스터에 담습니다. 방식이 벡터형이면 인터럽트는
기준 주소에서 원인 번호에 4를 곱한 만큼 떨어진 자리로 뜁니다. 명세가 든 예가 머신 모드 타이머
인터럽트입니다. 이때 프로그램 카운터는 base+0x1c 로 설정됩니다.
Zephyr 의 인터럽트 등록
마이크로컨트롤러용 커널에서는 처리 루틴을 실행 중에 등록합니다. 켜는 것은 그 다음 따로 합니다. Zephyr 공식 문서가 든 코드는 이렇습니다.
#define my_dev_irq 24 /* device uses irq 24 */
#define my_dev_prio 2 /* device uses interrupt priority 2 */
void my_isr_installer(void)
{
irq_connect(my_dev_irq, my_dev_prio, my_isr, my_isr_arg, my_irq_flags);
irq_enable(my_dev_irq);
}
24번 인터럽트 요청 라인에 우선순위 2로 처리 루틴과 그 인자를 붙입니다. 다음 줄에서 그 라인을 켭니다. 같은 문서는 등록만으로는 끝이 아니라 켜는 호출이 이어져야 한다고 적습니다. 처리 루틴은 밑에 깔린 하드웨어가 두는 제약 안에서 램이 허락하는 만큼 정의할 수 있습니다. 다만 한 인터럽트 요청 라인에 한 번에 붙을 수 있는 처리 루틴은 하나뿐입니다.
관련 항목
이것을 이루는 하드웨어 부품
인터럽트 컨트롤러 · 인터럽트 요청 라인 · 인터럽트 라인 공유 · DMA(Direct Memory Access, 직접 메모리 접근) · 타이머
이것이 갈 곳을 찾는 자료구조
벡터 테이블 · 트랩 벡터 · 벡터 번호 · 인터럽트 디스크립터 · IDT(Interrupt Descriptor Table, 인터럽트 디스크립터 테이블)
여러 인터럽트가 겹칠 때 순서를 정하는 규칙
인터럽트 우선순위 · 특권 모드 · 인터럽트 중첩
이것에 관여하는 역할과 참여자
인터럽트 서비스 루틴 · 인터럽트 핸들러 · 스레드 인터럽트 · 디바이스 드라이버 · API
제어를 넘기는 다른 방식
인터럽트를 늦추거나 대신하는 방식
인터럽트 마스킹 · 인터럽트 잠금 · 인터럽트 지연 · 제로 지연 인터럽트 · 폴링
이것을 정의하거나 구현한 시스템
다른 이름: interrupt · IRQ · 인터럽트 요청