[Linux Kernel] Interrupt
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라고 한다.
인터럽트 비활성화 시기
- SoC에서 정의한 하드웨어 블록에 정확한 시퀀스를 줘야 할 경우
- 시스템이 유휴 상태에 진입하기 직전의 '시스템 상태 정보' 값을 저장하는 동작을 수행하고 있는 경우
- 각 디바이스 드라이버가 서스펜드 모드로 진입할 때 디바이스 드라이버에 데이터 시트에서 명시한 대로 정확한 특정 시퀀스를 줘야 할 경우
- 예외가 발생해서 시스템 리셋을 시키기 전 (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로 로그 남길 수 있게끔 만든다.