[리눅스] 7. Interrupts and Interrupt Handlers

Nanggu_Pine·2023년 9월 19일
1

linux-kernel

목록 보기
4/12

Ch7. Interrupts and Interrupt Handlers

커널은 기계와 연결된 하드웨어를 관리해야한다. 따라서 커널은 각각의 하드웨어 기기들과 통신해야한다. 하드웨어가 응답이 상대적으로 늦기 때문에, 커널은 그 시간동안 다른 일을 하는 것이 좋으며, 하드웨어가 일을 끝내면 그 때 처리해도 늦지 않다. 두가지 방법이 있는데, 첫째는 polling이다. Polling이란 하드웨어의 상태를 주기적으로 체크하는 방법이다. 계속해서 실행되기 때문에 이 방법은 오버헤드가 크다. 더 괜찮은 방법은 하드웨어가 커널에 신호를 보내는 방법이고, 이것이 이 챕터에서 논할 Interrupt이다.

1. Interrupts

Interrupt는 하드웨어로 하여금 프로세서에 신호를 보낼 수 있도록 한다. 예를 들면 키보드를 입력하면 그 전기신호가 프로세서로 전달되는데, 이 전기신호가 인터럽트라고 볼 수 있다. 하드웨어는 인터럽트를 언제나 비동기적으로 발생시킬 수 있다.
물리적으로, 인터럽트는 전기신호에 의해 발생되며, 인터럽트 컨트롤러의 입력값으로 들어간다. 컨트롤러는 신호를 프로세서에 전달하고, 프로세서는 신호를 감지해 인터럽트를 처리한다. 이후 프로세서가 운영체제에 인터럽트가 발생했음을 알리며, 운영체제가 인터럽트를 처리한다.
서로 다른 기기들은 서로 다른 인터럽트 값을 가진다. 그렇기 때문에 운영체제가 어떤 하드웨어에서 어떤 인터럽트를 발생시켰는지 알 수 있고, 이에 상응하는 핸들러를 적용할 수 있다. 이 인터럽트 값들을 interrupt request(IRQ) lines라고 부른다. 일반적으로 컴퓨터에서는 IRQ 0은 타이머 인터럽트에, IRQ 1은 키보드 인터럽트에 해당한다. 모든 인터럽트가 이렇게 정해져 있는 것은 아니고, PCI 버스에 있는 기기들에 대해 동적으로 할당된다. 중요한 점은 특정 인터럽트는 특정 기기와 연관지어진다는 것이며, 커널이 이를 인지하고 있다는 점이다.

Exceptions
인터럽트와 다르게, 예외는 프로세서의 클락에 대해 동기적으로 발생하며, 때문에 동기적인 인터럽트로 불리기도 한다. 예외는 프로세서가 명령어를 수행하는 중 프로그래밍 에러가 있거나, 비정상적인 조건에 의해 발생된다. 커널에서는 예외와 인터럽트를 비슷하게 처리한다.

2. Interrupt Handlers

커널이 인터럽트에 대한 응답으로 실행하는 함수를 interrupt handler 혹은 interrupt service routine(ISR)이라고 한다. 인터럽트를 발생시키는 각각의 기기들은 그에 맞는 인터럽트 핸들러를 가지고 있다. 기기의 인터럽트 핸들러는 기기의 드라이버(기기를 관리하는 커널 코드)에 포함되어 있다.
리눅스에서 인터럽트 핸들러는 일반적인 C언어 함수이다. 인터럽트 핸들러와 일반적인 커널 함수들과의 차이는 인터럽트의 발생으로 커널이 깨어나면, 커널은 interrupt context라고 하는 곳에서 실행된다는 것이다. Interrupt context는 block할 수 없어 atomic context라고도 불린다.
인터럽트는 언제 어디서나 발생할 수 있기 때문에 인터럽트 핸들러 역시 언제든지 실행될 수 있어야한다. 따라서 하드웨어 입장에서도 운영체제가 인터럽트를 지연 없이 처리하는 것이 중요하고, 시스템의 남은 부분 입장에서도 인터럽트 핸들러가 가능한 짧은 시간에 처리를 끝내는 것이 중요하다.
허나 보통 인터럽트 핸들러는 많은 양의 일을 처리해야한다(네트워크 패킷을 메모리에 올리고 철하는 등).

3. Top Halves Versus Bottom Halves

인터럽트 핸들러가 많은 양의 일을 빠르게 처리해야한다는 목표는 서로 충돌된다. 인터럽트 핸들러는 top half이다. Top half는 인터럽트 발생시 즉시 실행되며, 시간이 중요한 일만 실행하는 것이다. 나중에 실행되어도 괜찮은 일들은 bottom half까지 연기된다. Bottom half는 괜찮은 시간, 미래에 실행한다.
네트워크 패킷으로 예를 들면, 네트워크 패킷이 들어온다는 인터럽트가 발생되면 해당 패킷들을 메모리로 올린다. 이를 빨리 수행하지 않으면 네트워크 인터페이스의 버퍼가 꽉 차 데이터를 잃을 수 있기 때문에 우선적으로 수행한다(Top half). 패킷을 처리하는 과정은 후에 bottom half에서 일어난다.

4. Registering an Interrupt Handler

각각의 기기는 드라이버를 가지고 있고, 만약 그 기기가 인터럽트를 발생시킨다면, 드라이버는 적어도 하나의 인터럽트 핸들러를 등록해야한다. 드라이버는 request_irq() 함수를 통해 인터럽트 핸들러를 등록할 수 있다.

int request_irq(unsigned int irq,
				irq_handler_t handler,
                unsigned long flags,
                const char *name,
                void *dev)

irq 인자는 할당할 인터럽트 번호를 의미한다. PC와 같은 기기에서는 타이머나 키보드 인터럽트의 경우 그 값이 하드코딩되어있으나, 다른 기기들은 동적으로 결정된다. handler 인자는 인터럽트 핸들러가 실제로 실행할 함수 포인터이다.

4.1. Interrupt Handler Flags

request_irq()의 세번째 인자인 flags는 0이나 비트마스크이다. 플래그의 일부는 아래와 같다.

  • IRQF_DISABLED: 설정되면 커널이 인터럽트 핸들러를 실행하는 동안 다른 인터럽트를 비활성화 한다. 위의 플래그는 성능이 중요한 인터럽트에게 예약되어있다.
  • IRQF_SAMPLE_RANDOM: 주기적으로 발생하는 인터럽트나 외부의 공격자에 의해 영향받을 수 있는 인터럽트는 설정하지 않는 것이 좋다. 반면 하드웨어가 정해지지 않은 시간에 인터럽트를 발생시킨다면 설정해주는 것이 좋다. 허나 리눅스 3.6부터는 커널이 이를 자동으로 처리하기 때문에 사용하지 않는다.
  • IRQF_TIMER: 해당 해들러 프로세스가 시스템 타이머를 위해 인터럽트 되었음을 의미한다.
  • IRQF_SHARED: 여러 인터럽트 핸들러에 공유될 수 있음을 의미한다.

네번째 인자인 name은 인터럽트와 연관된 기기에 대한 아스키 문자 표현이다. 다섯번째 인자인 dev는 공유된 인터럽트 라인에 이용된다. 인터럽트 핸들러가 등록 해제되면 dev는 인터럽트 라인에서 해당 핸들러를 삭제할 수 있도록 쿠키를 제공한다. 공유된 핸들러가 아니라면 NULL을 입력해도 괜찮지만, 공유된 핸들러는 반드시 쿠키를 인자에 포함해야한다.
request_irq()가 성공하면 0일 반환하고, 에러가 발생하면 0이 아닌 값을 반환한다. 에러코드는 -EBUSY로, 인터럽트 라인이 이미 사용중이라는 것을 의미한다.

4.2. Freeing an Interrupt Handler

드라이버가 해제될 때, 인터럽트 핸들러와 인터럽트 라인을 해제해야한다. free_irq()함수가 해당 기능을 제공한다.

void free_irq(unsigned int irq, void *dev)

인터럽트 라인이 공유된 경우, dev로 구분되는 핸들러들이 삭제된다. 공유된 인터럽트 라인은 마지막 핸들러가 제거되면 해제된다.

5. Writing an Interrupt Handler

static irqreturn_t intr_handler(int irq, void *dev)

인자는 request_irq()의 인자와 그 역할이 동일하다. 반환 값인 irqreturn_t는 두개의 특별한 값이 있다. IRQ_NONE은 인터럽트 핸들러가 해당 기기가 발생시킨 인터럽트가 아닌걸 감지했을 때 반환되며, IRQ_HANDLED는 알맞은 인터럽트가 들어와 핸들러가 깨어날 때 반환된다.

IRQ_RETVAL(0) == IRQ_NONE, IRQ_RETVAL(NONZERO) == IRQ_HANDLED

어떤 인터럽트 라인에서 모든 인터럽트 핸들러가 IRQ_NONE을 반환한다면, 커널이 문제가 있다고 판단한다.

5.1. Shared Handlers

공유된 핸들러는 기존의 핸들러들과 아래와 같은 차이만을 보인다

  • IRQF_SHARED 플래그거 설정되어있음.
  • dev 인자가 유일함. device 구조체를 넣으면 unique할 뿐 아니라 핸들러 입장에서도 잠재적으로 활용할 수 있음.
  • 인터럽트 핸들러는 해당 디바이스가 실제로 인터럽트를 발생하였는지 구분할 수 있어야 함. 이를 위해 하드웨어측의 도움도 필요하고, 인터럽트 핸들러의 로직도 필요함.

핸들러는 주어진 인터럽트가 자신의 라인에서 발생한 것인지 구분할 수 있어야한다. 아니라면 빠르게 종료해야한다. 이를 위해 하드웨어 기기에서는 상태를 등록하는것과 같이 핸들러가 체크할 수 있는 방법을 제공해야한다.

5.2. A Real-Life Interrupt Handler

Real-time clock(RTC)는 시스템 타이머와 별개로 시스템 시간을 설정함으로써 알람이나, 주기적인 타이머 등을 제공하는 기기이다. 대부분의 아키텍쳐에서 원하는 시간을 레지스터나 I/O 범위에 작성함으로써 시스템 시간이 절성된다. 알람이나 주기적 타이머 기능은 인터럽트로 구현된다.
RTC 핸들러의 코드에서 주목해 봐야할 부분은 다음과 같다.

  • rtc_irq_data와 rtc_callback은 스핀 락을 통해 동시 접근이 제한된다.
  • rtc_irq_data 변수는 unsigned long 타입으로, RTC에 대한 정보를 저장하고, 인터럽트가 발생할 때 마다 업데이트 된다.
  • RTC periodic timer가 설정되면 mod_timer()로 업데이트 된다.
  • RTC 드라이버는 콜백 함수를 등록하고 각각의 RTC 인터럽트마다 실행될 수 있도록 한다.
  • 핸들러 코드는 반드시 IRQ_HANDLED를 반환한다.

6. Interrupt Context

인터럽트 핸들러가 실행될 때, 커널은 interrupt context에 있다. Interrupt Context는 프로세스와 연관되어있지 않다. current 매크로도 사용할 수 없고, interrupt context는 sleep 상태에 빠질 수 없다. 그렇기 때문에 interrupt context에서 다른 특정한 함수를 실행할 수 없다.
인터럽트 핸들러가 다른 코드를 인터럽트 하기 때문에 interrupt context는 time-critical하다. 모든 인터럽트 핸들러는 가능한 빠르고 간결해야한다. 빠르게 처리한 후 인터럽트 핸들러가 아닌 bottom half에서 마저 처리한다.
인터럽트 핸들러는 자신들이 인터럽트한 프로세스의 스택을 공유한다. 커널 스택은 두 페이지정도의 크기를 가진다(32비트 시스템에선 8KB, 64비트 시스템에선 16KB). 리눅스 커널 2.6 초기 버전에서 스택 크기를 페이지 하나로 줄일 수 있는 옵션이 추가되었고, 이에 대응하기 위해 인터럽트 핸들러는 자신의 스택을 가질 수 있게 되었다(프로세서당 스택 하나, 1페이지 크기). 이 스택이 interrupt stack이다. 자신만의 스택이 있기 때문에 인터럽트 핸들러가 사용할 수 있는 메모리가 더 크게 증가하였다.

7. Implementing Interrupt Handlers

리눅스에서의 인터럽트 핸들링은 아키텍쳐에 의존한다. 프로세서, 인터럽트 컨트롤러의 유형, 아키텍쳐와 기기의 디자인 등에 따라 구현이 달라진다.

기기가 전기 신호를 인터럽트 컨트롤러에 전달함으로써 인터럽트를 발생시킨다. 인터럽트 라인이 사용 가능하면 컨트롤러가 인터럽트를 프로세서에 전달한다. 인터럽트가 비활성화 되지 않는 한 프로세서는 즉시 하던일을 멈추고 인터럽트 시스템을 비활성화 하고, 코드를 실행한다. 이 경우 커널은 인터럽트의 IRQ 번호를 알고, 이 값과 현재 레지스터 값들을 스택에 저장한다. 이후 커널은 do_IRQ()를 호출한다.

unsigned int do_IRQ(struct pt_regs regs)

pt_regs 구조체는 이전 어셈블리 루틴에서 저장된 초기 레지스터 값을 포함하며, 인터럽트 값 또한 저장되어있기 때문에 이 값들을 추출할 수 있다. 인터럽트 라인이 계산되면 do_IRQ()는 인터럽트를 수신하고, 해당 라인에 인터럽트를 비활성화한다. do_IRQ()는 라인에 등록된 유효한 핸들러가 있고, 사용할 수 있으며, 현재 실행중이지 않음을 보장한다. 그리고 handle_IRQ_event()를 호출하여 인터럽트 핸들러를 실행한다.
첫째로 프로세서가 인터럽트를 비활성화 하였기 때문에, 핸들러 등록시 IRQF_DISABLED가 설정되었다면 모를까 인터럽트는 다시 켜진다. 다음으로 각 핸들러들은 루프를 돌며 실행된다. 라인이 공유된 상태가 아니라면 한번만 실행되고 루프가 종료되며, 그렇지 않으면 모든 핸들러는 실행된다. IRQF_SAMPLE_RANDOM이 설정된 경우 그 다음 단계로 add_interrupt_randomness()가 호출된다. 이 함수는 인터럽트의 타이밍을 통해 엔트로피를 생성한다. 마지막으로 인터럽트는 다시 비활성화되며 ret_from_intr()로 이동한다. ret_from_intr()은 reschedule이 보류중인지 확인한다. Reschedule이 보류중이고 커널이 user-space로 돌아가는 중(유저 프로세스를 인터럽트 한 경우)이라면 schedule()함수가 호출된다. 커널이 kernel-space로 돌아가는 중(커널 스스로를 인터럽트 한 경우)이라면 preempt_count가 0일때만 schedule()함수가 호출된다. schedule()함수가 종료되거나 보류중인 일이 없으면 레지스터가 복구되고, 커널은 이전 작업으로 복귀한다.

7.1. /proc/interrupts

Procfs는 커널 메모리에만 존재하는 가상 메모리이다. Procfs의 파일을 읽고 쓰는 것은 커널 함수를 깨운다.

첫번째 열은 인터럽트 라인을 나타낸다. 핸들러가 설치되지 않은 라인은 표시되지 않는다. 두번째 열은 인터럽트 받은 횟수를 나타낸다. 세번째 열은 해당 라인을 다루는 인터럽트 컨트롤러를 나타낸다. 마지막 열은 인터럽트와 연관된 기기를 나타낸다. 인터럽트가 공유된 경우, 4번처럼 등록된 모든 기기들을 나열한다.

8. Interrupt Control

리눅스는 인터럽트의 상태를 조작하기 위한 인터페이스를 구현한다. 이 인터페이스들을 통해 인터럽트 시스템을 비활성화 시키거나 인터럽트 라인을 모든 기기로부터 숨기는 것이 가능하다. 인터럽트 시스템을 통제하는 이유는 동기화를 제공하는 필요성으로 요약된다. 인터럽트를 비활성화 함으로써 인터럽트 핸들러가 현재 실행중인 코드를 preempt 하지 못하도록 할 뿐더러, 커널의 preemption도 예방할 수 있다.
리눅스가 멀티 프로세서를 지원하기 때문에 커널 코드는 일반적으로 락을 획득해 다른 프로세서가 공유 데이터에 동시에 접근하는 것을 막고있다. 이런 락들은 로컬 인터럽트를 비활성화 하는 것과 함께 이용된다. 락은 다른 프로세서가 동시에 접근하는 것을 막고, 인터럽트의 비활성화는 인터럽트 핸들러가 동시에 접근하는 것을 막는다.

8.1. Disabling and Enabling Interrupts

local_irq_disable();
local_irq_enable();

아키텍쳐마다 차이는 있겠지만, 이 함수들은 단일 어셈블리 연산으로 구현되는게 일반적이다. 이 전에 이미 인터럽트가 비활성화 된 경우, local_irq_disable() 루틴은 굉장히 위험하다. 이런 방법 대신 인터럽트를 이전 단계로 복구하는 메커니즘이 필요하다. 커널이 커질수록 코드의 콜 체인(혹은 코드의 경로)이 크고 복잡해지기 때문에 이전의 인터럽트 시스템의 상태를 저장하는 것이 필요하다. 따라서 다음 함수를 이용한다

unsigned long flags;

local_irq_save(flags);
local_irq_restore(flags);

flags 값은 다른 함수로 전달될 수 없기 때문에 반드시 한 함수 안에서 save와 restore가 모두 호출되어야한다.

8.2. Disabling a Specific Interrupt Line

이전 단계에서 프로세서에 전달되는 모든 인터럽트를 비활성화 시키는 방법에 대해 논하였다면, 어떤 경우에는 특정 인터럽트 라인만 비활성화하는 것이 필요하다. 리눅스에서는 4개의 인터페이스를 제공한다

void disable_irq(unsigned int irq);
void disable_irq_nosync(unsigned int irq);
void enable_irq(unsigned int irq);
void synchronize_irq(unsigned int irq);

첫 두 함수는 주어진 인터럽트 라인을 모든 프로세서에 대해 비활성화 한다. disable_irq()함수는 현재 실행중인 핸들러가 종료되기 전엔 끝나지 않으나, disable_irq_nosync()함수는 현재 실행중인 핸들러가 종료되는 것을 기다리지 않는다. synchronize_irq()함수는 특정한 인터럽트 핸들러가 끝나기를 기다린다. 이 함수들은 중첩적으로 호출되어야한다. 이 말은, 각각의 disable 호출에 상응하는 enable 콜이 호출되어야 한다는 의미이다.
이 함수들은 인터럽트나 프로세스 컨텍스트로부터 호출될 수 있으며, sleep상태에 놓이지 않는다. 만약 인터럽트 컨텍스트에서 호출한다면, 인터럽트 핸들러가 동작하는 중에 인터럽트 라인이 활성화 되는 것을 조심해야한다(핸들러가 동작하는 중에는 인터럽트 라인이 masked out).
인터럽트 라인을 비활성화 하는 것은 같은 라인의 모든 기기들의 인터럽트 전달을 불가능하게 하기때문에 최신 기기들의 드라이버는 이 인터페이스들을 이용하지 않는다. 대신 인터럽트 라인 공유를 지원한다.

8.3. Status of the Interrupt System

인터럽트 시스템의 상태를 알거나, 인터럽트 컨텍스트에 있는지 확인하는 것은 유용하다. irq_disabled() 매크로는 로컬 프로세서의 인터럽트 시스템이 비활성화 된 경우 0이 아닌 값을 반환한다. 리눅스에서는 커널의 현재 컨텍스트를 확인할 수 있는 두가지 인터페이스를 제공한다

in_interrupt()
in_irq()

in_interrupt()가 가장 유용한데, 이 함수는 커널이 어떤 종류건 인터럽트 핸들링을 하고 있다면 0이 아닌 값을 반환한다. 이는 인터럽트 핸들러가 bottom half에서 실행중인 경우도 포함한다. in_irq()함수는 커널이 특별히 인터럽트 핸들러를 실행하고 있는 동안에만 0이 아닌 값을 반환한다.
in_interrupt()함수는 현재 프로세스 컨텍스트에 있는지 확인하는 것이 주요한 목적인데, 이 함수가 0을 반환하면 커널이 프로세스 컨텍스트에 있다는 것이다. 인터럽트 컨텍스트에 있지 않다는 것이 확정되기 때문에, sleep과 같이 프로세스 컨텍스트에서만 가능한 작업들을 실행할 수 있다.


References

  • Linux Kernel Development (3rd Edition) by Robert Love
profile
학부생 기록남기기!

0개의 댓글