[Linux] IRQ management
소개
리눅스 커널의 인터럽트 관리는 어떻게 되고 있는지에 대한 설명(ver = 5.10) 입니다.
,--> hwirq
| |
`--------> `<irq_desc>
` | |
`----------------> `<irq_data>
` | | |
`------------------------> `<irq_chip>
| | .irq_startup
.handler | .irq_shutdown
.hwirq <--> irq .irq_enable
.domain .irq_disable
.parent_data .irq_ack
.irq_mask
.irq_unmask
.irq_eoi
.irq_set_affinity
...
.parent_device
hwirq
hwirq는 실제 하드웨어가 가지고 있는 인터럽트 고유 번호입니다.
리눅스 커널은 이 번호를 가상 인터럽트 번호로 매핑하여 관리합니다.
개발자는 device-tree 에 하드웨어 인터럽트 번호를 작성합니다.
label: device@10000000 {
status = "okay";
compatible = "samsung,device";
interrupts = <GIC_SPI 38 IRQ_TYPE_LEVEL_HIGH>;
...
}
38번은 하드웨어 인터럽트 번호이며, SoC나 디바이스 데이터 시트에 해당 정보를 확인할 수 있습니다.
리눅스 커널에서는 hwirq 으로 관리 됩니다.
IRQ 정보 관리
리눅스 커널은 IRQ 정보를 다양한 오브젝트로 관리합니다.
인터럽트에 대한 정보는 irq_chip , irq_desc , irq_data 등이 있습니다.
irq_desc
irq_desc 는 IRQ 라인의 최상위 디스크립터 입니다.
해당 IRQ의 전체 상태를 저장합니다.
그리고 해당 IRQ에 연결된 핸들러 정보를 매핑하고 있습니다.
구조체의 기본형태는 다음과 같습니다. (include/linux/irqdesc.h)
struct irq_desc {
struct irq_common_data irq_common_data;
struct irq_data irq_data;
unsigned int __percpu *kstat_irqs;
irq_flow_handler_t handle_irq;
struct irqaction *action; /* IRQ action list */
...
struct mutex request_mutex;
int parent_irq;
struct module *owner;
const char *name;
} ____cacheline_internodealigned_in_smp;
irq_data 을 포함하고 있기 때문에, irq_desc 를 알고 있으면 irq_data 를 불러올 수 있습니다.
역으로도 container_of 매크로를 활용하면 가능합니다.
커널에 구현되어 있습니다.
irq_data
irq_desc 내부(또는 계층 구조상)에서, 실제 하드웨어 IRQ 번호나, IRQ를 제어하기 위한 정보(irq_chip, 하드웨어 관련 플래그 등)를 담는 구조체 입니다.
하드웨어 레벨의 IRQ 번호(hwirq), 트리 구조(부모/자식 irq_data), IRQ line의 옵션(마스크, 트리거 방식 등) 정보를 저장합니다.
irq_chip을 통해 제공되는 메서드를 호출할 때 필요한, SoC나 아키텍처 의존적인 데이터를 저장할 수도 있습니다.
구조체의 기본형태는 다음과 같습니다. (include/linux/irq.h)
struct irq_data {
u32 mask;
unsigned int irq;
unsigned long hwirq;
struct irq_common_data *common;
struct irq_chip *chip;
struct irq_domain *domain;
#ifdef CONFIG_IRQ_DOMAIN_HIERARCHY
struct irq_data *parent_data;
#endif
void *chip_data;
};
irq_chip 을 포함하고 있기 때문에, irq_data 를 알고 있으면 irq_chip 를 불러올 수 있습니다.
역으로도 container_of 매크로를 활용하면 가능합니다.
커널에 구현되어 있습니다.
irq_chip
실제 하드웨어 인터럽트를 활성화/비활성화(mask/unmask), ack, eoi(End Of Interrupt) 등의 동작을 구현하는 함수 포인터 테이블입니다.
해당 IRQ 라인(또는 그룹)에 대한 제어 함수를 제공합니다.
irq_chip->irq_ack , irq_chip->irq_mask, irq_chip->irq_unmask, irq_chip->irq_eoi 등
보통 ARM GIC(Generic Interrupt Controller), GPIO 기반의 interrupt controller, PCI interrupt 등 하드웨어마다 다른 인터럽트 제어 로직을 각각 구현해 놓습니다.
구조체의 기본형태는 다음과 같습니다. (include/linux/irq.h)
struct irq_chip {
struct device *parent_device;
const char *name;
unsigned int (*irq_startup)(struct irq_data *data);
...
void (*irq_ack)(struct irq_data *data);
void (*irq_mask)(struct irq_data *data);
void (*irq_mask_ack)(struct irq_data *data);
void (*irq_unmask)(struct irq_data *data);
void (*irq_eoi)(struct irq_data *data);
int (*irq_set_affinity)(struct irq_data *data, const struct cpumask *dest, bool force);
...
unsigned long flags;
};
IRQ handler
리눅스 커널에서 인터럽트 핸들러 함수는 irqreturn_t 타입을 갖습니다.
각 IRQ의 핸들러는 irq_desc 의 irqaction 에서 관리됩니다.
(include/linux/interrupt.h)
typedef irqreturn_t (*irq_handler_t)(int, void *);
...
struct irqaction {
irq_handler_t handler;
void *dev_id;
void __percpu *percpu_dev_id;
struct irqaction *next;
irq_handler_t thread_fn;
...
unsigned int irq;
unsigned int flags;
unsigned long thread_flags;
unsigned long thread_mask;
const char *name;
struct proc_dir_entry *dir;
} ____cacheline_internodealigned_in_smp;
irqreturn
인터럽트는 cpu의 정상적인 스케줄링을 방해하고, 리소스를 점령합니다.
따라서, 인터럽트는 매우 빠르고 간결하게 처리되어야 합니다.
필요에 따라, 인터럽트 핸들러 안에 많은 동작을 넣어야 할 필요가 있을 수 있습니다.
이 부분은 리눅스 커널에서 인터럽트 후처리 기법(IRQ스레드, worker 등)이 있으나, 여기서는 생략하겠습니다.
인터럽트 핸들러는 복잡한 연산은 물론이고, 스케줄링 함수는 호출되어서는 안됩니다.
핸들러는 모든 동작이 완료 되면 IRQ_HANDLED 을 호출합니다.
(include/linux/irqreturn.h)
enum irqreturn {
IRQ_NONE = (0 << 0),
IRQ_HANDLED = (1 << 0),
IRQ_WAKE_THREAD = (1 << 1),
};
핸들러가 IRQ_HANDLED 를 return할 때마다, 커널은 /proc/interrupts 에 해당 인터럽트 카운트를 1씩 증가시킵니다.
callstack
ARM-A 아키텍처는 대부분의 인터럽트는 GIC(Generic Interrupt Controller)에서 관리합니다.
따라서, 대부분의 callstack은 다음과 같습니다.
=> ...
=> __handle_irq_event_percpu
=> handle_irq_event_percpu
=> handle_irq_event
=>handle_fasteoi_irq
=>generic_handle_irq
=>__handle_domain_irq
=>gic_handle_irq
=>el1_irq
여기서 각 드라이버의 인터럽트 핸들러는 handle_fasteoi_irq 내에서 호출됩니다.
드라이버 코드를 추적하다보면 다음과 같은 코드에서 인터럽트 핸들러가 호출됨을 알 수 있습니다.
irqreturn_t __handle_irq_event_percpu(struct irq_desc *desc, unsigned int *flags)
{
irqreturn_t retval = IRQ_NONE;
unsigned int irq = desc->irq_data.irq;
struct irqaction *action;
record_irq_time(desc);
for_each_action_of_desc(desc, action) {
irqreturn_t res;
...
trace_irq_handler_entry(irq, action);
res = action->handler(irq, action->dev_id);
trace_irq_handler_exit(irq, action, res);
...
switch (res) {
case IRQ_WAKE_THREAD:
...
__irq_wake_thread(desc, action);
fallthrough; /* to add to randomness */
case IRQ_HANDLED:
*flags |= action->flags;
break;
default:
break;
}
retval |= res;
}
return retval;
}
인터럽트 핸들러를 호출하는 부분은 res = action->handler(irq, action->dev_id); 부분입니다.
인터럽트 핸들러는 action으로 관리가 되고있고, action 구조체 멤버 handler를 호출하는 것을 확인할 수 있습니다.
핸들러 return값에 따라 어떻게 진행되는지도 알 수 있습니다.
IRQ_WAKE_THREAD return으로 인터럽트 후처리를 진행하고, 인터럽트 처리가 끝나면,
fallthrough로 IRQ_HANDLED 를 검사하는 것도 확인할 수 있습니다.
Leave a comment