사전 인터럽트 컨트롤러
개념

인터럽트 컨트롤러

gabury1고친 사람 github-actions[bot]

인터럽트 컨트롤러는 여러 장치가 보내는 인터럽트 신호를 한데 모아 프로세서에 넘겨 줍니다. 신호가 한꺼번에 몰리면 어느 것부터 넘길지 정합니다. 넘길 때는 어느 장치가 보낸 신호인지 번호로 알려 줍니다.

쉽고 빠른 이해

장치들의 알림을 대신 받아 순서를 정해 프로세서에 전해 주는 부품입니다. 키보드와 네트워크 카드가 동시에 알려 와도 프로세서는 이 부품에게서 하나씩 받습니다.

이 부품이 없으면 프로세서가 장치마다 선을 따로 받아야 합니다. 알림이 올 때마다 누가 보냈는지 장치를 하나씩 물어보며 찾아야 하기도 합니다.

어떻게 도나:

  1. 장치가 자기 선으로 알림을 올립니다
  2. 컨트롤러가 꺼 둔 선인지, 더 급한 알림이 있는지 보고 하나를 고릅니다
  3. 프로세서에 알립니다. 그 알림의 번호도 함께 건넵니다
  4. 처리가 끝나면 프로세서가 컨트롤러에 끝났다고 알립니다

대가도 있습니다. 운영체제가 부팅 때 이 부품을 설정해 줘야 합니다. 끝났다는 알림을 빠뜨리면 같은 선의 다음 알림이 막힙니다.

애플리케이션 코드가 이 부품을 직접 다룰 일은 없습니다. 설정은 운영체제가 맡습니다. 서버에서 네트워크 알림이 여러 코어 가운데 하나에만 몰려 그 코어가 바빠질 때 이 부품의 이름을 만납니다.

상세

이 절은 장치가 올린 신호 하나가 컨트롤러를 거쳐 프로세서에 닿는 길을 따라갑니다. 키보드 · 디스크 · 네트워크 카드 세 장치가 달린 작은 기계 하나를 예로 씁니다.

사무실 대표 전화를 떠올려 봅니다. 바깥에서 오는 전화는 여러 줄입니다. 사장 책상의 전화기는 하나뿐입니다. 교환원이 전화를 먼저 받습니다. 누가 걸었는지 메모를 붙여 급한 전화부터 사장에게 돌려 줍니다.

인터럽트와 컨트롤러

인터럽트는 프로세서가 하던 일을 멈추고 다른 일부터 처리하게 만드는 신호입니다. 디스크가 읽기를 마쳤을 때나 네트워크 카드에 데이터가 도착했을 때 장치가 이 신호를 올립니다. 프로세서가 장치에게 다 됐냐고 계속 묻지 않아도 되게 하려고 이 신호가 있습니다.

인터럽트 컨트롤러는 장치들과 프로세서 사이에 놓인 하드웨어입니다. 장치가 올린 신호는 프로세서로 바로 가지 않고 이 부품을 먼저 지납니다.

장치마다 신호를 올리는 전선이 한 가닥씩 있습니다. 이 전선을 인터럽트 요청 라인이라고 부릅니다. 라인마다 번호가 붙어서 몇 번 라인에서 왔는지가 곧 어느 장치가 보냈는지가 됩니다.

flowchart TD
    subgraph DEV["장치"]
        K["키보드"]
        D["디스크"]
        N["네트워크 카드"]
    end
    IC["인터럽트 컨트롤러 · 모아서 고른다"]
    CPU["프로세서"]
    K -->|"요청 라인 1"| IC
    D -->|"요청 라인 2"| IC
    N -->|"요청 라인 3"| IC
    IC -->|"한 가닥"| CPU

장치는 많고 입력은 적다

프로세서에는 인터럽트를 받는 입력이 몇 개 없습니다. 반면 기계에 달린 장치는 수십 개가 넘기도 합니다. 장치마다 프로세서에 선을 하나씩 꽂을 수는 없습니다.

선을 하나로 묶기만 하면 다른 문제가 생깁니다. 신호가 왔을 때 누가 보냈는지 모릅니다. 프로세서가 장치를 하나씩 찾아가 너였냐고 물어야 합니다. 장치에 차례로 물어 확인하는 이 방식을 폴링이라고 합니다. 장치가 많을수록 오래 걸립니다.

인터럽트 컨트롤러는 이 두 문제를 한 번에 맡습니다. 많은 라인을 받아 프로세서 쪽으로는 한 가닥만 냅니다. 대신 지금 넘기는 신호가 몇 번 라인에서 왔는지를 프로세서가 읽어 갈 수 있게 해 둡니다.

받지 않을 신호를 끄는 마스크

컨트롤러는 라인마다 켜고 끄는 스위치를 둡니다. 이 스위치를 마스크라고 부릅니다. 운영체제는 아직 처리 준비가 안 된 장치의 라인을 꺼 둡니다. 운영체제가 도중에 끊기면 안 되는 일을 하는 동안에도 라인을 잠시 꺼 둡니다.

꺼 둔 라인에서 신호가 오면 컨트롤러는 그 신호를 버리지 않습니다. 대기 중이라고 표시만 해 둡니다. 라인을 다시 켜는 순간 기다리던 신호가 프로세서로 넘어갑니다.

먼저 넘길 신호를 고르는 우선순위

디스크와 네트워크 카드가 거의 같은 순간에 신호를 올릴 수 있습니다. 컨트롤러는 라인마다 매겨 둔 급한 정도를 보고 하나를 먼저 넘깁니다. 이 급한 정도를 인터럽트 우선순위라고 합니다.

우선순위는 처리 도중에도 쓰입니다. 낮은 신호를 처리하는 사이에 더 높은 신호가 오면 컨트롤러가 그 신호를 바로 넘깁니다. 프로세서는 하던 처리를 멈추고 높은 쪽부터 끝냅니다. 처리 안에 다른 처리가 끼어드는 이 일을 인터럽트 중첩이라고 부릅니다.

같거나 낮은 신호는 앞의 처리가 끝날 때까지 기다립니다. 그래서 컨트롤러는 지금 어느 신호가 처리 중인지 기억해야 합니다.

번호를 건네고 끝 알림을 받는 순서

컨트롤러가 신호를 골랐으면 프로세서에 알립니다. 프로세서는 하던 명령을 마치고 컨트롤러에게서 번호를 읽어 갑니다. 이 번호를 벡터 번호라고 합니다.

프로세서는 번호를 들고 벡터 테이블로 갑니다. 벡터 테이블은 번호마다 그 신호를 처리할 코드의 주소를 적어 둔 표입니다. 번호만 알면 어느 코드로 건너뛸지 바로 찾습니다.

그 주소에 있는 코드가 핸들러입니다. 장치마다 할 일이 달라서 핸들러도 장치마다 따로 있습니다. 키보드 핸들러는 눌린 키를 읽어 옵니다. 디스크 핸들러는 읽기가 끝난 데이터를 넘겨받습니다.

핸들러는 일을 마치면 컨트롤러에 끝났다고 알립니다. 이 알림을 EOI(End of Interrupt, 인터럽트 종료)라고 부릅니다. EOI 를 받아야 컨트롤러가 그 신호를 처리 중 목록에서 지웁니다.

아래 그림이 한 번의 왕복입니다.

sequenceDiagram
    participant 장치
    participant 컨트롤러
    participant 프로세서
    participant 핸들러
    장치->>컨트롤러: 요청 라인으로 신호를 올린다
    Note over 컨트롤러: 마스크와 우선순위를 보고 고른다
    컨트롤러->>프로세서: 처리할 신호가 있다고 알린다
    프로세서->>컨트롤러: 번호를 읽어 간다
    Note over 컨트롤러: 그 신호를 처리 중으로 표시한다
    프로세서->>핸들러: 벡터 테이블에서 주소를 찾아 건너뛴다
    핸들러->>컨트롤러: EOI 로 끝났다고 알린다

라인 하나만 떼어 보면 아래 세 상태를 돕니다. 대기 중에 머무는 까닭은 둘입니다. 라인이 꺼져 있거나, 같거나 높은 신호가 아직 처리 중이기 때문입니다.

stateDiagram-v2
    state "대기 중" as W
    state "처리 중" as S
    조용함 --> W: 장치가 신호를 올린다
    W --> W: 마스크로 꺼져 있다
    W --> W: 같거나 높은 신호가 처리 중이다
    W --> S: 프로세서가 번호를 읽어 간다
    S --> 조용함: 핸들러가 EOI 를 보낸다

핸들러가 EOI 를 빠뜨리면 그 신호는 처리 중으로 남습니다. 그러면 같은 라인과 더 낮은 라인의 신호가 계속 기다리기만 합니다. 장치 하나가 멈춘 것처럼 보이는 고장이 여기서 생기기도 합니다.

신호를 올리는 두 방식

라인으로 신호를 올리는 방식은 둘로 갈립니다. 에지 트리거는 선의 전압이 바뀌는 순간을 신호로 칩니다. 한 번 바뀌면 한 번입니다. 컨트롤러는 그 순간을 대기 표시로 붙잡아 둡니다.

다만 대기 표시는 라인마다 하나뿐입니다. 꺼 둔 라인이든 차례를 기다리는 라인이든, 표시가 남아 있는 동안 같은 라인에서 신호가 또 오면 둘이 하나로 합쳐집니다. 에지 트리거에서 신호를 놓친다는 말은 이 경우를 가리킵니다.

레벨 트리거는 선이 켜진 상태로 머무는 동안 계속 신호로 칩니다. 장치는 자기 일이 처리될 때까지 선을 켜 둡니다. 핸들러가 장치의 일을 처리해 줘야 장치가 선을 끕니다. 그때 신호가 멈춥니다.

라인이 장치 수보다 모자라면 라인 하나를 여러 장치가 나눠 씁니다. 이 일을 인터럽트 라인 공유라고 합니다. 대신 신호가 오면 핸들러가 그 라인에 매달린 장치들을 차례로 확인해야 합니다. 컨트롤러가 없앤 폴링이 그 라인 안에서만 조금 돌아오는 셈입니다.

라인을 나눠 쓸 때는 레벨 트리거를 씁니다. 에지 트리거로 나눠 쓰면 한 장치가 선을 켠 채로 있는 동안 다른 장치가 신호를 올려도 전압이 바뀌지 않습니다. 그 신호는 컨트롤러에 닿지 않고 사라집니다. 레벨 트리거는 한 장치라도 선을 켜 두면 신호가 이어지니 놓칠 걱정이 없습니다.

전선 대신 메시지로 오는 신호

요즘 장치는 전선 대신 메모리 쓰기로 신호를 보내기도 합니다. 장치가 정해진 주소에 작은 값을 써 넣습니다. 컨트롤러는 그 쓰기를 받으면 인터럽트로 바꿔 넘깁니다. 이 방식을 MSI(Message Signaled Interrupts, 메시지 신호 인터럽트)라고 부릅니다.

전선이 필요 없으니 라인 수에 묶이지 않습니다. 라인을 나눠 쓰지 않아도 되므로 누가 보냈는지 확인하는 수고도 줄어듭니다.

장치 하나가 번호를 여러 개 받아 쓸 수도 있습니다. 네트워크 카드는 받은 데이터를 담는 큐를 여럿 둡니다. 큐마다 번호를 따로 받으면 어느 큐에 데이터가 왔는지 번호만 보고 압니다. 아래에서 볼 여러 코어에 신호를 나눠 보낼 때도 이 번호가 단위가 됩니다.

코어가 여럿일 때

코어는 프로세서 안에서 명령을 실행하는 단위입니다. 코어가 여럿이면 컨트롤러가 할 일이 하나 늘어납니다. 들어온 신호를 어느 코어로 보낼지 정해야 합니다.

그래서 컨트롤러를 두 부분으로 나눠 짓는 일이 흔합니다. 공용 부분은 기계 전체에 하나 있습니다. 장치의 신호를 받아 보낼 코어를 정합니다. 코어마다 붙은 부분은 자기 코어에 신호를 넣어 줍니다. 그 코어가 보내는 EOI 도 이쪽이 받습니다.

flowchart TD
    subgraph DEV["장치"]
        D["디스크"]
        N["네트워크 카드"]
    end
    SHARED["공용 부분 · 보낼 코어를 정한다"]
    subgraph C0["코어 0"]
        L0["코어마다 붙은 부분"]
    end
    subgraph C1["코어 1"]
        L1["코어마다 붙은 부분"]
    end
    D --> SHARED
    N --> SHARED
    SHARED --> L0
    SHARED --> L1

어느 장치의 신호를 어느 코어로 보낼지는 운영체제가 정해 둡니다. 이 설정을 인터럽트 친화도라고 합니다. 컨트롤러는 그 설정대로 신호를 나눠 보냅니다.

코어끼리 신호를 주고받는 데도 이 부품을 씁니다. 한 코어가 다른 코어를 깨우거나 할 일을 넘길 때 컨트롤러를 거쳐 인터럽트를 보냅니다. 이것을 프로세서 간 인터럽트라고 부릅니다.

아키텍처마다 붙은 이름

인터럽트 컨트롤러는 아키텍처마다 다른 이름으로 들어 있습니다.

x86 PC 에서는 APIC(Advanced Programmable Interrupt Controller, 고급 프로그래밍 가능 인터럽트 컨트롤러)를 씁니다. APIC 는 공용 부분인 I/O APIC(Input/Output APIC, 입출력 APIC)와 코어마다 붙은 로컬 APIC 로 나뉩니다.

Arm 계열에서는 GIC(Generic Interrupt Controller, 범용 인터럽트 컨트롤러)를 씁니다. GIC 도 공용 부분인 분배기와 코어마다 붙은 CPU(Central Processing Unit, 중앙처리장치) 인터페이스로 나뉩니다.

RISC-V 에서는 PLIC(Platform-Level Interrupt Controller, 플랫폼 수준 인터럽트 컨트롤러)가 장치 신호를 모읍니다. PLIC 은 공용 부분에 해당합니다. 모은 신호를 보낼 코어를 정합니다.

이름과 세부 구조는 달라도 하는 일은 위에서 본 것과 같습니다. 모으기 · 끄기 · 고르기 · 번호 건네기 · 끝 알림 받기입니다.

신호가 너무 잦을 때

인터럽트가 늘 이득인 것은 아닙니다. 네트워크 카드에 데이터가 아주 잦게 들어오면 신호도 그만큼 옵니다. 신호마다 하던 일을 멈추고 핸들러로 건너뛰는 비용이 쌓입니다. 프로세서가 핸들러만 돌다 정작 받은 데이터를 처리하지 못하게 됩니다.

그래서 장치를 다루는 운영체제 코드인 드라이버는 첫 신호를 받으면 그 라인을 마스크로 꺼 둡니다. 그다음 쌓인 데이터를 폴링으로 한꺼번에 가져갑니다. 다 비우면 라인을 다시 켭니다.

드물게 오는 일은 인터럽트로 받습니다. 몰려오는 일은 폴링으로 받습니다.

장치 쪽에서 신호를 줄이기도 합니다. 데이터가 몇 개 쌓이거나 정해 둔 시간이 지날 때만 신호를 한 번 보냅니다. 이것을 인터럽트 병합이라고 부릅니다. 신호가 줄어드는 대신 데이터 하나하나가 처리되기까지는 조금 더 기다립니다.

백엔드 개발자가 이 부품을 만나는 때

애플리케이션 코드가 컨트롤러를 직접 부르는 일은 없습니다. 설정하는 것은 커널과 드라이버입니다. 커널은 부팅할 때 컨트롤러를 초기화합니다. 드라이버는 자기 장치의 라인에 핸들러를 붙입니다.

그래도 트래픽이 많은 서버에서는 네트워크 카드의 신호가 코어 하나로만 몰리는 일이 있습니다. 그 코어는 핸들러를 도느라 바쁩니다. 다른 코어는 놀게 됩니다. 이럴 때 인터럽트 친화도를 고쳐 신호를 여러 코어로 나눕니다.

운영체제는 라인마다, 코어마다 인터럽트가 몇 번 왔는지 세어 보여 줍니다. 리눅스에서는 /proc/interrupts 파일이 그 표입니다. 어느 코어에 신호가 몰리는지는 여기서 먼저 확인합니다.

가상 머신도 이 부품과 닿아 있습니다. 가상 머신 안의 운영체제는 컨트롤러가 있다고 믿고 설정합니다. 그 컨트롤러는 하이퍼바이저가 흉내 내 주는 것입니다.

관련 항목

인터럽트 컨트롤러가 신호를 받아 넘기는 하드웨어

CPU · 코어 · 네트워크 카드 · 디스크 · 타이머 · DMA · 버스 · PCI Express

인터럽트 컨트롤러를 이루는 부품과 설정

인터럽트 요청 라인 · 인터럽트 마스크 · 인터럽트 우선순위 · EOI · 에지 트리거 · 레벨 트리거 · 인터럽트 라인 공유 · MSI · MSI-X

인터럽트 컨트롤러가 건넨 번호를 받아 쓰는 처리 구조

인터럽트 · 벡터 테이블 · 벡터 번호 · 인터럽트 핸들러 · IDT · 트랩 · 예외 · 인터럽트 중첩

아키텍처마다 다른 인터럽트 컨트롤러 구현

APIC · 로컬 APIC · GIC · PLIC · NVIC · x86 · RISC-V

인터럽트 컨트롤러가 여러 코어에 신호를 나누는 기능

인터럽트 친화도 · 프로세서 간 인터럽트 · SMP · irqbalance · 멀티 큐 네트워크 카드

인터럽트 컨트롤러를 설정하는 소프트웨어

커널 · 운영체제 · 디바이스 드라이버 · 디바이스 트리 · 펌웨어 · 하이퍼바이저 · 가상 머신

인터럽트 처리 비용과 그것을 줄이는 기법

폴링 · 인터럽트 지연 · 인터럽트 병합 · 인터럽트 폭주 · 인터럽트 잠금

다른 이름: interrupt controller · 인터럽트 제어기