1.概述
在前面文章中,已经分析汇编函数, 最终调用的是handle_arch_irq或arch_irq_handler_default函数。以zynq7k为例,其采用的是GIC pl390中断控制器,gic初始化的时候调用set_handle_irq函数,将handle_arch_irq函数指针指向了 gic_handle_irq 。因此这里以zynq7为例,介绍Linux内核对uart0的中断处理过程。
2.GIC中断入口函数
底层irq_handler调用gic_handle_irq处理中断,从函数名可以看出,此时的中断处理和具体的中断控制器有关,arm平台上中断控制器一般都为gic。传入的参数为struct pt_regs *regs,指向了保存中断发生时刻的寄存器信息地址。首先读取中断响应寄存器ICCIAR,获取硬件中断号及产生SGI中断的CPU ID,根据硬件中断号,判断中断类型。当读取了ICCIAR寄存器后,表示此CPU响应了中断,在此cpu处理完该中断之前,gic中断控制器不会再向此cpu发送任何中断。PPI中断和SPI中断调用handle_domain_irq处理,SGI中断调用handle_IPI处理。当然,SGI中断在SMP系统中才有效,在UP系统中无效,不做处理。 从下面的代码可以看出,整个中断处理逻辑是在一个死循环中,只要能从ICCIAR寄存器中读出有效的硬件中断号,就会一直进行中断处理,也就是说,只要中断一直产生,则中断服务函数gic_handle_irq会一直运行,不会退出。这正是Linux内核设计巧妙的地方,进入中断处理程序后,会处理所有pending或者active and pending状态的中断,直到没有中断产生,中断处理函数才退出,避免频繁进入和退出中断处理程序导致的性能下降。
[drivers/irqchip/irq-gic.c]
static struct gic_chip_data gic_data[CONFIG_ARM_GIC_MAX_NR] __read_mostly;
gic_handle_irq
struct gic_chip_data *gic = &gic_data[0] // 获取gic设备结构体指针
->gic_data_cpu_base // 获取GIC CPU interface的基地址空间
do {
// 读取中断响应寄存器ICCIAR,读取后就清除了gic中的中断,gic中对应的中断状态由pending变为acting
// ICCIAR的[bit9-0]为硬件中断ID,SPI和PPI中断用到,[bit12-10]产生中断的CPU的ID,SGI中断用到
irqstat = readl_relaxed(cpu_base + GIC_CPU_INTACK);
irqnr = irqstat & GICC_IAR_INT_ID_MASK; // 获取ICCIAR的[bit9-0],即硬件中断号
if (likely(irqnr > 15 && irqnr < 1020)) { // PPI和SPI中断走此流程
// 如果支持priority drop(降低优先级)and deactivation operations(中断状态变化)分离,
// 则先写ICCEOIR寄存器,中断完成后再写ICCDIR寄存器,表示中断处理完成,zynq7不支持
if (static_key_true(&supports_deactivate))
writel_relaxed(irqstat, cpu_base + GIC_CPU_EOI);
handle_domain_irq(gic->domain, irqnr, regs);
->__handle_domain_irq
continue;
}
if (irqnr < 16) { // SGI中断走此流程
writel_relaxed(irqstat, cpu_base + GIC_CPU_EOI);
if (static_key_true(&supports_deactivate))
writel_relaxed(irqstat, cpu_base + GIC_CPU_DEACTIVATE);
#ifdef CONFIG_SMP // SMP系统中,SGI中断才真正有意义
/*
* Ensure any shared data written by the CPU sending
* the IPI is read after we've read the ACK register
* on the GIC.
*
* Pairs with the write barrier in gic_raise_softirq
*/
smp_rmb();
handle_IPI(irqnr, regs); // SGI中断处理函数,内核中将软件产生的中断叫IPI中断
#endif
continue;
}
break; // 直到没有处于pending或者active and pending状态的中断,才跳出循坏,退出中断处理程序
} while (1)
gic_handle_irq函数调用handle_domain_irq处理PPI和SPI中断,首先根据硬件中断号找到软件中断号,根据软件中断号找到对应的中断描述符irq_desc,调用中断描述符中的通用中断处理函数handle_irq,zynq7k平台的通用中断处理函数handle_irq指向了handle_fasteoi_irq函数。__handle_domain_irq开始调用了irq_enter,用来增加preempt_count成员里HARDIRQ域的值,HARDIRQ域的值大于0,则表示此进程处于硬件中断上下文中,否则表示此进程不处于硬件中断上下文,__handle_domain_irq退出时调用irq_enter,递减preempt_count成员里HARDIRQ域的值,并判断当前上下文是否处于硬件中断和是否有软中断pending,如当前上下文不处于硬件中断并且有软中断pending,则处理软件中断。
handle_domain_irq
->__handle_domain_irq
->irq_enter // 进入中断上下文
->__irq_enter
// 增加当前进程struct thread_info中的preempt_count成员里HARDIRQ域的值
// HARDIRQ域用于判断当前进程是否处于硬件中断上下文
->preempt_count_add(HARDIRQ_OFFSET)
->irq_find_mapping // 根据中断控制的irq_domain和硬件中断号,获取软件中断号
// 如果硬件中断号小于直接映射的最大中断号,则直接比较domain中的hwirq和硬件中断号是否相等,
// 相等,则直接返回,zynq7k gic初始化时将revmap_direct_max_irq设为0,不满足条件
if (hwirq < domain->revmap_direct_max_irq)
{
data = irq_domain_get_irq_data(domain, hwirq);
if (data && data->hwirq == hwirq)
return hwirq;
}
// zynq7k gic初始化时将revmap_size设为96,满足线性映射的要求,则直接在映射数组中查找软件中断号
if (hwirq < domain->revmap_size) // 如果是线性映射
return domain->linear_revmap[hwirq]; // 返回硬件中断号对应的软件中断号
->rcu_read_lock();
// 上面的两个条件都不满足,则是非线性映射,则直接在revmap_tree树中查找软件中断号
->data = radix_tree_lookup(&domain->revmap_tree, hwirq);
->rcu_read_unlock();
// 如果软件中断号为0或者大于中断号的数量,返回-EINVAL
if (unlikely(!irq || irq >= nr_irqs))
{
ack_bad_irq(irq);
ret = -EINVAL;
}
else
{
generic_handle_irq(irq);
->irq_to_desc // 通过软件中断号获取中断描述符irq_desc
// 非线性映射,根据软件中断号在irq_desc_tree树中查找irq_desc
return radix_tree_lookup(&irq_desc_tree, irq);
// 线性映射,根据软件中断号在irq_desc数组中查找irq_desc
return (irq < NR_IRQS) ? irq_desc + irq : NULL;
->generic_handle_irq_desc
->desc->handle_irq(desc) // 调用通用的中断处理函数handle_fasteoi_irq
}
->irq_exit // 此时硬件中断处理部分已经完成,中断已打开
// 递减当前进程struct thread_info中的preempt_count成员里HARDIRQ域的值
// HARDIRQ域大于0,表示当前进程处于硬件中断上下文中,否则不处于硬件中断上下文中
->preempt_count_sub(HARDIRQ_OFFSET)
if (!in_interrupt() && local_softirq_pending())
invoke_softirq(); // 处理软中断,软中断不能在硬件中断上下文中处理,软件中断将在第六章介绍
->set_irq_regs
从上面的分析可以看出,中断处理函数handle_domain_irq首先从中断控制器ICCIAR寄存器中获取硬件中断号。若硬件中断号到软件中断号采用的是线性映射方式,则以硬件中断号为下表,在数组linear_revmap中的查找软件中断号,然后以软件中断号为下表,在irq_desc数组中查找该中断对应的irq_desc数据结构。若硬件中断号到软件中断号采用的是非线性映射方式,则l利用硬件中断号在树revmap_tree中查找软件中断号,然后利用软件中断号在树irq_desc_tree中查找irq_desc数据结构。
2.GIC SPI和PPI中断通用处理函数
handle_irq是中断引脚号映射过程中设置的通用中断处理函数。 在第三章《Linux之中断子系统-内核中断号映射过程(3)》中,uart0在gic_irq_domain_alloc函数中,调用了__irq_do_set_handler函数进行了设置,使handle_irq指向了handle_fasteoi_irq函数。下面分析一下handle_fasteoi_irq的执行流程,其主要工作是遍历irq_desc中的action链表,执行注册中断时提供的中断处理函数handler,如在第四章《Linux之中断子系统-内核中断注册源代码分析(4)》中,zynq7k串口0注册的中断处理函数为cdns_uart_isr,则此处执行的就是cdns_uart_isr函数。执行完所有的中断处理函数,调用gic_eoi_irq通知gic中断控制器中断已处理完毕,gic中断控制器将此中断的中断状态变更为inactive,同时也标志着gic又可以将中断发送到此cpu上。
这里需要注意的是同一个中断在SMP系统上的执行问题。假设有2个cpu组成的SMP系统,中断触发方式为边缘触发(电平触发的中断不会在中断处理完成之前重复触发):
(1)gic向SMP系统发出了中断。
(2)cpu0响应了此中断后开始处理,同时设置IRQD_IRQ_INPROGRESS标志。在cpu0处理完此中断前,gic不会再向cpu0发送任何中断。
(3)紧接着gic又向SMP系统发送了同样的中断。
(4)cpu1响应了此中断(读ICCIAR寄存器)。由于cpu0还为处理完中断,gic不会向cpu0发送此中断,只能由cpu1响应此中断。
(5)cpu1响应此中断(读ICCIAR寄存器),发现此中断描述符已经设置了IRQD_IRQ_INPROGRESS标志,说明此中断正在其他cpu上处理。
(6)cpu1不会处理此中断,直接向中断控制器发送eoi信号(写ICCEOIR寄存器),结束中断处理,退出中断处理程序。
(7)cpu0处理完中断,向中断控制器发送eoi信号(写ICCEOIR寄存器),结束中断处理,退出中断处理程序。
步骤(6)的中断实际上没有被处理,可以认为此中断被丢弃了。
[kernel/irq/chip.c]
handle_fasteoi_irq
struct irq_chip *chip = desc->irq_data.chip
->raw_spin_lock // 获取自旋锁
// 判断当前中断的执行状态,同一个硬件中断,不能同时在两个CPU上执行,
// 如此中断正在另外一个cpu上执行,则本cpu不处理中断,直接结束此次中断处理
if (!irq_may_run(desc))
goto out;
// 清空标志:IRQS_REPLAY和IRQS_WAITING
desc->istate &= ~(IRQS_REPLAY | IRQS_WAITING)
// 如果action链表为空或者此中断被关闭了,则将中断状态设置为IRQS_PENDING
if (unlikely(!desc->action || irqd_irq_disabled(&desc->irq_data))) {
desc->istate |= IRQS_PENDING;
mask_irq(desc); // 屏蔽中断,最终调用的是gic_mask_irq向ICDICER寄存器中写1
goto out;
}
->kstat_incr_irqs_this_cpu // 内核中断统计相关
// 如果是IRQS_ONESHOT类型的中断,需要屏蔽中断,等中断线程处理完中断再解除屏蔽
if (desc->istate & IRQS_ONESHOT)
mask_irq(desc);
->handle_irq_event // 处理硬件中断的入口
desc->istate &= ~IRQS_PENDING // 清除中断挂起标志
->irqd_set // 设置IRQD_IRQ_INPROGRESS标志,说明中断正在处理中
->raw_spin_unlock // 解锁
->handle_irq_event_percpu
// 遍历action链表,调用注册中断时提供的中断处理函数handler
for_each_action_of_desc(desc, action) {
irqreturn_t res;
// 调用中断处理函数,对于zynq7k的串口0中断,调用的是cdns_uart_isr函数
res = action->handler(irq, action->dev_id);
switch (res) {
// 如果中断处理函数返回IRQ_WAKE_THREAD,则唤醒中断处理线程,
case IRQ_WAKE_THREAD:
__irq_wake_thread(desc, action);
// 如果中断处理函数返回IRQ_HANDLED,则读取相关标志
case IRQ_HANDLED:
flags |= action->flags;
break;
default:
break;
}
retval |= res;
}
->raw_spin_lock // 加锁
->irqd_clear // 处理完中断,清除IRQD_IRQ_INPROGRESS标志
->cond_unmask_eoi_irq
// 如果不是IRQS_ONESHOT类型的中断,则调用gic提供的回调函数gic_eoi_irq,向ICCEOIR寄存器
// 写入硬件中断号,表示此中断已处理完毕,此后中断控制器可以继续向该cpu发送中断了
if (!(desc->istate & IRQS_ONESHOT)) {
chip->irq_eoi(&desc->irq_data);
return;
}
->raw_spin_unlock(&desc->lock) // 解锁
return;
out:
// 向中断控制器发送eoi信号结束此次中断处理
if (!(chip->flags & IRQCHIP_EOI_IF_HANDLED))
chip->irq_eoi(&desc->irq_data); // 具体调用的是gic_eoi_irq函数
raw_spin_unlock(&desc->lock); // 解锁
[kernel/irq/chip.c]
// 返回true,则该中断会被处理;返回false,则该中断不会被处理,handle_fasteoi_irq直接跳转到out处执行
static bool irq_may_run(struct irq_desc *desc)
{
// IRQD_IRQ_INPROGRESS表示该硬件中断正在被处理
// IRQD_WAKEUP_ARMED表示该中断被用作唤醒系统
unsigned int mask = IRQD_IRQ_INPROGRESS | IRQD_WAKEUP_ARMED;
// 判断irq_common_data结构体中的state_use_accessors变量是否设置了
// IRQD_IRQ_INPROGRESS或IRQD_WAKEUP_ARMED标志,没有设置返回true处理中断
if (!irqd_has_set(&desc->irq_data, mask))
return true;
/*
* If the interrupt is an armed wakeup source, mark it pending
* and suspended, disable it and notify the pm core about the
* event.
* 该中断被用作唤醒源,将中断标记为pending and suspended,禁止该中断,同时向其他核通知此事,
* 返回false,不处理此中断
*/
if (irq_pm_check_wakeup(desc))
return false;
/* Handle a potential concurrent poll on a different core. */
return irq_check_poll(desc);
}
[kernel/irq/chip.c]
// 检查该中断是否被多核轮训
static bool irq_check_poll(struct irq_desc *desc)
{
// 检查irq_desc结构体中的istate成员是否设置了IRQS_POLL_INPROGRESS标志
if (!(desc->istate & IRQS_POLL_INPROGRESS))
return false; // 没有设置,直接返回false,不处理中断
// 如果设置了IRQS_POLL_INPROGRESS标志,说明此中断正在被轮训,需要等待轮训结束
return irq_wait_for_poll(desc);
}
如果硬件中断处理函数返回了IRQ_WAKE_THREAD,此时调用__irq_wake_thread唤醒中断处理线程。首选判断线程是否设置了PF_EXITING标志,如设置,则说明线程要退出,则直接返回;然后测试并设置IRQTF_RUNTHREAD标志,如设置了说明中断线程正在运行,返回即可,反之则设置IRQTF_RUNTHREAD标志,表示中断线程将要运行;中断描述符irq_desc结构体中的threads_oneshot和threads_active都是为了处理oneshot类型的中断,oneshot类型的中断(包括硬件中断和中断线程)处理不允许嵌套。threads_oneshot是一个位图,每个比特位代表正在处理的(共享)oneshot类型中断线程,threads_active表示正在运行的中断线程数量。irqaction结构体中的thread_mask也是一个位图,在共享中断中每一个action由一个bit位来表示,此bit保存在thread_mask中;接着将中断线程所在的irqaction的位图保存到threads_oneshot中,记录运行的中断线程;随后将threads_active字段加1,增加唤醒的中断线程数量;最后调用wake_up_process唤醒中断线程。
// 唤醒中断处理线程
void __irq_wake_thread(struct irq_desc *desc, struct irqaction *action)
{
// 如果中断线程设置了关闭标志PF_EXITING,则直接返回
if (action->thread->flags & PF_EXITING)
return;
// 测试并设置IRQTF_RUNTHREAD标志,若已经设置,说明中断线程正在运行,直接返回
if (test_and_set_bit(IRQTF_RUNTHREAD, &action->thread_flags))
return;
// threads_oneshot代表一个位图,每一位代表一个运行的中断线程,thread_mask为本
// action的中断线程,将要运行的中断线程设置到irq_desc结构体的threads_oneshot成员中
desc->threads_oneshot |= action->thread_mask;
// 增加唤醒的中断线程数量
atomic_inc(&desc->threads_active);
// 唤醒线程
wake_up_process(action->thread);
->try_to_wake_up
}
中断处理线程在注册中断的时候已经使用kthread_create创建,使用wake_up_process唤醒此线程,第四章《Linux之中断子系统-内核中断注册源代码分析(4)》中有描述。中断线程的入口函数为irq_thread,实质上在中断注册的时候中断线程已经运行起来了,在irq_wait_for_interrupt处等待被唤醒,硬件中断处理函数唤醒后检测到IRQTF_RUNTHREAD标志被设置,则返回0;接着调用irq_thread_fn函数处理中断(强制中断线程化调用的是irq_forced_thread_fn);执行完需要唤醒等待在wait_for_threads上的线程。
[kernel/irq/manage.c]
irq_thread
->irq_to_desc // 根据软件中断号查找中断描述符irq_desc
if (force_irqthreads && test_bit(IRQTF_FORCED_THREAD, &action->thread_flags))
// 如果强制中断线程化,且设置了IRQTF_FORCED_THREAD标志,则使用irq_forced_thread_fn函数
handler_fn = irq_forced_thread_fn;
else
handler_fn = irq_thread_fn;
// 判断thread_flags变量是否设置了IRQTF_RUNTHREAD标志,如设置了则开始运行
while (!irq_wait_for_interrupt(action)) {
irqreturn_t action_ret;
irq_thread_check_affinity(desc, action);
action_ret = handler_fn(desc, action); // 调用irq_thread_fn函数处理中断
if (action_ret == IRQ_HANDLED)
atomic_inc(&desc->threads_handled); // 增加中断线程处理中断的次数
if (action_ret == IRQ_WAKE_THREAD) // 如果需要唤醒下半段的线程,则唤醒
irq_wake_secondary(desc, action);
// 活动的中断线程计数threads_active变量减1,如减到0,则说明所有线程执行完毕,
// 此时需要唤醒等待在wait_for_threads上的线程。关闭中断的函数disable_irq内部会调用
// synchornize_irq,如中断线程还未处理完毕,则会睡眠在wait_for_threads上,所有中断
// 线程执行完毕,需要唤醒等待的线程
wake_threads_waitq(desc);
}
函数调用了注册中断时提供的函数
irq_thread_fn,执行完毕调用
thread_fn函数处理oneshot类型的中断。
irq_finalize_oneshot
[kernel/irq/manage.c]
// irq_thread调用此函数执行注册的中断线程化函数thread_fn
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); // 处理oneshot类型的中断
return ret;
}
[kernel/irq/manage.c]
static void irq_finalize_oneshot(struct irq_desc *desc, struct irqaction *action)
{
// 如果不是oneshot类型的中断或者强制线程中断化了,则直接退出
if (!(desc->istate & IRQS_ONESHOT) ||
action->handler == irq_forced_secondary_handler)
return;
again:
// 循环判断硬件中断处理函数是否清除了IRQD_IRQ_INPROGRESS标志,清除了则跳出循环
chip_bus_lock(desc);
raw_spin_lock_irq(&desc->lock);
if (unlikely(irqd_irq_inprogress(&desc->irq_data))) {
raw_spin_unlock_irq(&desc->lock);
chip_bus_sync_unlock(desc);
cpu_relax();
goto again;
}
// 测试是否设置了IRQTF_RUNTHREAD标志,如设置了,直接跳到out_unlock处,不清除oneshot中断
if (test_bit(IRQTF_RUNTHREAD, &action->thread_flags))
goto out_unlock;
// 清除oneshot线程运行的位图
desc->threads_oneshot &= ~action->thread_mask;
// oneshot类型的中断线程执行完毕后,需要打开中断
if (!desc->threads_oneshot && !irqd_irq_disabled(&desc->irq_data) &&
irqd_irq_masked(&desc->irq_data))
unmask_threaded_irq(desc); // 打开中断
out_unlock:
raw_spin_unlock_irq(&desc->lock);
chip_bus_sync_unlock(desc);
}
handle_fasteoi_irq是现代形式的中断处理程序,需要中断控制器具有eoi功能。同时Linux内核也保留了之前使用的通用中断处理程序,这些函数都在[kernel/irq/chip.c]目录下,具体信息如下,这些函数的主要区别在于中断控制器的流控不同。
(1)handle_level_irq 处理电平触发的中断
(2)handle_simple_irq 通常用于多路复用中的子中断,由父中断的流控回调调用
(3)handle_nested_irq 处理中断线程发送的中断
(4)handle_edge_irq 处理边缘触发的中断
(5)handle_edge_eoi_irq 处理边缘触发的具有eoi功能中断控制器的中断
(6)handle_percpu_irq 处理本地cpu的中断
3.GIC SGI中断通用处理函数
硬件中断号小于16的为SGI中断,SGI中断调用函数处理,ipi_types中的handler处理函数由set_ipi_handler函数设置。
handle_IPI
void handle_IPI(int ipinr, struct pt_regs *regs)
{
unsigned int cpu = smp_processor_id(); // 获取被中断打断进程执行的cpu编号
struct pt_regs *old_regs = set_irq_regs(regs);
if (ipi_types[ipinr].handler) {
__inc_irq_stat(cpu, ipi_irqs[ipinr]);
irq_enter(); // 进入中断
// 根据SGI硬件中断号,调用相应的中断处理函数
(*ipi_types[ipinr].handler)();
irq_exit(); // 退出中断
} else
pr_debug("CPU%u: Unknown IPI message 0x%x
", cpu, ipinr);
set_irq_regs(old_regs);
}
4.具体外设的中断处理函数
为zynq7k注册的串口中断处理函数,首先读取串口0中断状态寄存器,然后清除中断,接着根据中断状态寄存器判断是发送中断还是接收中断,根据判断结果进行相应的处理,处理完成,返回IRQ_HANDLED。
cdns_uart_isr
[drivers/tty/serial/xilinx_uartps.c]
cdns_uart_isr
// 可以看出,串口中断服务函数不允许并发的执行
->spin_lock // 加锁
isrstatus = cdns_uart_readl(CDNS_UART_ISR_OFFSET); // 获取中断状态
cdns_uart_writel(isrstatus, CDNS_UART_ISR_OFFSET); // 清中断
static irqreturn_t cdns_uart_isr(int irq, void *dev_id)
{
struct uart_port *port = (struct uart_port *)dev_id;
unsigned int isrstatus;
spin_lock(&port->lock);
// 获取中断状态寄存器信息
isrstatus = cdns_uart_readl(CDNS_UART_ISR_OFFSET);
cdns_uart_writel(isrstatus, CDNS_UART_ISR_OFFSET); // 清中断
if (isrstatus & CDNS_UART_IXR_TXEMPTY) {
// 串口发送中断,处理发送逻辑
cdns_uart_handle_tx(dev_id);
isrstatus &= ~CDNS_UART_IXR_TXEMPTY;
}
if (isrstatus & CDNS_UART_IXR_MASK)
// 串口接收中断,处理接收逻辑
cdns_uart_handle_rx(dev_id, isrstatus);
spin_unlock(&port->lock); // 解锁
// 中断处理完成,返回IRQ_HANDLED
return IRQ_HANDLED;
}






![[C++探索之旅] 第一部分第十一课:小练习,猜单词 - 鹿快](https://img.lukuai.com/blogimg/20251015/da217e2245754101b3d2ef80869e9de2.jpg)










暂无评论内容