一般在有中断的系统中,中断ISR的设计应该尽可能的小,并且在处理中断时,不允许中断ISR再被其他后来的中断打断,也就是避免中断嵌套。现在大多数系统都是不支持中断嵌套的,Linux的实现就是个典型。防止中断嵌套的做法就是处理一个中断时,CPU执行关中断,不接收其他中断。但是这种关中断状态又不能持续太久,关中断时间过长,又会导致后续中断丢失,因此Linux中,将中断处理程序分为两个部分,即上半部和下半部,上半部通常执行时间很短,并且大多与硬件密切相关,所以需要在关中断的环境中运行,而下半部则处理比较费时的一些操作,这部分是在开中断中执行。一般称呼上半部为硬中断,下半部为软中断,Linux的设计中,软中断只能被硬中断打断。
软中断
2.6内核中,软中断的设计始终贯穿一个思想:“谁触发,谁执行(Who marks, who runs)”,所以能有效利用SMP系统的性能,提升了处理效率。2.4以及之前的版本使用的是一种叫Bottom Half的机制来实现下半部,它的致命缺点就是系统中一次能有一个CPU可以执行BH代码,这样在单核CPU中是没有问题的,但在SMP系统中,就严重损失硬件性能了。
2.6.34软中断实现
软中断请求描述include/linux/interrupt.h中:
struct softirq_action
{
void (*action)(struct softirq_action *);
};
看得出来,2.6.34中,定义了十种类型软中断
static struct softirq_action softirq_vec[NR_SOFTIRQS] __cacheline_aligned_in_smp;
enum
{
HI_SOFTIRQ=0, /*用于高优先级的tasklet*/
TIMER_SOFTIRQ, /*用于定时器的下半部*/
NET_TX_SOFTIRQ, /*用于网络层发包*/
NET_RX_SOFTIRQ, /*用于网络层收报*/
BLOCK_SOFTIRQ,
BLOCK_IOPOLL_SOFTIRQ,
TASKLET_SOFTIRQ, /*用于低优先级的tasklet*/
SCHED_SOFTIRQ,
HRTIMER_SOFTIRQ,
RCU_SOFTIRQ, /* Preferable RCU should always be the last softirq */
NR_SOFTIRQS
};
对软中断,linux是在中断处理程序中执行的,具体路径如下:
do_IRQ()->irq_exit()->invoke_softirq()->do_softirq()->__do_softirq()
/*读取本地CPU的软中断掩码并执行与每个设置位相关的可延迟函数,__do_softirq只做固定次数的循环然后就返回。
如果还有其余挂起的软中断,那么内核线程ksofirqd将会在预期的时间内处理他们*/
asmlinkage void __do_softirq(void)
{
struct softirq_action *h;
__u32 pending;
/*把循环计数器的值初始化为10*/
int max_restart = MAX_SOFTIRQ_RESTART;
int cpu;
/*把本地CPU(被local_softirq_pending选中的)软件中断的位掩码复制到局部变量pending中*/
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); /*清除本地CPU的软中断位图,以便可以激活新的软中断*/
/*激活本地中断*/
local_irq_enable();
h = softirq_vec;//h指向全局的软中断向量表
do {
/*根据pending每一位的的设置,执行对应的软中断处理函数*/
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) /*如果还有挂起的软中断,唤醒内核线程来处理本地CPU的软中断*/
wakeup_softirqd();
lockdep_softirq_exit();
account_system_vtime(current);
_local_bh_enable();/*软中断计数器-1,因而重新激活可延迟函数*/
}
软中断使用:
open_softirq():中断注册。开启一个指定的软中断向量nr,初始化nr对应的描述符softirq_vec[nr]。
raise_softirq():中断触发。
软中断机制中还有一个内核线程ksoftirqd,这个线程干嘛用,在kernel/softirq.c中的一端注释说的很清楚了。只为平衡系统负载
/*
* we cannot loop indefinitely here to avoid userspace starvation,
* but we also don't want to introduce a worst case 1/HZ latency
* to the pending events, so lets the scheduler to balance
* the softirq load for us.
*/
如果系统一直不断触发软中断请求,那么CPU就会一直去处理软中断,因为至少每次时钟中断(TIMER_SOFTIRQ)都会执行一次do_softirq(),这样一来,系统中其他重要任务就得不到CPU而一直处于饥饿状态,所以加这么个小线程,将过多的软中断请求放到系统何时的时间段执行。
中断触发,到唤醒内核线程ksoftirqd来处理中断的大致流程是这样的:
void raise_softirq(unsigned int nr)
{
unsigned long flags;
local_irq_save(flags);
raise_softirq_irqoff(nr);
local_irq_restore(flags);
}
inline void raise_softirq_irqoff(unsigned int nr)
{
__raise_softirq_irqoff(nr);
if (!in_interrupt())
wakeup_softirqd();//唤醒内核中断处理线程
}
#define __raise_softirq_irqoff(nr) do { or_softirq_pending(1UL << (nr)); } while (0)
#define or_softirq_pending(x) (local_softirq_pending() |= (x))
#define local_softirq_pending() \
__IRQ_STAT(smp_processor_id(), __softirq_pending)
#define __IRQ_STAT(cpu, member) (irq_stat[cpu].member)
要操作的数据结构在这里irq_cpustat_t:/*arch/xxx/include/asm/hardirq.h*/
typedef struct {
unsigned int __softirq_pending;
unsigned int __nmi_count; /* arch dependent */
unsigned int irq0_irqs;
#ifdef CONFIG_X86_LOCAL_APIC
unsigned int apic_timer_irqs; /* arch dependent */
unsigned int irq_spurious_count;
#endif
unsigned int x86_platform_ipis; /* arch dependent */
unsigned int apic_perf_irqs;
unsigned int apic_pending_irqs;
#ifdef CONFIG_SMP
unsigned int irq_resched_count;
unsigned int irq_call_count;
unsigned int irq_tlb_count;
#endif
#ifdef CONFIG_X86_THERMAL_VECTOR
unsigned int irq_thermal_count;
#endif
#ifdef CONFIG_X86_MCE_THRESHOLD
unsigned int irq_threshold_count;
#endif
} ____cacheline_aligned irq_cpustat_t;
上面所做的一切就是在属于本处理器的irq_cpustat_t结构中,操作__softirq_pending标志,raise_softirq()就是给__softirq_pending标志中相应的位置位,然后判断系统是否当前处于一个中断上下文中,如果不是,就立马唤醒内核软中断处理线程ksoftirqd来处理刚才触发的软中断。
ksoftirqd线程
2.6.34中ksoftirqd线程的实现是这样子的:
static int run_ksoftirqd(void * __bind_cpu)
{
/*设置进程状态为可中断*/
set_current_state(TASK_INTERRUPTIBLE);
while (!kthread_should_stop()) {/*不应该马上返回*/
preempt_disable();
/*实现软中断中一个关键数据结构是每个CPU都有的32位掩码(描述挂起的软中断),
他存放在irq_cpustat_t数据结构的__softirq_pending字段中。为了获取或设置位掩码的值,
内核使用宏local_softirq_pending,他选择cpu的软中断为掩码*/
if (!local_softirq_pending()) {/*位掩码为0,标示没有软中断*/
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();
rcu_sched_qs((long)__bind_cpu);
}
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;
}
tasklet
tasklet的本质也是软中断,软中断向量HI_SOFTIRQ和TASKLET_SOFTIRQ均是用tasklet机制来实现的。其特点就是:1、不同的tasklet代码在同一时刻可以在多个CPU上并行执行;2、与软中断相比,同一段tasklet代码在同一时刻只能在一个CPU上运行,而软中断中注册的中断服务函数在同一时刻可以在多个CPU上运行。
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;
};
next指针指向下一个tasklet,它用于将多个tasklet连接成一个单向循环链表。
state定义了tasklet的当前状态,这是一个32位无符号整数,不过目前只使用了bit 0和bit 1,bit 0为1表示tasklet已经被调度去执行了,而bit 1是专门为SMP系统设置的,为1时表示tasklet当前正在某个CPU上执行,这是为了防止多个CPU同时执行一个tasklet的情况。
enum
{
TASKLET_STATE_SCHED, /* Tasklet is scheduled for execution */
TASKLET_STATE_RUN /* Tasklet is running (SMP only) */
};
count是一个对tasklet引用的原子计数,count为0时,tasklet代码段才能执行,如果非0,则该tasklet是被禁止的,因此在执行tasklet代码之前,都必须先检查count是否为0。
软中断HI_SOFTIRQ和TASKLET_SOFTIRQ的实现差不多,只不过是优先级不一样,下面就挑高优先级的tasklet实现来走走流程:
static void tasklet_hi_action(struct softirq_action *a)
{
struct tasklet_struct *list;
/*临界区获取当前CPU的高优先级tasklet任务链表*/
local_irq_disable();
list = __get_cpu_var(tasklet_hi_vec).head;
__get_cpu_var(tasklet_hi_vec).head = NULL;
__get_cpu_var(tasklet_hi_vec).tail = &__get_cpu_var(tasklet_hi_vec).head;
local_irq_enable();
while (list) {
struct tasklet_struct *t = list;
list = list->next;
/*加锁,这个在非SMP系统中,直接通过,在SMP系统中,如果其他CPU在运行这段tasklet代码,则本CPU直接跳过*/
if (tasklet_trylock(t)) {
/*只有引用计数为0,表示本tasklet是使能的,才能继续执行*/
if (!atomic_read(&t->count)) {
/*如果到这地步了,这个tasklet的状态还是正在被调度,那就出问题了*/
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_hi_vec).tail = t;
__get_cpu_var(tasklet_hi_vec).tail = &(t->next);
/*这个很有意思,自己再重新触发HI_SOFTIRQ 软中断,这个会在内核软中断处理线程中处理的*/
__raise_softirq_irqoff(HI_SOFTIRQ);
local_irq_enable();
}
}
其他没什么好说的了。
workqueue(2.6.34)
基本术语:
- workqueue: 所有工作项(需要被执行的工作)被排列于该队列.
- worker thread: 是一个用于执行 workqueue 中各个工作项的内核线程, 当workqueue中没有工作项时, 该线程将变为 idle 状态.
- single threaded(ST): worker thread 的表现形式之一, 在系统范围内, 只有一个worker thread为workqueue 服务.
- multi threaded(MT): worker thread 的表现形式之一, 在多CPU系统上每个CPU上都有一个worker thread为 workqueue 服务.
workqueue机制在include/linux/workqueue.h和kernel/workqueue.c中定义和实现。
工作队列由workqueue_struct结构来维护,定义如下:
struct workqueue_struct {
struct cpu_workqueue_struct *cpu_wq;
struct list_head list;
const char *name;
int singlethread;
int freezeable; /* Freeze threads during suspend */
int rt;
#ifdef CONFIG_LOCKDEP
struct lockdep_map lockdep_map;
#endif
};
cpu_workqueue_struct结构是针对每个CPU定义的。对于每一个CPU,内核都为它挂接一个工作队列,这样就可以将新的工作动态放入到不同的CPU下的工作队列中去,以此体现对“负载平衡”的支持。
struct cpu_workqueue_struct {
spinlock_t lock; /*结构锁*/
struct list_head worklist; /*工作列表*/
wait_queue_head_t more_work; /*要处理的等待队列*/
struct work_struct *current_work; /*处理完毕的等待队列*/
struct workqueue_struct *wq; /*工作队列节点*/
struct task_struct *thread; /*工作者线程*/
} ____cacheline_aligned;
工作项定义
struct work_struct {
atomic_long_t data;
struct list_head entry;
work_func_t func; /*工作队列函数指针,指向具体需要处理的工作*/
#ifdef CONFIG_LOCKDEP
struct lockdep_map lockdep_map;
#endif
};
工作项的创建
静态
- DECLARE_WORK(n, f)
- DECLARE_DELAYED_WORK(n, f)
动态
- INIT_WORK(struct work_struct work, work_func_t func);
- PREPARE_WORK(struct work_struct work, work_func_t func);
- INIT_DELAYED_WORK(struct delayed_work work, work_func_t func);
- PREPARE_DELAYED_WORK(struct delayed_work work, work_func_t func);
内核默认全局工作队列 keventd_wq,位于kernel/workqueue.c
720 static struct workqueue_struct *keventd_wq __read_mostly;
1177 keventd_wq = create_workqueue("events");
工作项加入keventd_wq的接口:
int schedule_work(struct work_struct *work)
{
return queue_work(keventd_wq, work);
}
int schedule_delayed_work(struct delayed_work *dwork, unsigned long delay)
{
return queue_delayed_work(keventd_wq, dwork, delay);
}
用户自定义工作队列
创建:
- create_singlethread_workqueue(name) // 仅对应一个内核线程
- create_workqueue(name) // 对应多个内核线程, 同上文.
提交:
- int queue_work(workqueue_t *queue, work_t *work);
- int queue_delayed_work(workqueue_t *queue, work_t *work, unsigned long delay);
一个例子wq.c:
#include <linux/module.h>
#include <linux/init.h>
#include <linux/workqueue.h>
static struct workqueue_struct *queue = NULL;
static struct work_struct work;
static void work_handler(struct work_struct *data)
{
printk(KERN_ALERT "work handler for work_item in queue Test_wq \n");
/*workqueue 中的每个工作完成之后就被移除 workqueue.*/
}
static int __init wq_init(void)
{
/*创建工作队列*/
queue = create_singlethread_workqueue("Test_wq");
if (!queue)
{
goto err;
}
/*创建工作项*/
INIT_WORK(&work, work_handler);
/*挂载工作项到工作队列中*/
queue_work(queue, &work);
return 0;
err:
return -1;
}
static void __exit wq_exit(void)
{
destroy_workqueue(queue);
}
MODULE_LICENSE("GPL");
module_init(wq_init);
module_exit(wq_exit);
Makefile
obj-m := wq.o
KERNELBUILD :=/lib/modules/$(shell uname -r)/build
default:
make -C $(KERNELBUILD) M=$(shell pwd) modules
clean:
rm -rf *.o *.ko *.mod.c .*.cmd *.markers *.order *.symvers .tmp_versions
简单总结一下吧:
软中断:
1、软中断是在编译期间静态分配的。
2、最多可以有32个软中断,2.6.34用了10个。
3、软中断不会抢占另外一个软中断,唯一可以抢占软中断的是中断处理程序(ISR)。
4、可以并发运行在多个CPU上,必须设计为可重入的函数,需要使用自旋锁来保护其数据结构。
6、执行时间有:从硬件中断代码返回时、在ksoftirqd内核线程中和某些显示检查并执行软中断的代码中。
tasklet:
1、tasklet是使用两类软中断实现的:HI_SOFTIRQ和TASKLET_SOFTIRQ。
2、可以动态增加减少,没有数量限制。
3、同一类tasklet不能并发执行。
4、不同类型可以并发执行,同一类型不能并发。
5、大部分情况使用tasklet。
工作队列:
1、处于进程上下文,由内核线程去执行。
2、可以睡眠,阻塞。
通过对下半部机制的三种实现分析(softirq,tasklet,workqueue),在具体需要使用时就不再犯难了,需要睡眠,有阻塞的,只能用工作队列了,其次再选tasklet,直接使用软中断的机会比较低吧,一般都是在需要提高性能的时候才考虑了,使用软中断的重点就在于如何采取有效的措施,才能保证共享数据的安全。因为软中断在多CPU上会并发执行。