Interrupt

Posted by yunki kim on March 12, 2023

  현대의 범용 PC는 한 개 이상의 CPU와 한 개 이상의 디바이스 컨트롤러가 버스를 통해 이어져 있는 형태를 띠고 있습니다. 버스는 컴포넌트들과 shared memory 간의 접근을 제공합니다. 버스는 data control signal을 싣고 다니는 통로입니다. 각 디바이스 컨트롤러는 특정 유형의 장치들을 제어합니다. 디바이스 컨트롤러에는 로컬 버퍼 스토리지와 레지스터가 있습니다. 디바이스 컨트롤러는 자신이 제어하는 주변 기기와 로컬 버퍼 스토리지 간에 데이터를 이동하는 역할을 합니다.

  OS는 각 디바이스 컨트롤러에 대응되는 디바이스 드라이버를 가지고 있습니다. 디바이스 드라이버는 커널과 컨트롤러 디바이스 사이에서 균일한 인터페이스를 제공합니다. CPU와 디바이스 컨트롤러들은 병렬 실행이 가능합니다. 따라서, 순차적으로 shared memory에 접근하게 하기 위해 메모리 컨트롤러는 메모리에 대한 접근을 동기화합니다.                         

Interrupt

  위와 같은 구조에서 프로그램이 I/O를 행한다 해보자. 디바이스 드라이버는 적절한 디바이스 컨트롤러 내에 있는 레지스터를 로드해 I/O 연산을 시작합니다. 디바이스 컨트롤러는 레지스터에 로드된 콘텐츠를 확인해 어떤 액션을 취해야 하는지 판별합니다. 그 후, 디바이스 컨트롤러는 데이터를 디바이스에서 로컬 버퍼로 옮깁니다. 데이터를 모두 옮기면, 디바이스 드라이버에게 연산 완료를 알립니다. 이제 디바이스 드라이버는 OS의 다른 부분에게 제어를 넘길 것이고 읽고자 하는 데이터 또는 데이터 포인터를 반환할 것입니다. 이 과정에서 디바이스 컨트롤러가 디바이스 드라이버에게 연산 완료를 알리는 방식이 interrupt입니다.

  하드웨어는 시스템 버스를 통해 CPU에게 신호를 보냄으로써 인터럽트를 트리거할 수 있습니다. 인터럽트는 여러 목적으로 많은 곳에서 사용하며 OS와 하드웨어의 상호작용에서 중요 역할을 합니다.

  CPU가 인터럽트 되면 현재 하고 있는 일을 중단하고 고정된 장소로 이동합니다. 이 장소에는 인터럽트에 대한 서비스 루틴의 시작 주소가 존재합니다.

  인터럽트는 적절한 인터럽트 서비스 루틴으로 제어를 넘겨야 합니다. 제어를 넘기는 가장 좋은 방법은 일반 루틴을 호출해 인터럽트 정보를 검증하는 것입니다. 이 루틴을 interrupt-specific handler라 합니다. 하지만, 인터럽트는 최대한 빠르게 처리되야 합니다. 따라서, 인터럽트 루틴을 가리키는 테이블을 이용합니다. 필요한 인터럽트 루틴은 테이블을 통해 간접적으로 호출됩니다. 테이블 포인터 주소는 low memory에 존재합니다. 이곳에서 여러 디바이스들에 대한 인터럽트 서비스 루틴 주소를 배열 형태로 가지고 있습니다. 인터럽트 백터는 인터럽트 요청과 함께 주어진 고유한 숫자에 의해 인덱싱 되어 있습니다. 이를 통해 인터럽트 한 장치에 대한 인터럽트 서비스 루틴의 주소를 제공합니다. 인터럽트 아키텍처를 인터럽트를 처리한 후 중단한 연산을 복원할 수 있게 중단한 모든 상태 정보를 저장해야 합니다.

동작 과정

  Interrupt 발생 과정은 다음과 같습니다.

  1. CPU 하드웨어에는 interrupt-request line를 가지고 있다. 매번 명령어를 실행한 뒤 interrupt-request line을 확인한다.

  2. inetrrupt-request line에 디바이스 컨트롤러가 보낸 신호가 감지되었다면, CPU는 interrupt number를 읽고, 이 넘버를 interrupt vector의 인덱스로 사용해 interrupt-handler routine으로 이동한다.

  3. interrupt-handler routine을 실행한다.

  4. interrupt handler는 연산 중 변경될 모든 상태를 저장한다. 인터럽트의 원인을 파악하고, 필요한 처리를 수행하고, 상태 복원을 수행하며, return_from_interrupt 명령을 실행해 CPU를 interrupt 실행 이전상태로 돌려놓는다.

  위 과정에서 디바이스 컨트롤러가 interrupt-request line에 interrupt를 알리기 위해 신호를 보내는 것은 raise an interrupt라고 한다. CPU가 이 신호를 감지하는 것은 catch 한다고 하며 interrupt handler를 실행하는 것을 dispatch 한다고 한다.  

  위 과정은 디바이스 컨트롤러가 서비스 준비가 되었을 때, 비동기 이벤트에 대한 응답을 주기 위한 기본 interrupt 메커니즘입니다. 하지만, 위와 같은 과정뿐만 아니라, 중요한 처리 중 인터럽트 연기, 효율적인 dispatch, interrupt의 우선순위 판별 같은 기능들도 필요합니다. 이 기능들은 CPU와 interrupt-controller hardware에 의해 제공됩니다.

  대부분의 CPU는 두 개의 interrupt-request line을 가지고 있습니다. 이들은 각각 nonmaskable interrupt, maskable interrupt입니다. nonmaskable interrupt는 복구할 수 없는 메모리 에러 같이 당장 실행해야 하는 interrupt입니다. maskable interrupt는 CPU가 판단해 실행을 뒤로 미룰 수 있는, 덜 중요한 interrupt 입니다.

  컴퓨터는 interrupt vector에 명시한 원소 도바 더 많은 디바이스들을 가지고 있을 수 있습니다. 이는 interrupt chaining으로 해결합니다. Interrupt chaining은 interrupt vector의 각 원소가 interrupt handler 리스트를 가리키는 것입니다. 이 상태에서 interrupt가 raise 되면, 핸들러는 관련 리스트에서 요청을 서비스할 수 있는 interrupt-handler routine을 찾습니다.

  아래 그림은 인텔 프로세서의 interrupt vector입니다.