reference:
- "리눅스 커널 내부구조" / 백승재, 최종무
- "Operating Systems: Three Easy Pieces" / Remzi H. Arpaci-Dusseau and Andrea C. Arpaci-Dusseau
인터럽트란 주변 장치와 커널이 통신하는 방식 중의 하나로, 주변장치나 CPU가 자신에게 발생한 사건을 리눅스 커널에게 알리는 메커니즘이다.
ex) 외부에서 네트워크를 통해 도착했음을 알리기 위해, 혹은 키보드가 눌렸음을 알리기 위해.
=> 외부 인터럽트 설명에 가깝다.
인터럽트가 발생되면 운영체제는 왜 인터럽트가 발생했는지 살펴보고 적절한 작업을 처리해야한다. 이때 작업을 처리하는 함수를 인터럽트 핸들러라고 부른다.
시스템이 운영되는 도중 발생하는 인터럽트는 원인에 따라 크게 2가지로 구분할 수 있다.
외부 인터럽트: 현재 수행중인 태스크와 관련없는 주변장치에서 발생된 비동기적인 하드웨어적인 사건을 의미.
SW적 인터럽트(예외 or 트랩이라고 함): 현재 수행중인 태스크와 관련있는 즉 동기적으로 발생하는 SW적 사건이다.
다음과 같은 상황들이 발생하면 예외가 발생한다.
모든 CPU는 인터럽트가 발생하면 PC(Program Counter, 또는 instruction pointer) 레지스터의 값을 미리 정해진 특정 번지로 변경하도록 설계되어있다. 즉 현재 수행중인 프로세스의 기계명령어가 아닌 다른 로직을 수행한다는 것이다.
예를 들어 ARM계열 CPU라면 인터럽트가 발생하는 순간 0x00000000+offset 번지로 점프한다. 이때 offset은 인터럽트의 종류에 따라 결정되는데 reset 인터럽트인 경우 offset은 0이며, undefined instruction인 경우 4, SW적 인터럽트(예외)인 경우 8과 같은 식이다.
reset 인터럽트가 발생하면 ARM CPU의 제어는 0x00000000 번지로 점프하게 된다. 하지만 아무리 인터럽트 핸들러를 간결하게 작성한다 할지라도 대게 4바이트보다는 커지기 마련이다. 따라서 운영체제는 해당 번지에 reset 인터럽트 핸들러를 직접 기록하는대신, 다른 위치에 reset 인터럽트 핸들러를 작성해 두고, 그곳으로 점프하는 명령어를 기록하는 방식을 사용한다.
ARM CPU의 IDT(Interrupt Descriptor Table), IVT(Interrupt Vector Table)에 인터럽트 핸들러로의 분기 명령어가 있음.
리눅스는 외부 인터럽트와 예외(SW적 인터럽트)를 동일한 방식으로 처리한다.
구체적으로 외부 인터럽트와 예외를 처리하기 위한 루틴을 함수로 구현해 놓은 뒤(이것이 인터럽트 핸들러), 각 핸들러 함수의 시작 주소를 리눅스의 IDT(Interrupt Descriptor Table)인 idt_table이라는 이름의 배열에 기록해 놓는다.
인터럽트가 발생하면 발생 인터럽트와 매칭되는 idt_table의 핸들러 함수의 주소를 PC 레지스터에 가져오고, 이 핸들러 함수를 호출한다.
보통 0~31까지 32개의 엔트리를 CPU의 예외 핸들러를 위해 할당하고, 그 외의 엔트리는 외부 인터럽트 핸들러를 위해 할당한다.
80번은 특별히 시스템 콜 테이블을 참조하도록 할당하였다.
source: https://wiki.kldp.org/KoreanDoc/html/EmbeddedKernel-KLDP/idt.html
PC 환경에서 외부 인터럽트를 발생시킬 수 있는 주변장치들은 H/W적으로 PIC라는 칩의 각 핀에 연결되어 있다. 또한 PIC는 CPU의 한 핀에 연결되어 있다. x86 CPU에서는 idt_table의 31번 엔트리까지를 예외 핸들러가 사용하므로 PIC는 32번부터 사용가능하다. 이는 리눅스 커널의 부팅 중에 설정된다.
만약 태스크가 수행되는 도중 'devide by zero error'가 발생했다고 가정하면, 'devide by zero error'를 발견한 루틴에서 0x00번을 인자로 커널에 예외(SW적 인터럽트)를 발생시킨다. 이후 커널은 idt_table에서 0x00번째 엔트리에 등록되어 있는 핸들러를 호출한다.
예외와 시그널(Signal)
https://velog.io/@jinh2352/%EC%8B%9C%EA%B7%B8%EB%84%90signal
source: 리눅스 커널 내부구조 / 백승제, 최종무
만약 timer로부터 인터럽트가 발생했다면 timer는 PIC와 연결된 선에 펄스를 보내게 된다. 그럼 PIC는 수신한 펄스를 적절한 번호로 변환하고 이를 I/O포트에 저장하여 CPU가 버스를 통해 읽을 수 있도록 한다. 그런 뒤 CPU에게 '외부 인터럽트'가 발생했음을 알리기 위해 CPU와 연결된 라인에 펄스를 보낸다. 이때 CPU는 외부 인터럽트가 발생했음을 알게 되고 PIC의 I/O포트를 읽어 발생한 외부 인터럽트의 벡터 번호를 확인하고, 인터럽트 선을 원래대로 복원시켜 PIC가 다른 외부 인터럽트를 받을 수 있도록 한다.
리눅스 커널은 현재 발생한 외부 인터럽트의 벡터 번호를 확인한 뒤, 이 번호를 통해 idt_table 인덱싱하여 해당 엔트리에 있는 핸들러를 수행시킨다.
외부에서 인터럽트를 발생시킬 수 있는 라인의 수는 한정된다. 따라서 함부로 할당하거나 무조건 독점하여 사용하는 것은 불합리하다.
이를 위해 트랩으로 사용되지 않는 즉 외부 인터럽트를 위한 번호는 별도로 관리하는데 이것이 바로 irq_desc 테이블이다. 이를 위해 리눅스 커널은 128번(for 시스템 콜)을 제외한 32~255번까지의 idt_table에는 같은 인터럽트 핸들러 함수가 등록 되어 있으며, 이 함수는 do_IRQ()라는 함수를 호출한다.
do_IRQ() 함수는 발생된 외부 인터럽트 번호를 가지고 irq_desc 테이블을 인덱싱하여 해당 외부 인터럽트 번호와 관련된 irq_desc_t 자료구조를 찾는다. 이 자료구조 안에는 하나의 인터럽트를 공유할 수 있도록 action이라는 자료구조의 리스트를 유지하고 있다. 이에 이 리스트를 이용하여 단일 인터럽트 라인을 공유하는 것이 가능해진다.