1. 软中断是什么 ?
软中断是一种延时机制,代码执行的优先级比进程要高,比硬中断要低。相比于硬件中断,软中段是在开中断的环境中执行的(长时间关中断对系统的开销太大), 代码是执行在中断/线程上下文的,是不能睡眠的,虽然每个cpu都有一个对应的ksoftirqd/n线程来执行软中断,但是do_softirq这个函数也还会在中断退出时调用到,因此不能睡眠(中断上下文不能睡眠的原因是由于调度系统是以进程为基本单位的,调度时会把当前进程的上下文保存在task_struct这个数据结构中,当进程被调度重新执行时会找到执行的断点,但是中断上下文是没有特定task_struct结构体的,当然现在有所谓的线程话中断,可以满足在中断处理函数执行阻塞操作,但是实时性可能会有问题。还有就是中断代表当前进程执行的概念,个人感觉有点扯淡,毕竟整个内核空间是由所有进程共享的,不存在代表的概念)
2. 软中段是怎么实现的?
一个模块或者子系统的实现都是数据结构+算法来实现的,算法一般由特定的函数来表征。数据结构一般会定义一些全局变量来支撑算法的实现。软中断牵涉到的数据结构主要是一个全局的向量数组,来映射一个软中断向量号和对应的处理函数:
static struct sotfirq_action softirq_vec[NR_SOFTIRQS]; struct softirq_action { void (*action)(struct softirq_action *); }
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, NR_SOFTIRQS }
核心函数:
open_softirq(int nr, void (*action)(struct softirq_action *)) //一个软中断和对应的处理函数的绑定, { softirq_vec[nr].action = action; }
raise_softirq(unsingned int nr) //表明一个软中断处于pending状态等待处理, 在do_softirq函数里会检查处于pending的软中断,然后调用对应的处理函数 { or_softirq_pending(1UL << nr); // 主要置位per-cpu类型的irq_stat.__softirq_pending成员的第nr位 if (!in_interrupt()) //如果不在中断上下文,则唤醒内核线程ksoftirqd/n来处理pending的软中断,如果在中断上下文的话,可能是由于软中断被禁止执行了 wakeup_softirqd(); }
do_softirq(void) { if(in_interrupt()) // 这里有两种情况:do_softirq已经在中断上下文被调用一次了,例如在执行软中断的时候又发生了硬件中断,在中断退出的时候又会去执行软中断,防止代码的重入,第二种情况是软中断系统被显示地禁止执行了(全局变量preempt_count的第二个字段softirq counter用来记录软中断被disable的次数) return; pending = local_softirq_pending(); if (pending) __do_softirq(); -->__local_bh_disable(); // 进入软中断的处理前,会显示地disable掉软中断,防止重入 --> do { if (pending & 1) { softirq_vec[nr]->action(h); //调用对应的软中断处理函数 } pending >>=1; //优先级从0开始 } while (pending); }
3. tasklet的概念和实现
驱动程序里最常见的就是在中断处理函数里面调度一个tasklet来执行延时的任务,tasklet是在软中断的基础之上实现的,牵涉到HI_SOFTIRQ和TASKLET_SOFTIRQ两种软中断,两种软中断对应的处理函数tasklet_action/tasklet_hi_action会去tasklet_vec和tasklet_hi_vec数组(基于per cpu结构)中取所存在的tasklet,然后执行相应的tasklet处理函数。
static DEFINE_PER_CPU(struct tasklet_head, tasklet_vec); //数组中的元素为一个链表头, 所有的tasklet是链接在一个链表上的 static DEFINE_PER_CPU(struct tasklet_head, tasklet_hi_vec); struct tasket_head { struct tasklet_struct *head; struct tasklet_struct **tail; } struct tasklet_struct { struct tasklet_struct *next; unsigned long state; //一个tasklet对象所处的状态,可以为TASKLET_STATE_SCHED或者TASKLET_STATE_RUN atomic_t count; //不为0表示禁止执行tasklet void (*func)(unsigned long); unsigned data; } static inline void tasklet_schedule(struct tasklet_struct *t) { if (!test_and_set_bit(TASKLET_STATE_SCHED, &t->state)) //同一个tasklet只能被调度一次,如果另一个CPU正在执行同一个tasklet,如果刚好清掉TASKLET_STATE_SCHED标志,这个tasklet可以被挂到这个CPU上的tasklet_vec链表上,但是不能被这个CPU被执行,因为在执行tasklet之前,会检查当前tasklet的状态是否为TASKLET_STATE_RUN, 如果被置位,则不会执行 __tasklet_schedule(t); //将该tasklet链接到本地CPU对应的tasklet链表上去 -->raise_softirq_irqoff(TASKLET_SOFTIRQ); //通知软中断系统,TASKLET_SOFTIRQ类型的软中断处于pending状态 } void tasklet_action(struct softirq_action *a) { struct tasklet_struct *list = __this_cpu_read(tasklet_vec.head); while(list) { struct tasklet_struct *t = list; list = list->next; if (tasklet_trylock(t)) { //测试TASKLET_STATE_RUN标志有没有置位,然后置位该标志位 if(!atomic_read(&t->count)) { //该tasklet被disabled了吗? if(!test_and_clear_bit(TASKLET_STATE_SCHED, &t->sched)) BUG(); t->func(t->data); tasklet_unlock(t); continue; } tasklet_unlock(t); //复位TASKLET_STATE_RUN标志位 } } } static inline void tasklet_disable(struct tasklet_struct *t) { atomic_inc(&t->count); tasklet_unlock_wait(t); //等待正在执行的tasklet执行完毕,确保调用该函数后,在调用tasklet_schedule后tasklet是不会被执行的 } void tasklet_kill(struct tasklet_struct *t) //感觉这个函数和tasklet_disable类似,只是等待当前正在执行的tasklet完成,下次调用tasklet_schedule应该还是会执行的,貌似和kill这个单词不匹配啊,还没tasklet_disable彻底 { while(test_and_set_bit(TASKLET_STATE_SCHED, &t->state)) { do { yield(); } while(test_bit(TASKLET_STATE_SCHED, &t->sched)); } tasklet_unlock_wait(t); clear_bit(TASKLET_STATE_SCHED, &t->state); }
从上面的分析可以看出,tasklet的处理函数是不可重入的,换句话说,就是不可能在两个CPU上跑相同的代码,因为一个tasklet只能被调度一次,也就是挂接在一个链表里面,而且在哪个CPU上被调度,就会在哪个CPU被执行,因为是基于per cpu结构的, 但是软中断对应的处理函数是可重入的, 需要处理同步的问题,不过一般驱动里面还是用tasklet,简单方便,而且软中断是在编译时决定的,开销太大。