本文基於Linux kernel 4.19.0, 體系結構是aarch64
中斷處理入口
在ARM GICv3 GIC代碼分析一文中,有分析到在GIC 控制器初始化時會設置ARM中斷控制器的中斷處理函數 handle_arch_irq。
static int __init gic_init_bases(void __iomem *dist_base,
struct redist_region *rdist_regs,
u32 nr_redist_regions,
u64 redist_stride,
struct fwnode_handle *handle)
{
set_handle_irq(gic_handle_irq); ------- (1)
}
#ifdef CONFIG_GENERIC_IRQ_MULTI_HANDLER
int __init set_handle_irq(void (*handle_irq)(struct pt_regs *))
{
if (handle_arch_irq)
return -EBUSY;
handle_arch_irq = handle_irq;
return 0;
}
#endif
那麼handle_arch_irq被誰調用的呢?
中斷可以通過異常向量表的形式提交給 CPU,CPU 在發生硬件中斷時跳轉到 el1_irq 或者 el0_irq 這樣的函數體內
arch/arm64/kernel/entry.S 文件的函數 elx_irq 中進入 irq_handler 這個宏函數
.macro irq_handler
ldr_l x1, handle_arch_irq
mov x0, sp
irq_stack_entry
blr x1
irq_stack_exit
.endm
.text
因此arm64 中斷的入口函數從中斷向量表中__irq_svc–>irq_handler–>handle_arch_irq–>gic_handle_irq
GIC控制器中斷處理過程
gic_handle_irq:[drivers/irqchip/irq-gic-v3.c]
static asmlinkage void __exception_irq_entry gic_handle_irq(struct pt_regs *regs)
{
u32 irqnr;
do {
irqnr = gic_read_iar(); ------------- (1)
if (likely(irqnr > 15 && irqnr < 1020) || irqnr >= 8192) { ------------ (2)
int err;
if (static_branch_likely(&supports_deactivate_key))
gic_write_eoir(irqnr); ------------(3)
else
isb();
err = handle_domain_irq(gic_data.domain, irqnr, regs); ------------- (4)
if (err) {
WARN_ONCE(true, "Unexpected interrupt received!\n");
if (static_branch_likely(&supports_deactivate_key)) {
if (irqnr < 8192)
gic_write_dir(irqnr);
} else {
gic_write_eoir(irqnr);
}
}
continue;
}
if (irqnr < 16) {
gic_write_eoir(irqnr);
if (static_branch_likely(&supports_deactivate_key))
gic_write_dir(irqnr); ------------- (5)
#ifdef CONFIG_SMP
/*
* Unlike GICv2, we don't need an smp_rmb() here.
* The control dependency from gic_read_iar to
* the ISB in gic_write_eoir is enough to ensure
* that any shared data read by handle_IPI will
* be read after the ACK.
*/
handle_IPI(irqnr, regs);
#else
WARN_ONCE(true, "Unexpected SGI received!\n");
#endif
continue;
}
} while (irqnr != ICC_IAR1_EL1_SPURIOUS);
}
(1) gic_read_iar。CPU通過讀取GIC控制器的GICC_IAR( Interrupt Acknowledge Register)寄存器, 應答該中斷, 並且可以得到當前發生中斷的是哪一個硬件中斷號。
(2) 硬件中斷號0-15表示SGI類型的中斷,15~1020 表示外設中斷(SPI或PPI類型),8192 - MAX表示LPI類型的中斷。
硬件中斷號 | 中斷類型 |
---|---|
0-15 | SGI |
16 - 31 | PPI |
32 - 1019 | SPI |
1020 - 1023 | 用於指示特殊情況的特殊中斷 |
1024 - 8191 | Reservd |
8192 - MAX | LPI |
(3) 和 (5) : gic_write_eoir 和 gic_write_dir放在一起分析。這兩個函數分別往ICC_EOIR1_EL1和ICC_DIR_EL1寄存器中寫入硬件中斷號。
ICC_EOIR1_EL1, Interrupt Controller End Of Interrupt Register 1, 對寄存器的寫操作表示中斷的結束
ICC_DIR_EL1, Interrupt Controller Deactivate Interrupt Register, 對該寄存器的寫操作將deactive指定的中斷。
那麼問題來了? 假設讀取到了一個SPI中斷, 爲什麼一開始就寫EOI表示中斷結束,此時中斷處理不是還沒有執行麼?
在GIC v3協議中定義, 處理完中斷後,軟件必須通知中斷控制器已經處理了中斷,以便狀態機可以轉換到下一個狀態。
GICv3架構將中斷的完成分爲2個階段:
Priority Drop: 將運行優先級降回到中斷之前的值。
**Deactivation:**更新當前正在處理的中斷的狀態機。 從活動狀態轉換到非活動狀態。
這兩個階段可以在一起完成,也可以分爲2步完成。 卻決於EOImode的值。
如果EOIMode = 0, 對ICC_EOIR1_EL1寄存器的操作代表2個階段(priority drop 和 deactivation)一起完成。
如果EOImode = 1, 對ICC_EOIR1_EL1寄存器的操作只會導致Priority Drop, 如果想要表示中斷已經處理完成,還需要寫ICC_DIR_EL1。
所以回答上面的問題, 當前Linux GIC的代碼,默認irq chip是EIOmode=1, 所以單獨的寫EOIR1_EL1不是代表中斷結束。
static int gic_irq_domain_map(struct irq_domain *d, unsigned int irq,
irq_hw_number_t hw)
{
if (static_branch_likely(&supports_deactivate_key))
chip = &gic_eoimode1_chip;
}
(4)handle_domain_irq. 中斷控制器中斷處理的主體。
handle_domain_irq:[kernel/irq/irqdesc.c]
int __handle_domain_irq(struct irq_domain *domain, unsigned int hwirq,
bool lookup, struct pt_regs *regs)
{
struct pt_regs *old_regs = set_irq_regs(regs);
unsigned int irq = hwirq;
int ret = 0;
irq_enter(); ------------------- (1)
#ifdef CONFIG_IRQ_DOMAIN
if (lookup)
irq = irq_find_mapping(domain, hwirq); --------------- (2)
#endif
/*
* Some hardware gives randomly wrong interrupts. Rather
* than crashing, do something sensible.
*/
if (unlikely(!irq || irq >= nr_irqs)) {
ack_bad_irq(irq);
ret = -EINVAL;
} else {
generic_handle_irq(irq); ---------------- (4)
}
irq_exit(); --------------- (3)
set_irq_regs(old_regs);
return ret;
}
(1) irq_enter顯式的告訴Linux 內核現在要進入中斷上下文了。
#define __irq_enter() \
do { \
account_irq_enter_time(current); \
preempt_count_add(HARDIRQ_OFFSET); \
trace_hardirq_enter(); \
} while (0)
__irq_enter宏通過preempt_count_add()增加當前進程struct thread_info中的preempt_count成員裏的HARDIRQ域的值。
(2) irq_find_mapping()通過硬件中斷號去查找IRQ中斷號。
(3) irq_exit() 表示硬件中斷處理已經完成。與irq_enter()相反, 通過preempt_count_sub(),減少HARDIRQ域的值。
(4) 接下來看generic_handle_irq()函數
generic_handle_irq:[kernel/irq/irqdesc.c]
generic_handle_irq–>generic_handle_irq_desc
static inline void generic_handle_irq_desc(struct irq_desc *desc)
{
desc->handle_irq(desc); ------------- (1)
}
(1) 調用desc->handle_irq指向的回調函數.
這個回調函數是在哪裏定義的呢?
在解析ACPI表或者DTS時,會枚舉映射中斷號(參考:硬中斷和軟中斷的映射)
irq_of_parse_and_map-> irq_create_of_mapping->irq_create_fwspec_mapping->irq_domain_alloc_irqs
irq_domain_alloc_irqs會調用irq_domain_ops定義的alloc函數
static const struct irq_domain_ops gic_irq_domain_ops = {
.translate = gic_irq_domain_translate,
.alloc = gic_irq_domain_alloc,
.free = gic_irq_domain_free,
.select = gic_irq_domain_select,
};
gic_irq_domain_alloc()->gic_irq_domain_map(), 然後定義irq_desc->handle_irq的回調函數
static int gic_irq_domain_map(struct irq_domain *d, unsigned int irq,
irq_hw_number_t hw)
{
struct irq_chip *chip = &gic_chip;
if (static_branch_likely(&supports_deactivate_key))
chip = &gic_eoimode1_chip;
/* PPIs */
if (hw < 32) {
irq_set_percpu_devid(irq);
irq_domain_set_info(d, irq, hw, chip, d->host_data,
handle_percpu_devid_irq, NULL, NULL);
irq_set_status_flags(irq, IRQ_NOAUTOEN);
}
/* SPIs */
if (hw >= 32 && hw < gic_data.irq_nr) {
irq_domain_set_info(d, irq, hw, chip, d->host_data,
handle_fasteoi_irq, NULL, NULL);
irq_set_probe(irq);
irqd_set_single_target(irq_desc_get_irq_data(irq_to_desc(irq)));
}
/* LPIs */
if (hw >= 8192 && hw < GIC_ID_NR) {
if (!gic_dist_supports_lpis())
return -EPERM;
irq_domain_set_info(d, irq, hw, chip, d->host_data,
handle_fasteoi_irq, NULL, NULL);
}
return 0;
}
SPI和LPI類型的中斷, desc->handler()回調函數是handle_fasteoi_irq()。
PPI類型的中斷, desc->handler()回調函數是handle_percpu_devid_irq()。
handle_fasteoi_irq:[kernel/irq/chip.c]
void handle_fasteoi_irq(struct irq_desc *desc)
{
struct irq_chip *chip = desc->irq_data.chip;
raw_spin_lock(&desc->lock);
if (!irq_may_run(desc))
goto out;
desc->istate &= ~(IRQS_REPLAY | IRQS_WAITING);
/*
* If its disabled or no action available
* then mask it and get out of here:
*/
if (unlikely(!desc->action || irqd_irq_disabled(&desc->irq_data))) { -------- (1)
desc->istate |= IRQS_PENDING;
mask_irq(desc);
goto out;
}
kstat_incr_irqs_this_cpu(desc); ------------- (2)
if (desc->istate & IRQS_ONESHOT)
mask_irq(desc);
preflow_handler(desc);
handle_irq_event(desc); -------------- (3)
cond_unmask_eoi_irq(desc, chip); -------------- (4)
raw_spin_unlock(&desc->lock);
return;
out:
if (!(chip->flags & IRQCHIP_EOI_IF_HANDLED))
chip->irq_eoi(&desc->irq_data);
raw_spin_unlock(&desc->lock);
}
EXPORT_SYMBOL_GPL(handle_fasteoi_irq);
(1) 如果某個中斷沒有定義action描述符或者該中斷被關閉了IRQD_IRQ_DISABLED, 那麼設置該中斷狀態爲IRQS_PENDING, 並調用irq_mask()函數屏蔽該中斷。
(2) 我們一般在終端通過cat /proc/intterrupts查看中斷計數, 這個計數是在這裏進行增加的。
(3) handle_irq_event()是中斷處理的核心函數,開始真正的處理硬件中斷了。這一部分放在下面分析。
(4) cond_unmask_eoi_irq(). 呼應gic_handle_irq入口函數的EOI處理, 這裏寫EOI寄存器,表示完成了硬中斷處理。
handle_irq_event:[kernel/irq/handle.c]
irqreturn_t handle_irq_event(struct irq_desc *desc)
{
irqreturn_t ret;
desc->istate &= ~IRQS_PENDING;
irqd_set(&desc->irq_data, IRQD_IRQ_INPROGRESS);
raw_spin_unlock(&desc->lock);
ret = handle_irq_event_percpu(desc);
raw_spin_lock(&desc->lock);
irqd_clear(&desc->irq_data, IRQD_IRQ_INPROGRESS);
return ret;
}
首先把pending標誌位清除,然後設置IRQD_IRQ_INPROGRESS標誌,表示正在處理硬件中斷。
handle_irq_event()->handle_percpu_devid_irq()->__handle_irq_event_percpu()
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); -------------- (1)
trace_irq_handler_exit(irq, action, res);
if (WARN_ONCE(!irqs_disabled(),"irq %u handler %pF enabled interrupts\n",
irq, action->handler))
local_irq_disable();
switch (res) {
case IRQ_WAKE_THREAD:
/*
* Catch drivers which return WAKE_THREAD but
* did not set up a thread function
*/
if (unlikely(!action->thread_fn)) {
warn_no_thread(irq, action);
break;
}
__irq_wake_thread(desc, action); ----------- (2)
/* Fall through to add to randomness */
case IRQ_HANDLED: ---------- (3)
*flags |= action->flags;
break;
default:
break;
}
retval |= res;
}
return retval;
}
(1) 用irq號找到對應的irq_desc,irq_desc->action->handler就是註冊中斷時,申請的中斷處理函數。
(2) 如果中斷號的處理函數返回IRQ_WAKE_THREAD,表示需要喚醒中斷線程。
__irq_wake_thread()->喚醒中斷線程->irq_thread()
static irqreturn_t irq_thread_fn(struct irq_desc *desc,
struct irqaction *action)
{
irqreturn_t ret;
ret = action->thread_fn(action->irq, action->dev_id);
irq_finalize_oneshot(desc, action);
return ret;
}
action()->thread_fn()對應的是request_threaded_irq()時, 中斷線程執行的部分.
中斷申請API:request_threaded_irq
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)
@handler: Function to be called when the IRQ occurs. Primary handler for threaded interrupts.
If NULL and thread_fn != NULL the default primary handler is installed
@thread_fn: Function called from the irq handler thread.
If NULL, no irq thread is created
(3) 如果中斷號的處理函數返回IRQ_HANDLED,說明該action的中斷處理函數已經處理完畢。
總結
以一個流程圖對本文進行補充: