Linux内核可以有三种方法来实现中断下半部:sotfirq 、tasklet 和workqueue
1、softirq
软中断一般很少用于实现中断下半部,但tasklet是通过软中断实现的,所以先介绍软中断。字面理解,软中断就是软件实现的异步中断,它的优先级比硬中断低,但比普通进程优先级高,同时,它和硬中断一样不能休眠。
在kernel/softirq.c文件中有这样一个数组
static struct softirq_action softirq_vec[NR_SOFTIRQS] __cacheline_aligned_in_smp;
内核通过一个 softirq_action结构体数组来维护软中断,NR_SOFTIRQS是当前支持的软中断枚举类型中最后的一个成员。
/* PLEASE, avoid to allocate new softirqs, if you need not _really_ high frequency threaded job scheduling. For almost all the purposes tasklets are more than enough. F.e. all serial device BHs etal. should be converted to tasklets, not to softirqs.*/
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, /* Preferable RCU should always be the last softirq */
};
如果要想有一个新的软中断,可以在这个枚举结构体的 NR_SOFTIRQS 上面添加。不过,从注释中我们发现内核并不建议我们添加新的软中断,毕竟softirq很少用于实现中断的下半部。
看一下softirq_action结构体
struct softirq_action
{
void (*action)(struct softirq_action *);
};
结构体里面就一个软中断函数,它的参数就是本身结构体的指针。之所以这样设计,是为了以后的拓展,如果在结构体中添加了新成员,也不需要修改函数接口。在2.4内核中,该结构体里面还有一个data的成员,用于传参,不过现在没有了。
使用softirq机制需要通过 open_softirq 来注册软中断处理函数,使中断索引号与中断处理函数对应。该函数定义在 kernel/softirq.c文件中。
void open_softirq(int nr, void (*action)(struct softirq_action *))
{
softirq_vec[nr].action = action;
}
该函数将软中断的中断处理函数指针赋值给相应的softirq_vec, 当这个softirq被挂起后,内核会在某个时刻去执行这个中断处理函数。
在中断处理函数完成了紧急的硬件操作后,就应该调用 raise_softirq 函数来触发软中断,让软中断来处理耗时的中断下半部操作。
void raise_softirq(unsigned int nr)
{
unsigned long flags;
local_irq_save(flags);
raise_softirq_irqoff(nr);
local_irq_restore(flags);
}
该函数挂起相应的软中断,并在没有中断时唤醒线程 ksoftirq,让内核在下次执行软中断的时候去执行这个软中断的处理函数。
注意:若在模块中直接使用open_softirq 和raise_softirq 这两个函数,模块加载时会报错,提醒这两个符号未声明。为了让模块正常加载,可以在kernel/softirq.c文件中将这两个符号导出。
EXPORT_SYMBOL(raise_softirq);
EXPORT_SYMBOL(open_softirq);
上面介绍,触发软中断函数raise_softirq并不会让软中断处理函数马上执行,它只是打了个标记,等到适合的时候再被执行。如在中断处理函数返回后,内核就会检查软中断是否被触发并执行触发的软中断。
软中断会在do_softirq中被执行,其中核心部分在do_softirq中调用的__do_softirq中:
asmlinkage void __do_softirq(void)
{
.
.
.
h = softirq_vec;
do {
if (pending & 1) { //如果被触法,调用中断处理函数
int prev_count = preempt_count();
kstat_incr_softirqs_this_cpu(h - softirq_vec);
trace_softirq_entry(h, softirq_vec);
h->action(h); //调用中断处理函数
.
.
.
}
rcu_bh_qs(cpu);
}
h++; //下移,获取另一个软中断
pending >>= 1;
} while (pending); //直到所有被触发的软中断都执行完
.
.
.
}
2、tasklet
在实际的项目中我们一般使用tasklet实现中断的下半部。在介绍软中断索引号的时候,有两个用于实现tasklet的软中断索引号:HI_SOFTIRQ和TASKLET_SOFTIRQ。两个tasklet唯一的区别就是HI_SOFTIRQ优先级高些,一般使用TAKSLET_SOFTIRQ。
内核中是通过tasklet_struct来维护一个tasklet,介绍一下tasklet_struct结构体里面的成员:
include/linux/interrupt.h
struct tasklet_struct
{
struct tasklet_struct *next;
unsigned long state;
atomic_t count;
void (*func)(unsigned long);
unsigned long data;
};
各成员的含义如下:
(1)next指针:指向下一个tasklet的指针。
(2)state:定义了这个tasklet的当前状态。这一个32位的无符号长整数,当前只使用
了bit[1]和bit[0]两个状态位。其中,bit[1]=1表示这个tasklet当前正在某个
CPU上被执行,它仅对SMP系统才有意义,其作用就是为了防止多个CPU同时执行一个
tasklet的情形出现;bit[0]=1表示这个tasklet已经被调度去等待执行了。对这两个
状态位的宏定义如下所示(interrupt.h):
enum
{
TASKLET_STATE_SCHED, /* Tasklet is scheduled for execution */
TASKLET_STATE_RUN /* Tasklet is running (SMP only) */
};
(3)原子计数count:对这个tasklet的引用计数值。只有当count等于0时,
tasklet代码段才能执行,即此时tasklet是被使能的;如果count非零,则这个
tasklet是被禁止的。任何想要执行一个tasklet代码段的人都首先必须先检查其count成
员是否为0。
(4)函数指针func:指向以函数形式表现的可执行tasklet代码段。
(5)data:函数func的参数。这是一个32位的无符号整数,其具体含义可供func函数自
行解释,比如将其解释成一个指向某个用户自定义数据结构的地址值。
在模块加载函数,或者open函数中可以使用下面两种方式初始化tasklet。
DECLARE_TASKLET(name, func, data);或者
void tasklet_init(struct tasklet_struct *t, void (*func)(unsigned long), unsigned long data)
然后在中断函数返回前调用
static inline void tasklet_schedule(struct tasklet_struct *t)
触发tasklet软中断。
static inline void tasklet_schedule(struct tasklet_struct *t)
{
if (!test_and_set_bit(TASKLET_STATE_SCHED, &t->state))
__tasklet_schedule(t);
}
函数__tasklet_schedule得到当前CPU的tasklet_vec链表,并执行TASKLET_SOFTIRQ软中断。
void fastcall __tasklet_schedule(struct tasklet_struct *t)
{
unsigned long flags;
local_irq_save(flags);
t->next = __get_cpu_var(tasklet_vec).list;
__get_cpu_var(tasklet_vec).list = t;
raise_softirq_irqoff(TASKLET_SOFTIRQ);
local_irq_restore(flags);
}
函数raise_softirq_irqoff设置软中断nr为挂起状态,并在没有中断时唤醒线程ksoftirqd。函数raise_softirq_irqoff必须在关中断情况下运行。
3、workqueue
内核使用work_struct结构体来维护一个加入工作队列的任务:
atomic_long_t data;
#define WORK_STRUCT_PENDING 0 /* T if work item pending execution */
#define WORK_STRUCT_STATIC 1 /* static initializer (debugobjects) */
#define WORK_STRUCT_FLAG_MASK (3UL)
#define WORK_STRUCT_WQ_DATA_MASK (~WORK_STRUCT_FLAG_MASK)
struct list_head entry;
work_func_t func; //下半部实现的处理函数指针
#ifdef CONFIG_LOCKDEP
struct lockdep_map lockdep_map;
#endif
};
要使用工作队列来实现中断下半部,首先需要定义一个 work_struct 结构体变量和一个处理函数。
struct work_struct my_wq; //定义一个工作队列
void xxxx_func(struct work_struct * xx) //处理函数
在模块加载函数或者open函数中通过 INIT_WORK()初始化这个工作队列并与处理函数绑定,在中断处理函数返回前调用 schedule_work(struct work_struct *work)。同样的,调度并不代表处理函数能够马上执行,这由内核进程调度决定。
总结:
软中断支持SMP,同一个softirq可以在不同的CPU上同时运行,softirq是可重入的,如果在软中断的处理函数中操作共享数据,则需要对共享数据保护机制。软中断是在编译期间静态分配的,它不像tasklet那样能被动态的注册或去除。
引入tasklet,最主要的是考虑支持SMP,提高SMP多个cpu的利用率;不同的tasklet可以在不同的cpu上运行。某一段tasklet代码在某个时刻只能在一个CPU上运行,但不同的tasklet代码在同一时刻可以在多个CPU上并发地执行。
软中断和tasklet优先级较高,性能较好,调度快,但不能睡眠。而工作队列是内核的进程调度,相对来说较慢,但能睡眠。所以,如果你的下半部需要睡眠,那只能选择动作队列。否则最好用tasklet。