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,简单方便,而且软中断是在编译时决定的,开销太大。