1.概述
外设产生中断后,Linux内核会执行一个具体的函数来响应中断,此函数被称为中断服务函数。中断服务函数运行在中断上下文中,若中断线程化后,则中断事件在进程上下文中处理。不同的外设中断源,其对应的中断服务函数一般也不同。因此在使用具体的外设中断之前,需要注册对应的中断服务函数,并指明软件中断号、中断标志、中断名称及传递给中断服务函数的参数等。
2.中断注册接口介绍
2.1.request_irq
使用request_irq
注册一个中断服务函数,内部调用的是request_threaded_irq
函数,只是将thread_fn
设置为NULL
。irq
为软件中断号,handler
为中断服务函数,flags
为中断标志,name
中断名称,dev
为传递给中断服务函数的参数,可为NULL
,指明IRQF_SHARED
时,必须传递此参数。返回值为0表示注册成功,非0表示注册失败。
[include/linux/interrupt.h]
static inline int __must_check
request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags,
const char *name, void *dev)
irq:软件中断号
handler:中断服务函数
flags:中断标志
#define IRQF_TRIGGER_RISING 0x00000001 // 上升沿触发
#define IRQF_TRIGGER_FALLING 0x00000002 // 下降沿触发
#define IRQF_TRIGGER_HIGH 0x00000004 // 高电平触发
#define IRQF_TRIGGER_LOW 0x00000008 // 下降沿触发
// 共享中断,多个设备共享一个中断号(中断引脚),在中断处理程序中需要查询那个外设发生了中断
#define IRQF_SHARED 0x00000080
#define IRQF_PROBE_SHARED 0x00000100 // 中断处理程序允许sharing mismach发生
#define __IRQF_TIMER 0x00000200
// 时钟中断
#define IRQF_TIMER (__IRQF_TIMER | IRQF_NO_SUSPEND | IRQF_NO_THREAD)
#define IRQF_PERCPU 0x00000400 // 某个CPU特有的中断
#define IRQF_NOBALANCING 0x00000800 // 禁止CPU之间的中断均衡
#define IRQF_IRQPOLL 0x00001000 // 中断被用作轮训
// 中断线程执行完成之前不会打开中断,直至中断线程处理完所有中断,才会重新使能中断
#define IRQF_ONESHOT 0x00002000
#define IRQF_NO_SUSPEND 0x00004000 // 系统suspend期间不要关闭中断
#define IRQF_FORCE_RESUME 0x00008000 // 系统resume时必须强制使能此中断
#define IRQF_NO_THREAD 0x00010000 // 此中断不能中断线程化
#define IRQF_EARLY_RESUME 0x00020000 // 系统resume时提前使能此中断
name:中断名称
dev:传递给中断服务函数的参数,可为NULL,共享中断必须传递此参数
2.2.request_threaded_irq
使用request_threaded_irq
注册一个中断服务函数和中断线程化后调用的函数。irq
为软件中断号,handler
为中断服务函数,thread_fn
中断线程化后内核线程执行的函数,flags
为中断标志,name
中断名称,dev
为传递给中断服务函数的参数,可为NULL
,指明IRQF_SHARED
时,必须传递此参数。返回值为0表示注册成功,非0表示注册失败。
[include/linux/interrupt.h]
int __must_check
request_threaded_irq(unsigned int irq, irq_handler_t handler,
irq_handler_t thread_fn,
unsigned long flags, const char *name, void *dev)
irq:软件中断号
handler:中断服务函数
thread_fn:中断线程化后调用的函数
flags:中断标志,和request_irq的中断标志含义一样
name:中断名称
dev:给中断服务函数传递的参数
2.3.free_irq
request_irq
和request_threaded_irq
注册的中断服务函数使用free_irq
函数释放。
[include/linux/interrupt.h]
void free_irq(unsigned int irq, void *dev_id)
irq:软件中断号
dev_id:给中断服务函数传递的参数
3.源代码分析
由于request_irq
内部调用的是request_threaded_irq
,因此这里只分析request_threaded_irq
函数。request_threaded_irq
函数的主要工作是分配一个struct irqaction
结构体并设置相关成员,设置的主要成员有中断服务函数handler
,软件中断号irq
、中断线程化执行的函数thread_fn
及中断标志flags
,handler
、irq
、thread_fn
和flags
根据传入的参数设置。最后将分配的struct irqaction
结构体挂接到struct irq_desc
结构体中的action
链表下面,非共享中断action
链表只有一个节点,共享中断action
链表有多个节点。中断发生后,会遍历action
链表,使用handler
或thread_fn
等处理中断。使用request_threaded_irq
注册中断时需要注意一下几点:
(1)IRQ为软件(虚拟)中断号,是由硬件中断号映射而来。
(2)primary handler(中断上半部分处理函数)和threaded_fn(中断下半部分处理函数)不能同时为NULL。
(3)当primary handler为NULL且硬件中断控制器不支持硬件ONESHOT功能时,应设置IRQF_ONESHOT标志来确保不会产生中断风暴。
(4)启用了中断线程化,primary handler函数应该返回IRQ_WAKE_THREAD,以唤醒中断处理线程。
[include/linux/interrupt.h]
// 和具体的中断逻辑处理相关
struct irqaction {
irq_handler_t handler; // 中断服务函数,注册中断时设置
void *dev_id; // 中断服务函数参数
void __percpu *percpu_dev_id;
struct irqaction *next; // 共享中断有多个irqaction结构体,next指向下一个irqaction
irq_handler_t thread_fn; // 中断线程化执行的函数
struct task_struct *thread; // 指向被中断线程的task_struct
struct irqaction *secondary; // 指向中断下半部分的irqaction
unsigned int irq; // 软件中断号
unsigned int flags; // 中断标志
unsigned long thread_flags; // 线程标志
unsigned long thread_mask; // 跟踪中断线程活动的位图
const char *name; // 中断名称
struct proc_dir_entry *dir; // 指向/proc/irq/NN/name
} ____cacheline_internodealigned_in_smp;
[include/linux/interrupt.h]
request_threaded_irq
// 检查参数,设置IRQF_SHARED标志时,必须向中断服务函数传递参数,即必须传入dev_id参数,共享中断根据
// dev_id区分产生中断的外设
if (((irqflags & IRQF_SHARED) && !dev_id) ||
(!(irqflags & IRQF_SHARED) && (irqflags & IRQF_COND_SUSPEND)) ||
((irqflags & IRQF_NO_SUSPEND) && (irqflags & IRQF_COND_SUSPEND)))
->irq_to_desc // 获取软件中断号对应的irq_desc
->radix_tree_lookup // 定义CONFIG_SPARSE_IRQ,从irq_desc_tree中查找
// 没有定义CONFIG_SPARSE_IRQ,从irq_desc数组中查找,索引为软件中断号
return (irq < NR_IRQS) ? irq_desc + irq : NULL
->handler = irq_default_primary_handler // 若没有设置中断服务函数,则设置默认的中断服务函数
->kzalloc // 分配irqaction结构体内存
// 设置irqaction结构体
action->handler = handler; // 设置中断处理函数
action->thread_fn = thread_fn; // 设置中断线程化内核线程执行的函数
action->flags = irqflags; // 中断标志
action->name = devname; // 中断名称
action->dev_id = dev_id; // 传递给中断服务函数的参数
->__setup_irq
new->irq = irq // 设置irqaction结构体中的软件中断号irq
->irq_settings_is_nested_thread // 检查此中断是否嵌套到另一个中断线程中
return desc->status_use_accessors & _IRQ_NESTED_THREAD
->new->handler = irq_nested_primary_handler // 如嵌套,则设置新的中断处理函数
->irq_settings_can_thread // 中断是否可以线程化
->irq_setup_forced_threading
Linux中断强制线程化是一个过渡方案,目前还有很多的驱动使用旧版本的API注册中断,这些驱动的中断处理通常采用上下半部分的方式。
force_irqthreads // 如果force_irqthreads定义为true,则强制将中断线程化
new->flags |= IRQF_ONESHOT // 强制中断服务函数线程化,其在关中断的状态下运行,因此需要设置此标志
// 如果handler不为irq_default_primary_handler且设置了中断线程化执行的函数
new->secondary = kzalloc() // 则分配中断下半部分使用的irqaction结构体
// 设置强制中断线程化的中断服务函数
new->secondary->handler = irq_forced_secondary_handler;
// 设置中断线程化内核线程执行的函数
new->secondary->thread_fn = new->thread_fn;
new->secondary->dev_id = new->dev_id; // 设置传递给中断服务函数的参数
new->secondary->irq = new->irq; // 设置软件中断号
new->secondary->name = new->name; // 设置中断名称
->set_bit // 设置强制中断线程化标志,设置到irqaction的thread_flags标志中
new->thread_fn = new->handler
// 设置默认的中断服务函数为irq_default_primary_handler
new->handler = irq_default_primary_handler
->setup_irq_thread // 如果传入了thread_fn,则创建中断线程,优先级为50,调度策略为SCHED_FIFO
if (!secondary) { // 没有强制中断线程化
// kthread_create创建的内核线程由内核线程kthreadd创建,返回创建线程的task_struct指针
// 中断线程唤醒后运行的第一个函数为irq_thread
t = kthread_create(irq_thread, new, "irq/%d-%s", irq, new->name);
} else { // 强制中断线程化
t = kthread_create(irq_thread, new, "irq/%d-s-%s", irq, new->name);
param.sched_priority -= 1;
}
->sched_setscheduler_nocheck // 设置内核线程的优先级
new->thread = t // 使irqaction中的thread指向创建线程的task_struct
->set_bit // 设置irqaction中的标记为IRQTF_AFFINITY
->setup_irq_thread // 如果需要中断下半部分,则启动处理中断下半部分的内核线程
// 将新分配的irqaction结构体追加到irq_desc结构体的action链表的末尾,共享中断,action
// 链表有多个节点,非共享中断,action链表只有一个节点
*old_ptr = new
->irq_pm_install_action // 电源管理相关
desc->irq_count = 0
desc->irqs_unhandled = 0
// kthread_create创建的内核线程默认不运行,需要使用wake_up_process唤醒
->wake_up_process(new->thread) // 唤醒中断线程化的线程
->wake_up_process(new->secondary->thread) // 唤醒中断下半部分的线程
->register_irq_proc // 将中断注册到proc文件系统中
下面分析一下free_irq
的执行过程,主要功能是释放掉request_threaded_irq
注册的资源。
[include/linux/interrupt.h]
free_irq
->irq_to_desc // 根据软件中断号获取中断描述符
desc->affinity_notify = NULL // 如果定义CONFIG_SMP,则将中断对CPU的亲和力设置为NULL
->__free_irq
->irq_to_desc // 根据软件中断号获取中断描述符
->in_interrupt // 检测是否在中断上下文中free IRQ
->raw_spin_lock_irqsave // 获取自旋锁并禁止中断
// 遍历action链表,查找要释放的action
action_ptr = &desc->action;
for (;;) {
action = *action_ptr;
if (action->dev_id == dev_id) // action中的dev_id和传入的dev_id相等,则跳出循环
break;
action_ptr = &action->next;
}
// 从链表中移除此action
*action_ptr = action->next
->irq_pm_remove_action // 电源管理相关操作
// 如果action链表为空,说明此中断不需要处理,需要关闭中断
if (!desc->action) { // action链表为空
irq_shutdown(desc); // 关闭此中断
irq_release_resources(desc); // 释放中断资源
}
->raw_spin_unlock_irqrestore // 释放自旋锁
->unregister_handler_proc // 注销proc文件系统中的中断信息
->synchronize_irq // 同步中断,等待其他CPU处理完要free的中断任务
->irq_to_desc // 获取中断描述符
->__synchronize_hardirq
do {
unsigned long flags;
while (irqd_irq_inprogress(&desc->irq_data)) // 获取中断处理状态
// arm32 cpu_relax执行一条内存屏障指令,arm64执行一条yield指令和内存屏障指令
// yield指令可以降低CPU功耗
cpu_relax();
// 上述获取中断处理状态缺乏严格的内存屏障,可能存在误读的情况,因此这里再次读取
raw_spin_lock_irqsave(&desc->lock, flags); // 加锁
inprogress = irqd_irq_inprogress(&desc->irq_data); // 读取
raw_spin_unlock_irqrestore(&desc->lock, flags); // 解锁
} while (inprogress); // 循环读取,直到其他CPU处理完中断
->wait_event // 等待中断处理线程处理完中断
->kthread_stop // 停止中断处理线程
->kthread_stop // 停止中断下半部分处理线程
->kfree // 释放中断下半部分secondary的内存
->kfree // 释放action对应的内存
4.zynq7k串口0的中断注册
zynq7k串口0的中断注册在cdns_uart_startup
函数中完成,irq
为已经映射好的软件中断号, cdns_uart_isr
为注册的中断处理函数,flags
为0,中断类型和中断触发方式已在设备树中指定,即串口0的中断类型为SPI中断,中断触发方式为高电平,中断名称为"xuartps"
,port
为给中断处理函数传递的参数。中断处理函数cdns_uart_startup
在Linux内核高层中断处理中分析。
[drivers/tty/serial/xilinx_uartps.c]
#define CDNS_UART_NAME "xuartps" // 中断名称
cdns_uart_startup
->request_irq(port->irq, cdns_uart_isr, 0, CDNS_UART_NAME, (void *)port) // 注册中断
参考资料
- Linux kernel V4.6版本源码
- 《奔跑吧 Linux内核:基于Linux 4.x内核源代码问题分析》
- 《Zynq-7000 SoC Technical Reference Manual》
- 《ARM® Generic Interrupt Controller Architecture version 2.0》