一、tasklet概述
tasklet是利用软中断实现的一种下半部机制。我们之前提到过,它和进程没有任何关系。tasklet和软中断在本质上很相似,行为表现也相近,但是,
它的接口更简单,锁保护也要求较低
选择到底是用软中断还tasklet其实很简单:通常你应该用tasklet。就像我们在前面看到的,
软中断的使用者屈指可数。它只在那些执行频率很高和连续性要求很高的情况下才需要使用。而tasklet却有更广泛的用途。大多数情况下用tasklet效果都不错,而且它们还非常容易使用
二、tasklet的实现
因为tasklet是通过软中断实现的,
所以它们本身也是软中断
前面讨论过了,
tasklet由两类软中断代表:HI_SOFTIRQ和TASKLET_SOFTIRQ。这两者之间唯一的实际区别在于,HI_ SOFTIRQ类型的软中断
先于TASKLET_SOFTIRQ类型的软中断执行(参阅前一篇文章:https://www.daimajiaoliu.com/series/linux_kernel/4870ca9369003f4)
①tasklet结构体(struct tasklet_struct)
tasklet由tasklet_struct结构表示。
**每个结构体单独代表一个tasklet,**它在<linux/interrupt.h>中定义为:
**func:**是tasklet的处理程序(像软中断中的action一样)
**data:**是func函数的唯一参数
**state:**只能在0、TASKLET_STATE_SCHED、TASKLET_STATE_RUN之间取值
TASKLET_STATE_SCHED:表明tasklet已被调度,正准备投入运行
* TASKLET_STATE_RUN:表明该tasklet智能在多处理器的系统上才会作为一种优化来使用,单处理器系统任何时候都清楚单个tasklet是不是正在运行(它要么就是当前正在执行的代码,要么不是)
**count:**是tasklet的引用计数器。如果count不为0,则tasklet被禁止,不允许执行;如果count为0,tasklet才被激活,并且在被设置为挂起状态时,该tasklet才能够执行
②调度tasklet(tasklet_schedule()、tasklet_hi_schedule()、tasklet_action()、tasklet_hi_action())
已调度的tasklet(等同于被触发的软中断)
** 存放在两个单处理器数据结构:**
tasklet_vec(普通tasklet)
tasklet_hi_vec(高优先级的tasklet)
这两个数据结构都是由tasklet_struct结构体构成的链表。链表中的每个tasklet_struct代表一个不同的tasklet
tasklet
**由tasklet_schedule()和tasklet_hi_schedule()函数进行调度,**它们接受一个指向tasklet_ struct结构的指针作为参数。两个函数非常类似(区别在于一个使用TASKLET_SOFTIRQ而另一 个用HI_SOFTIRQ)。在接下来的内容中我们将仔细研究怎么编写和使用tasklets。现在,让我们先来考察一下tasklet_schedule()的细节:
tasklet_schedule()的执行步骤:
1.检查tasklet的状态是否为TASKLET_STATE_SCHED。如果是,说明tasklet已经被调度过了,该函数立即返回
2.调用_tasklety_schedule()
3.保存中断状态,然后禁止本地中断。在我们执行tasklet代码时,这么做能够保证党tasklet_schedule()处理这些tasklet时,处理器上的数据不会弄乱
4.把需要调度的tasklet加到每个处理器一个的tasklet_vec链表或tasklet_hi_vec链表的表头上去
5.唤起TASKLET_SOFTIRQ或HI_SOFTIRQ软中断,这样在下一次调用do_softirq()时就会执行该tasklet
6.恢复中断到原状态并返回
在前面的内容中我们曾经提到过挂起,
do_softirq()会尽可能早地在下一个合适的时机执行。 由于大部分tasklet和软中断都是
在中断处理程序中被设置成待处理状态,所以最近一个中断返回的时候看起来就是执行do_softirq()的最佳时机。因为TASKLET_SOFTIRQ和HI_SOFTIRQ已经被触发了,所以do_softirq()会执行相应的软中断处理程序。而这两个处理程序,
**tasklet_ action()和tasklet_hi_action(),就是tasklet处理的核心。**让我们观察它们做了什么:
1.禁止中断(没有必要首先保存其状态,因为这里的代码总是作为软中断被调用,而且中断总是被激活的),并为当前处理器检索tasklet_vec或tasklet_hig_vec链表
2.将当前处理器上的该链表设置为NULL,达到清空的效果
3.允许应中断。没有必要再恢复它们回原状态,因为这段程序本身就是作为软中断处理程序被调用的,所以中断是应该被允许的
4.循环遍历获得链表上的每一个待处理的tasklet
5.如果是多处理器系统,通过检査TASKLET_STATE_RUN来判断这个tasklet是否正在其他处理器上运行。如果它正在运行,那么现在就不要执行,跳到下一个待处理的tasklet去(回忆一下,同一时间里,相同类型的tasklet只能有一个执行)
6.如果当这个tasklet没有执行,将其状态设置为TASKLET_STATE_RUN,这样別的处理器就不会再去执行它了
7.检查count值是否为0,确保tasklet没有被禁止。如果tasklet被禁止了,则跳到下一个挂起的tasklet去
8.我们已经清楚地知道这个tasklet没有在其他地方执行,并且被我们设置成执行状态,这样它在丼他部分就不会被执行,并且引用计数为0,现在可以执行tasklet的处理程序了
9.tasklet运行完毕蛮清楚tasklet的state域的TASKLET_STATE_RUN状态标志
10.重复执行下一个tasklet,直至没有剩余的等待处理的tasklet
tasklet的实现很简单,但非常巧妙。我们可以看到,所有的tasklet都通过重复运用H I_ SOFTIRQ和TASKLET_SOFTIRQ这两个软中断实现。当一个tasklet被调度时,内核就会唤起 这两个软中断中的一个。随后,该软中断会被特定的函数处理,执行所有已调度的tasklet。这个函数保证同一时间里只有一个给定类别的tasklet会被执行(但其他不同类型的tasklet可以同时执行)。所有这些复杂性都被一个简洁的接口隐藏起来了
三、使用tasklet
大多数情况下,为了控制一个寻常的硬件设备,
tasklet机制都是实现自己的下半部的最佳选 择。tasklet可以动态创建,使用方便,执行起来也还算快。此外,尽管它们的名字使人混淆,但 能加深你的印象:那是逗人喜爱的
①声明自己的tasklet(DECLARE_TASKLET、DECLARE_TASKLET_DISABLED、tasklet_init)
你既
可以静态地创建tasklet,也可以动态地创建它。选择哪种方式取决于你到底是有(或者时想要)一个对tasklet的
直接引用还是间接引用
如果你准备
静态地创建一个tasklet(也就是有一个它的直接引用),使用下<linux/interrupt.h>中定义的两个宏中的一个:
这两个宏都能根据给定的名称静态地创建一个task_struct结构
当该tasklet被调度以后
,给定的函数func会被执行,它的参数由data给出
* 这两个宏之间的区别
**在于引用计数器的初始值设置不同。**前面一个宏把创建的tasklet的引用计数器设置为0,该tasklet处于激活状态。另一 个把引用计数器设置为1,所以该tasklet处于禁止状态
下面是一个例子:
还可以通过将一个间接引用(一个指针)赋给一个
动态创建的tasklet_struct结构的方式来初始化一个tasklet_init():
②编写自己的tasklet处理程序(tasklet_handler)
tasklet处理程序必须符合规定的函数类型:
为是靠软中断实现,所以
tasklet不能睡眠。这意味着你
不能在tasklet中使用信号量或者其他什么阻塞式的函数。由于tasklet运行时允许相应中断,所以你必须做好预防工作(如屏蔽中断然后获取一个锁),如果你的tasklet和中断处理程序之间共享了某些数据的话。两个相同的tasklet决不会同时执行,这点和软中断不同——尽管两个不同的tasklet可以在两个处理器上同时执行。如果你的tasklet和其他的tasklet或者是软中断共享了数据,
你必须进行适当地锁保护
③调度自己的tasklet(tasklet_schedule)
通过调用tasklet_schedule()函数并穿肚给它相应的tasklt_struct的指针,该tasklet就会被调度以便执行:
在tasklet被调度以后,只要有机会它就会尽可能早地运行。在它还没有得到运行机会之前, 如果有一个相同的tasklet又被调度了
,那么它仍然只会运行一次。而如果这时它已经开始运行了,比如说在另外一个处理器上,那么这个新的tasklet会被重新调度并再次运行。作为一种优化措施,
一个tasklet总在调度它的处理器上执行——这是希望能更好地利用处理器的高速缓存
④禁止/激活/消除tasklet
你可以调用
tasklet_disable()函数来禁止某个指定的tasklet。如果该tasklet当前正在执行,这个函数会等到它执行完毕再返回
你也可以调用
tasklet_disable_nsync()函数,它也用来禁止指定的tasklet,不过它无须在返回前等待tasklet执行完毕。这么做往往不太安全,因为你无法估计该tasklet是否仍在执行
调用
tasklet_enable()函数可以激活一个tasklet,如果希望激活DECLARE_TASKLET_DISABLED()创建的tasklet,你也得调用这个函数,如:
你可以通过调用
tasklet_kill()函数从挂起的队列中去掉一个tasklet。该函数的参数是一个指向某个tasklet的tasklet_struct的长指针 在处理一个经常重新调度它自身的tasklet的时候,从 挂起的队列中移去已调度的tasklet会很有用。这个函数首先等待该tasklet执行完毕,然后再将 它移去。当然,没有什么可以阻止其他地方的代码重新调度该tasklet。由于该函数可能会引起休眠 ,所以禁止在中断上下文中使用它
四、ksoftirqd内核线程
每个处理器都有一组辅助处理软中断(和tasklet)的内核线程。当内核中出现大量软中断的时候,这些内核进程就会辅助处理它们。因为tasklet通过用软件中断实施,下面的讨论同样适用于软中断和tasklet。简洁起见,我们将主要参考软中断
我们前面曾经阐述过,对于软中断,内核会选择在几个特殊时机进行处理。而在中断处理 程序返回时处理是最常见的。软中断被触发的频率有时可能很高(像在进行大流量的网络通信期间)。更不利的是,处理函数有时还会自行重复触发。也就是说,当一个软中断执行的时候,它 可以重新触发自己以便再次得到执行(事实上,网络子系统就会这么做)。如果软中断本身出现的频率就高,再加上它们又有将自己重新设置为可执行状态的能力,那么就会导致用户空间进程无法获得足够的处理器时间,因而处于饥饿状态。而且,单纯的对重新触发的软中断采取不立即处理的策略,也无法让人接受。当软中断最初提出时,就是一个让人进退维谷的问題,亟待解 决,而直观的解决方案又都不理想。首先,就让我们看看两种最容易想到的直观的方案
第一种方案是,只要还有被触发并等待处理的软中断,本次执行就要负责处理,重新触发的软中断也在本次执行返回前被处理。这样做可以保证对内核的软中断采取即时处理的方式,关键 在于,对重新触发的软中断也会立即处理。当负载很高的时候这样做就会出问题,此时会有大最被触发的软中断,而它们本身又会重复触发。系统可能会一直处理软中断,根本不能完成其他任 务。用户空间的任务被忽略了——实际上,只有软中断和中断处理程序轮流执行,而系统的用户 只能等待。只有在系统永远处于低负载的情况下,这种方案才会有理想的运行效果;只要系统有 哪怕是中等程度的负载量,这种方案就无法让人满意。用户空间根本不能容忍有明显的停顿 现
第二种方案选择不处理重新触发的软中断。在从中断返回的时候,内核和平常一样,也会检查所有挂起的软中断并处理它们。但是,任何自行重新触发的软中断都不会马上处理,它们被放到下一个软中断执行时去处理。而这个时机通常也就是下一次中断返回的时候,这等于是说,一定得等一段时间,新的(或者重新触发的)软中断才能被执行。可是,在比较空闲的系统中,立 即处理软中断才是比较好的做法。很不幸,这个方案显然又是一个时好时坏的选择。尽管它能保证用户空间不处于饥饿状态,但它却让软中断忍受饥饿的痛苦,而根本没有好好利用闲置的系统资源
在设计软中断时,开发者就意识到需要一些折中。最终在内核中实现的方案是不会立即处理 重新触发的软中断。而作为改进,当大量软中断出现的时候,内核会唤醒一组内核线程来处理这些负载。这些线程在最低的优先级上运行(nice值是19),这能避免它们跟其他重要的任务抢夺资源。但它们最终肯定会被执行,所以,这个折中方案能够保证在软中断负担很重的时候,用户程序不会因为得不到处理时间而处于饥饿状态。相应的,也能保证“过量”的软中断终究会得到处理。最后,在空闲系统上,这个方案同样表现良好,软中断处理得非常迅速(因为仅存的内核线程肯定会马上调度)
每个处理器都有一个这样的线程。所有线程的名字都叫做ksoftirqd/n,区别在于n,它对应的是处理器的编号。在一个双CPU的机器上就有两个这样的线程,分别叫ksoftirqd/0和ksoftirqd/1。为了保证只要有空闲的处理器,它们就会处理软中断,所以给每个处理器都分配一个这样的线程。一旦该线程被初始化,它就会执行类似下面这样的死循环:
只要有待处理的软中断(由softirq_pending()函数负责实现),ksoftirq就会调用do_softirq()去处理它们。通过重复执行这样的操作,重新触发的软中断也会被执行。如果有必要的话,每次迭代后都会调用schedule()以便让更重要的进程得到处理机会。当所有需要执行的操作都完成以后,该内核线程将自己设置为TASK_INTERRUPTIBLE状态,唤起调度程序选择其他可执行进程投入运行
只要do_softirq()函数发现已经执行过的内核线程重新触发了它自己,软中断内核线程就会被唤醒
五、老的BH机制
尽管BH机制令人欣慰地退出了历史舞台,在2.6版内核中已经难觅踪迹了。可是,它毕竟曾经历经了漫长的时光——从最早版本的内核就开始了。由于其余威尚存,所以仅仅不经意地提起它是不够的,尽管在2.6版本中已经不再使用它了,但历史就是历史,应该被了解
BH很古老,但它能揭示一些东西。所有BH都是静态定义的,最多可以有32个。由于处理函数必须在编译时就被定义好,所以实现模块时不能直接使用BH接口。不过业已存在的BH倒是可以利用。随着时间的推移,这种静态要求和最大为32个的数目限制最终妨碍了它们的应用
每个BH处理程序都严格地按顺序执行——不允许任何两个BH处理程序同时执行,即使它 们的类型不同。这样做倒是使同步变得简单了,可是却不利于多处理器的可扩展性,也不利于大型SMP的性能。使用BH的驱动程序很难从多个处理器上受益,特别是网络层,可以说为此饱受困扰
除了这些特点,BH机制和 tasklet就很像了。实际上,在 2.4内核中,BH就是基于tasklet实现的。所有可能的32个BH都通过<Linux/interrupt.h>中定义的常量表示。如果需要将一个BH标志为挂起状态,可以把相应的BH号传给mark_bh()函数。在2.4内核中,这将导致随后调BH tasklet,具体工作是由函数bh_action()完成的。而在2.4内核以前,BH机制独立实现,不依赖任何低级BH机制,这 现在的软中断很像
由于这种形式的下半部机制存在缺点,内核开发者们希望引入任务队列机制来代替它。尽管任务队列得到了不少使用者的认可,但它实际上并没有达成这个目的。在2.3版的内核中,引入新的软中断和tasklet机制也就结束了对BH的使用。BH机制基于tasklet重新实现。不幸的是,为新接口本身降低了对执行的序列化保障,所以从BH接口移植到tasklet或软中断接口上操作起来非常复杂。在2.5版中,这种移植最终在定时器和SCSI(最后的BH使用)转换到软中断机制后完成了。于是内核开发者们立即除去了BH接口。终于解脱了,BH
还没有评论,来说两句吧...