[리눅스 커널 구조 원리] #9. 인터럽트

문연수·3일 전
0
post-thumbnail

0. Interrupt 란?

  • 하드웨어 관점에서 인터럽트란 하드웨어의 변화를 감지해서 외부 입력으로 전달되는 전기 신호를 뜻한다.
  • CPU 관점에서 인터럽트란 실행 중이던 작업을 멈추고 정해진 코드 를 실행하는, 인터럽트 벡터인터럽트 핸들러를 뜻한다. 소프트웨어적으로 처리하는 이러한 정해진 코드인터럽트 서비스 루틴(Interrupt Service Routine, ISR) 이라 부른다.

 CPU 아키텍처 별로 인터럽트를 처리하는 방식이 서로 상이한데, ARM 에서는 이러한 인터럽트를 익셉션(Exception) 의 한 종류로 처리한다. ARM 프로세서는 외부 하드웨어 입력이나 오류 이벤트가 발생하면 익셉션 모드로 진입하게 되고 익셉션 종류에 따라 정해진 주소로 분기(branch)한다.

 이렇게 정해진 주소 의 코드를 익셉션 벡터(Exception Vector) 라 부른다.

 이러한 인터럽트를 처리할 때 가장 중요한 포인트는 바로, 인터럽트 핸들러는 빨리 실행 되어야 한다는 것이다. 그 이유는 인터럽트가 발생하게 되면 실행 중인 코드가 멈추기 때문이다.

1. 인터럽트 핸들러

 인터럽트가 발생하면 이를 처리하기 위한 함수가 호출되는데 이를 인터럽트 핸들러 라고 부른다. 인터럽트 핸들러는 함수의 형태로 구현되고, 커널 내부의 IRQ (Interrupt ReQuest) 서브 시스템을 통해 호출된다. 이렇게 특정한 이벤트에 동작할 인터럽트 핸들러는 request_irq() 함수로 등록할 수 있다.

 필자가 분석하고 있는 drivers/net/ethernet/dlink/dl2k.c 의 코드이다. 이는 rio_open() 함수가 호출 되었을 때 request_irq() 함수를 호출하여 rio_interrupt() 함수를 인터럽트 핸들러로 등록하는 코드이다.

2. 인터럽트 컨텍스트

 인터럽트 컨텍스트는 현재 실행 중인 코드가 인터럽트를 처리 중이라는 의미이다. 위 코드에서 보자면, 단순히 rio_interrupt() 함수를 호출한다고 해서 인터럽트 컨텍스트로 들어가는 것이 아닌, 커널 내부에서 인터럽트를 받아 해당 함수를 실행했을 때를 의미한다. 실행 흐름은 다음과 같다:

  1. 프로세스 실행 중
  2. 인터럽트 벡터 실행
  3. 커널 인터럽트 내부 함수 호출
  4. 인터럽트 종류별로 인터럽트 핸들러 호출 (이게 위 코드의 rio_interrupt())
    1. 인터럽트 컨텍스트 시작
  5. 인터럽트 핸들러의 서브 루틴 실행 시작
  6. 인터럽트 핸들러의 서브루틴 실행 마무리
    1. 인터럽트 컨텍스트 마무리

3. 인터럽트 디스크립터

include/linux/irqdesc.h

 인터럽트 디스크립터는 인터럽트의 세부 속성을 관리하는 자료구조이다. 필드가 무지하게 많은데 핵심적으로 다음의 자료를 관리한다:

  • 인터럽트 핸들러
  • 인터럽트 핸들러 매개변수
  • 논리적인 인터럽트 번호
  • 인터럽트 실행 횟수

4. 인터럽트을 알아야 하는 이유

리눅스 커널 디바이스 드라이버 분석에 있어서는 다음의 이점이 있다:

  • 대부분의 리눅스 디바이스 드라이버는 인터럽트를 통해 하드웨어 디바이스와 통신하기 때문에 드라이버 코드를 처음 분석할 때 인터럽트를 처리하는 함수나 코드를 먼저 확인한다. 인터럽트의 동작 방식을 잘 알고 있으면 디바이스 드라이버 코드를 빨리 이해할 수 있다.

커널의 핵심 동작을 이해하는 데에 있어서도 이점이 있다:

  • 스케줄링에서 선점(Preemptive) 스케줄링 진입 경로 중 하나가 인터럽트 처리를 끝낸 시점이다.
    (이건 #7. 컨텍스트 정보 에서 정리했음)
  • 유저 공간에서 등록한 시그널 핸들러는 인터럽트 핸들러를 실행한 다음 처리를 시작한다.
  • 레이스 컨디션이 발생하는 가장 큰 이유 중 하나는 비동기적으로 인터럽트가 발생해서 임계 영역의 코드를 오염시키기 때문이다.

5. 인터럽트 처리의 큰 흐름

- 아키텍처와 종속적인 부분

arch/arm64/kernel/entry.S

 이건 이전 장에서 설명했듯이 최초에는 Exception Vector 에서부터 코드가 실행된다. el1h_64_irq_handler() 와 같은 함수로 이어진다. 전체 경로는 다음과 같다:

  1. el1h_64_irq_handler()
  2. el1_interrupt() (여기에서 handle_arch_irq 를 인자로 넘김)
  3. __el1_irq() 혹은 __el1_pnmi()
  4. 어떤 루트를 타던 do_interrupt_handler() 를 호출
  5. call_on_irq_stack() 혹은 전달받은 인자인 handler() 호출. 어느 루트를 타던 관계없이 결국 handler() 를 호출. 이 handler 가 바로 handle_arch_irq 이다.

System Call 이 들어와 소프트웨어 인터럽트를 받은 경우에는 살짝 다른 코드 패스를 타는데 결국 호출하는 함수는 같다.

 여기에서 el1h_64_irq 는 코드 상에서는 보이지 않는데 이전 장에서 소개한 것처럼 아마 entry.S 에서 등록한 함수로 추정된다.

vmlinuxobjdump 하게 되면 확실하게 el1h_64_irq 가 등록되어 있다. 그렇다면 handle_arch_irq 는 어디서에서 누가 등록하는가?

arch/arm64/kernel/irq.c

handle_arch_irqarch/arm64/kernel/irq.c 에 선언되어 있는 전역 변수이다. 이 값은 최초에 default_handle_irq 로 초기화되어 있는데 실제로 이 함수가 호출되진 않는다. 그럼 무엇이 호출되는가? 우선 이 값을 변경하는 유일한 경로는 삽화에 있는 set_handle_irq() 이다. 그렇다면 다시 한번 이 함수는 누가 호출해서 무슨 함수를 등록하는가? 이 함수를 호출하는 경로는 다양한데 다음과 같다:

 잘린 경로는 drivers/irqchip/ 이다. 확인하는 방법은 여러가지가 있는데 필자는 generic_handle_domain_irq() 함수에 ftrace 를 걸어서 확인해보았다:

 필자의 플랫폼에서는 bcm2836_arm_irqchip_handle_irq() 가 호출된다. 여기까지가 아키텍처에 종속적인 인터럽트 루틴이다. generic_handle_domain_irq() 함수로 넘어가는 아키텍처와 무관한 리눅스 커널의 코드로 이어지게 된다.

- irq 서브 시스템

 이어지는 handle_fasteoi_irq 등에 대해서도 분석을 하려 했는데 코드 분량과 배경이 너무나도 많이 필요하기 때문에 우선은 생략한다. 핵심적인 내용만 말하자면 다음과 같다:

  1. 인터럽트 처리는 PIC (Programmable Interrupt Controller) 와 아키텍처에 종속적이다.
  2. 하나의 CPU 에서 인터럽트를 받은 상태에서 다른 인터럽트에게 뺏기는게 가능하냐 하면 가능하다. 우선 순위가 높은 인터럽트는 우선 순위가 다른 인터럽트를 선점할 수 있다. 그러나 최근 커널에서는 복잡성 문제(커널 스택 오버 플로우)로 인해 사용하지 않는다고 한다.
  3. SMP 환경에서 동일한 인터럽트가 여러 코어에 동시에 들어와도 반드시 하나만 실행됨이 보장된다.

출처

https://movefast.tistory.com/346
https://karatus.tistory.com/195
https://linux-kernel-labs.github.io/refs/heads/master/lectures/interrupts.html
https://yohda.tistory.com/entry/%EB%A6%AC%EB%88%85%EC%8A%A4-%EC%BB%A4%EB%84%90-interrupt-IRQ-number

profile
2000.11.30

0개의 댓글

관련 채용 정보