第六章 Linux内核的Softirq机制 (By 詹荣开,NUDT) Copyright © 2003 by 詹荣开 E-mail:zhanrk@sohu.com Linux-2.4.0 Version 1.0.0,2003-2-14 摘要:本文主要从内核实现的角度分析了Linux 2.4.0内核的Softirq机制。本文是为那些想要了解Linux I/O子系统的读者和Linux驱动程序开发人员而写的。 关键词:Linux、Softirq、软中断、Bottom half、设备驱动程序 申明:这份文档是按照自由软件开放源代码的精神发布的,任何人可以免费获得、使用和重新发布,但是你没有限制别人重新发布你发布内容的权利。发布本文的目的是希望它能对读者有用,但没有任何担保,甚至没有适合特定目的的隐含的担保。更详细的情况请参阅GNU通用公共许可证(GPL),以及GNU自由文档协议(GFDL)。 你应该已经和文档一起收到一份GNU通用公共许可证(GPL)的副本。如果还没有,写信给: The Free Software Foundation, Inc., 675 Mass Ave, Cambridge,MA02139, USA 欢迎各位指出文档中的错误与疑问。 前言 中断服务程序往往都是在CPU关中断的条件下执行的,以避免中断嵌套而使控制复杂化。但是CPU关中断的时间不能太长,否则容易丢失中断信号。为此,Linux将中断服务程序一分为二,各称作“Top Half”和“Bottom Half”。前者通常对时间要求较为严格,必须在中断请求发生后立即或至少在一定的时间限制内完成。因此为了保证这种处理能原子地完成,Top Half通常是在CPU关中断的条件下执行的。具体地说,Top Half的范围包括:从在IDT中登记的中断入口函数一直到驱动程序注册在中断服务队列中的ISR。而Bottom Half则是Top Half根据需要来调度执行的,这些操作允许延迟到稍后执行,它的时间要求并不严格,因此它通常是在CPU开中断的条件下执行的。 但是,Linux的这种Bottom Half(以下简称BH)机制有两个缺点,也即:(1)在任意一时刻,系统只能有一个CPU可以执行Bottom Half代码,以防止两个或多个CPU同时来执行Bottom Half函数而相互干扰。因此BH代码的执行是严格“串行化”的。(2)BH函数不允许嵌套。 这两个缺点在单CPU系统中是无关紧要的,但在SMP系统中却是非常致命的。因为BH机制的严格串行化执行显然没有充分利用SMP系统的多CPU特点。为此,Linux2.4内核在BH机制的基础上进行了扩展,这就是所谓的“软中断请求”(softirq)机制。 6.1 软中断请求机制 Linux的softirq机制是与SMP紧密不可分的。为此,整个softirq机制的设计与实现中自始自终都贯彻了一个思想:“谁触发,谁执行”(Who marks,Who runs),也即触发软中断的那个CPU负责执行它所触发的软中断,而且每个CPU都由它自己的软中断触发与控制机制。这个设计思想也使得softirq机制充分利用了SMP系统的性能和特点。 6.1.1 软中断描述符 Linux在include/linux/interrupt.h头文件中定义了数据结构softirq_action,来描述一个软中断请求,如下所示: /* softirq mask and active fields moved to irq_cpustat_t in * asm/hardirq.h to get better cache usage. KAO */ struct softirq_action { void (*action)(struct softirq_action *); void *data; }; 其中,函数指针action指向软中断请求的服务函数,而指针data则指向由服务函数自行解释的数据。 基于上述软中断描述符,Linux在kernel/softirq.c文件中定义了一个全局的softirq_vec[32]数组: static struct softirq_action softirq_vec[32] __cacheline_aligned; 在这里系统一共定义了32个软中断请求描述符。软中断向量i(0≤i≤31)所对应的软中断请求描述符就是softirq_vec[i]。这个数组是个系统全局数组,也即它被所有的CPU所共享。这里需要注意的一点是:每个CPU虽然都由它自己的触发和控制机制,并且只执行他自己所触发的软中断请求,但是各个CPU所执行的软中断服务例程却是相同的,也即都是执行softirq_vec[]数组中定义的软中断服务函数。 6.1.2 软中断触发机制 要实现“谁触发,谁执行”的思想,就必须为每个CPU都定义它自己的触发和控制变量。为此,Linux在include/asm-i386/hardirq.h头文件中定义了数据结构irq_cpustat_t来描述一个CPU的中断统计信息,其中就有用于触发和控制软中断的成员变量。数据结构irq_cpustat_t的定义如下: /* entry.S is sensitive to the offsets of these fields */ typedef struct { unsigned int __softirq_active; unsigned int __softirq_mask; unsigned int __local_irq_count; unsigned int __local_bh_count; unsigned int __syscall_count; unsigned int __nmi_count; /* arch dependent */ } ____cacheline_aligned irq_cpustat_t; 结构中每一个成员都是一个32位的无符号整数。其中__softirq_active和__softirq_mask就是用于触发和控制软中断的成员变量。 ①__softirq_active变量:32位的无符号整数,表示软中断向量0~31的状态。如果bit[i](0≤i≤31)为1,则表示软中断向量i在某个CPU上已经被触发而处于active状态;为0表示处于非活跃状态。 ②__softirq_mask变量:32位的无符号整数,软中断向量的屏蔽掩码。如果bit[i](0≤i≤31)为1,则表示使能(enable)软中断向量i,为0表示该软中断向量被禁止(disabled)。 根据系统中当前的CPU个数(由宏NR_CPUS表示),Linux在kernel/softirq.c文件中为每个CPU都定义了它自己的中断统计信息结构,如下所示: /* No separate irq_stat for s390, it is part of PSA */ #if !defined(CONFIG_ARCH_S390) irq_cpustat_t irq_stat[NR_CPUS]; #endif /* CONFIG_ARCH_S390 */ 这样,每个CPU都只操作它自己的中断统计信息结构。假设有一个编号为id的CPU,那么它只能操作它自己的中断统计信息结构irq_stat[id](0≤id≤NR_CPUS-1),从而使各CPU之间互不影响。这个数组在include/linux/irq_cpustat.h头文件中也作了原型声明。 l 触发软中断请求的操作函数 函数__cpu_raise_softirq()用于在编号为cpu的处理器上触发软中断向量nr。它通过将相应的__softirq_active成员变量中的相应位设置为1来实现软中断触发。如下所示(include/linux/interrupt.h): static inline void __cpu_raise_softirq(int cpu, int nr) { softirq_active(cpu) |= (1action(h); h ; active >>= 1; } while (active); local_irq_disable(); active = softirq_active(cpu); if ((active &= mask) != 0) goto retry; } local_bh_enable(); /* Leave with locally disabled hard irqs. It is critical to close * window for infinite recursion, while we help local bh count, * it protected us. Now we are defenceless. */ return; retry: goto restart; } 结合上述源码,我们可以看出软中断服务的执行过程如下: (1)调用宏in_interrupt()来检测当前CPU此次是否已经处于中断服务中。该宏定义在hardirq.h,请参见5.7节。 (2)调用local_bh_disable()宏将当前CPU的中断统计信息结构中的__local_bh_count成员变量加1,表示当前CPU已经处在软中断服务状态。 (3)由于接下来要读写当前CPU的中断统计信息结构中的__softirq_active变量和__softirq_mask变量,因此为了保证这一个操作过程的原子性,先用local_irq_disable()宏(实际上就是cli指令)关闭当前CPU的中断。 (4)然后,读当前CPU的__softirq_active变量值和__softirq_mask变量值。当某个软中断向量被触发时(即__softirq_active变量中的相应位被置1),只有__softirq_mask变量中的相应位也为1时,它的软中断服务函数才能得到执行。因此,需要将__softirq_active变量和__softirq_mask变量作一次“与”逻辑操作。 (5)如果active变量非0,说明需要执行软中断服务函数。因此:①先将当前CPU的__softirq_active中的相应位清零,然后用local_irq_enable()宏(实际上就是sti指令)打开当前CPU的中断。②将局部变量mask中的相应位清零,其目的是:让do_softirq()函数的这一次执行不对同一个软中断向量上的再次软中断请求进行服务,而是将它留待下一次do_softirq()执行时去服务,从而使do_sottirq()函数避免陷入无休止的软中断服务中。③用一个do{}while循环来根据active的值去执行相应的软中断服务函数。④由于接下来又要检测当前CPU的__softirq_active变量,因此再一次调用local_irq_disable()宏关闭当前CPU的中断。⑤读取当前CPU的__softirq_active变量的值,并将它与局部变量mask进行与操作,以看看是否又有其他软中断服务被触发了(比如前面所说的那种情形)。如果有的话,那就跳转到entry程序段(实际上是跳转到restart程序段)重新执行软中断服务。如果没有的话,那么此次软中断服务过程就宣告结束。 (6)最后,通过local_bh_enable()宏将当前CPU的__local_bh_count变量值减1,表示当前CPU已经离开软中断服务状态。宏local_bh_enable()也定义在include/asm-i386/softirq.h头文件中。 6.2 tasklet机制 Tasklet机制是一种较为特殊的软中断。Tasklet一词的原意是“小片任务”的意思,这里是指一小段可执行的代码,且通常以函数的形式出现。软中断向量HI_SOFTIRQ和TASKLET_SOFTIRQ均是用tasklet机制来实现的。 从某种程度上讲,tasklet机制是Linux内核对BH机制的一种扩展。在2.4内核引入了softirq机制后,原有的BH机制正是通过tasklet机制这个桥梁来纳入softirq机制的整体框架中的。正是由于这种历史的延伸关系,使得tasklet机制与一般意义上的软中断有所不同,而呈现出以下两个显著的特点: 1. 与一般的软中断不同,某一段tasklet代码在某个时刻只能在一个CPU上运行,而不像一般的软中断服务函数(即softirq_action结构中的action函数指针)那样——在同一时刻可以被多个CPU并发地执行。 2. 与BH机制不同,不同的tasklet代码在同一时刻可以在多个CPU上并发地执行,而不像BH机制那样必须严格地串行化执行(也即在同一时刻系统中只能有一个CPU执行BH函数)。 6.2.1 tasklet描述符 Linux用数据结构tasklet_struct来描述一个tasklet。该数据结构定义在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的引用计数值。NOTE!只有当count等于0时,tasklet代码段才能执行,也即此时tasklet是被使能的;如果count非零,则这个tasklet是被禁止的。任何想要执行一个tasklet代码段的人都首先必须先检查其count成员是否为0。 (4)函数指针func:指向以函数形式表现的可执行tasklet代码段。 (5)data:函数func的参数。这是一个32位的无符号整数,其具体含义可供func函数自行解释,比如将其解释成一个指向某个用户自定义数据结构的地址值。 Linux在interrupt.h头文件中又定义了两个用来定义tasklet_struct结构变量的辅助宏: #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 } 显然,从上述源代码可以看出,用DECLARE_TASKLET宏定义的tasklet在初始化时是被使能的(enabled),因为其count成员为0。而用DECLARE_TASKLET_DISABLED宏定义的tasklet在初始时是被禁止的(disabled),因为其count等于1。 6.2.2 改变一个tasklet状态的操作 在这里,tasklet状态指两个方面:(1)state成员所表示的运行状态;(2)count成员决定的使能/禁止状态。 (1)改变一个tasklet的运行状态 state成员中的bit[0]表示一个tasklet是否已被调度去等待执行,bit[1]表示一个tasklet是否正在某个CPU上执行。对于state变量中某位的改变必须是一个原子操作,因此可以用定义在include/asm/bitops.h头文件中的位操作来进行。 由于bit[1]这一位(即TASKLET_STATE_RUN)仅仅对于SMP系统才有意义,因此Linux在Interrupt.h头文件中显示地定义了对TASKLET_STATE_RUN位的操作。如下所示: #ifdef CONFIG_SMP #define tasklet_trylock(t) (!test_and_set_bit(TASKLET_STATE_RUN, &(t)->state)) #define tasklet_unlock_wait(t) while (test_bit(TASKLET_STATE_RUN, &(t)->state)) { /* NOTHING */ } #define tasklet_unlock(t) clear_bit(TASKLET_STATE_RUN, &(t)->state) #else #define tasklet_trylock(t) 1 #define tasklet_unlock_wait(t) do { } while (0) #define tasklet_unlock(t) do { } while (0) #endif 显然,在SMP系统同,tasklet_trylock()宏将把一个tasklet_struct结构变量中的state成员中的bit[1]位设置成1,同时还返回bit[1]位的非。因此,如果bit[1]位原有值为1(表示另外一个CPU正在执行这个tasklet代码),那么tasklet_trylock()宏将返回值0,也就表示上锁不成功。如果bit[1]位的原有值为0,那么tasklet_trylock()宏将返回值1,表示加锁成功。而在单CPU系统中,tasklet_trylock()宏总是返回为1。 任何想要执行某个tasklet代码的程序都必须首先调用宏tasklet_trylock()来试图对这个tasklet进行上锁(即设置TASKLET_STATE_RUN位),且只能在上锁成功的情况下才能执行这个tasklet。建议!即使你的程序只在CPU系统上运行,你也要在执行tasklet之前调用tasklet_trylock()宏,以便使你的代码获得良好可移植性。 在SMP系统中,tasklet_unlock_wait()宏将一直不停地测试TASKLET_STATE_RUN位的值,直到该位的值变为0(即一直等待到解锁),假如:CPU0正在执行tasklet A的代码,在此期间,CPU1也想执行tasklet A的代码,但CPU1发现tasklet A的TASKLET_STATE_RUN位为1,于是它就可以通过tasklet_unlock_wait()宏等待tasklet A被解锁(也即TASKLET_STATE_RUN位被清零)。在单CPU系统中,这是一个空操作。 宏tasklet_unlock()用来对一个tasklet进行解锁操作,也即将TASKLET_STATE_RUN位清零。在单CPU系统中,这是一个空操作。 (2)使能/禁止一个tasklet 使能与禁止操作往往总是成对地被调用的,tasklet_disable()函数如下(interrupt.h): static inline void tasklet_disable(struct tasklet_struct *t) { tasklet_disable_nosync(t); tasklet_unlock_wait(t); } 函数tasklet_disable_nosync()也是一个静态inline函数,它简单地通过原子操作将count成员变量的值减1。如下所示(interrupt.h): static inline void tasklet_disable_nosync(struct tasklet_struct *t) { atomic_inc(&t->count); } 函数tasklet_enable()用于使能一个tasklet,如下所示(interrupt.h): static inline void tasklet_enable(struct tasklet_struct *t) { atomic_dec(&t->count); } 6.2.3 tasklet描述符的初始化与杀死 函数tasklet_init()用来初始化一个指定的tasklet描述符,其源码如下所示(kernel/softirq.c): void tasklet_init(struct tasklet_struct *t, void (*func)(unsigned long), unsigned long data) { t->func = func; t->data = data; t->state = 0; atomic_set(&t->count, 0); } 函数tasklet_kill()用来将一个已经被调度了的tasklet杀死,即将其恢复到未调度的状态。其源码如下所示(kernel/softirq.c): void tasklet_kill(struct tasklet_struct *t) { if (in_interrupt()) printk("Attempt to kill tasklet from interrupt\n"); while (test_and_set_bit(TASKLET_STATE_SCHED, &t->state)) { current->state = TASK_RUNNING; do { current->policy |= SCHED_YIELD; schedule(); } while (test_bit(TASKLET_STATE_SCHED, &t->state)); } tasklet_unlock_wait(t); clear_bit(TASKLET_STATE_SCHED, &t->state); } 6.2.4 tasklet对列 多个tasklet可以通过tasklet描述符中的next成员指针链接成一个单向对列。为此,Linux专门在头文件include/linux/interrupt.h中定义了数据结构tasklet_head来描述一个tasklet对列的头部指针。如下所示: struct tasklet_head { struct tasklet_struct *list; } __attribute__ ((__aligned__(SMP_CACHE_BYTES))); 尽管tasklet机制是特定于软中断向量HI_SOFTIRQ和TASKLET_SOFTIRQ的一种实现,但是tasklet机制仍然属于softirq机制的整体框架范围内的,因此,它的设计与实现仍然必须坚持“谁触发,谁执行”的思想。为此,Linux为系统中的每一个CPU都定义了一个tasklet对列头部,来表示应该有各个CPU负责执行的tasklet对列。如下所示(kernel/softirq.c): struct tasklet_head tasklet_vec[NR_CPUS] __cacheline_aligned; struct tasklet_head tasklet_hi_vec[NR_CPUS] __cacheline_aligned; 其中,tasklet_vec[]数组用于软中断向量TASKLET_SOFTIRQ,而tasklet_hi_vec[]数组则用于软中断向量HI_SOFTIRQ。也即,如果CPUi(0≤i≤NR_CPUS-1)触发了软中断向量TASKLET_SOFTIRQ,那么对列tasklet_vec[i]中的每一个tasklet都将在CPUi服务于软中断向量TASKLET_SOFTIRQ时被CPUi所执行。同样地,如果CPUi(0≤i≤NR_CPUS-1)触发了软中断向量HI_SOFTIRQ,那么队列tasklet_vec[i]中的每一个tasklet都将CPUi在对软中断向量HI_SOFTIRQ进行服务时被CPUi所执行。 队列tasklet_vec[I]和tasklet_hi_vec[I]中的各个tasklet是怎样被所CPUi所执行的呢?其关键就是软中断向量TASKLET_SOFTIRQ和HI_SOFTIRQ的软中断服务程序——tasklet_action()函数和tasklet_hi_action()函数。下面我们就来分析这两个函数。 6.2.5 软中断向量TASKLET_SOFTIRQ和HI_SOFTIRQ Linux为软中断向量TASKLET_SOFTIRQ和HI_SOFTIRQ实现了专用的触发函数和软中断服务函数。其中,tasklet_schedule()函数和tasklet_hi_schedule()函数分别用来在当前CPU上触发软中断向量TASKLET_SOFTIRQ和HI_SOFTIRQ,并把指定的tasklet加入当前CPU所对应的tasklet队列中去等待执行。而tasklet_action()函数和tasklet_hi_action()函数则分别是软中断向量TASKLET_SOFTIRQ和HI_SOFTIRQ的软中断服务函数。在初始化函数softirq_init()中,这两个软中断向量对应的描述符softirq_vec[0]和softirq_vec[3]中的action函数指针就被分别初始化成指向函数tasklet_hi_action()和函数tasklet_action()。 (1)软中断向量TASKLET_SOFTIRQ的触发函数tasklet_schedule() 该函数实现在include/linux/interrupt.h头文件中,是一个inline函数。其源码如下所示: static inline void tasklet_schedule(struct tasklet_struct *t) { if (!test_and_set_bit(TASKLET_STATE_SCHED, &t->state)) { int cpu = smp_processor_id(); unsigned long flags; local_irq_save(flags); t->next = tasklet_vec[cpu].list; tasklet_vec[cpu].list = t; __cpu_raise_softirq(cpu, 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的中断。 (2)软中断向量TASKLET_SOFTIRQ的服务程序tasklet_action() 函数tasklet_action()是tasklet机制与软中断向量TASKLET_SOFTIRQ的联系纽带。正是该函数将当前CPU的tasklet队列中的各个tasklet放到当前CPU上来执行的。该函数实现在kernel/softirq.c文件中,其源代码如下: static void tasklet_action(struct softirq_action *a) { int cpu = smp_processor_id(); struct tasklet_struct *list; local_irq_disable(); list = tasklet_vec[cpu].list; tasklet_vec[cpu].list = NULL; local_irq_enable(); while (list != NULL) { struct tasklet_struct *t = list; list = list->next; if (tasklet_trylock(t)) { if (atomic_read(&t->count) == 0) { clear_bit(TASKLET_STATE_SCHED, &t->state); t->func(t->data); /* * talklet_trylock() uses test_and_set_bit that imply * an mb when it returns zero, thus we need the explicit * mb only here: while closing the critical section. */ #ifdef CONFIG_SMP smp_mb__before_clear_bit(); #endif tasklet_unlock(t); continue; } tasklet_unlock(t); } local_irq_disable(); t->next = tasklet_vec[cpu].list; tasklet_vec[cpu].list = t; __cpu_raise_softirq(cpu, 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循环继续遍历队列。 (3)软中断向量HI_SOFTIRQ的触发函数tasklet_hi_schedule() 该函数与tasklet_schedule()几乎相同,其源码如下(include/linux/interrupt.h): static inline void tasklet_hi_schedule(struct tasklet_struct *t) { if (!test_and_set_bit(TASKLET_STATE_SCHED, &t->state)) { int cpu = smp_processor_id(); unsigned long flags; local_irq_save(flags); t->next = tasklet_hi_vec[cpu].list; tasklet_hi_vec[cpu].list = t; __cpu_raise_softirq(cpu, HI_SOFTIRQ); local_irq_restore(flags); } } (4)软中断向量HI_SOFTIRQ的服务函数tasklet_hi_action() 该函数与tasklet_action()函数几乎相同,其源码如下(kernel/softirq.c): static void tasklet_hi_action(struct softirq_action *a) { int cpu = smp_processor_id(); struct tasklet_struct *list; local_irq_disable(); list = tasklet_hi_vec[cpu].list; tasklet_hi_vec[cpu].list = NULL; local_irq_enable(); while (list != NULL) { struct tasklet_struct *t = list; list = list->next; if (tasklet_trylock(t)) { if (atomic_read(&t->count) == 0) { clear_bit(TASKLET_STATE_SCHED, &t->state); t->func(t->data); tasklet_unlock(t); continue; } tasklet_unlock(t); } local_irq_disable(); t->next = tasklet_hi_vec[cpu].list; tasklet_hi_vec[cpu].list = t; __cpu_raise_softirq(cpu, HI_SOFTIRQ); local_irq_enable(); } } 6.3 Bottom Half机制 Bottom Half机制在新的softirq机制中被保留下来,并作为softirq框架的一部分。其实现也似乎更为复杂些,因为它是通过tasklet机制这个中介桥梁来纳入softirq框架中的。实际上,软中断向量HI_SOFTIRQ是内核专用于执行BH函数的。 6.3.1 数据结构的定义 原有的32个BH函数指针被保留,定义在kernel/softirq.c文件中: static void (*bh_base[32])(void); 但是,每个BH函数都对应有一个tasklet,并由tasklet的可执行函数func来负责调用相应的bh函数(func函数的参数指定调用哪一个BH函数)。与32个BH函数指针相对应的tasklet的定义如下所示(kernel/softirq.c): struct tasklet_struct bh_task_vec[32]; 上述tasklet数组使系统全局的,它对所有的CPU均可见。由于在某一个时刻只能有一个CPU在执行BH函数,因此定义一个全局的自旋锁来保护BH函数,如下所示(kernel/softirq.c): spinlock_t global_bh_lock = SPIN_LOCK_UNLOCKED; 6.3.2 初始化 在softirq机制的初始化函数softirq_init()中将bh_task_vec[32]数组中的每一个tasklet中的func函数指针都设置为指向同一个函数bh_action,而data成员(也即func函数的调用参数)则被设置成该tasklet在数组中的索引值,如下所示: void __init softirq_init() { …… for (i=0; isync)) { unsigned long flags; spin_lock_irqsave(&tqueue_lock, flags); list_add_tail(&bh_pointer->list, bh_list); spin_unlock_irqrestore(&tqueue_lock, flags); ret = 1; } return ret; } 6.4.3 运行任务队列 函数run_task_queue()用于实现指定的任务队列。它只有一个参数:指针list——指向待运行的任务队列头部task_queue结构变量。该函数实现在tqueue.h头文件中: static inline void run_task_queue(task_queue *list) { if (TQ_ACTIVE(*list)) __run_task_queue(list); } 显然,函数首先调用宏TQ_ACTIVE()来判断参数list指定的待运行任务队列是否为空。如果不为空,则调用__run_task_queue()函数来实际运行这个有效的任务队列。 函数__run_task_queue()实现在kernel/softirq.c文件中。该函数将依次遍历任务队列中的每一个元数,并调用执行每一个元数的可执行函数。其源码如下: void __run_task_queue(task_queue *list) { struct list_head head, *next; unsigned long flags; spin_lock_irqsave(&tqueue_lock, flags); list_add(&head, list); list_del_init(list); spin_unlock_irqrestore(&tqueue_lock, flags); next = head.next; while (next != &head) { void (*f) (void *); struct tq_struct *p; void *data; p = list_entry(next, struct tq_struct, list); next = next->next; f = p->routine; data = p->data; wmb(); p->sync = 0; if (f) f(data); } } 对该函数的注释如下: (1)首先,用一个局部的表头head来代替参数list所指向的表头。这是因为:在__run_task_queue()函数的运行期间可能还会有新的任务加入到list任务队列中来,但是__run_task_queue()函数显然不想陷入无休止的不断增加的任务处理中,因此它用局部的表头head来代替参数list所指向的表头,以使要执行的任务个数固定化。为此:①先对全局的自旋锁tqueue_lock进行加锁,以实现对任务队列的互斥访问;②将局部的表头head加在表头(*list)和第一个元数之间。③将(*list)表头从队列中去除,并将其初始化为空。④解除自旋锁tqueue_lock。 (2)接下来,用一个while循环来遍历整个队列head,并调用执行每一个队列元素中的函数。注意!任务队列是一个双向循环队列。 6.4.4 内核预定义的任务队列 Bottom Half机制与任务队列是紧密相连的。大多数BH函数都是通过调用run_task_queue()函数来执行某个预定义好的任务队列。最常见的内核预定义任务队列有: l tq_timer:对应于TQUEUE_BH。 l tq_immediate:对应于IMMEDIATE_BH。 l tq_disk:用于块设备任务。 任务队列tq_timer和tq_immediate都定义在kernel/timer.c文件中,如下所示: DECLARE_TASK_QUEUE(tq_timer); DECLARE_TASK_QUEUE(tq_immediate); BH向量TQUEUE_BH和IMMEDIATE_BH的BH函数分别是:queue_bh()函数和immediate_bh()函数,它们都仅仅是简单地调用run_task_queue()函数来分别运行任务队列tq_timer和tq_immediate,如下所示(kernel/timer.c): void tqueue_bh(void) { run_task_queue(&tq_timer); } void immediate_bh(void) { run_task_queue(&tq_immediate); } 附件: www.pointopen.com/resources/kernel/kernel_softirq.pdf |
|小黑屋|最新主题|手机版|微赢网络技术论坛 ( 苏ICP备08020429号 )
GMT+8, 2024-9-29 23:28 , Processed in 0.173610 second(s), 12 queries , Gzip On, MemCache On.
Powered by Discuz! X3.5
© 2001-2023 Discuz! Team.