一.中断简介
什么是中断
Interrupt Request,简称IRQ,中断是指在CPU正常运行期间,由于内外部事件或由程序预先安排的事件引起的 CPU 暂时停止正在运行的程序,转而为该内部或外部事件或预先安排的事件服务的程序中去,服务完毕后再返回去继续运行被暂时中断的程序。
软件对硬件进行配置后,软件期望等待硬件的某种状态(比如,收到了数据,GPIO状态变化),这里有两种方式,一种是轮询(polling): CPU 不断的去读硬件状态。另一种是当硬件完成某种事件后,给 CPU 一个中断,让 CPU 停下手上的事情,去处理这个中断。很显然,中断的交互方式提高了系统的吞吐。
当 CPU 收到一个中断 (IRQ)的时候,会去执行该中断对应的处理函数(ISR)。普通情况下,会有一个中断向量表(IDT),向量表中定义了 CPU 对应的每一个外设资源的中断处理程序的入口,当发生对应的中断的时候, CPU 直接跳转到这个入口执行程序。也就是中断上下文。(注意:中断上下文中,不可阻塞睡眠)。
二.内核中断初始化流程
2.1 中断相关的数据结构
结构名称 | 作用 |
中断描述符:irq_desc | IRQ 的软件层面上的资源描述 |
响应函数:irqaction | IRQ 的通用操作,例如函数入口 |
中断数据:irq_data | 中断描述符中应该会包括底层irq chip相关的数据结构 |
操作合集:irq_chip | 对应每个芯片的具体实现 |
2.1.1 中断描述符 irq_desc
struct irq_desc {
struct irq_data irq_data;
unsigned int __percpu *kstat_irqs;------IRQ的统计信息
irq_flow_handler_t handle_irq;--------流控函数
struct irqaction *action; -----------处理函数
unsigned int status_use_accessors;-----中断描述符的状态,参考IRQ_xxxx
unsigned int core_internal_state__do_not_mess_with_it;
unsigned int depth;----------描述嵌套深度的信息
unsigned int wake_depth;--------电源管理中的wake up source相关
unsigned int irq_count;
unsigned long last_unhandled;
unsigned int irqs_unhandled;
raw_spinlock_t lock;
struct cpumask *percpu_enabled;
#ifdef CONFIG_SMP
const struct cpumask *affinity_hint;----和irq affinity相关,后续单独文档描述
struct irq_affinity_notify *affinity_notify;
#ifdef CONFIG_GENERIC_PENDING_IRQ
cpumask_var_t pending_mask;
#endif
#endif
unsigned long threads_oneshot;
atomic_t threads_active;
wait_queue_head_t wait_for_threads;
#ifdef CONFIG_PROC_FS
struct proc_dir_entry *dir;--------该IRQ对应的proc接口
#endif
int parent_irq;
struct module *owner;
const char *name;
} ____cacheline_internodealigned_in_smp
2.1.2 响应函数 irqaction
主要是用来存用户注册的中断处理函数,一个中断可以有多个处理函数 ,当一个中断有多个处理函数,说明这个是共享中断,所谓共享中断就是一个中断的来源有很多,这些来源共享同一个引脚。
所以在irq_desc结构体中的action成员是个链表,以action为表头,若是一个以上的链表就是共享中断
struct irqaction {
irq_handler_t handler; //等于用户注册的中断处理函数,中断发生时就会运行这个中断处理函数
unsigned long flags; //中断标志,注册时设置,比如上升沿中断,下降沿中断等
cpumask_t mask; //中断掩码
const char *name; //中断名称,产生中断的硬件的名字
void *dev_id; //设备id
struct irqaction *next; //指向下一个成员
int irq; //中断号,
struct proc_dir_entry *dir; //指向IRQn相关的/proc/irq/
};
2.1.3 中断数据 irq_data
中断描述符中会包括底层irq chip相关的数据结构,linux kernel中把这些数据组织在一起,形成struct irq_data,具体代码如下:
struct irq_data {
u32 mask;----------TODO
unsigned int irq;--------IRQ number
unsigned long hwirq;-------HW interrupt ID
unsigned int node;-------NUMA node index
unsigned int state_use_accessors;--------底层状态,参考IRQD_xxxx
struct irq_chip *chip;----------该中断描述符对应的irq chip数据结构
struct irq_domain *domain;--------该中断描述符对应的irq domain数据结构
void *handler_data;--------和外设specific handler相关的私有数据
void *chip_data;---------和中断控制器相关的私有数据
struct msi_desc *msi_desc;
cpumask_var_t affinity;-------和irq affinity相关
};
2.1.4 操作合集 irq_chip
irq_chip 是一串和芯片相关的函数指针,这里定义的非常的全面,基本上和 IRQ 相关的可能出现的操作都全部定义进去了,具体根据不同的芯片,需要在不同的芯片的地方去初始化这个结构,然后这个结构会嵌入到通用的 IRQ 处理软件中去使用,使得软件处理逻辑和芯片逻辑完全的分开。
struct irq_chip {
const char *name;
unsigned int (*irq_startup)(struct irq_data *data);-------------初始化中断
void (*irq_shutdown)(struct irq_data *data);----------------结束中断
void (*irq_enable)(struct irq_data *data);------------------使能中断
void (*irq_disable)(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);---------------------发送EOI信号,表示硬件中断处理已经完成。
int (*irq_set_affinity)(struct irq_data *data, const struct cpumask *dest, bool force);--------绑定中断到某个CPU
int (*irq_retrigger)(struct irq_data *data);----------------重新发送中断到CPU
int (*irq_set_type)(struct irq_data *data, unsigned int flow_type);----------------------------设置触发类型
int (*irq_set_wake)(struct irq_data *data, unsigned int on);-----------------------------------使能/关闭中断在电源管理中的唤醒功能。
void (*irq_bus_lock)(struct irq_data *data);
void (*irq_bus_sync_unlock)(struct irq_data *data);
void (*irq_cpu_online)(struct irq_data *data);
void (*irq_cpu_offline)(struct irq_data *data);
void (*irq_suspend)(struct irq_data *data);
void (*irq_resume)(struct irq_data *data);
void (*irq_pm_shutdown)(struct irq_data *data);
...
unsigned long flags;
}
对应关系:
参考文献:https://stephenzhou.blog.csdn.net/article/details/90648475?spm=1001.2014.3001.5502
2.2 中断初始化流程
内核启用中断以前,必须把IDT表的初始地址装到idtr寄存器,并初始化表中的每一项。这个动作是在初始化系统时完成的。
从 start_kernel 开始,在这里面,调用了和 machine 相关的 IRQ 的初始化 init_IRQ():
asmlinkage __visible void __init start_kernel(void)
{
char *command_line;
char *after_dashes;
.....
early_irq_init();
init_IRQ();
.....
}
在 init_IRQ 中,调用了 machine_desc->init_irq():
void __init init_IRQ(void)
{
int ret;
if (IS_ENABLED(CONFIG_OF) && !machine_desc->init_irq)
irqchip_init();
else
machine_desc->init_irq();
if (IS_ENABLED(CONFIG_OF) && IS_ENABLED(CONFIG_CACHE_L2X0) &&
(machine_desc->l2c_aux_mask || machine_desc->l2c_aux_val)) {
if (!outer_cache.write_sec)
outer_cache.write_sec = machine_desc->l2c_write_sec;
ret = l2x0_of_init(machine_desc->l2c_aux_val,
machine_desc->l2c_aux_mask);
if (ret && ret != -ENODEV)
pr_err("L2C: failed to init: %d\n", ret);
}
uniphier_cache_init();
}
machine_desc->init_irq() 完成对中断控制器的初始化,为每个irq_desc结构安装合适的流控handler,为每个irq_desc结构安装irq_chip指针,使他指向正确的中断控制器所对应的irq_chip结构的实例,同时,如果该平台中的中断线有多路复用(多个中断公用一个irq中断线)的情况,还应该初始化irq_desc中相应的字段和标志,以便实现中断控制器的级联。这里初始化的时候回调用到具体的芯片相关的中断初始化的地方。
这些函数定义在 include/linux/irq.h 文件。是对芯片初始化的时候可见的 APIs,用于指定中断“流控”中的 :
irq_flow_handler_t handle
也就是中断来的时候,最后那个函数调用。
中断流控函数,分几种,电平触发的中断,边沿触发的,等:
/*
* Built-in IRQ handlers for various IRQ types,
* callable via desc->handle_irq()
*/
extern void handle_level_irq(struct irq_desc *desc);
extern void handle_fasteoi_irq(struct irq_desc *desc);
extern void handle_edge_irq(struct irq_desc *desc);
extern void handle_edge_eoi_irq(struct irq_desc *desc);
extern void handle_simple_irq(struct irq_desc *desc);
extern void handle_untracked_irq(struct irq_desc *desc);
extern void handle_percpu_irq(struct irq_desc *desc);
extern void handle_percpu_devid_irq(struct irq_desc *desc);
extern void handle_bad_irq(struct irq_desc *desc);
extern void handle_nested_irq(unsigned int irq);
而在这些处理函数里,会去调用到 : handle_irq_event
/**
* handle_level_irq - Level type irq handler
* @desc: the interrupt description structure for this irq
*
* Level type interrupts are active as long as the hardware line has
* the active level. This may require to mask the interrupt and unmask
* it after the associated handler has acknowledged the device, so the
* interrupt line is back to inactive.
*/
void handle_level_irq(struct irq_desc *desc)
{
raw_spin_lock(&desc->lock);
mask_ack_irq(desc);
if (!irq_may_run(desc))
goto out_unlock;
desc->istate &= ~(IRQS_REPLAY | IRQS_WAITING);
/*
* If its disabled or no action available
* keep it masked and get out of here
*/
if (unlikely(!desc->action || irqd_irq_disabled(&desc->irq_data))) {
desc->istate |= IRQS_PENDING;
goto out_unlock;
}
kstat_incr_irqs_this_cpu(desc);
handle_irq_event(desc);
cond_unmask_irq(desc);
out_unlock:
raw_spin_unlock(&desc->lock);
}
而这个 handle_irq_event 则是调用了处理,handle_irq_event_percpu:
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;
}
handle_irq_event_percpu->__handle_irq_event_percpu-> 【action->handler()】
这里终于看到了调用 的地方了,就是咱们通过 request_irq 注册进去的函数;
三.中断的处理流程
3.1 发生中断
CPU执行异常向量vector_irq的代码, 即异常向量表中的中断异常的代码,它是一个跳转指令,跳去执行真正的中断处理程序,在vector_irq里面,最终会调用中断处理的总入口函数。
C 语言的入口为 : asm_do_IRQ(unsigned int irq, struct pt_regs *regs)
asmlinkage void __exception_irq_entry
asm_do_IRQ(unsigned int irq, struct pt_regs *regs)
{
handle_IRQ(irq, regs);
}
该函数的入参 irq 为中断号。
asm_do_IRQ -> handle_IRQ
void handle_IRQ(unsigned int irq, struct pt_regs *regs)
{
__handle_domain_irq(NULL, irq, false, regs);
}
handle_IRQ -> __handle_domain_irq
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();
#ifdef CONFIG_IRQ_DOMAIN
if (lookup)
irq = irq_find_mapping(domain, hwirq);
#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);
}
irq_exit();
set_irq_regs(old_regs);
return ret;
}
这里请注意:
先调用了 irq_enter 标记进入了硬件中断:
irq_enter是更新一些系统的统计信息,同时在__irq_enter宏中禁止了进程的抢占。虽然在产生IRQ时,ARM会自动把CPSR中的I位置位,禁止新的IRQ请求,直到中断控制转到相应的流控层后才通过local_irq_enable()打开。那为何还要禁止抢占?这是因为要考虑中断嵌套的问题,一旦流控层或驱动程序主动通过local_irq_enable打开了IRQ,而此时该中断还没处理完成,新的irq请求到达,这时代码会再次进入irq_enter,在本次嵌套中断返回时,内核不希望进行抢占调度,而是要等到最外层的中断处理完成后才做出调度动作,所以才有了禁止抢占这一处理
再调用 generic_handle_irq
最后调用 irq_exit 删除进入硬件中断的标记
__handle_domain_irq -> generic_handle_irq
int generic_handle_irq(unsigned int irq)
{
struct irq_desc *desc = irq_to_desc(irq);
if (!desc)
return -EINVAL;
generic_handle_irq_desc(desc);
return 0;
}
EXPORT_SYMBOL_GPL(generic_handle_irq);
首先在函数 irq_to_desc 中根据发生中断的中断号,去取出它的 irq_desc 中断描述结构,然后调用 generic_handle_irq_desc:
static inline void generic_handle_irq_desc(struct irq_desc *desc)
{
desc->handle_irq(desc);
}
这里调用了 handle_irq 函数。
总结:
vector_irq()->vector_irq()->__irq_svc()
->svc_entry()-----------------------------------------------保护中断现场
->irq_handler()->gic_handle_irq()---------具体到GIC中断控制器对应的就是gic_handle_irq(),此处从架构相关进入了GIC相关处理。
->GIC_CPU_INTACK---------------------------读取IAR寄存器,响应中断。
->handle_domain_irq()
->irq_enter()------------------------------------进入硬中断上下文
->generic_handle_irq()
->generic_handle_irq_desc()->handle_fasteoi_irq()--根据中断号分辨不同类型的中断,对应不同处理函数,这里中断号取大于等于32。
->handle_irq_event()->handle_irq_event_percpu()
->action->handler()---------------------对应到特定中断的处理函数,即上半部。
->__irq_wake_thread()--如果中断函数处理返回IRQ_WAKE_THREAD,则唤醒中断下半部,但不是立即执行中断线程。
->irq_exit()------------------------------------退出硬中断上下文。视情况处理软中断。
->invoke_softirq()---------------处理软中断,超出一定条件任务就会交给软中断线程处理。
->GIC_CPU_EOI-----------------------写EOI寄存器,表示结束中断。至此GIC才会接收新的硬件中断,此前一直是屏蔽硬件中断的。
->svc_exit---------------------------------------------------------恢复中断现场
参考文献:https://stephenzhou.blog.csdn.net/article/details/90707175?spm=1001.2014.3001.5502
3.2. 注册中断
涉及文件:interrupt.h
调用接口request_irq()或request_threaded_irq()向系统申请注册一个中断。
int request_threaded_irq(unsigned int irq, irq_handler_t handler,
irq_handler_t thread_fn,
unsigned long flags, const char *name, void *dev);
int request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags,
const char *name, void *dev)
{
return request_threaded_irq(irq, handler, NULL, flags, name, dev);
}
参数含义:
irq | 表了该中断的中断号,一般 CPU 的中断号都会事先定义好。 |
handler | 中断发生后的 ISR(中断服务程序) |
flags | 中断标志 |
name | 中断相关的设备 ASCII 文本,例如 "keyboard",这些名字会在 /proc/irq 和 /proc/interrupts 文件使用 |
dev | 用于共享中断线,传递驱动程序的设备结构。非共享类型的中断,直接设置成为 NULL |
中断标志: flag 的含义:
#define IRQF_TRIGGER_NONE 0x00000000
#define IRQF_TRIGGER_RISING 0x00000001-----------------------上升沿触发
#define IRQF_TRIGGER_FALLING 0x00000002----------------------下降沿触发
#define IRQF_TRIGGER_HIGH 0x00000004-------------------------高电平触发
#define IRQF_TRIGGER_LOW 0x00000008--------------------------地电平触发
#define IRQF_TRIGGER_MASK (IRQF_TRIGGER_HIGH | IRQF_TRIGGER_LOW | \
IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING)--------四种触发类型
#define IRQF_TRIGGER_PROBE 0x00000010
#define IRQF_SHARED 0x00000080---------------------------多个设备共享一个中断号
#define IRQF_PROBE_SHARED 0x00000100-------------------------中断处理程序允许sharing mismatch发生
#define __IRQF_TIMER 0x00000200--------------------------标记一个时钟中断
#define IRQF_PERCPU 0x00000400---------------------------属于某个特定CPU的中断
#define IRQF_NOBALANCING 0x00000800--------------------------禁止在多CPU之间做中断均衡
#define IRQF_IRQPOLL 0x00001000--------------------------中断被用作轮询
#define IRQF_ONESHOT 0x00002000--------------------------一次性触发中断,不允许嵌套。
#define IRQF_NO_SUSPEND 0x00004000-----------------------在系统睡眠过程中不要关闭该中断
#define IRQF_FORCE_RESUME 0x00008000-------------------------在系统唤醒过程中必须抢孩子打开该中断
#define IRQF_NO_THREAD 0x00010000------------------------表示该中断不会给线程化
#define IRQF_EARLY_RESUME 0x00020000
#define IRQF_COND_SUSPEND 0x00040000
#define IRQF_TIMER (__IRQF_TIMER | IRQF_NO_SUSPEND | IRQF_NO_THREAD)
调用 request _irq 成功执行返回 0。常见错误是 -EBUSY,表示给定的中断线已经在使用(或者没有指定 IRQF_SHARED)
注意:request_irq 函数可能引起睡眠,所以不允许在中断上下文或者不允许睡眠的代码中调用。
3.3. 释放中断
调用接口free_irq()向系统释放注册的中断处理程序。
const void *free_irq(unsigned int irq, void *dev_id)
注意:Linux 中的中断处理程序是无须重入的。当给定的中断处理程序正在执行的时候,其中断线在所有的处理器上都会被屏蔽掉,以防在同一个中断线上又接收到另一个新的中断。通常情况下,除了该中断的其他中断都是打开的,也就是说其他的中断线上的重点都能够被处理,但是当前的中断线总是被禁止的,故,同一个中断处理程序是绝对不会被自己嵌套的。
3.4. 中断上下文
与进程上下文不一样,内核执行中断服务程序的时候,处于中断上下文。中断处理程序并没有自己的独立的栈,而是使用了内核栈,其大小一般是有限制的(32bit 机器 8KB)。所以其必须短小精悍。同时中断服务程序是打断了正常的程序流程,这一点上也必须保证快速的执行。同时中断上下文中是不允许睡眠,阻塞的。
中断上下文不能睡眠的原因是:
- 中断处理的时候,不会发生进程切换,因为在中断context中,唯一能打断当前中断handler的只有更高优先级的中断,它不会被进程打断,如果在 中断context中休眠,则没有办法唤醒它,因为所有wake_up_xxx都是针对某个进程而言的,而在中断context中,没有进程的概念,没有一个task_struct(这点对于softirq和tasklet一样),因此真的休眠了,比如调用了会导致block的例程,内核几乎肯定会死。
- schedule()在切换进程时,保存当前的进程上下文(CPU寄存器的值、进程的状态以及堆栈中的内容),以便以后恢复此进程运行。中断发生后,内核会先保存当前被中断的进程上下文(在调用中断处理程序后恢复);但在中断处理程序里,CPU寄存器的值肯定已经变化了吧(最重要的程序计数器PC、堆栈SP等),如果此时因为睡眠或阻塞操作调用了schedule(),则保存的进程上下文就不是当前的进程context了.所以不可以在中断处理程序中调用schedule()。
- 内核中schedule()函数本身在进来的时候判断是否处于中断上下文:
if(unlikely(in_interrupt())) BUG();
因此,强行调用schedule()的结果就是内核BUG异常重启。
- 中断handler会使用被中断的进程内核堆栈,但不会对它有任何影响,因为handler使用完后会完全清除它使用的那部分堆栈,恢复被中断前的原貌。
- 处于中断context时候,内核是不可抢占的。因此,如果休眠,则内核一定挂起
3.5. 启用和禁用中断
有时设备驱动程序必须在一个时间段内阻塞中断的发出。通常来说,我们必须在拥有自旋锁的时候阻塞中断,以免死锁系统。理论上我们应该少使用这种接口。
3.5.1 禁用单个中断
当驱动程序需要禁用某个特定的中断线的中断产生时,可以使用内核提供的三个接口。
涉及文件:manage.c
void disable_irq(int irq); //不但会禁止给定的中断,同时也会等待当前正在执行的中断处理例程完成。
void disable_irq_nosync(int irq); //该操作禁止中断后会立即返回,但是会让驱动程序处于竞态状态。
void enable_irq(int irq); //启用指定的中断
调用这些函数中任何一个都会更新可编程中断控制器(PIC)中指定中断的掩码,因而可以在所有处理器上禁用或启用IRQ,且这些函数调用可以嵌套,既调用两次次disable_irq()就需要在执行两次enable_irq()恢复。
3.5.2 禁用所有中断
内核提供了两个函数接口禁用所有中断。
#define local_irq_enable() do { raw_local_irq_enable(); } while (0)
#define local_irq_disable() do { raw_local_irq_disable(); } while (0)
#define local_irq_save(flags) do { raw_local_irq_save(flags); } while (0)
#define local_irq_restore(flags) do { raw_local_irq_restore(flags); } while (0)
local_irq_save()的调用将当前中断状态保存到flags中,然后禁用当前处理器上的中断发送。
local_irq_disable()不保存状态而关闭本地处理器上的中断发送;只有我们知道中断并未在其他地方被禁用的情况下,才能使用这个版本。
四. 中断顶半部和底半部
在linux里,中断处理分为顶半(top half),底半(bottomhalf),在顶半里处理优先级比较高的事情,要求占用中断时间尽量的短,在处理完成后,就激活底半,有底半处理其余任务。
玩过 MCU 的人都知道,中断服务程序的设计最好是快速完成任务并退出,因为此刻系统处于被中断中。但是在 ISR 中又有一些必须完成的事情,比如:清中断标志,读/写数据,寄存器操作等。
在 Linux 中,同样也是这个要求,希望尽快的完成 ISR。但事与愿违,有些 ISR 中任务繁重,会消耗很多时间,导致响应速度变差。
Linux 中针对这种情况,将中断分为了两部分:
顶半部(top half):收到一个中断,立即执行,有严格的时间限制,只做一些必要的工作,比如:应答,复位等。这些工作都是在所有中断被禁止的情况下完成的。
底半部(bottom half):能够被推迟到后面完成的任务会在底半部进行。在适合的时机,下半部会被开中断执行。
4.1 顶/底的分配
-
- 如果一个任务对时间非常敏感,将其放在顶半部中执行;
- 如果一个任务和硬件有关,将其放在顶半部中执行;
- 如果一个任务要保证不被其他中断打断,将其放在顶半部中执行;
- 其他所有任务,考虑放置在底半部执行。
4.2 底半部实现机制分类
中断处理程序包括上半部硬件中断处理程序,下半部处理机制,包括:软中断tasklet、工作列队workqueue、中断线程化。
4.2.1 tasklet
tasklet执行上下文是软中断,执行时机通常是顶半部返回的时候。使用时只需要定义tasklet及其处理函数,并将两者关联即可,例如:
void my_tasklet_func(unsigned long); /*定义一个处理函数*/
/*定义一个tasklet结构 my_tasklet 与my_tasklet_func(data)函数相关联 */
DECLARE_TASKLET(my_tasklet, my_tasklet_func, data);
代码DECLARE_TASKLET(my_tasklet,my_tasklet_func,data)实现了定义名称为my_tasklet的 tasklet,并将其与my_tasklet_func()这个函数绑定,而传入这个函数的参数为data。
在需要调度tasklet的时候引用一个tasklet_schedule() 函数就能使系统在适当的时候进行调度运行:
tasklet_schedule(&my_tasklet);
tasklet使用模板:
/* 定义tasklet和底半部函数并将它们关联 */
void xxx_do_tasklet(unsigned long);
DECLARE_TASKLET(xxx_tasklet, xxx_do_tasklet, 0);
/* 中断处理底半部 */
void xxx_do_tasklet(unsigned long)
{
...
}
/* 中断处理顶半部 */
irqreturn_t xxx_interrupt(int irq, void *dev_id)
{
...
tasklet_schedule(&xxx_tasklet);
...
}
/* 设备驱动模块加载函数 */
int __init xxx_init(void)
{
...
/* 申请中断 */
result = request_irq(xxx_irq, xxx_interrupt, 0, "xxx", NULL);
...
return IRQ_HANDLED;
}
/* 设备驱动模块卸载函数 */
void __exit xxx_exit(void)
{
...
/* 释放中断 */
free_irq(xxx_irq, xxx_interrupt);
...
}
参考文献:https://stephenzhou.blog.csdn.net/article/details/86529841?spm=1001.2014.3001.5502
4.2.2 工作队列 workqueue
工作队列的使用方法和tasklet非常相似,但是工作队列的执行上下文是内核线程,因此可以调度和睡 眠。下面的代码用于定义一个工作队列和一个底半部执行函数:
struct work_struct my_wq; /* 定义一个工作队列 */
void my_wq_func(struct work_struct *work); /* 定义一个处理函数 */
通过INIT_WORK()可以初始化这个工作队列并将工作队列与处理函数绑定:
INIT_WORK(&my_wq, my_wq_func);
/* 初始化工作队列并将其与处理函数绑定 */
与tasklet_schedule()对应的用于调度工作队列执行的函数为schedule_work(),如:
schedule_work(&my_wq); /* 调度工作队列执行 */
工作队列使用模板 :
/* 定义工作队列和关联函数 */
struct work_struct xxx_wq;
void xxx_do_work(struct work_struct *work);
/* 中断处理底半部 */
void xxx_do_work(struct work_struct *work)
{
...
}
/*中断处理顶半部*/
irqreturn_t xxx_interrupt(int irq, void *dev_id)
{
...
schedule_work(&xxx_wq);
...
return IRQ_HANDLED;
}
/* 设备驱动模块加载函数 */
int xxx_init(void)
{
...
/* 申请中断 */
result = request_irq(xxx_irq, xxx_interrupt, 0, "xxx" , NULL);
...
/* 初始化工作队列 */
INIT_WORK(&xxx_wq, xxx_do_work);
...
}
/* 设备驱动模块卸载函数 */
void xxx_exit(void)
{
...
/* 释放中断 */
free_irq(xxx_irq, xxx_interrupt);
...
}
4.2.3 线程化IRQ request_threaded_irq
2009年2.6.xx开始,可以使用request_threaded_irq申请线程化中断,handler不是在中断上下文里执行,而是在新创建的线程里执行,这样,该handler非常像执行workqueue,拥有所有workqueue的特性,但是省掉了创建,初始化,调度workqueue的繁多步骤。处理起来非常简单。让我们看看这个接口。
int request_threaded_irq(unsigned int irq, irq_handler_t handler,
irq_handler_t thread_fn,
unsigned long flags, const char *name, void *dev);
int devm_request_threaded_irq(struct device *dev, unsigned int irq,
irq_handler_t handler, irq_handler_t thread_fn,
unsigned long irqflags, const char *devname,
void *dev_id);
和request_irq非常类似,irq是中断号, handler是在发生中断时,首先要执行的code,非常类似于顶半,
该函数最后会return IRQ_WAKE_THREAD来唤醒中断线程,thread_fn,是要在线程里执行的handler,
非常类似于底半。 后三个参数基本和request_irq相同。irqsflags新增加了一个标志,IRQF_ONESHOT,
用来标明是在中断线程执行完后在打开该中断,该标志非常有用,否则中断有可能一直在顶半执行,
而不能处理中断线程。例如对于gpio level中断,如果不设置该位,在顶半执行完成后,会打开中断,
此时由于电平没有变化,马上有执行中断,永远没有机会处理线程。
由此可见,它们比request_irq()、devm_request_irq()多了一个参数thread_fn。用这两个API申请中断的时候,内核会为相应的中断号分配一个对应的内核线程。注意这个线程只针对这个中断号,如果其他中断也通过request_threaded_irq()申请,自然会得到新的内核线程。
参数handler对应的函数执行于中断上下文,thread_fn参数对应的函数则执行于内核线程。如果handler结束的时候,返回值是IRQ_WAKE_THREAD,内核会调度对应线程执行thread_fn对应的函数。
线程化IRQ示例:
//中断上半部执行
static irqreturn_t ipcl_request_irq_handler(int irq, void *private)
{
struct driver_data *data = private;
ipcl_update_stats(data, IPCL_STATS_VIP_SRQ);
disable_irq_nosync(data->irq);
ipcl_disable_j6_srq(data);
return IRQ_WAKE_THREAD;
}
//中断下半部执行
static struct sched_param param = { .sched_priority = MAX_RT_PRIO - 9 };
static irqreturn_t ipcl_request_irq_thread(int irq, void *private)
{
struct driver_data *data = private;
if (current->rt_priority != param.sched_priority)
sched_setscheduler(current, SCHED_FIFO, ¶m);
ipcl_sm(data);
enable_irq(data->irq);
/* reqs is not empty, trigger VIP to start transmit immediately */
if (!ipcl_reqs_is_empty(data)) {
ipcl_enable_j6_srq(data);
}
return IRQ_HANDLED;
}
//初始化中断线程
static int ipcl_irq_setup(struct spi_device *spi)
{
int ret;
const char *of_name = "spi,vip-srq";
struct device_node *of_node = spi->dev.of_node;
struct driver_data *drv_data = spi_get_drvdata(spi);
drv_data->vip_srq = of_get_named_gpio(of_node, of_name, 0);
if (!gpio_is_valid(drv_data->vip_srq)) {
ipcl_err(IPCL_DBG_CORE_FLOW, "Bad %s property value %d\n",
of_name, drv_data->vip_srq);
return -EINVAL;
}
gpio_request_one(drv_data->vip_srq, GPIOF_IN, "srq-irq");
drv_data->irq = gpio_to_irq(drv_data->vip_srq);
if (drv_data->irq < 0) {
ipcl_err(IPCL_DBG_CORE_FLOW, "failed to map GPIO to IRQ: %d\n",
drv_data->vip_srq);
gpio_free(drv_data->vip_srq);
return -EINVAL;
}
ret = request_threaded_irq(drv_data->irq, ipcl_request_irq_handler,
ipcl_request_irq_thread, IRQF_TRIGGER_FALLING, "ipcl", drv_data);
if (ret) {
ipcl_err(IPCL_DBG_CORE_FLOW, "Unable to request ipcl IRQ, ret=%d\n", ret);
gpio_free(drv_data->vip_srq);
}
return 0;
}
4.2.5 中断共享
多个设备共享一根硬件中断线的情况在实际的硬件系统中广泛存在,Linux支持这种中断共享。下面是中断共享的使用方法。
a. 共享中断的多个设备在申请中断时,都应该使用IRQF_SHARED标志,而且一个设备以IRQF_SHARED申请某中断成功的前提是该中断未被申请,或该中断虽然被申请了,但是之前申请该中断
的所有设备也都以IRQF_SHARED标志申请该中断。
b. 尽管内核模块可访问的全局地址都可以作为request_irq(…,void*dev_id)的最后一个参数dev_id,但是设备结构体指针显然是可传入的最佳参数。
c. 在中断到来时,会遍历执行共享此中断的所有中断处理程序,直到某一个函数返回IRQ_HANDLED。在中断处理程序顶半部中,应根据硬件寄存器中的信息比照传入的dev_id参数迅速地判
断是否为本设备的中断,若不是,应迅速返回IRQ_NONE,如图10.5所示。
4.2.6 共享中断编程模板:
/* 中断处理顶半部 */
irqreturn_t xxx_interrupt(int irq, void *dev_id)
{
...
int status = read_int_status(); /* 获知中断源 */
if(!is_myint(dev_id,status)) /* 判断是否为本设备中断 */
return IRQ_NONE; /* 不是本设备中断,立即返回 */
/* 是本设备中断,进行处理 */
...
return IRQ_HANDLED; /* 返回IRQ_HANDLED表明中断已被处理 */
}
/* 设备驱动模块加载函数 */
int xxx_init(void)
{
...
/* 申请共享中断 */
result = request_irq(sh_irq, xxx_interrupt, IRQF_SHARED, "xxx", xxx_dev);
...
}
/* 设备驱动模块卸载函数 */
void xxx_exit(void)
{
...
/* 释放中断 */
free_irq(xxx_irq, xxx_interrupt);
...
}
五.内核定时器
软件意义上的定时器最终依赖硬件定时器来实现,内核在时钟中断发生后检测各定时器是否到期,到期后的定时器处理函数将作为软中断在底半部执行。实质上,时钟中断处理程序会唤起TIMER_SOFTIRQ软中断,运行当前处理器上到期的所有定时器。
Linux内核所提供的用于操作定时器的数据结构和函数如下。
1.数据结构 timer_list
timer_list在Linux内核中,timer_list结构体的一个实例对应一个定时器,如代码所示。
struct timer_list {
/*
* All fields that change during normal runtime grouped to the
* same cacheline
*/
struct list_head entry;
unsigned long expires;
struct tvec_base *base;
void (*function)(unsigned long);
unsigned long data;
int slack;
#ifdef CONFIG_TIMER_STATS
int start_pid;
void *start_site;
char start_comm[16];
#endif
#ifdef CONFIG_LOCKDEP
struct lockdep_map lockdep_map;
#endif
};
当定时器期满后,其中第10行的function()成员将被执行,而第11行的data成员则是传入其中的参数,第7行的expires则是定时器到期的时间(jiffies)。
如下代码定义一个名为my_timer的定时器:
struct timer_list my_timer;
2.初始化定时器
init_timer是一个宏,它的原型等价于:
void init_timer(struct timer_list * timer);
上述init_timer()函数初始化timer_list的entry的next为NULL,并给base指针赋值。TIMER_INITIALIZER(_function,_expires,_data)宏用于赋值定时器结构体的function、expires、data和base成员,这个宏等价于:
#define TIMER_INITIALIZER(_function, _expires, _data) { \
.entry = { .prev = TIMER_ENTRY_STATIC }, \
.function = (_function), \
.expires = (_expires), \
.data = (_data), \
.base = &boot_tvec_bases, \
}
DEFINE_TIMER(_name,_function,_expires,_data)宏是定义并初始化定时器成员的“快捷方式”,这个宏定义为:
#define DEFINE_TIMER(_name, _function, _expires, _data)\
struct timer_list _name =\
TIMER_INITIALIZER(_function, _expires, _data)
此外,setup_timer()也可用于初始化定时器并赋值其成员,其源代码为:
#define __setup_timer(_timer, _fn, _data, _flags) \
do { \
__init_timer((_timer), (_flags)); \
(_timer)->function = (_fn); \
(_timer)->data = (_data); \
} while (0)
3.增加定时器
void add_timer(struct timer_list * timer);
上述函数用于注册内核定时器,将定时器加入到内核动态定时器链表中。
4.删除定时器
int del_timer(struct timer_list * timer);
上述函数用于删除定时器。del_timer_sync()是del_timer()的同步版,在删除一个定时器时需等待其被处理完,因此该函数的调用不能发生在中断上下文中。
5.修改定时器的expire
int mod_timer(struct timer_list *timer, unsigned long expires);
上述函数用于修改定时器的到期时间,在新的被传入的expires到来后才会执行定时器函数。
6.定时器中断使用模板
/* xxx设备结构体 */
struct xxx_dev {
struct cdev cdev;
...
timer_list xxx_timer; /* 设备要使用的定时器 */
};
/* xxx驱动中的某函数 */
xxx_func1(…)
{
struct xxx_dev *dev = filp->private_data;
...
/* 初始化定时器 */
init_timer(&dev->xxx_timer);
dev->xxx_timer.function = &xxx_do_timer;
dev->xxx_timer.data = (unsigned long)dev;
/* 设备结构体指针作为定时器处理函数参数 */
dev->xxx_timer.expires = jiffies + delay;
/* 添加(注册)定时器 */
add_timer(&dev->xxx_timer);
...
}
/* xxx驱动中的某函数 */
xxx_func2(…)
{
...
/* 删除定时器 */
del_timer (&dev->xxx_timer);
...
}
/* 定时器处理函数 */
static void xxx_do_timer(unsigned long arg)
{
struct xxx_device *dev = (struct xxx_device *)(arg);
...
/* 调度定时器再执行 */
dev->xxx_timer.expires = jiffies + delay;
add_timer(&dev->xxx_timer);
...
}
从代码第18、39行可以看出,定时器的到期时间往往是在目前jiffies的基础上添加一个时延,若为Hz,则表示延迟1s。
在定时器处理函数中,在完成相应的工作后,往往会延后expires并将定时器再次添加到内核定时器链
表中,以便定时器能再次被触发。
6.内核中延迟的工作delayed_work
对于周期性的任务,除了定时器以外,在Linux内核中还可以利用一套封装得很好的快捷机制,其本质是利用工作队列和定时器实现,这套快捷机制就是delayed_work,delayed_work结构体的定义如代码所示。
struct delayed_work {
struct work_struct work;
struct timer_list timer;
/* target workqueue and CPU ->timer uses to queue ->work */
struct workqueue_struct *wq;
int cpu;
};
我们可以通过如下函数调度一个delayed_work在指定的延时后执行:
int schedule_delayed_work(struct delayed_work *work, unsigned long delay);
当指定的delay到来时,delayed_work结构体中的work成员work_func_t类型成员func()会被执行。work_func_t类型定义为:
typedef void (*work_func_t)(struct work_struct *work);
其中,delay参数的单位是jiffies,因此一种常见的用法如下:
schedule_delayed_work(&work, msecs_to_jiffies(poll_interval));
msecs_to_jiffies()用于将毫秒转化为jiffies。如果要周期性地执行任务,通常会在delayed_work的工作函数中再次调用schedule_delayed_work(),周而复始。如下函数用来取消delayed_work:
int cancel_delayed_work(struct delayed_work *work);
int cancel_delayed_work_sync(struct delayed_work *work);
六. 查看中断统计
/proc/interrupts中的字段依次是逻辑中断号、中断在各CPU上发生的次数,中断所属父设备名称(中断控制器的名字)、硬件中断号、中断触发方式(电平或边沿)、中断名称
中断绑核:
BusyBox v1.20.2 (2024-01-17 02:46:04 CST) built-in shell (ash)
Enter 'help' for a list of built-in commands.
/*查看usb的中断统计*/
# cat /proc/interrupts |grep usb
cat /proc/interrupts |grep usb
274: 3071522 0 0 0 0 0 MT_SYSIRQ 122 Level 11271000.usb
313: 0 0 0 0 0 0 MT_SYSIRQ 123 Level xhci-hcd:usb1
314: 1535437 0 0 0 0 0 MT_SYSIRQ 249 Level xhci-hcd:usb3
/*调整usb中断到CPU1处理*/
# echo 2 >/proc/irq/274/smp_affinity
echo 2 >/proc/irq/274/smp_affinity
/*再次查看中断统计*/
# cat /proc/interrupts |grep usb
cat /proc/interrupts |grep usb
274: 3081637 971 0 0 0 0 MT_SYSIRQ 122 Level 11271000.usb
313: 0 0 0 0 0 0 MT_SYSIRQ 123 Level xhci-hcd:usb1
314: 1537705 0 0 0 0 0 MT_SYSIRQ 249 Level xhci-hcd:usb3
#
七.总结:
1.Linux的中断处理分为两个半部,顶半部处理紧急的硬件操作,底半部处理不紧急的耗时操作。tasklet
和工作队列都是调度中断底半部的良好机制,tasklet基于软中断实现,内核定时器也依靠软中断实现。
2.内核中的延时可以采用忙等待或睡眠等待,为了充分利用CPU资源,使系统有更好的吞吐性能,在对延迟时间的要求并不是很精确的情况下,睡眠等待通常是值得推荐的,而ndelay()、udelay()忙等待机制在驱动中通常是为了配合硬件上的短时延迟要求。
3.中断函数可能引起睡眠,所以不允许在中断上下文或者不允许睡眠的代码中调用,例如,在中断中使用kmalloc(1024, GFP_KERNEL)会在内存紧张时进入睡眠,可以使用GFP_ATOMIC代替。
4.在所有关于 CPU 的底层编程中,无一例外的,都要求,中断处理尽可能的快,一方面是处理完毕后,能够更快的响应其他中断的请求,另一方面,能够快速的回到进程上下文,处理相关业务。
5.中断线程化是实时Linux项目开发的一个新特性,目的是降低中断处理对系统实时延迟的影响。
6.在LInux内核里,中断具有最高优先级,只要有中断发生,内核会暂停手头的工作转向中断处理,等到所有挂起等待的中断和软终端处理完毕后才会执行进程调度,因此这个过程会造成实时任务得不到及时处理。
7.中断上下文总是抢占进程上下文,中断上下文不仅是中断处理程序,还包括softirq、tasklet等,中断上下文成了优化Linux实时性的最大挑战之一。