linux kernel之软中断和tasklet(小任务)机制
本文摘自和内核源码linux-2.6.32
软中断是用软件方式模拟硬件中断的念,实现宏观上的异步执行效果,tasklet也是基于软中断实现的
硬中断、软中断和信号的区别:硬中断是外部设备对 CPU 的中断,软中断通常是硬中断服务程序对内
核的中断,而信号则是由内核(或其他进程)对某个进程的中断----------《linux设备驱动开发详解》
一个处理者在中断时运行, 因此, 它能做的事情遭受一些限制. 这些限
制与我们在内核定时器上看到的相同. 一个处理者不能传递数据到或者从用户空间, 因为
它不在进程上下文执行. 处理者也不能做任何可能睡眠的事情,
中断所谓的前半部是实
际响应中断的函数 – 你使用 request_irq 注册的那个. 后半部是由前半部调度来延后
执行的函数, 在一个更安全的时间. 最大的不同在前半部处理和后半部之间是所有的中断
在后半部执行时都使能
tasklet(小任务)机制是中断处理下半部分最常用的一种方法,其使用也是非常简单的。一个使用tasklet的中断程序首先会通过执行中断处理程序来快速完成上半部分的工作
,接着通过调用tasklet使得下半部分的工作得以完成。下半部分被上半部分所调用,至于下半部分何时执行则属于内核的工作。
interrupt.h中
- If tasklet_schedule() is called, then tasklet is guaranteed
to be executed on some cpu at least once after this. - If the tasklet is already scheduled, but its excecution is still not
started, it will be executed only once.
struct tasklet_struct
{
struct tasklet_struct *next;
unsigned long state;
atomic_t count;
void (*func)(unsigned long);
unsigned long data;
};
第一个成员代表链表中的下一个tasklet。第二个变量代表此刻tasklet的状态,一般为TASKLET_STATE_SCHED,表示此tasklet已被调度且正准备运行;此变量还可取
TASKLET_STATE_RUN,表示正在运行,但只用在多处理器的情况下。count成员是一个引用计数器,只有当其值为0时候,tasklet才会被激活;否则被禁止,不能被执行。而接下
来的func变量很明显是一个函数指针,它指向tasklet处理函数,这个处理函数的唯一参数为data
在<linux/interrupt.h>中的两个宏:
静态创建tasklet的两种方法。
通过第一个宏创建的tasklet处于激活状态,再通过调度函数被挂起尽而被内核执行;而通过第二个宏创建的tasklet处于禁止状态
#define DECLARE_TASKLET(name, func, data) \
struct tasklet_struct name = { NULL, 0, ATOMIC_INIT(0), func, data }
#define DECLARE_TASKLET_DISABLED(name, func, data) \
struct tasklet_struct name = { NULL, 0, ATOMIC_INIT(1), func, data }
动态创建,通过给tasklet_init函数传递一个事先定义的指针,来动态创建一个tasklet
softirq.c
void tasklet_init(struct tasklet_struct *t,
void (*func)(unsigned long), unsigned long data)
{
t->next = NULL;
t->state = 0;
atomic_set(&t->count, 0);
t->func = func;
t->data = data;
}
创建好之后,我们还要通过如下的方法对tasklet进行调度:
tasklet_schedule(&my_tasklet)
通过此函数的调用,我们的tasklet就会被挂起,等待机会被执行
interrupt.h中
static inline void tasklet_schedule(struct tasklet_struct *t)
{
if (!test_and_set_bit(TASKLET_STATE_SCHED, &t->state))
__tasklet_schedule(t);
}
驱动程序在初始化时,通过函数task_init建立一个tasklet,然后调用函数tasklet_schedule将这个tasklet放在 tasklet_vec链表的头部,并唤醒后台线程ksoftirqd。当后台线程ksoftirqd运行调用__do_softirq时,会执行在中断向量表softirq_vec里中断号TASKLET_SOFTIRQ对应的tasklet_action函数,然后tasklet_action遍历 tasklet_vec链表,调用每个tasklet的函数完成软中断操作
softirq.c中
void __tasklet_schedule(struct tasklet_struct *t)
{
unsigned long flags;
local_irq_save(flags);
t->next = NULL;
*__get_cpu_var(tasklet_vec).tail = t;
__get_cpu_var(tasklet_vec).tail = &(t->next);
raise_softirq_irqoff(TASKLET_SOFTIRQ); //所以说task_let是基于软中断实现的,因此也运行于软中断上下文
//raise_softirq(unsigned int nr)是触发软中断的,此函数也调用了raise_softirq_irqoff
//函数__tasklet_schedule得到当前CPU的tasklet_vec链表,并执行TASKLET_SOFTIRQ软中断。
local_irq_restore(flags);
}
该函数的参数t指向要在当前CPU上被执行的tasklet。对该函数的NOTE如下:
①调用test_and_set_bit()函数将待调度的tasklet的state成员变量的bit[0]位(也
即TASKLET_STATE_SCHED位)设置为1,该函数同时还返回TASKLET_STATE_SCHED位的原有
值。因此如果bit[0]为的原有值已经为1,那就说明这个tasklet已经被调度到另一个
CPU上去等待执行了。由于一个tasklet在某一个时刻只能由一个CPU来执行,因此
tasklet_schedule()函数什么也不做就直接返回了。否则,就继续下面的调度操作。
②首先,调用local_irq_save()函数来关闭当前CPU的中断,以保证下面的步骤在当前
CPU上原子地被执行。
③然后,将待调度的tasklet添加到当前CPU对应的tasklet队列的首部。
④接着,调用__cpu_raise_softirq()函数在当前CPU上触发软中断请求
TASKLET_SOFTIRQ。
⑤最后,调用local_irq_restore()函数来开当前CPU的中断。
softirq.c
inline fastcall void raise_softirq_irqoff(unsigned int nr)
{
__raise_softirq_irqoff(nr);
//函数raise_softirq_irqoff设置软中断nr为挂起状态,并在没有中断时唤醒线程ksoftirqd。函数raise_softirq_irqoff必须在关中断情况下运行。
if (!in_interrupt())
wakeup_softirqd();
}
irqflags.h 中
#define local_irq_save(flags)
do {
typecheck(unsigned long, flags);
raw_local_irq_save(flags);
trace_hardirqs_off();
} while (0)
#define local_irq_restore(flags)
do {
typecheck(unsigned long, flags);
if (raw_irqs_disabled_flags(flags)) {
raw_local_irq_restore(flags);
trace_hardirqs_off();
} else {
trace_hardirqs_on();
raw_local_irq_restore(flags);
}
} while (0)
摘录于《Linux内核分析及编程>
http://hi.baidu.com/ryderlee/blog/item/ceeec316e8d1f318962b431a.html
1.Tasklet 可被hi-schedule和一般schedule,hi-schedule一定比一般shedule早运行;
2.同一个Tasklet可同时被hi-schedule和一般schedule;
3.同一个Tasklet若被同时hi-schedule多次,等同于只hi-shedule一次,因为,在tasklet未运行时,hi-
shedule同一tasklet无意义,会冲掉前一个tasklet;
4.对于一般shedule, 同上。
5.不同的tasklet不按先后shedule顺序运行,而是并行运行。
6.Tasklet的运行时间:
a.若在中断中schedule tasklet, 中断结束后立即运行;
b.若CPU忙,在不在此次中断后立即运行;
c.不在中断中shedule tasklet;
d.有软或硬中断在运行;
e.从系统调用中返回;(仅当process闲时)
f.从异常中返回;
g.调试程序调度。(ksoftirqd运行时,此时CPU闲)
7.Taskelet的hi-schedule 使用softirq 0, 一般schedule用softirq 30;
8.Tasklet的运行时间最完在下一次time tick 时。(因为最外层中断一定会运行使能的softirq, 面不在中断中便能或shedule的softirq在下一定中断后一定会被调用。)
/
void raise_softirq(unsigned int nr)//本函数可以触发一次软中断
{
unsigned long flags;
local_irq_save(flags);
raise_softirq_irqoff(nr);
local_irq_restore(flags);
}
/*
- This function must run with irqs disabled!
/
inline void raise_softirq_irqoff(unsigned int nr)
{
__raise_softirq_irqoff(nr);
/ - If we’re in an interrupt or softirq, we’re done
- (this also catches softirq-disabled code). We will
- actually run the softirq once we return from
- the irq or softirq.
- Otherwise we wake up ksoftirqd to make sure we
- schedule the softirq soon.
/
if (!in_interrupt())
wakeup_softirqd();
}
interrupt.h中定义
#define __raise_softirq_irqoff(nr) do { or_softirq_pending(1UL << (nr)); } while (0)
#define set_softirq_pending(x) (local_softirq_pending() = (x))
#define or_softirq_pending(x) (local_softirq_pending() |= (x))
irq_cpustat.h中定义
/ arch independent irq_stat fields */
#define local_softirq_pending()
__IRQ_STAT(smp_processor_id(), __softirq_pending)
extern irq_cpustat_t irq_stat[]; /* defined in asm/hardirq.h */
#define __IRQ_STAT(cpu, member) (irq_stat[cpu].member) ///很奇妙吧,宏也可以这么用
smp.h中定义
define smp_processor_id() raw_smp_processor_id()
#define raw_smp_processor_id() 0
hardirq.h中定义
typedef struct {
unsigned int __softirq_pending;
unsigned int local_timer_irqs;
} irq_cpustat_t;
//
main.c的start_kernel()中调用
void __init softirq_init(void)
{
int cpu;
for_each_possible_cpu(cpu) {
int i;
per_cpu(tasklet_vec, cpu).tail =
&per_cpu(tasklet_vec, cpu).head;
per_cpu(tasklet_hi_vec, cpu).tail =
&per_cpu(tasklet_hi_vec, cpu).head;
for (i = 0; i < NR_SOFTIRQS; i++)
INIT_LIST_HEAD(&per_cpu(softirq_work_list[i], cpu));
}
register_hotcpu_notifier(&remote_softirq_cpu_notifier);
open_softirq(TASKLET_SOFTIRQ, tasklet_action);
open_softirq(HI_SOFTIRQ, tasklet_hi_action);
}
intterupt.h中
enum
{
HI_SOFTIRQ=0,
TIMER_SOFTIRQ,
NET_TX_SOFTIRQ,
NET_RX_SOFTIRQ,
BLOCK_SOFTIRQ,
TASKLET_SOFTIRQ,
SCHED_SOFTIRQ,
#ifdef CONFIG_HIGH_RES_TIMERS
HRTIMER_SOFTIRQ,
#endif
RCU_SOFTIRQ, /* Preferable RCU should always be the last softirq */
NR_SOFTIRQS
};
在softirq.c中
void open_softirq(int nr, void (*action)(struct softirq_action *))
{
softirq_vec[nr].action = action;
}
interrupt.h
static struct softirq_action softirq_vec[NR_SOFTIRQS] ;
struct softirq_action
{
void (*action)(struct softirq_action *);
};
软中断向量TASKLET_SOFTIRQ的服务程序tasklet_action()
函数tasklet_action()是tasklet机制与软中断向量TASKLET_SOFTIRQ的联系纽带。正是
该函数将当前CPU的tasklet队列中的各个tasklet放到当前CPU上来执行的。该函数实现
在kernel/softirq.c文件中,其源代码如下:
static void tasklet_action(struct softirq_action *a)
{
struct tasklet_struct *list;
local_irq_disable();
list = __get_cpu_var(tasklet_vec).head;
__get_cpu_var(tasklet_vec).head = NULL;
__get_cpu_var(tasklet_vec).tail = &__get_cpu_var(tasklet_vec).head;
local_irq_enable();
while (list) {
struct tasklet_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;
*__get_cpu_var(tasklet_vec).tail = t;
__get_cpu_var(tasklet_vec).tail = &(t->next);
__raise_softirq_irqoff(TASKLET_SOFTIRQ);
local_irq_enable();
}
}
注释如下:
①首先,在当前CPU关中断的情况下,“原子”地读取当前CPU的tasklet队列头部指针,
将其保存到局部变量list指针中,然后将当前CPU的tasklet队列头部指针设置为NULL,
以表示理论上当前CPU将不再有tasklet需要执行(但最后的实际结果却并不一定如此,
下面将会看到)。
②然后,用一个while{}循环来遍历由list所指向的tasklet队列,队列中的各个元素就
是将在当前CPU上执行的tasklet。循环体的执行步骤如下:
l 用指针t来表示当前队列元素,即当前需要执行的tasklet。
l 更新list指针为list->next,使它指向下一个要执行的tasklet。
l 用tasklet_trylock()宏试图对当前要执行的tasklet(由指针t所指向)进行加锁,如
果加锁成功(当前没有任何其他CPU正在执行这个tasklet),则用原子读函数
atomic_read()进一步判断count成员的值。如果count为0,说明这个tasklet是允许执行
的,于是:(1)先清除TASKLET_STATE_SCHED位;(2)然后,调用这个tasklet的可执
行函数func;(3)执行barrier()操作;(4)调用宏tasklet_unlock()来清除
TASKLET_STATE_RUN位。(5)最后,执行continue语句跳过下面的步骤,回到while循环
继续遍历队列中的下一个元素。如果count不为0,说明这个tasklet是禁止运行的,于是
调用tasklet_unlock()清除前面用tasklet_trylock()设置的TASKLET_STATE_RUN位。
l 如果tasklet_trylock()加锁不成功,或者因为当前tasklet的count值非0而不允许执
行时,我们必须将这个tasklet重新放回到当前CPU的tasklet队列中,以留待这个CPU下
次服务软中断向量TASKLET_SOFTIRQ时再执行。为此进行这样几步操作:(1)先关CPU中
断,以保证下面操作的原子性。(2)把这个tasklet重新放回到当前CPU的tasklet队列
的首部;(3)调用__cpu_raise_softirq()函数在当前CPU上再触发一次软中断请求
TASKLET_SOFTIRQ;(4)开中断。
l 最后,回到while循环继续遍历队列。
///
asmlinkage void do_softirq(void)
{
__u32 pending;
unsigned long flags;
if (in_interrupt())
return;
local_irq_save(flags);
pending = local_softirq_pending();
if (pending)
__do_softirq();
local_irq_restore(flags);
}
//
asmlinkage void __do_softirq(void)
{
struct softirq_action h;
__u32 pending;
int max_restart = MAX_SOFTIRQ_RESTART;
int cpu;
pending = local_softirq_pending();
account_system_vtime(current);
__local_bh_disable((unsigned long)__builtin_return_address(0));
lockdep_softirq_enter();
cpu = smp_processor_id();
restart:
/ Reset the pending bitmask before enabling irqs /
set_softirq_pending(0);
local_irq_enable();
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);
trace_softirq_exit(h, softirq_vec);
if (unlikely(prev_count != preempt_count())) {
printk(KERN_ERR “huh, entered softirq %td %s %p”
“with preempt_count %08x,”
" exited with %08x?\n", h - softirq_vec,
softirq_to_name[h - softirq_vec],
h->action, prev_count, preempt_count());
preempt_count() = prev_count;
}
rcu_bh_qs(cpu);
}
h++;
pending >>= 1;
} while (pending);
local_irq_disable();
pending = local_softirq_pending();
if (pending && --max_restart)
goto restart;
if (pending)
wakeup_softirqd();
lockdep_softirq_exit();
account_system_vtime(current);
_local_bh_enable();
}
/
softirq.c
static int ksoftirqd(void * __bind_cpu)
{
set_current_state(TASK_INTERRUPTIBLE);
while (!kthread_should_stop()) {
preempt_disable();
if (!local_softirq_pending()) {
preempt_enable_no_resched();
schedule();
preempt_disable();
}
__set_current_state(TASK_RUNNING);
while (local_softirq_pending()) {
/ Preempt disable stops cpu going offline.
If already offline, we’ll be on wrong CPU:
don’t process /
if (cpu_is_offline((long)__bind_cpu))
goto wait_to_die;
do_softirq();
preempt_enable_no_resched();
cond_resched();
preempt_disable();
}
preempt_enable();
set_current_state(TASK_INTERRUPTIBLE);
}
__set_current_state(TASK_RUNNING);
return 0;
wait_to_die:
preempt_enable();
/ Wait for kthread_stop */
set_current_state(TASK_INTERRUPTIBLE);
while (!kthread_should_stop()) {
schedule();
set_current_state(TASK_INTERRUPTIBLE);
}
__set_current_state(TASK_RUNNING);
return 0;
}
//
softirq.c
static int __cpuinit cpu_callback(struct notifier_block *nfb,
unsigned long action,
void *hcpu)
{
int hotcpu = (unsigned long)hcpu;
struct task_struct p;
switch (action) {
case CPU_UP_PREPARE:
case CPU_UP_PREPARE_FROZEN:
p = kthread_create(ksoftirqd, hcpu, “ksoftirqd/%d”, hotcpu);
if (IS_ERR§) {
printk(“ksoftirqd for %i failed\n”, hotcpu);
return NOTIFY_BAD;
}
kthread_bind(p, hotcpu);
per_cpu(ksoftirqd, hotcpu) = p;
break;
case CPU_ONLINE:
case CPU_ONLINE_FROZEN:
wake_up_process(per_cpu(ksoftirqd, hotcpu));
break;
#ifdef CONFIG_HOTPLUG_CPU
case CPU_UP_CANCELED:
case CPU_UP_CANCELED_FROZEN:
if (!per_cpu(ksoftirqd, hotcpu))
break;
/ Unbind so it can run. Fall thru. /
kthread_bind(per_cpu(ksoftirqd, hotcpu),
any_online_cpu(cpu_online_map));
case CPU_DEAD:
case CPU_DEAD_FROZEN: {
struct sched_param param = { .sched_priority = MAX_RT_PRIO-1 };
p = per_cpu(ksoftirqd, hotcpu);
per_cpu(ksoftirqd, hotcpu) = NULL;
sched_setscheduler_nocheck(p, SCHED_FIFO, ¶m);
kthread_stop§;
takeover_tasklets(hotcpu);
break;
}
#endif / CONFIG_HOTPLUG_CPU /
}
return NOTIFY_OK;
}
/
/
- Declared notifiers so far. I can imagine quite a few more chains
- over time (eg laptop power reset chains, reboot chain (to clean
- device units up), device [un]mount chain, module load/unload chain,
- low memory chain, screenblank chain (for plug in modular screenblankers)
- VC switch chains (for loadable kernel svgalib VC switch helpers) etc…
/
include/linux/notifier.h文件中定义了 CPU的几种状态
#define SYS_DOWN 0x0001 / Notify of system down /
#define SYS_RESTART SYS_DOWN
#define SYS_HALT 0x0002 / Notify of system halt /
#define SYS_POWER_OFF 0x0003 / Notify of system power off /
#define NETLINK_URELEASE 0x0001 / Unicast netlink socket released /
#define CPU_ONLINE 0x0002 / CPU (unsigned)v is up /
#define CPU_UP_PREPARE 0x0003 / CPU (unsigned)v coming up /
#define CPU_UP_CANCELED 0x0004 / CPU (unsigned)v NOT coming up /
#define CPU_DOWN_PREPARE 0x0005 / CPU (unsigned)v going down /
#define CPU_DOWN_FAILED 0x0006 / CPU (unsigned)v NOT going down /
#define CPU_DEAD 0x0007 / CPU (unsigned)v dead /
#define CPU_DYING 0x0008 / CPU (unsigned)v not running any task,- not handling interrupts, soon dead.
- Called on the dying cpu, interrupts
- are already disabled. Must not
- sleep, must not fail /
#define CPU_POST_DEAD 0x0009 / CPU (unsigned)v dead, cpu_hotplug - lock is dropped /
#define CPU_STARTING 0x000A / CPU (unsigned)v soon running. - Called on the new cpu, just before
- enabling interrupts. Must not sleep,
- must not fail /
/ Used for CPU hotplug events occuring while tasks are frozen due to a suspend
- operation in progress
*/
#define CPU_TASKS_FROZEN 0x0010
#define CPU_ONLINE_FROZEN (CPU_ONLINE | CPU_TASKS_FROZEN)
#define CPU_UP_PREPARE_FROZEN (CPU_UP_PREPARE | CPU_TASKS_FROZEN)
#define CPU_UP_CANCELED_FROZEN (CPU_UP_CANCELED | CPU_TASKS_FROZEN)
#define CPU_DOWN_PREPARE_FROZEN (CPU_DOWN_PREPARE | CPU_TASKS_FROZEN)
#define CPU_DOWN_FAILED_FROZEN (CPU_DOWN_FAILED | CPU_TASKS_FROZEN)
#define CPU_DEAD_FROZEN (CPU_DEAD | CPU_TASKS_FROZEN)
#define CPU_DYING_FROZEN (CPU_DYING | CPU_TASKS_FROZEN)
#define CPU_STARTING_FROZEN (CPU_STARTING | CPU_TASKS_FROZEN)