一个中断处理程序的一个或几个中断服务例程在执行结束之前,内核处于中断环境中,当前CPU不再响应同类型的中断,如果不允许中断嵌套,则CPU需要屏蔽掉所有中断。也就是说,一个CPU忙于服务于一个中断事件时,就不能处理其他中断,同时CPU不能执行其他进程,即不能被抢占,这种情况下,如果在中断服务例程中消耗的时间过多,就会对性能产生潜在的影响。
一般情况下,一个中断事件所触发的动作可能需要占用很多CPU时间,但通常其中多数内容都是可以等待的。为了保证对硬件保持较短的响应时间,在一个中断事件到来时,可以先抢占CPU,将必须尽快处理的事情做完,然后释放CPU,在稍后的某一时刻,当内核不需要再做一些紧迫之事,再处理中断事件剩下的事情。
这些可以延后的处理程序被称为中断下半部。例如网卡收包的处理,当CPU收到一个收包中断时,需要把数据包从DMA中搬运到内存中内核预先设置好的位置,并设置某些标记来通知内核有数据包到来,然后内核分配一个skb缓冲区,将数据内容拷贝到缓冲区里,并初始化一个skb实例,然后将数据包交给上层协议栈处理,这个过程非常复杂,需要耗费大量CPU时间,不可能都在中断处理程序中完成。有了中断下半部机制,在中断处理程序中只需将数据包放到内存并设好标记,这可以很快完成,而剩下实际的数据包处理过程则放到下半部中去执行。
内核使用软中断(softirq)和微任务(tasklet)两种可延迟函数来实现中断下半部机制,他们是一种非紧迫、可中断的内核函数,因为他们在执行过程是开中断的。
tasklet是在软中断之上实现的,所以在内核代码中“软中断”通常表示可延迟函数的所有种类。另外一个被广泛使用的术语“中断上下文”表示内核当前正在执行一个中断处理程序或一个可延迟函数。
内核2.6.31中使用有限个软中断,所有的软中断类型定义如下。
/*PLEASE, avoid to allocate new softirqs, if you need not _really_ high frequencythreaded job scheduling. For almost all the purposes taskletsare more than enough. F.e. all serial device BHs et al.should be converted to tasklets, not to softirqs. */ enum { HI_SOFTIRQ=0, TIMER_SOFTIRQ, NET_TX_SOFTIRQ, NET_RX_SOFTIRQ, BLOCK_SOFTIRQ, TASKLET_SOFTIRQ, SCHED_SOFTIRQ, HRTIMER_SOFTIRQ, RCU_SOFTIRQ, /*Preferable RCU should always be the last softirq */ NR_SOFTIRQS };
内核中共有NR_SOFTIRQS种软中断,标号从0到NR_SOFTIRQS-1,优先级由高到低,即HI_SOFTIRQ的优先级最高,在执行软中断处理函数时,将按照优先级的顺序来执行。
在每个进程的thread_info结构中,都有一个抢占计数器preempt_count。
struct thread_info { …… __u32 cpu; /* current CPU */ int preempt_count; /* 0 => preemptable */ …… };
在thread_info结构体中,preempt_count成员用来跟踪内核抢占和内核控制路径的嵌套,这个整型数包含三个计数器和两个标记位:
每个进程都有一个thread_info结构,所以每个进程都有自己的preempt_count,通过current_thread_info()->preempt_count可以获取该计数器。
每个CPU都维护一个全局的irq_cpustat_t结构体,在mips中该结构体只有一个成员:挂起的软中断的掩码,它是32位整型,也就是说系统最多支持32个软中断,现有的软中断在上面已列出。
typedef struct { unsigned int __softirq_pending; }____cacheline_aligned irq_cpustat_t; irq_cpustat_t irq_stat[NR_CPUS] ____cacheline_aligned;
通过查看__softirq_pending成员的值(每个bit位对应一个软中断号)就可以知道哪些软中断正等待被处理,函数接口为local_softirq_pending()。
#define__IRQ_STAT(cpu, member) (irq_stat[cpu].member) #define local_softirq_pending() \ __IRQ_STAT(smp_processor_id(),__softirq_pending)
注册软中断:
所有软中断的处理函数都放到一个全局数组softirq_vec中,
static struct softirq_action softirq_vec[NR_SOFTIRQS]__cacheline_aligned_in_smp;
注册软中断处理函数的接口为open_softirq():
void open_softirq(int nr, void (*action)(struct softirq_action *)) { softirq_vec[nr].action = action; }
该函数接受两个参数:软中断类型的标号nr和将要注册的处理函数action。从第二个参数action可以看出,注册的处理函数需要一个参数:指向特定类型软中断的struct softirq_action实例,不过在处理函数中一般不会使用这个参数。
激活软中断:
想要执行某个软中断时,需要激活该软中断,让相应处理函数函数后续得到执行。raise_softirq()用来激活软中断,它接受软中断类型标号nr作为参数。
void raise_softirq(unsigned int nr) { unsigned long flags; local_irq_save(flags); //保存当前中断状态到flags并关中断。 raise_softirq_irqoff(nr); local_irq_restore(flags); //从flags中恢复之前的中断状态 }
可见该函数主体部分需要在关中断环境下进行。
raise_softirq_irqoff()做的事情:
1. 将当前CPU的irq_stat.__softirq_pending的第nr位置为1,即挂起该软中断(挂起即是等待稍后被执行)。
2. 如果当前处在中断环境(preempt_count的softirq/hardirq/nmi三个计数器任何一个不为0)中,就直接返回,因为这时可能已经在中断上下文中调用了raise_softirq,或者当前禁用了软中断。不过这时已经设置了pending位,下次唤醒软中断时就会执行到。
3. 第2步如果为假,则唤醒本地CPU的ksoftirqd内核线程来处理挂起的软中断(将ksoftirqd对应的task_struct加入可运行队列等待被调度)。
ksoftirqd内核线程:
执行软中断处理函数的实际工作交给了ksoftirqd内核线程,每个CPU都有一个ksoftirqd线程。其定义如下:
static DEFINE_PER_CPU(struct task_struct *, ksoftirqd);
即定义了一个per-cpu的task_struct指针。创建ksoftirqd线程的工作在cpu_callback()中完成:
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,名称为"ksoftirqd/%d"*/ p = kthread_create(ksoftirqd, hcpu,"ksoftirqd/%d", hotcpu); if (IS_ERR(p)) { printk("ksoftirqd for %ifailed\n", hotcpu); return NOTIFY_BAD; } /* 将p的task_struct结构绑定到当前CPU */ kthread_bind(p, hotcpu); /* 将线程p赋值给per-cpu变量ksoftirqd */ per_cpu(ksoftirqd,hotcpu) = p; …… }
上述代码创建了per-cpu线程"ksoftirqd/%d",其处理函数为ksoftirqd(),线程名中%d为CPU的逻辑编号。所以,前面讲到的raise_softirq()函数的工作就是尝试唤醒ksoftirqd内核线程并执行ksoftirqd()函数,它的参数为当前CPU的逻辑编号(单CPU系统编号为0)。
static int ksoftirqd(void * __bind_cpu) { set_current_state(TASK_INTERRUPTIBLE); while (!kthread_should_stop()) { preempt_disable(); //关闭抢占 /* 如果没有pending的软中断,则放弃CPU */ if (!local_softirq_pending()) { preempt_enable_no_resched(); schedule(); preempt_disable(); } __set_current_state(TASK_RUNNING); /* 如果有未决软中断,则进行do_softirq() */ while (local_softirq_pending()) { /* Preempt disable stops cpu goingoffline. 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(); /* 检查是否需要re-schedule */ preempt_enable_no_resched(); cond_resched(); preempt_disable(); rcu_qsctr_inc((long)__bind_cpu); } preempt_enable();//打开抢占 set_current_state(TASK_INTERRUPTIBLE); } __set_current_state(TASK_RUNNING); return 0; wait_to_die: …… return 0; }
处理软中断的任务在do_softirq()函数中进行,它调用了__do_softirq()。
asmlinkage void __do_softirq(void) { struct softirq_action *h; __u32 pending; /* 限制每次处理软中断的数量 */ int max_restart = MAX_SOFTIRQ_RESTART; int cpu; /* 获取未决的软中断,赋值给临时变量pending */ pending = local_softirq_pending(); account_system_vtime(current); /* 防止软中断重入 */ __local_bh_disable((unsignedlong)__builtin_return_address(0)); lockdep_softirq_enter(); cpu = smp_processor_id(); restart: /* 重置未决软中断位图,以便可以激活新的软中断 */ 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, enteredsoftirq %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_qsctr_inc(cpu); } h++; pending >>= 1; } while (pending); local_irq_disable(); /* 重新获取未决软中断 */ pending = local_softirq_pending(); /* 如果还有未决软中断并且restart的次数没超过限制,则继续处理 软中断*/ if (pending && --max_restart) goto restart; /* 如果restart次数超过限制,但仍有未决软中断,则重新唤醒ksoftirqd */ if (pending) wakeup_softirqd(); lockdep_softirq_exit(); account_system_vtime(current); _local_bh_enable(); }
__do_softirq()函数的主要工作为:
1. 获得当前未决的软中断位图和softirq_vec数组,二者一一对应,例如位图的bit0位为1,则说明软中断HI_SOFTIRQ 待处理,即softirq_vec[HI_SOFTIRQ]->action()函数需要执行。
2. 打开硬件中断,从软中断位图的bit0开始(即从优先级由高到低)检查被置为1的位, 并依次执行相应的处理函数。
3. 如果已经处理的软中断又被激活,则__do_softirq()重新调用wakeup_softirqd()唤醒ksoftirqd线程,而不是直接判断__softirq_pending的值来处理软中断,这样做是为了防止高频率的软中断导致其他任务无法得到执行。因为内核线程优先级较低,所以如果有用户进程等待执行,则wakeup_softirqd()不会使ksoftirqd立即被唤醒,但如果机器空闲,挂起的软中断很快就可以被执行。
由第2步可知,在软中断处理过程中是开中断的,所以如果这时产生了一个硬件中断,则系统会先处理硬件中断,这样,就不用担心软中断进行复杂的处理导致长时间占用CPU了。
在软中断的处理函数中,HI_SOFTIRQ 和TASKLET_SOFTIRQ两种tasklet对应的处理函数分别为tasklet_hi_action ()和tasklet_action(),他们是在softirq_init()中注册到softirq_vec数组中的。
open_softirq(TASKLET_SOFTIRQ,tasklet_action); open_softirq(HI_SOFTIRQ,tasklet_hi_action);
所有已注册的tasklet也都存放在per-cpu的链表中,HI_SOFTIRQ 和TASKLET_SOFTIRQ 两种tasklet链表的链表头分别记录在tasklet_hi_vec和tasklet_vec 中,用来找到对应链表。链表元素为struct tasklet_struct类型。
static DEFINE_PER_CPU(struct tasklet_head, tasklet_vec); static DEFINE_PER_CPU(struct tasklet_head, tasklet_hi_vec); struct tasklet_struct { struct tasklet_struct *next; unsigned long state; atomic_t count; void (*func)(unsigned long); unsigned long data; };
tasklet是在软中断之上实现的,二者区别为:
软中断:编译时静态分配,同一类型的软中断可以并发运行在多个CPU,所以处理函数必须是可重入的且必须使用自旋锁保护其数据结构。
tasklet:分配和初始化可以在运行时进行(如安装内核模块),相同类型的tasklet总是被串行的执行,即不能在两个CPU上同时运行相同类型的tasklet,所以tasklet函数不必是可重入的。
几个tasklet可以同时与一个软中断号相关联,各自执行自己的函数,开发者可以在内核运行过程中注册自己的tasklet,而不需要预先定义好所有的tasklet类型。
注册一个tasklet:
tasklet_init(structtasklet_struct *t, void (*func)(unsigned long), unsigned long data);
第一个参数t是一个指向tasklet结构体的指针,第二个参数给t->func赋值,第三个参数是要传给func的参数。
也可以方便的使用宏DECLARE_TASKLET(name, func, data)来注册tasklet。
激活tasklet:即把tasklet加入到处理列表中,两种tasklet分别用下面的两个函数激活:
tasklet_schedule(struct tasklet_struct *t);
tasklet_hi_schedule(struct tasklet_struct *t);
两种tasklet都接收一个tasklet_struct参数,不需要软中断号,因为是固定的(HI_SOFTIRQ或TASKLET_SOFTIRQ)。对比一下激活softirq的函数:
raise_softirq(unsigned int nr);
激活softirq要传入nr,不需要传入其他内容,因为nr和处理函数是一一对应并且是预先定义好的而不是tasklet的一对多的关系。
tasklet_schedule和tasklet_hi_schedule函数做的事情基本相同:
1. 如果待激活的tasklet对象的状态已经是TASKLET_STATE_SCHED了,则已经被激活了,所以直接返回。
2. 将tasklet对象的状态的TASKLET_STATE_SCHED标记标记位置位,将tasklet对象添加到tasklet_hi_vec或tasklet_vec链表的末尾,并激活HI_SOFTIRQ或TASKLET_SOFTIRQ软中断。
tasklet_schedule()和tasklet_hi_schedule()激活tasklet的区别就是前者激活的tasklet处于软中断优先级最低的位置,其处理函数要等一段时间才能执行,后者激活的tasklet处于软中断优先级最高的位置,在激活后很快就可以看到其处理函数被执行。
还有一个函数tasklet_hi_schedule_first()是将tasklet对象添加到链表的开头。
先要说一下struct tasklet_struct结构体的state和count字段:
state字段有两个标记位:
TASKLET_STATE_SCHED:该标记被设置时,表示tasklet是挂起的(被tasklet_schedule或tasklet_hi_schedule过),即被插入到了tasklet_hi_vec或tasklet_vec链表中。
TASKLET_STATE_RUN:该标记被设置时,表示tasklet正在被执行。在单处理器系统上不使用这个标志。
count:标识相应tasklet对象是否被禁止,count的值为0时表示tasklet可以正常激活并执行。有时可能想禁止一个tasklet,就可以用tasklet_disable_nosync()或tasklet_disable ()函数增加count的值,两个函数的区别为:后者必须要等到tasklet已经运行的实例结束后(state的TASKLET_STATE_RUN标记位清零)才返回。重新使能一个tasklet使用tasklet_enable()函数。
处理tasklet:
我们知道tasklet是在软中断基础上实现的,在激活tasklet之后,ksoftirqd内核线程处理软中断的时候就会去调用tasklet_action()和tasklet_hi_action()函数去处理挂起的tasklet实例。两个函数的实现基本大同小异,我们只看一下tasklet_action()的实现:
static void tasklet_action(struct softirq_action *a) { struct tasklet_struct *list; local_irq_disable(); /* 从tasklet_vec列表中取出列表头 */ list = __get_cpu_var(tasklet_vec).head; /* 把tasklet_vec列表清空,即*tail = head = NULL */ __get_cpu_var(tasklet_vec).head = NULL; __get_cpu_var(tasklet_vec).tail =&__get_cpu_var(tasklet_vec).head; local_irq_enable(); /* 遍历tasklet_vec列表*/ while (list) { /* 获取一个tasklet_struct实例*/ struct tasklet_struct *t = list; list = list->next; /* 如果tasklet的state已经是RUN了,比如有其他CPU在用,则跳过该tasklet实例 */ if (tasklet_trylock(t)) { /* 如果tasklet的count不为0,即被disable了,则跳过该tasklet实例*/ if (!atomic_read(&t->count)) { if (!test_and_clear_bit(TASKLET_STATE_SCHED,&t->state)) BUG(); /* 执行tasklet的处理函数 */ t->func(t->data); tasklet_unlock(t); /* 清掉TASKLET_STATE_RUN标记 */ continue; //接着取list中的下一个tasklet。 } tasklet_unlock(t); /* 清掉TASKLET_STATE_RUN标记 */ } /* 如果上面因为state或count而没有处理tasklet,则把这个未处理的tasklet节点重新放回tasklet_vec列表中。 */ local_irq_disable(); /* 放到末尾,tail指向最后一个节点的next */ t->next = NULL; *__get_cpu_var(tasklet_vec).tail = t; __get_cpu_var(tasklet_vec).tail =&(t->next); /* 重新放回列表后,将相应软中断的softirq_pending位置1,下次处理的时候就可以看到了 */ __raise_softirq_irqoff(TASKLET_SOFTIRQ); local_irq_enable(); } }
tasklet_action()函数的流程如下:
1. 先把tasklet_vec链表赋值给一个临时变量list,然后清空链表,使tasklet可以重新被激活。
2. 从list中获取一个tasket,如果state的TASKLET_STATE_RUN被置位或count!=0,即tasklet实例的func函数可能在其他CPU上正在被执行,或者被禁用了,则跳到第4步。
3.如果state的TASKLET_STATE_SCHED被置位,说明可以被执行了,将state的TASKLET_STATE_RUN置位以防止其他CPU尝试执行,如果检查count值发现没有被禁用,就清掉TASKLET_STATE_SCHED标记以允许tasklet被重新调度,接着开始执行func函数,执行完后将state的TASKLET_STATE_RUN位清除来允许其他CPU执行此tasklet。
4. 如果第2步中tasklet的func因为state或count没有被执行,将tasklet重新加入到链表末尾,激活TASKLET_SOFTIRQ软中断,如果上面由于tasklet在其他CPU上执行,通过重新激活,可以达到延迟执行的效果,或者响应重新使能的tasket。
5. 获取list中的下一个tasklet,继续2-4步,直到结束遍历。
tasklet_hi_action()和tasklet_action()的过程相同,只是遍历的链表不同。
本文一开始提到过“中断上下文”表示内核当前正在执行一个中断处理程序或一个可延迟函数。
为了保证中断上下文中的内容可以尽快执行,在中断上下文中不允许睡眠,因为睡眠就意味着可能发生进程切换。在schedule()函数中也会做这样的判断,如果在schedule()的时候正处于中断上下文,会引发一个错误。
在需要处理软中断时,有两种方式可以执行软中断处理函数,一个是硬件中断返回时检查是否有挂起的软中断,一个是ksoftirqd自身调度执行软中断。前者是在中断上下文中执行的,后者是在进程上下文执行的。所以在do_softirq中的任何代码,都必须考虑中断上下文中运行时可能会出现的问题。所以在软中断(包括tasklet)处理函数中也是不允许睡眠的。
在进程的preempt_count中,硬件中断计数器、软中断计数器和nmi标记共同标识了当前是否处在中断上下文中。
通过local_irq_save()或local_irq_disable()禁止中断的结果为关闭了内核的全局中断,通过irqs_disabled()可以判断当前是否关闭了全局中断。但是这样禁止中断并没有操作preemp_count,所以对in_interrupt(),in_irq(), in_nmi()没有影响。因此,在local_irq_save()之后进行睡眠,内核仍然会正常运行,当然不建议这样做。
由上可知,执行可睡眠(阻塞)函数的唯一方式是在进程上下文中运行。使用工作队列也可以使内核函数被激活并在稍后由内核中一个特定的线程来执行,因此工作队列可以实现任务的延迟执行,但是它和可延迟函数又有区别:可延迟函数运行在中断上下文,而工作队列中的函数是由内核线程来执行的,因此工作在进程上下文。这样一来,工作列队中的任务可以被调度和睡眠。
每个工作队列都通过一个线程来执行,内核中有一个预定义的工作队列keventd_wq,对应的线程名为“[events/n]”,每创建一个工作队列都会在系统中每个CPU上都创建一个相应的线程,系统中所有的CPU都可以执行挂载到队列上的函数。
使用工作队列非常简单,只需将需要稍后执行的函数插入工作队列即可。如果使用预定义的工作队列keventd_wq,则只需两步:
1. 初始化一个work_struct结构ws,并指定将要执行的函数func:
static struct work_struct ws;
INIT_WORK(&ws, fun);
注意,其中func函数的原型为:
typedef void (*work_func_t)(structwork_struct *work);
2. 将ws添加到工作队列中等待执行:
schedule_work(&ws);
之后内核线程[events/n]便会执行到该函数。如果需要不断的执行func,可以在func函数的末尾重新调用schedule_work(&ws)。
内核还提供了schedule_delayed_work()函数可以让函数在指定时间之后执行,使用方法为:
1. 初始化一个delayed_work结构ws,并指定将要执行的函数func:
static struct delayed_work dw;
INIT_DELAYED_WORK (&dw, func);
2. 将dw添加到工作队列中等待执行:
schedule_delayed_work(&dw, HZ /10);
第二个参数为需要等待的jiffies数。
在一个函数被添加到工作队列中,可以通过cancel_work_sync(work)或cancel_delayed_work_sync(dwork)来删除它,但如果这时函数已经在执行,这两个cancel函数会阻塞直到函数执行完毕。
如果需要等待某个函数执行完成,例如卸载一个模块时必须确保与其相关的工作队列中的函数都被执行完,可以使用flush_work(work),该函数会阻塞直到挂起的函数执行完毕,如果需要等待工作队列keventd_wq上所有函数执行完,可以使用flush_scheduled_work()函数。但是这两个flush函数不会等待调用flush_work(work)或flush_scheduled_work()之后被重新加入工作队列的挂起函数。
在定义work_struct或delayed_work结构时,除了可以用INIT_WORK()和INIT_DELAYED_WORK(),还可以使用DECLARE_WORK()和DECLARE_DELAYED_WORK()两个宏来静态的定义。如:
static DECLARE_WORK(button_work, do_button_work);
上面的函数都是针对内核预定义的工作队列keventd_wq。当函数很少被调用时,预定义的工作队列节省了系统资源,另一方面,由于工作队列链表中的挂起函数是在每个CPU上以串行的方式执行的,所以不应该使预定义工作队列中执行的函数长时间处于阻塞状态,否则会影响工作队列中其他函数。
用户可以定义自己的工作队列,方法也很简单:
1. 创建自定义工作队列:
static struct workqueue_struct * wq;
wq = create_workqueue("new_wq");
其中参数“new_wq”是工作队列的名字。创建成功后,会创建出per-cpu的内核线程[new_wq/n]。
2. 初始化一个work_struct结构ws,并指定将要执行的函数func:
INIT_WORK(&ws,func);
3. 将ws添加到工作队列中等待执行:
queue_work(wq,&ws);
同样可以指定延迟执行的时间,初始化接口为INIT_DELAYED_WORK(dw, func),将待执行函数添加到工作队列的接口为queue_delayed_work(wq, dw, delay)。
flush工作队列的接口为flush_workqueue(wq)。
我们还可以销毁自定义的工作队列:destroy_workqueue(wq)。当我们需要延迟执行一个任务时,可以使用下半部机制,如果需要比较精确的延迟,可以使用可指定延迟的工作队列,当然也可以使用内核定时器。
如果函数中需要申请某些资源并且可能导致阻塞,比如等待信号量或执行阻塞I/O操作,那就要用工作队列。
内核中不建议新增软中断了,所以如果可延迟函数不需要睡眠,就尽量使用tasklet来实现,并且tasklet在多处理器中不必实现为可重入函数,降低了实现难度。