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

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

/*****************************************************************************************************//*****************************************************************************************************/

1. 软件中断的数据结构1.1 struct softirq_action 内核用softirq_action结构管理软件中断的注册和激活等操作,它的定义如下:[cpp]view plaincopy

    structsoftirq_action{void(*action)(structsoftirq_action*);};

非常简单,只有一个用于回调的函数指针。软件中断的资源是有限的,内核目前只实现了10种类型的软件中断,它们是:[cpp]view plaincopy

    enum{HI_SOFTIRQ=0,TIMER_SOFTIRQ,NET_TX_SOFTIRQ,NET_RX_SOFTIRQ,BLOCK_SOFTIRQ,BLOCK_IOPOLL_SOFTIRQ,TASKLET_SOFTIRQ,SCHED_SOFTIRQ,HRTIMER_SOFTIRQ,RCU_SOFTIRQ,/*PreferableRCUshouldalwaysbethelastsoftirq*/NR_SOFTIRQS};

内核的开发者们不建议我们擅自增加软件中断的数量,如果需要新的软件中断,尽可能把它们实现为基于软件中断的tasklet形式。与上面的枚举值相对应,内核定义了一个softirq_action的结构数组,每种软中断对应数组中的一项:[cpp]view plaincopy

    staticstructsoftirq_actionsoftirq_vec[NR_SOFTIRQS]__cacheline_aligned_in_smp;

1.2 irq_cpustat_t 多个软中断可以同时在多个cpu运行,就算是同一种软中断,也有可能同时在多个cpu上运行。内核为每个cpu都管理着一个待决软中断变量(pending),它就是irq_cpustat_t:[cpp]view plaincopy

    typedefstruct{unsignedint__softirq_pending;}____cacheline_alignedirq_cpustat_t;

[cpp]view plaincopy

    irq_cpustat_tirq_stat[NR_CPUS]____cacheline_aligned;

__softirq_pending字段中的每一个bit,对应着某一个软中断,某个bit被置位,说明有相应的软中断等待处理。1.3 软中断的守护进程ksoftirqd 在cpu的热插拔阶段,内核为每个cpu创建了一个用于执行软件中断的守护进程ksoftirqd,同时定义了一个per_cpu变量用于保存每个守护进程的task_struct结构指针:[cpp]view plaincopy

    DEFINE_PER_CPU(structtask_struct*,ksoftirqd);

大多数情况下,软中断都会在irq_exit阶段被执行,在irq_exit阶段没有处理完的软中断才有可能会在守护进程中执行。2. 触发软中断 要触发一个软中断,只要调用api:raise_softirq即可,它的实现很简单,先是关闭本地cpu中断,然后调用:raise_softirq_irqoff[cpp]view plaincopy

    voidraise_softirq(unsignedintnr){unsignedlongflags;local_irq_save(flags);raise_softirq_irqoff(nr);local_irq_restore(flags);}

再看看raise_softirq_irqoff:[cpp]view plaincopy

    inlinevoidraise_softirq_irqoff(unsignedintnr){__raise_softirq_irqoff(nr);……if(!in_interrupt())wakeup_softirqd();}

先是通过__raise_softirq_irqoff设置cpu的软中断pending标志位(irq_stat[NR_CPUS] ),然后通过in_interrupt判断现在是否在中断上下文中,或者软中断是否被禁止,如果都不成立,则唤醒软中断的守护进程,在守护进程中执行软中断的回调函数。否则什么也不做,软中断将会在中断的退出阶段被执行。3. 软中断的执行 基于上面所说,软中断的执行既可以守护进程中执行,也可以在中断的退出阶段执行。实际上,软中断更多的是在中断的退出阶段执行(irq_exit),以便达到更快的响应,加入守护进程机制,只是担心一旦有大量的软中断等待执行,会使得内核过长地留在中断上下文中。3.1 在irq_exit中执行 看看irq_exit的部分:[cpp]view plaincopy

    voidirq_exit(void){……sub_preempt_count(IRQ_EXIT_OFFSET);if(!in_interrupt()&&local_softirq_pending())invoke_softirq();……}

如果中断发生嵌套,in_interrupt()保证了只有在最外层的中断的irq_exit阶段,invoke_interrupt才会被调用,当然,local_softirq_pending也会实现判断当前cpu有无待决的软中断。代码最终会进入__do_softirq中,内核会保证调用__do_softirq时,本地cpu的中断处于关闭状态,进入__do_softirq:[cpp]view plaincopy

    asmlinkagevoid__do_softirq(void){……pending=local_softirq_pending();__local_bh_disable((unsignedlong)__builtin_return_address(0),SOFTIRQ_OFFSET);restart:/*Resetthependingbitmaskbeforeenablingirqs*/set_softirq_pending(0);local_irq_enable();h=softirq_vec;do{if(pending&1){……trace_softirq_entry(vec_nr);h->action(h);trace_softirq_exit(vec_nr);……}h++;pending>>=1;}while(pending);local_irq_disable();pending=local_softirq_pending();if(pending&&–max_restart)gotorestart;if(pending)wakeup_softirqd();lockdep_softirq_exit();__local_bh_enable(SOFTIRQ_OFFSET);}

首先取出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 plaincopy

    void__initsoftirq_init(void){……open_softirq(TASKLET_SOFTIRQ,tasklet_action);open_softirq(HI_SOFTIRQ,tasklet_hi_action);}

内核用一个tasklet_struct来表示一个tasklet,它的定义如下:[cpp]view plaincopy

    structtasklet_struct{structtasklet_struct*next;unsignedlongstate;atomic_tcount;void(*func)(unsignedlong);unsignedlongdata;};

next用于把同一个cpu的tasklet链接成一个链表,state用于表示该tasklet的当前状态,目前只是用了最低的两个bit,分别用于表示已经准备被调度执行和已经在另一个cpu上执行:[cpp]view plaincopy

    enum{TASKLET_STATE_SCHED,/*Taskletisscheduledforexecution*/TASKLET_STATE_RUN/*Taskletisrunning(SMPonly)*/};

原子变量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 plaincopy

    structtasklet_structtasklet_xxx;……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 plaincopy

    structtasklet_head{structtasklet_struct*head;structtasklet_struct**tail;};staticDEFINE_PER_CPU(structtasklet_head,tasklet_vec);staticDEFINE_PER_CPU(structtasklet_head,tasklet_hi_vec);

回到4.1节,我们知道,tasklet是利用TASKLET_SOFTIRQ和HI_SOFTIRQ这两个软中断来实现的,两个软中断只是有优先级的差别,所以我们只讨论TASKLET_SOFTIRQ的实现,TASKLET_SOFTIRQ的中断回调函数是tasklet_action,我们看看它的代码:[cpp]view plaincopy

    staticvoidtasklet_action(structsoftirq_action*a){structtasklet_struct*list;local_irq_disable();list=__this_cpu_read(tasklet_vec.head);__this_cpu_write(tasklet_vec.head,NULL);__this_cpu_write(tasklet_vec.tail,&__get_cpu_var(tasklet_vec).head);local_irq_enable();while(list){structtasklet_struct*t=list;list=list->next;if(tasklet_trylock(t)){if(!atomic_read(&t->count)){if(!test_and_clear_bit(TASKLET_STATE_SCHED,&t->state))BUG();t->func(t->data);tasklet_unlock(t);continue;}tasklet_unlock(t);}local_irq_disable();t->next=NULL;*__this_cpu_read(tasklet_vec.tail)=t;__this_cpu_write(tasklet_vec.tail,&(t->next));__raise_softirq_irqoff(TASKLET_SOFTIRQ);local_irq_enable();}}

解析如下:关闭本地中断的前提下,移出当前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之间如果有资源冲突,应该要用自旋锁进行同步保护;不会因为忧伤而风情万种。

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

相关文章:

你感兴趣的文章:

标签云: