Kernel/Theory

[Linux Kernel] Interrupt

Karatus 2021. 10. 6. 12:03
반응형

ARMv7 코어 기준

Interrupt란

하드웨어적 관점 : 하드웨어에서 발생하는 전기 신호
CPU 관점 : 프로세스가 하던 일 멈추고 인터럽트 서비스 루틴(ISR; Interrupt Service Routine) 실행

 

Ω 진리 Ω

인터럽트는 그 자체로 프로세스의 진행을 멈추고 실행하는 것이기 때문에 시스템에 많은 부하가 걸리지 않게 짧고 간결하게 실행되어야 한다.
그렇기 때문에 인터럽트 컨텍스트에서는 휴면 상태로 진입할 수 없고 인터럽트 컨텍스트에서 호출 가능한 커널 함수는 제한되어 있다(안 되는 함수는 예를 들어 lock 얻는 함수 같은 거).

 

주요 개념

  • 인터럽트 핸들러 - 발생된 인터럽트를 실제로 처리하는 함수
  • 인터럽트 벡터 - 각 인터럽트 별로 정해진 주소가 있는 주소 테이블
  • 인터럽트 디스크립터 - 인터럽트 종류별 세부 속성을 관리하는 자료구조
  • 인터럽트 컨텍스트 - 현재 실행 중인 코드가 인터럽트 처리 중

 

ftrace 로그 분석

실습을 위해 ftrace 켜놓고 USB 연결해서 다음과 같은 로그를 얻었다. 책과는 좀 다르지만 여기서 인터럽트가 발생한 지점은 바로 __irq_svc 부분이다. 스케줄링 돌다가 인터럽트 처리한 것으로 보인다.
그리고 __handle_domain_irq() 부분부터 마지막 콜 스택까지가 인터럽트 컨텍스트 구간이다. dwc_otg_common_irq() 함수는 인터럽트 핸들러다.

in_interrupt() 함수

현재 실행 중인 코드가 인터럽트 컨택스트인지 알려주는 매크로 함수.

/*         PREEMPT_MASK:    0x000000ff
 *         SOFTIRQ_MASK:    0x0000ff00
 *         HARDIRQ_MASK:    0x000f0000
 *             NMI_MASK:    0x00100000
 * PREEMPT_NEED_RESCHED:    0x80000000
 */

#define irq_count()    (preempt_count() & (HARDIRQ_MASK | SOFTIRQ_MASK \
                 | NMI_MASK))
#define in_interrupt()        (irq_count())

preempt_count() 함수로 현재 프로세스의 thread_info 구조체의 preempt_count 값을 가져온다. 이후 다음의 3개의 마스크와 &(AND) 연산을 한다.
만약 preempt_count의 값이 0x00010002라면 연산의 결과는 다음과 같다.

0x00010002
0x001fff00
---------- & (AND)
0x00010000 ( true )

HARDIRQ_OFFSET에 걸리는데 preempt_count에 해당 비트를 설정한 함수는 __irq_enter()다.

__irq_enter() 함수

__irq_svc
  └ bcm2836_arm_irqchip_handle_irq
      └ __handle_domain_irq
          └ irq_enter
             └ __irq_enter

__handle_domain_irq() 함수에서 인터럽트 핸들러를 호출하기 전에 irq_enter() 함수를 호출해서 인터럽트의 시작을 처리함.

/*
 * It is safe to do non-atomic ops on ->hardirq_context,
 * because NMI handlers may not preempt and the ops are
 * always balanced, so the interrupted value of ->hardirq_context
 * will always be restored.
 */
#define __irq_enter()                    \
    do {                        \
        account_irq_enter_time(current);    \
        preempt_count_add(HARDIRQ_OFFSET);    \
        trace_hardirq_enter();            \
    } while (0)

여기서 preempt_count_add()로 HARDIRQ_OFFSET을 설정한다. 김동현 저자님께 주석에 대해 질문한 결과, 15년 전 nmi_enter를 인터럽트 컨택스트에서 실행했을 당시의 패치라 신경 쓸 필요가 없다고 하셨다.

__irq_exit() 함수

__irq_svc
  └ bcm2836_arm_irqchip_handle_irq
      └ __handle_domain_irq (finishing the interrupt handling)
          └ irq_exit
             └ __irq_exit

__irq_enter() 함수와는 반대로 HARDIRQ_OFFSET을 빼는 역할을 수행.

/*
 * Exit irq context without processing softirqs:
 */
#define __irq_exit()                    \
    do {                        \
        trace_hardirq_exit();            \
        account_irq_exit_time(current);        \
        preempt_count_sub(HARDIRQ_OFFSET);    \
    } while (0)

__irq_enter(), __irq_exit() 함수를 보면 알 수 있는 점은 현재 코드가 인터럽트를 처리 중인지 HARDIRQ_OFFSET 비트를 보면 확인 가능하다는 것이다.

 

인터럽트 핸들러

인터럽트의 처리 방식은 CPU 의존적이다. ARMv7 기준으로는 인터럽트가 호출되면 익셉션(Exception)의 한 종류로 처리된다. 그래서 익셉션 벡터 테이블의 베이스 주소( 0xffff0000 )를 기준으로 익셉션의 종류에 따라 offset을 더해 브랜치 한다. 참고로 이 동작은 하드웨어 적으로 처리된다.

IRQ 인터럽트 익셉션 벡터 주소는 0xffff0018 <vector_irq>. 해당 주소에 위치한 명령어는 인터럽트 익셉션 벡터 핸들러다. 해당 주소로 브랜치 해서 가보면 처리하는 어셈블리어 코드가 있다. ARM mode에 따라 커널에서 실행된 코드면 __irq_svc로, 유저에서 실행된 코드면 __irq_usr로 브랜치 한다.

__irq_svc 레이블 기준. 코드는 다음과 같다.
https://elixir.bootlin.com/linux/v4.19.98/source/arch/arm/kernel/entry-armv.S#L213

__irq_svc:
    svc_entry
    irq_handler

#ifdef CONFIG_PREEMPT
    ldr    r8, [tsk, #TI_PREEMPT]        @ get preempt count
    ldr    r0, [tsk, #TI_FLAGS]        @ get flags
    teq    r8, #0                @ if preempt count != 0
    movne    r0, #0                @ force flags to 0
    tst    r0, #_TIF_NEED_RESCHED
    blne    svc_preempt
#endif

    svc_exit r5, irq = 1            @ return from exception
 UNWIND(.fnend        )
ENDPROC(__irq_svc)

svc_entry : 같은 파일 내 선언된 매크로. 현재 실행 중인 프로세스의 레지스터를 스택에 백업해두는 코드.
irq_handler : 같은 파일 내 선언된 매크로. 칩셋 구조에 맞게 handle_arch_irq 전역 변수에 irq handler 함수 주소 저장.

 

인터럽트 핸들러의 호출 흐름 분석

__irq_svc (<- vector_irq: 인터럽트 벡터)
  └ bcm2836_arm_irqchip_handle_irq
      └ __handle_domain_irq
          └ generic_handle_irq
             └ bcm2836_chained_handle_irq
                └ generic_handle_irq
                   └ handle_level_irq
                      └ handle_irq_event
                         └ __handle_irq_event_percpu

환경은 라즈베리 파이 OS다. __handle_irq_event_percpu() 함수에서 인터럽트 번호에 맞는 인터럽트 핸들러를 부른다.

인터럽트 핸들러 등록

부팅 과정에서 인터럽트를 해당 핸들러와 함께 등록한다. 초기화 과정에 관여하는 함수는 request_irq()다.

request_irq() 함수 선언부

static inline int __must_check
request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags,
	    const char *name, void *dev)
  • unsigned int irq: 인터럽트 번호
  • irq_handler_t handler: 해당 인터럽트 핸들러
  • unsigned long flags: 인터럽트 속성 플래그
  • const char *name: 인터럽트 이름
  • void *dev: 인터럽트 핸들러에 전달하는 매개변수(보통 디바이스 드라이버를 제어하는 구조체 주소를 전달. 디바이스 드라이버와 인터럽트 핸들러를 연결하는 중요한 인터페이스)

request_irq() 함수 구현부

static inline int __must_check
request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags,
	    const char *name, void *dev)
{
	return request_threaded_irq(irq, handler, NULL, flags, name, dev);
}
int request_threaded_irq(unsigned int irq, irq_handler_t handler,
			 irq_handler_t thread_fn, unsigned long irqflags,
			 const char *devname, void *dev_id)
{
	struct irqaction *action;
	struct irq_desc *desc;
	int retval;
    ...
	desc = irq_to_desc(irq);
	if (!desc)
		return -EINVAL;
    ...
	action = kzalloc(sizeof(struct irqaction), GFP_KERNEL);
	if (!action)
		return -ENOMEM;

	action->handler = handler;
	action->thread_fn = thread_fn;
	action->flags = irqflags;
	action->name = devname;
	action->dev_id = dev_id;
	...
}

실제 인터럽트 등록을 수행할 request_threaded_irq() 함수의 기능을 살펴보자.

  • 인터럽트 번호로 인터럽트 디스크립터 가져오기
  • irqaction 구조체로 동적 메모리 할당
  • irqaction 구조체 초기화(인터럽트 핸들러, 인터럽트 플래그, 인터럽트 핸들러 매개변수 등)

인터럽트 디버깅할 때 request_threaded_irq() 함수 내에 부하 안 걸리게 디버깅하고 싶은 인터럽트 번호 필터링해서 패치하면 된다.

 

인터럽트 디스크립터

지금까지는 request_irq() 함수로 인터럽트의 속성과 핸들러 설정하는 방법에 대해 알아봤다. 그렇다면 인터럽트 별 정보는 어떻게 관리할까. 바로 인터럽트 디스크립터인 irq_desc 구조체로 인터럽트 별 속성 정보를 관리한다.

▶ 커널에서 디스크립터란?
    - 커널이 특정 드라이버나 메모리 같은 중요한 객체를 관리하려고 쓰는 자료구조
    - ex) 태스크 디스크립터, 페이지 디스크립터, 파일 디스크립터 등

인터럽트 디스크립터의 개수 == 발생되는 인터럽트의 종류만큼.

 

★ Top-Half, Bottom-Half

인터럽트 핸들러 호출하는 시점에서 바로 처리되는 코드들(Top-Half)과 시스템에 부하를 많이 줄 수 있는 코드들은 이후에 커널 스레드에서 처리(Bottom-Half)된다. 이렇게 인터럽트를 처리하는 코드를 2단계로 나누는 것은 리눅스 드라이버에서 아주 흔하다. 임베디드 용어로 Top-Half, Bottom-Half라고 한다.

 

인터럽트 비활성화 시기

  1. SoC에서 정의한 하드웨어 블록에 정확한 시퀀스를 줘야 할 경우
  2. 시스템이 유휴 상태에 진입하기 직전의 '시스템 상태 정보' 값을 저장하는 동작을 수행하고 있는 경우
  3. 각 디바이스 드라이버가 서스펜드 모드로 진입할 때 디바이스 드라이버에 데이터 시트에서 명시한 대로 정확한 특정 시퀀스를 줘야 할 경우
  4. 예외가 발생해서 시스템 리셋을 시키기 전 (ex. 커널 패닉 일으키기 전 시스템에 익셉션 발생 알리기 in bad_mode() function)

local_irq_disable() - 해당 CPU 인터럽트 라인을 비활성화
local_irq_enable() - 해당 CPU 인터럽트 라인을 활성화
→ 해당 코드 존재하면 중요한 제어를 수행하는 중이라는 것을 짐작할 수 있음.

 

인터럽트 디버깅

● /proc/interrupts
   출력 시 호출하는 함수 -> show_interrupts() 함수
   proc 파일 시스템에 등록된 함수가 불린 것임.
   언제 /proc/interrupts 파일이 생성될까?
      - proc_interrupts_init() 함수에서 proc_create_seq() 함수 호출할 때 생성됨.
● ftrace의 이벤트 출력하는 커널 함수 분석
   trace_(event name) 식의 함수 이름이 해당 이벤트 출력해주는 함수.
   ex. irq_handler_entry event -> trace_irq_handler_entry() 함수

인터럽트 별로 인터럽트 핸들러가 무엇인지 알고 싶다면, 핸들러를 호출하는 과정(kernel/irq/handle.c)에서 호출하는 함수인 __handle_irq_event_percpu() 함수 내부에서 trace_printk() 함수에 인터럽트 디스크립터, 컴파일러 매크로 등을 이용해 ftrace로 로그 남길 수 있게끔 만든다.

반응형