转载自兰新宇专栏,https://zhuanlan.zhihu.com/p/85454778
1. ISR的安装
IRQ对应的设备驱动程序需要在中断到来之前安装/注册该中断的处理函数,也就是ISR。由于历史原因,Linux的中断子系统是按照服务中断共享的模式设计的,因此ISR的安装需要分为两级,第一级是针对这个IRQ线的,称为generic handler(或high level handler),第二级是针对挂载在这个IRQ线上的不同设备的,称为specific handler。
Generic handler在初始化的时候由irq_set_handler(irq, handle)添加,其中"irq"和"handle"分别是IRQ号和第一级的处理函数。
Specific handler的安装则是由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)
{
//分配irq_action
action = kzalloc(sizeof(struct irqaction), GFP_KERNEL);
if (!action)
return -ENOMEM;
//填写irq_action
action->handler = handler;
action->thread_fn = thread_fn;
action->flags = irqflags;
action->name = devname;
action->dev_id = dev_id;
//添加到IRQ链表
struct irq_desc *desc = irq_to_desc(irq);
retval = __setup_irq(irq, desc, action);
...
}
所谓"request",是指中断向CPU请求服务,对于级联的中断控制器,外侧(离CPU较远)的中断控制器不能直接向CPU递交请求,但它可以向与之相连的内侧(离CPU较近)的中断控制器请求服务。
在Linux的dts中,对于一个IRQ,"interrupt-parent"表示其所连接的中断控制器,而对于一个中断控制器,"interrupt-parent"就表示级联模式下,其所连接的这个内侧的中断控制器。
request_threaded_irq()的主要作用就是填写前文介绍的"irq_action"结构体,包括第二级的处理函数"handler"和"thread_fn",区分共享IRQ的不同设备的"dev_id"和"devname"等。这一过程涉及到"irq_action"的分配,可能引起睡眠和调度,因而不能在中断上下文(interrupt context)中使用。
从内核的角度,所谓中断上下文就是指执行ISR时的上下文。确保中断上下文中不会出现线程的切换并不是件容易的事,需要仔细审查ISR中的每行代码,包括调用的每一个函数,确保它们不会进入睡眠状态而使调度器介入其中。
在处理中断时不能睡眠似乎已经成为了大家默认的一条法则,但并不是在中断处理的过程中睡眠了就一定会出现问题,这只是Linux内核采取的设计理念,并不是不容置疑的金科玉律。
request_threaded_irq()的前身是request_irq(),新的API引入的变化体现在增加的参数"thread_fn"上,这个"thread_fn"是用于中断线程化的(关于中断线程化的介绍请参考这篇文章)。
"irqflags"作为标志位,可以表达的内容很多,这里仅介绍一个和中断共享有关的"IRQF_SHARED"。中断源的触发类型主要分为电平触发(level sensitive)和边沿触发(edge-triggered)两种,这个在安装ISR的时候就要指定清楚。
在中断共享的情境下,使用某个IRQ的设备指定了触发类型后,与它共享IRQ的设备在安装自己的第二级处理函数时,就不能指定一个与之相悖的触发类型,否则会安装失败,或引起不可预知的错误。如果没有使用"IRQF_SHARED",说明这是一个不可共享的IRQ,则request_threaded_irq()只能被调用一次,重复调用会出现"-EBUSY"的错误。
__setup_irq()
执行后,设备对应的"irq_action"将被添加到所在IRQ链表的末尾,并会在/proc/irq/<irq number>
目录下生成对应的节点。
如果一个IRQ链表上没有"irq_action",说明还没有驱动程序安装,也就是没有设备在使用这条IRQ线。对于一个没有安装ISR的中断,应该选择屏蔽掉它,直到它的处理函数被安装上,否则设备将可能因为自己中断请求得不到服务,而一直不停地中断CPU。
解除安装可使用free_irq()函数。如果已经没有任何设备驱动安装在一个IRQ线上,那么该IRQ应该被disable。
void *free_irq (unsigned int irq, void *dev_id)
2. ISR的执行
当中断经过中断控制器到达CPU后,Linux会首先通过irq_find_mapping()函数,根据物理中断号"hwirq"的值,查找上文讲到的包含映射关系的radix tree或者线性数组,得到"hwirq"对应的虚拟中断号"irq"。
unsigned int irq_find_mapping(struct irq_domain *domain, irq_hw_number_t hwirq)
{
struct irq_data *data;
//线性映射的查找
if (hwirq < domain->revmap_size)
return domain->linear_revmap[hwirq];
//radix tree映射的查找
rcu_read_lock();
data = radix_tree_lookup(&domain->revmap_tree, hwirq);
rcu_read_unlock();
return data ? data->irq : 0;
}
而后就可以调用do_IRQ(irq, ...)函数进行实际的中断处理了。
进入ISR之前自然要先保存被中断的线程的上下文,这主要是由set_irq_regs()函数来完成的:
struct pt_regs *set_irq_regs(struct pt_regs *new_regs)
{
struct pt_regs *old_regs = get_irq_regs();
__this_cpu_write(irq_regs, new_regs);
return old_regs;
}
线程的上下文信息被保存在"old_regs"中,以便中断退出之后可以恢复。
irq_enter()和irq_exit()函数分别用于标记ISR的进入和退出,具体的标记方法请参考介绍preempt_count()的这篇文章。
而generic_handle_irq()函数,实际就是回调IRQ之前安装的第一级处理函数(desc->handle_irq
)。
void generic_handle_irq_desc(struct irq_desc *desc)
{
//执行第一级处理函数
desc->handle_irq(desc);
}
对于电平触发,注册的"handle_irq"是handle_level_irq()。对于边沿触发,注册的"handle_irq"是handle_edge_irq()(相关代码位于"/kernel/irq/chip.c")。关于电平触发和边沿触发的具体处理过程,请参考这篇文章。
第一级的中断处理完成后,就是遍历IRQ线上的链表,依次执行通过前面讲的request_threaded_irq()安装的各个"irq_action",也就是第二级的处理函数(action->handler)。这里需要判断是不是自己的设备产生的中断,方法已经在前文阐述过了。
irqreturn_t __handle_irq_event_percpu(struct irq_desc *desc, unsigned int *flags)
{
//遍历IRQ链表
for_each_action_of_desc(desc, action) {
irqreturn_t res;
//执行第二级处理函数
res = action->handler(irq, action->dev_id);
...
}
第二级的处理函数通常也不是一口气就能把中断响应所需的所有操作执行完的,具体原因请看下文分解。
评论 (0)