一般在有中断的系统中,中断ISR的设计应该尽可能的小,并且在处理中断时,不允许中断ISR再被其他后来的中断打断,也就是避免中断嵌套。现在大多数系统都是不支持中断嵌套的,Linux的实现就是个典型。防止中断嵌套的做法就是处理一个中断时,CPU执行关中断,不接收其他中断。但是这种关中断状态又不能持续太久,关中断时间过长,又会导致后续中断丢失,因此Linux中,将中断处理程序分为两个部分,即上半部和下半部,上半部通常执行时间很短,并且大多与硬件密切相关,所以需要在关中断的环境中运行,而下半部则处理比较费时的一些操作,这部分是在开中断中执行。一般称呼上半部为硬中断,下半部为软中断,Linux的设计中,软中断只能被硬中断打断。
2.6内核中,软中断的设计始终贯穿一个思想:“谁触发,谁执行(Who marks, who runs)”,所以能有效利用SMP系统的性能,提升了处理效率。2.4以及之前的版本使用的是一种叫Bottom Half的机制来实现下半部,它的致命缺点就是系统中一次能有一个CPU可以执行BH代码,这样在单核CPU中是没有问题的,但在SMP系统中,就严重损失硬件性能了。
软中断请求描述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 };
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)
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;
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的本质也是软中断,软中断向量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机制在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 };
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 };
工作项的创建
静态
动态
内核默认全局工作队列 keventd_wq,位于kernel/workqueue.c
720 static struct workqueue_struct *keventd_wq __read_mostly; 1177 keventd_wq = create_workqueue("events");
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); }
一个例子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);
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内核线程中和某些显示检查并执行软中断的代码中。
1、tasklet是使用两类软中断实现的:HI_SOFTIRQ和TASKLET_SOFTIRQ。
2、可以动态增加减少,没有数量限制。
3、同一类tasklet不能并发执行。
4、不同类型可以并发执行,同一类型不能并发。
5、大部分情况使用tasklet。
1、处于进程上下文,由内核线程去执行。
2、可以睡眠,阻塞。
通过对下半部机制的三种实现分析(softirq,tasklet,workqueue),在具体需要使用时就不再犯难了,需要睡眠,有阻塞的,只能用工作队列了,其次再选tasklet,直接使用软中断的机会比较低吧,一般都是在需要提高性能的时候才考虑了,使用软中断的重点就在于如何采取有效的措施,才能保证共享数据的安全。因为软中断在多CPU上会并发执行。