linux中断处理总结
Posted
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了linux中断处理总结相关的知识,希望对你有一定的参考价值。
ARM64中断处理过程: https://www.daodaodao123.com/?p=146 上文总结了ARM64裸机中断处理的详细过程,这里主要总结下linux中断处理相关内容;
0.为什么有中断?
中断,本质上是外设发生了事变,需要异步的通知(经由中断控制器,路由给)CPU; 这个过程涉及三部分硬件:外设->中断控制器->CPU ## 1中断处理过程:
外设事变发生后,发送中断信号给中断控制器,中断控制器经过仲裁,路由给CPU; 上图是中断控制器的状态机转换图,一个完整的中断发生过程如下(假设现在有一个触摸屏的外设,配置为高电平触发): - 中断控制器默认是inactive状态;
- 外设有事件发生(比如按下触摸屏),触摸屏会产生一个中断信号,送给中断控制器, 如上图中的A1, 中断控制器变为pending状态,此时外设送给中断控制器的信号线保持高电平;
- 中断控制器经过仲裁,选择一个最高优先级中断信号,路由给CPU,CPU没有ACK之前,一直维持信号线的电平状态;
- ARM收到中断信号,开始响应时,硬件自动屏蔽CPU中断(清除daif位),软件从中断控制器读出具体是哪个中断产生,然后对中断控制器进行ACK;若边沿触发中断控制器可能清掉pending(电平触发,不会清),此时中断控制器处于active and pending(D路径)或active(C路径);
- CPU执行中断处理程序,中断服务程序退出时,软件恢复CPSR,并对中断控制器EOI;
- 中断控制器,收到EOI,变为inactive状态,再选择下一个pending的中断信号,发给CPU;
- 中断处理的底半部,处理掉外设中断事件源(比如读取触摸屏的坐标,访问相应寄存器)之后,外设的pending信号被清除掉;
linux相对应的API:
disable_irq(n); //操作中断控制器,屏蔽n号中断;
local_irq_disable(); //关闭CPU中断
补充: ## 2.disable_irq(n)可能并未生效?
linux里的设计,很多地方都是惰性原则,禁止n号中断,可能并未真正执行,假如逻辑执行期间,n中断并未产生,不会有任何问题,还提高性能; ```cpp disable_irq(n); ... //n号中断发生, 延后执行 enable_irq(n)
若期间有中断产生呢? **enable_irq(n)使能n号中断后,n号中断服务程序立即执行;**这样,就算disable_irq(n)期间有n号中断产生,也不会漏掉,只是延后执行; ```cpp
//__enable_irq-->irq_startup-->check_irq_resend->irq_sw_resend
/* Tasklet to handle resend: */
static DECLARE_TASKLET(resend_tasklet, resend_irqs);
static int irq_sw_resend(struct irq_desc *desc)
unsigned int irq = irq_desc_get_irq(desc);
/*
* Validate whether this interrupt can be safely injected from
* non interrupt context
*/
if (handle_enforce_irqctx(&desc->irq_data))
return -EINVAL;
/*
* If the interrupt is running in the thread context of the parent
* irq we need to be careful, because we cannot trigger it
* directly.
*/
if (irq_settings_is_nested_thread(desc))
/*
* If the parent_irq is valid, we retrigger the parent,
* otherwise we do nothing.
*/
if (!desc->parent_irq)
return -EINVAL;
irq = desc->parent_irq;
/* Set it pending and activate the softirq: */
set_bit(irq, irqs_resend);
tasklet_schedule(&resend_tasklet);
return 0;
3.向量中断和非向量中断
向量中断:不同的中断跳转到不同地址,比如x86; 非向量中断,跳转到一个入口地址,通过寄存器来判断具体是哪个中断; linux的实现,最终不同中断信号跳转到不同的irq_desc[]分支; ## 4.什么是中断号?
linux中断号是个纯软件概念,其与中断控制器的硬件中断号,非线性一 一对应; 实际硬件电路中,中断控制器可能是多级级联的; 硬件中断号由硬件电路决定,通常对应配置在设备树里;软件中断号由linux决定,一一对应; ## 5.中断分类
在一个多核系统中,中断分为三类; PPI:只能本核响应,比如TWD; IPI: 用于多核间的通信,比如smp调度; SPI: 共享外围设备中断,可以路由给任何一个核; 对于SPI类型的中断,内核可以通过API设定中断触发的CPU核,默认都是在CPU0上产生的; ```cpp extern int irq_set_affinity(unsigned int irq, const struct cpumask *cpumask)
irq_set_affinity(n,cpumask_of(i));//把n中断设定到CPU_i上;
![](https://www.daodaodao123.com/wp-content/uploads/2022/05/irq4.png)参考一个gic的内部电路图: ![](https://www.daodaodao123.com/wp-content/uploads/2022/05/irq5.png)## 6.为什么一定要有底半步?
案例:CPU外接I2C触摸屏 当触摸事件发生时,产生中断信号,中断控制器将中断信号路由给CPU; CPU执行中断服务程序,必须处理外部事件,清理触摸屏的中断pending信号; 而读I2C设备可以睡眠,在中断ISR读取I2C设备,可能引起死锁; 不读的话,中断退出,触摸屏中断信号未清除,又会触发中断; ### 解决方案:
必须在进程上下文,读I2C设备清除外设中断电平信号; 在中断服务程序顶半部,屏蔽**中断控制器**对应中断 ```cpp
disable_irq_nosync(num);
注:n号中断上下文不能调disable_irq(n),进程上下文可以调用disable_irq(n); ```cpp void disable_irq(unsigned int irq) if (!__disable_irq_nosync(irq)) ///等待正在执行的ISR结束,在n号中断ISR中调用disable_irq(n)会导致死锁 synchronize_irq(irq);
退出顶半部后,在底半步处理完外设事件,再使能对应中断号 ```cpp
enable_irq(num);
而在进程上下文中,可以直接调用disable_irq(n)/enable_irq(n); ```cpp disable_irq(n); ... //n号中断发生,不会死锁 enable_irq(n)
## 7.底半部:
顶半部不能太久,堵住了后面的进程; 顶半部屏蔽了中断,堵住了后面的中断; 顶半部不能睡眠,但是i2c,spi外设访问可能睡眠; 底半部有**软中断**,**工作队列**,**线程化irq**; |---|
|-----|
| | 顶半部 | | 中断上下文 | | 不可以 | |
| | softirq(tasklet) | | 软中断上下文 | | 不可以 | |
| | workqueue | | 进程上下文 | | 可以 | |
| | threaded_irq | | 进程上下文 | | 可以 | |
顶半部退出,立马执行软中断,**软中断可以被硬件中断打断**; 工作队列和线程化IRQ,跟普通线程一样接受调度; ### 7.1 软中断,tasklet
内核中采用softirq的地方包括HI_SOFTIRQ、TIMER_SOFTIRQ、NET_TX_SOFTIRQ、NET_RX_SOFTIRQ、TASKLET_SOFTIRQ等,实际驱动编程一般不直接使用软中断(内核固定了用途),用tasklet(softirq一种)代替; ```cpp
tasklet_init(&tasklet, xxx_func_tasklet,xxx_data)
///中断上下文:
xx_isr()
...
tasklet_hi_schedule(&tasklet);//优先级高于tasklet_schedule
tasklet_schedule(&tasklet);
wakeup_softirqd也是执行软中断的一个路径,当软中断过多时,放到[ksoftirqd/n]线程; 软中断的执行点: 62. IRQ上半部返回时,先执行软中断,再执行其他线程;
- BH_ENABLE相关函数,比如spin_unlock_bh();
- kthread_irqd线程与普通线程一样被调度;
bh_enable相关函数会调用到这里: ```cpp void __local_bh_enable_ip(unsigned long ip, unsigned int cnt) WARN_ON_ONCE(in_irq()); lockdep_assert_irqs_enabled(); #ifdef CONFIG_TRACE_IRQFLAGS local_irq_disable(); #endif /* * Are softirqs going to be turned on now: / if (softirq_count() == SOFTIRQ_DISABLE_OFFSET) lockdep_softirqs_on(ip); / * Keep preemption disabled until we are done with * softirq processing: */ __preempt_count_sub(cnt - 1);
if (unlikely(!in_interrupt() && local_softirq_pending()))
/*
* Run softirq if any pending. And do it in its own stack
* as we may be calling this deep in a task call stack already.
*/
do_softirq(); ///执行软中断
preempt_count_dec();
#ifdef CONFIG_TRACE_IRQFLAGS local_irq_enable(); #endif preempt_check_resched();
### 7.2 workqueue
传统的workqueue用法语tasklet类似: ```cpp
//申请workqueue
struct work_struct my_wq;
void my_wq_func(struct work_struct *work);
INIT_WORK(&my_wq, my_wq_func);
///中断上下文:
xx_isr()
...
schedule_work(&my_wq); //调度工作队列
...
return IRQ_HANDLED;
7.3 中断线程化
Linux实时补丁: 清掉软中断上下文,所有软中断放在softirqd线程执行; 线程化执行,可以睡眠; 老版本实现,每个核一个线程,该核所有softirq顺序执行; 新内核用线程池实现,动态创建撤销线程; ```cpp 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) xxx_isr() return IRQ_WAKE__THREAD;
第一次回调参数:irq_handler_t handler,中断顶半部,运行在中断上下文; 第二个回调参数:irq_handler_t thread_fn,运行在进程上下文; handler可以为NULL,这时内核默认用irq_default_primary_handler()代替handler,并会使用IRQF_ONESHOT。 ```cpp
static irqreturn_t irq_default_primary_handler(int irq, void *dev_id)
return IRQ_WAKE_THREAD;
7.4 IRQF_ONESHOT:
1.linux内核自动disable_irq(n); 2.执行xxx_isr,唤醒内核线程irq/n; 3.内核线程irq/n执行xxx_thread_fn; 4.内核自动enable_irq(n); 设置IRQF_ONESHOT, 可以把顶半部置为空,内核用默认函数irq_default_primary_handler()替换顶半部; ## 8. preempt_rt补丁
强制中断线程化,将低半部内容放到线程执行; ```cpp request_irq(n, xxx_isr, 0); 转化为 request_threaded_irq(n,irq_default_primary_handler,xxx_isr,IRQF_ONESHOT);
## 9.Linux常用到的中断,锁相关API:
**屏蔽本地中断:**```cpp
local_irq_disable()
local_irq_enable()
在驱动中使用local_irq_disable通常是个bug; 禁止底半部``` local_bh_disable() local_bh_enable()
**屏蔽中断源:**```cpp
disable_irq(n)
enable_irq(n)
当中断和进程竟态;```cpp //进程上下文 spin_lock_irqsave(); spin_unlock_restore();
//中断上下文 spin_lock(); spin_unlock();
irq:解决本核的抢占问题; spin_lock:解决多核间的抢占; **软中断和进程竟态:**软中断的抢占由中断引起,中断退出时,调用软中断; ```cpp
//进程上下文
spin_lock_bh();
spin_unlock_bh();///软中断调用点bh_enable()
//软中断上下文
spin_lock();
spin_unlock();
以上是关于linux中断处理总结的主要内容,如果未能解决你的问题,请参考以下文章