Linux中断(interrupt)子系统之五:软件中断(softIRQ)

软件中断(softIRQ)是内核提供的一种延迟执行机制,它完全由软件触发,虽然说是延迟机制,实际上,在大多数情况下,它与普通进程相比,能得到更快的响应时间。软中断也是其他一些内核机制的基础,比如tasklet,高分辨率timer等。

/*****************************************************************************************************/
声明:本博内容均由http://blog.youkuaiyun.com/droidphone原创,转载请注明出处,谢谢!
/*****************************************************************************************************/

1.  软件中断的数据结构


1.1  struct softirq_action


        内核用softirq_action结构管理软件中断的注册和激活等操作,它的定义如下:
[cpp]  view plain copy
  1. struct softirq_action  
  2. {  
  3.     void    (*action)(struct softirq_action *);  
  4. };  
非常简单,只有一个用于回调的函数指针。软件中断的资源是有限的,内核目前只实现了10种类型的软件中断,它们是:
[cpp]  view plain copy
  1. enum  
  2. {  
  3.     HI_SOFTIRQ=0,  
  4.     TIMER_SOFTIRQ,  
  5.     NET_TX_SOFTIRQ,  
  6.     NET_RX_SOFTIRQ,  
  7.     BLOCK_SOFTIRQ,  
  8.     BLOCK_IOPOLL_SOFTIRQ,  
  9.     TASKLET_SOFTIRQ,  
  10.     SCHED_SOFTIRQ,  
  11.     HRTIMER_SOFTIRQ,  
  12.     RCU_SOFTIRQ,    /* Preferable RCU should always be the last softirq */  
  13.   
  14.     NR_SOFTIRQS  
  15. };  
内核的开发者们不建议我们擅自增加软件中断的数量,如果需要新的软件中断,尽可能把它们实现为基于软件中断的tasklet形式。与上面的枚举值相对应,内核定义了一个softirq_action的结构数组,每种软中断对应数组中的一项:
[cpp]  view plain copy
  1. static struct softirq_action softirq_vec[NR_SOFTIRQS] __cacheline_aligned_in_smp;  


1.2  irq_cpustat_t


        多个软中断可以同时在多个cpu运行,就算是同一种软中断,也有可能同时在多个cpu上运行。内核为每个cpu都管理着一个待决软中断变量(pending),它就是irq_cpustat_t:
[cpp]  view plain copy
  1. typedef struct {  
  2.     unsigned int __softirq_pending;  
  3. } ____cacheline_aligned irq_cpustat_t;  
[cpp]  view plain copy
  1. irq_cpustat_t irq_stat[NR_CPUS] ____cacheline_aligned;  
__softirq_pending字段中的每一个bit,对应着某一个软中断,某个bit被置位,说明有相应的软中断等待处理。


1.3  软中断的守护进程ksoftirqd


        在cpu的热插拔阶段,内核为每个cpu创建了一个用于执行软件中断的守护进程ksoftirqd,同时定义了一个per_cpu变量用于保存每个守护进程的task_struct结构指针:
[cpp]  view plain copy
  1. DEFINE_PER_CPU(struct task_struct *, ksoftirqd);  
大多数情况下,软中断都会在irq_exit阶段被执行,在irq_exit阶段没有处理完的软中断才有可能会在守护进程中执行。


2.  触发软中断


        要触发一个软中断,只要调用api:raise_softirq即可,它的实现很简单,先是关闭本地cpu中断,然后调用:raise_softirq_irqoff
[cpp]  view plain copy
  1. void raise_softirq(unsigned int nr)  
  2. {  
  3.     unsigned long flags;  
  4.   
  5.     local_irq_save(flags);  
  6.     raise_softirq_irqoff(nr);  
  7.     local_irq_restore(flags);  
  8. }  
再看看raise_softirq_irqoff:
[cpp]  view plain copy
  1. inline void raise_softirq_irqoff(unsigned int nr)  
  2. {  
  3.     __raise_softirq_irqoff(nr);  
  4.   
  5.         ......  
  6.     if (!in_interrupt())  
  7.         wakeup_softirqd();  
  8. }  
先是通过__raise_softirq_irqoff设置cpu的软中断pending标志位(irq_stat[NR_CPUS] ),然后通过in_interrupt判断现在是否在中断上下文中,或者软中断是否被禁止,如果都不成立,则唤醒软中断的守护进程,在守护进程中执行软中断的回调函数。否则什么也不做,软中断将会在中断的退出阶段被执行。


3.  软中断的执行


        基于上面所说,软中断的执行既可以守护进程中执行,也可以在中断的退出阶段执行。实际上,软中断更多的是在中断的退出阶段执行(irq_exit),以便达到更快的响应,加入守护进程机制,只是担心一旦有大量的软中断等待执行,会使得内核过长地留在中断上下文中。


3.1  在irq_exit中执行

        看看irq_exit的部分:
[cpp]  view plain copy
  1. void irq_exit(void)  
  2. {  
  3.         ......  
  4.     sub_preempt_count(IRQ_EXIT_OFFSET);  
  5.     if (!in_interrupt() && local_softirq_pending())  
  6.         invoke_softirq();  
  7.         ......  
  8. }  
如果中断发生嵌套,in_interrupt()保证了只有在最外层的中断的irq_exit阶段,invoke_interrupt才会被调用,当然,local_softirq_pending也会实现判断当前cpu有无待决的软中断。代码最终会进入__do_softirq中,内核会保证调用__do_softirq时,本地cpu的中断处于关闭状态,进入__do_softirq:
[cpp]  view plain copy
  1. asmlinkage void __do_softirq(void)  
  2. {  
  3.         ......  
  4.     pending = local_softirq_pending();  
  5.   
  6.     __local_bh_disable((unsigned long)__builtin_return_address(0),  
  7.                 SOFTIRQ_OFFSET);  
  8. restart:  
  9.     /* Reset the pending bitmask before enabling irqs */  
  10.     set_softirq_pending(0);  
  11.   
  12.     local_irq_enable();  
  13.   
  14.     h = softirq_vec;  
  15.   
  16.     do {  
  17.         if (pending & 1) {  
  18.                     ......  
  19.             trace_softirq_entry(vec_nr);  
  20.             h->action(h);  
  21.             trace_softirq_exit(vec_nr);  
  22.                         ......  
  23.         }  
  24.         h++;  
  25.         pending >>= 1;  
  26.     } while (pending);  
  27.   
  28.     local_irq_disable();  
  29.   
  30.     pending = local_softirq_pending();  
  31.     if (pending && --max_restart)  
  32.         goto restart;  
  33.   
  34.     if (pending)  
  35.         wakeup_softirqd();  
  36.   
  37.     lockdep_softirq_exit();  
  38.   
  39.     __local_bh_enable(SOFTIRQ_OFFSET);  
  40. }  
  • 首先取出pending的状态;
  • 禁止软中断,主要是为了防止和软中断守护进程发生竞争;
  • 清除所有的软中断待决标志;
  • 打开本地cpu中断;
  • 循环执行待决软中断的回调函数;
  • 如果循环完毕,发现新的软中断被触发,则重新启动循环,直到以下条件满足,才退出:
    • 没有新的软中断等待执行;
    • 循环已经达到最大的循环次数MAX_SOFTIRQ_RESTART,目前的设定值时10次;
  • 如果经过MAX_SOFTIRQ_RESTART次循环后还未处理完,则激活守护进程,处理剩下的软中断;
  • 推出前恢复软中断;


3.2  在ksoftirqd进程中执行

        从前面几节的讨论我们可以看出,软中断也可能由ksoftirqd守护进程执行,这要发生在以下两种情况下:
  • 在irq_exit中执行软中断,但是在经过MAX_SOFTIRQ_RESTART次循环后,软中断还未处理完,这种情况虽然极少发生,但毕竟有可能;
  • 内核的其它代码主动调用raise_softirq,而这时正好不是在中断上下文中,守护进程将被唤醒;
守护进程最终也会调用__do_softirq执行软中断的回调,具体的代码位于run_ksoftirqd函数中,内核会关闭抢占的情况下执行__do_softirq,具体的过程这里不做讨论。


4.  tasklet


       因为内核已经定义好了10种软中断类型,并且不建议我们自行添加额外的软中断,所以对软中断的实现方式,我们主要是做一个简单的了解,对于驱动程序的开发者来说,无需实现自己的软中断。但是,对于某些情况下,我们不希望一些操作直接在中断的handler中执行,但是又希望在稍后的时间里得到快速地处理,这就需要使用tasklet机制。 tasklet是建立在软中断上的一种延迟执行机制,它的实现基于TASKLET_SOFTIRQ和HI_SOFTIRQ这两个软中断类型。


4.1  tasklet_struct        


在软中断的初始化函数softirq_init的最后,内核注册了TASKLET_SOFTIRQ和HI_SOFTIRQ这两个软中断:
[cpp]  view plain copy
  1. void __init softirq_init(void)  
  2. {  
  3.         ......  
  4.     open_softirq(TASKLET_SOFTIRQ, tasklet_action);  
  5.     open_softirq(HI_SOFTIRQ, tasklet_hi_action);  
  6. }  
        内核用一个tasklet_struct来表示一个tasklet,它的定义如下:
[cpp]  view plain copy
  1. struct tasklet_struct  
  2. {  
  3.     struct tasklet_struct *next;  
  4.     unsigned long state;  
  5.     atomic_t count;  
  6.     void (*func)(unsigned long);  
  7.     unsigned long data;  
  8. };  
next用于把同一个cpu的tasklet链接成一个链表,state用于表示该tasklet的当前状态,目前只是用了最低的两个bit,分别用于表示已经准备被调度执行和已经在另一个cpu上执行:
[cpp]  view plain copy
  1. enum  
  2. {  
  3.     TASKLET_STATE_SCHED,    /* Tasklet is scheduled for execution */  
  4.     TASKLET_STATE_RUN   /* Tasklet is running (SMP only) */  
  5. };  
原子变量count用于tasklet对tasklet_disable和tasklet_enable的计数,count为0时表示允许tasklet执行,否则不允许执行,每次tasklet_disable时,该值加1,tasklet_enable时该值减1。func是tasklet被执行时的回调函数指针,data则用作回调函数func的参数。


4.2  初始化一个tasklet


有两种办法初始化一个tasklet,第一种是静态初始化,使用以下两个宏,这两个宏定义一个tasklet_struct结构,并用相应的参数对结构中的字段进行初始化:
  • DECLARE_TASKLET(name, func, data);定义名字为name的tasklet,默认为enable状态,也就是count字段等于0。
  • DECLARE_TASKLET_DISABLED(name, func, data);定义名字为name的tasklet,默认为disable状态,也就是count字段等于1。
第二个是动态初始化方法:先定义一个tasklet_struct,然后用tasklet_init函数进行初始化,该方法默认tasklet处于enable状态:
[cpp]  view plain copy
  1. struct tasklet_struct tasklet_xxx;  
  2. ......  
  3. tasklet_init(&tasklet_xxx, func, data);  


4.3  tasklet的使用方法


使能和禁止tasklet,使用以下函数:
  • tasklet_disable()  通过给count字段加1来禁止一个tasklet,如果tasklet正在运行中,则等待运行完毕才返回(通过TASKLET_STATE_RUN标志)。
  • tasklet_disable_nosync()  tasklet_disable的异步版本,它不会等待tasklet运行完毕。
  • tasklet_enable()  使能tasklet,只是简单地给count字段减1。
调度tasklet的执行,使用以下函数:
  • tasklet_schedule(struct tasklet_struct *t)  如果TASKLET_STATE_SCHED标志为0,则置位TASKLET_STATE_SCHED,然后把tasklet挂到该cpu等待执行的tasklet链表上,接着发出TASKLET_SOFTIRQ软件中断请求。
  • tasklet_hi_schedule(struct tasklet_struct *t)  效果同上,区别是它发出的是HI_SOFTIRQ软件中断请求。
销毁tasklet,使用以下函数:
  • tasklet_kill(struct tasklet_struct *t)  如果tasklet处于TASKLET_STATE_SCHED状态,或者tasklet正在执行,则会等待tasklet执行完毕,然后清除TASKLET_STATE_SCHED状态。


4.4  tasklet的内部执行机制


内核为每个cpu用定义了一个tasklet_head结构,用于管理每个cpu上的tasklet的调度和执行:
[cpp]  view plain copy
  1. struct tasklet_head  
  2. {  
  3.     struct tasklet_struct *head;  
  4.     struct tasklet_struct **tail;  
  5. };  
  6.   
  7. static DEFINE_PER_CPU(struct tasklet_head, tasklet_vec);  
  8. static DEFINE_PER_CPU(struct tasklet_head, tasklet_hi_vec);  
回到4.1节,我们知道,tasklet是利用TASKLET_SOFTIRQ和HI_SOFTIRQ这两个软中断来实现的,两个软中断只是有优先级的差别,所以我们只讨论TASKLET_SOFTIRQ的实现,TASKLET_SOFTIRQ的中断回调函数是tasklet_action,我们看看它的代码:
[cpp]  view plain copy
  1. static void tasklet_action(struct softirq_action *a)  
  2. {  
  3.     struct tasklet_struct *list;  
  4.   
  5.     local_irq_disable();  
  6.     list = __this_cpu_read(tasklet_vec.head);  
  7.     __this_cpu_write(tasklet_vec.head, NULL);  
  8.     __this_cpu_write(tasklet_vec.tail, &__get_cpu_var(tasklet_vec).head);  
  9.     local_irq_enable();  
  10.   
  11.     while (list) {  
  12.         struct tasklet_struct *t = list;  
  13.   
  14.         list = list->next;  
  15.   
  16.         if (tasklet_trylock(t)) {  
  17.             if (!atomic_read(&t->count)) {  
  18.                 if (!test_and_clear_bit(TASKLET_STATE_SCHED, &t->state))  
  19.                     BUG();  
  20.                 t->func(t->data);  
  21.                 tasklet_unlock(t);  
  22.                 continue;  
  23.             }  
  24.             tasklet_unlock(t);  
  25.         }  
  26.   
  27.         local_irq_disable();  
  28.         t->next = NULL;  
  29.         *__this_cpu_read(tasklet_vec.tail) = t;  
  30.         __this_cpu_write(tasklet_vec.tail, &(t->next));  
  31.         __raise_softirq_irqoff(TASKLET_SOFTIRQ);  
  32.         local_irq_enable();  
  33.     }  
  34. }  
解析如下:
  • 关闭本地中断的前提下,移出当前cpu的待处理tasklet链表到一个临时链表后,清除当前cpu的tasklet链表,之所以这样处理,是为了处理当前tasklet链表的时候,允许新的tasklet被调度进待处理链表中。
  • 遍历临时链表,用tasklet_trylock判断当前tasklet是否已经在其他cpu上运行,而且tasklet没有被禁止:
    • 如果没有运行,也没有禁止,则清除TASKLET_STATE_SCHED状态位,执行tasklet的回调函数。
    • 如果已经在运行,或者被禁止,则把该tasklet重新添加会当前cpu的待处理tasklet链表上,然后触发TASKLET_SOFTIRQ软中断,等待下一次软中断时再次执行。
分析到这了我有个疑问,看了上面的代码,如果一个tasklet被tasklet_schedule后,在没有被执行前被tasklet_disable了,岂不是会无穷无尽地引发TASKLET_SOFTIRQ软中断?
通过以上的分析,我们需要注意的是,tasklet有以下几个特征:
  • 同一个tasklet只能同时在一个cpu上执行,但不同的tasklet可以同时在不同的cpu上执行;
  • 一旦tasklet_schedule被调用,内核会保证tasklet一定会在某个cpu上执行一次;
  • 如果tasklet_schedule被调用时,tasklet不是出于正在执行状态,则它只会执行一次;
  • 如果tasklet_schedule被调用时,tasklet已经正在执行,则它会在稍后被调度再次被执行;
  • 两个tasklet之间如果有资源冲突,应该要用自旋锁进行同步保护;
<think>好的,我需要详细解释Linux中断系统的工作原理,特别是外设、驱动程序、内核和PLIC的作用。首先,我得回忆一下中断处理的基本流程,然后结合各个组件的作用来分析。 中断系统的工作流程大致是这样的:当外设需要处理时,它会发送中断信号给中断控制器,比如PLIC。然后中断控制器将中断传递给CPU,CPU暂停当前任务,保存上下文,跳转到中断处理程序。处理程序识别中断源,调用对应的驱动程序的中断服务例程(ISR),处理完成后恢复之前任务。 外设方面,每个外设都有自己的中断线,比如GPIO或UART。例如,引用中提到UART0的tx信号线是UART0_UARTTXD,GPIO0的第0个信号线是GPIO0_0[^2]。这些外设在需要时会触发中断,比如数据到达或发送完成。 驱动程序的作用是注册中断处理函数。在Linux中,使用request_irq函数注册,指定中断号和对应的ISR。当该中断触发时,内核会调用注册的函数进行处理。驱动程序还需要管理中断的使能、屏蔽等,可能涉及中断的上下半部机制,比如顶半部处理紧急任务,底半部处理耗时任务。 内核部分主要负责管理中断的分配、优先级和调度。内核中的中断子系统处理中断的入口,保存和恢复上下文,调用驱动注册的处理函数。内核还提供API供驱动程序使用,例如中断注册和释放,以及底半部机制如tasklet和工作队列。 PLIC(Platform-Level Interrupt Controller)是RISC-V平台的中断控制器,负责收集和分发外设中断到CPU。PLIC会仲裁多个中断的优先级,将最高优先级的中断传递给CPU。当CPU处理完中断后,需要通知PLIC完成处理。例如,引用中提到RNMI中断的处理,包括保存PC到mnepc,中断类型到mncause,以及特权模式到mnstatus,同时清除rnmie位禁用其他中断[^1]。虽然这里说的是RNMI,但普通中断的处理流程可能类似,只是具体的CSR和状态位不同。 需要进一步确认PLIC在Linux中的具体实现,比如驱动程序如何与PLIC交互,中断号如何映射,以及内核中的中断处理流程如何与PLIC协同工作。同时,要区分不同架构的中断控制器,比如ARM中的GIC,而这里是RISC-V的PLIC。 总结各部分的作用:外设触发中断,PLIC管理和传递中断,内核处理中断入口,驱动程序执行具体的中断服务。整个过程需要硬件和软件的协作,包括中断控制器的配置、驱动程序的注册、内核的中断处理机制等。</think>### Linux中断系统工作机制详解 Linux中断系统是硬件与操作系统协同工作的核心机制,涉及**外设、驱动程序、内核中断子系统及PLIC**四个关键部分,以下是详细解析: --- #### 一、外设触发中断 1. **中断信号生成** 外设(如UART、GPIO)在特定事件(数据到达/发送完成)时会通过物理信号线触发中断。例如: - UART0的TX信号线名称为`UART0_UARTTXD` - GPIO0第0号引脚信号线为`GPIO0_0` 2. **中断类型** - **电平触发**:中断信号持续有效,需手动清除 - **边沿触发**:信号跳变时触发,适用于快速响应场景 --- #### 二、PLIC(平台级中断控制器)的作用 PLIC是RISC-V架构的中断控制器,负责**中断的聚合、优先级仲裁与分发**: 1. **中断收集**:接收所有外设中断信号 2. **优先级仲裁**:比较中断优先级,选择最高优先级中断传递给CPU 3. **中断路由**:将中断发送到指定CPU核心 4. **状态管理**:记录中断处理状态,例如: - 处理完成后需向PLIC发送完成通知 - 中断处理期间可能禁用其他中断(类似`rnmie`位被清除的状态[^1]) --- #### 三、内核中断处理流程 1. **中断入口** CPU收到中断后: - 保存现场(PC、寄存器状态等到特定CSR,如`mnepc`) - 跳转到统一的中断处理入口(如`handle_arch_irq`) 2. **中断分发** 内核通过**中断描述符表(IRQ Domain)**确定中断源,并调用对应的**驱动程序注册的中断处理函数**。 3. **中断上下文处理** - **顶半部(Top Half)**:快速处理关键任务(如读取寄存器状态),需在中断禁用环境下执行 - **底半部(Bottom Half)**:延迟处理耗时操作(如数据处理),通过软中断SoftIRQ)、Tasklet或工作队列实现 --- #### 四、驱动程序的中断注册与处理 驱动程序通过以下步骤参与中断处理: 1. **申请中断号** 使用`platform_get_irq()`或`of_irq_get()`获取设备树中定义的中断号。 2. **注册中断处理函数** ```c request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags, const char *name, void *dev); ``` - `handler`:顶半部函数,返回`IRQ_WAKE_THREAD`可触发底半部 - `flags`:指定触发类型(如`IRQF_TRIGGER_RISING`) 3. **编写中断服务例程(ISR)** - 读取外设状态寄存器确认中断原因 - 清除外设中断标志(部分硬件需手动操作) --- #### 、关键协作流程 1. **外设** → **PLIC** → **CPU** → **内核** → **驱动** ```mermaid graph LR A[外设触发中断] --> B(PLIC仲裁优先级) B --> C{CPU核心} C --> D[内核中断入口] D --> E[驱动注册的ISR] E --> F[顶半部处理] F --> G[底半部调度] ``` 2. **中断嵌套与屏蔽** - 默认情况下,处理中断时会禁用同级/低优先级中断 - RNMI等不可屏蔽中断会直接更新CSR状态(如`mncause`和`mnstatus`[^1]) ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值