인터럽트를 처리하는것은 운영체제의 입장에서 아주 효율적으로 정밀히 다뤄야 하는 주제이다. 타이머 인터럽트만 하더라도 엄청나게 많은 양이 들어오며, 다양한 종류의 인터럽트가 커널에 신호를 주는 만큼 효율적인 인터럽트 처리는 커널의 속도에 지대한 영향을 미친다. 인터럽트는 크게 두가지로 나눌수 있는데 첫번째는 exception, 또는 hardware interrupt라고 불리는 것들이다. Exception같은 경우 유저모드에서 프로세스가 돌아갈적에 '의도적으로' 커널영역에 신호를 주기 위해서 발생하는 인터럽트이다. Segmentation fault, division by 0과 같은 것들이며 Exception이 발생할 경우 mode swithcing을 통해서 커널 모드로 들어가게 된다. 두번째는 hardware interrupt이며, CPU 외부에 있는 하드웨어 장치들에 의해서 발생한 인터럽트이다. 모드 스위칭으로 인해서 유저모드/커널모드가 나누어지듯이 인터럽트 처리를 위해서 커널의 동작을 process context/interrupt context로 나누게 된다. Process context는 커널의 관점에서 '프로세스'를 돌리는 모드이며, 시스템 콜, 커널쓰레드를 돌리는 기간이 이에 해당한다. Interrupt context는 인터럽트가 불러온 인터럽트 핸들러를 처리하기 위해 존재하는 모드이다. Interrupt context의 경우 인터럽트를 처리해야 되고, 인터럽트의 경우 중간에 다른 프로세스가 끼어들지 말아야 하므로 Interrupt context를 스케줄이 불가능하며, 엄중이 보호된 상태에서 실행된다. 추가적으로 커널이 인터럽트를 처리하기 위해서 고려해야 할 점이 있다. 첫번째는 커널의 입장에서 인터럽트가 어느 순간에든 발생할 수 있다는 점을 인지해야 되며, 발생하는 즉시 다른 프로세스 처리를 위해서 해당 인터럽트를 처리해야 되며 실제 인터럽트에 대한 처리를 늦추는 것이 좋다. 두번째는 인터럽트를 처리하는 와중에 다른 인터럽트가 발생할수 있다는 점이며, 이를 nested 구조로 처리해야 된다는 점이다. 리눅스가 여러개의 인터럽트를 nestes 구조로 처리하는 이유를 생각해보면 여러개의 인터럽트가 동시에 들어오는 경우 nested 구조의 경우 인터럽트간의 우선순위 같은 것을 지정하지 않아도 되며, 더 이상의 구현이 필요하지 않기 때문에 커널의 효율적인 측면에서 이렇게 처리하게 된다. 그리고 이러한 nested 구조를 통해서 인터럽트를 처리하기 위해서 전제되어야하는 조건은 이를 처리해야되는 interrupt context에서 다른 프로세스가 스퀘줄 되어서는 안된다는 점이며, 이 전제는 엄격하게 제한되서 상관없다. 마지막으로 인터럽트를 처리하는 와중에 커널의 critical section을 건드릴수 있다는 점에서 인터럽트 처리에 있어서 동기화의 문제를 고려해야 한다.
인터럽트가 발생하게 될 경우 IRQ(Interrupt ReQuest)로 전해지게 되고, 인터럽트의 종류에 따라서 각각 해당 인터럽트에 해당하는 PIC(Programmable Interrupt Controller)로 전해지게 된다. 자세히는 인터럽트가 발생해 해당 IRQ 라인에 신호가 전해지는 순간, 해당 신호를 벡터로 변환해 대응하는 PIC의 I/O 포트에 전해지게 되고, 현재 실행하고 있는 프로세스에 인터럽트가 발생했다는 신호를 보내게 된다. 마지막으로 CPU에서 해당 신호를 승인하게 되면 인터럽트가 처리되게 된다. 인터럽트 핸들링에 대해서 커널코드 단계로 보게 되면, 커널에서 자체적으로 strcut irqaction 포인터를 통해서 각각의 irq 라인에 해당하는 인터럽트의 동작들을 저장하고 있으며, 리눅스에서 자체적으로 정의하는 인터럽트 핸들링 이외에도 request_irq를 통해서 사용자가 인터럽트 핸들러를 추가할 수 있다.
인터럽트의 특성을 보게 되면 interrupt context에서 돌게 되며, 해당 컨텍스트에서 돌게 될 경우, blocking이 불가능하게 된다. 하지만 돌아보게 되면 인터럽트 중에서도 급하지 않는 인터럽트가 있음에도 불구하고 interrupt context를 통해서 커널의 전체적인 동작을 멈추는 것은 효율적이지 않으며, 이에 따라서 인터럽트의 종류를 구분하고 추가적인 조치를 해줘야 될 필요성이 생긴다. 일반적으로 생각해보면 즉각적으로 조치를 해줘야 되는 인터럽트가 있으며, 조금 지연되도 되는 인터럽트도 있을것이며, 후에 처리되어도 되는 인터럽트가 존재할 것이다. 또한 이상적인 인터럽트의 처리속도에 대해서 생각해보면, interrupt context는 현재 돌아가고 있는 process context를 침해하기 때문에 빨리 즉각적으로 처리되어야만 할 것 같지만, 어떠한 인터럽트는 처리하는 시간이 오래 결리며, 해당 인터럽트의 경우 커널이 조금 한가할때 나중에 처리되어야지 커널의 속도에 영향을 별로 안줄 것으로 보인다. 이에 따라서 리눅스에서는 인터럽트를 크게 Top-half, Bottom-half 두가지로 정의하고 있으며, Top-half에서의 인터럽트는 즉각적으로 interrupt context에서 인터럽트가 처리되며 도중에 다른 인터럽트도 금지하고, Bottom-half에서의 인터럽트는 다시 두가지 나뉘어서 실행되게 된다. 리눅스에서는 아래와 같이 인터럽트 나누어서 처리하게 된다.
Top half (hard IRQ handler)
- harware interrupt context에서 처리 (다른 인터럽트를 허용하지 않음)
- 인터럽트가 발생하는 즉시 처리
- 인터럽트 처리중에 sleep/block 될 수 없음
- time sensitivie (hardware 관련) 된 인터럽트들이 이에 해당함
Bottom half
- softirq,tasklet
- software interrupt context에서 처리(다른 인터럽트를 허용)
- 도중에 하드웨어 인터럽트 허용
- 인터럽트 처리중에 sleep/block 될 수 없음
- 오직 IRQ handler에 의해서 preemption될 수 있음
- workqueue,kernelthread(ksoftirqd)
- process context에서 처리
- block/sleep 될 수 있음
- 도중에 하드웨어 인터럽트 허용
softirq는 locking을 이용해서 여러개의 프로세스에서 동시적으로 돌 수 있는 Bottom half에 해당하는 인터럽트이며, 어떠한 인터럽트가 softirq handler에 의해서 처리되어야 하는지는 커널 컴파일 타임에 정해진다. Bottom half에 해당하는 인터럽트에 대해서도 커널이 CPU 시간에 있어서 특별대우해주며, softirq의 경우 처리하는 와중에 인터럽트를 허용하며, 이를 확인하기 위해서 해당 인터럽트 핸들러를 돌린 다음에 돌리는 와중에 인터럽트가 발생한것이 있는지를 확인해주고 있으며 처리해준다. 리눅스에서는 다시 확인해주는 작업을 10번을 해주며, 그 이상의 인터럽트에 대해서는 ksoftirqd에 넘겨버린다. tasklet의 경우 softirq와 같지만 다른점은 여러개의 프로세스에서 동시적으로 돌 수 없으며, 이러한 의미에서 동기화 문제를 고민하지 않기 때문에 좀 더 효율적인 softirq라고도 불린다. workqueue와 kernel thread의 경우 process context에서 처리되는 인터럽트들이며, process context에서 돌기 때문에 다른 프로세스들과의 경쟁을 통해서 실행되게 된다. work queue의 경우 의도적으로 특정 인터럽트들을 지연시키고 싶을때 돌게 되며, kernel thread의 경우 softirq와 tasklet이 반복적으로 돌게 될 경우 성능을 떨어뜨리기 때문에 이에 대한 처리를 하기 위해서 만들어졌다.