Tasklet
是在I/O驱动中实现延迟执行函数的首选方法。如前所述,Tasklet
是建立在两个名为HI_SOFTIRQ
和TASKLET_SOFTIRQ
的软中断之上。多个Tasklet可以与同一个软中断相关联,每个Tasklet都包含自己的函数。实际上,这两个软中断并没有什么区别,只是在do_softirq()
函数中,它先执行HI_SOFTIRQ
的Tasklet,再执行TASKLET_SOFTIRQ
的Tasklet。
Tasklet
和高优先级Tasklet
存储在tasklet_vec
和tasklet_hi_vec
数组中。两个数组都包括NR_CPUS
个tasklet_head
类型的元素,每个元素都包含一个指向Tasklet
描述符列表的指针。
Linux 2.3.x:Tasklet机制首次引入。
这个版本中,内核开发人员实现了基本的Tasklet API,用于创建、调度和执行Tasklet。此外,这个版本还引入了底半部(bottom half)和软中断概念,与Tasklet一起组成了Linux内核中的软中断处理机制。
Linux 2.4.x:这个版本的内核对Tasklet的实现进行了优化,提高了Tasklet的性能。同时,这个版本对Tasklet机制的调度和执行策略进行了改进,使得Tasklet在处理中断任务时具有更高的优先级。
Linux 2.6.x:这个版本中,内核开发人员对Tasklet的API进行了扩展,增加了新的功能,如动态创建和销毁Tasklet。此外,这个版本还对Tasklet的调度策略进行了进一步优化,提高了Tasklet在高并发场景下的性能。
Linux 3.x:这个版本中,内核开发人员对Tasklet的实现进行了细微调整,以提高其在多核系统中的性能。此外,这个版本还对Tasklet的资源管理和同步机制进行了优化,使得Tasklet在处理复杂中断任务时表现更加稳定。
Linux 4.x:这个版本的内核对Tasklet的调度策略进行了改进,以适应不断增长的硬件性能。此外,这个版本还引入了新的API,以支持更高级的Tasklet功能,如基于优先级的调度和任务分组。
Linux 5.x:在这个版本中,内核开发人员继续对Tasklet进行优化,提高其在高负载场景下的性能。此外,这个版本还对Tasklet的资源管理和同步机制进行了进一步优化,以提高其在多核系统中的可扩展性。
分担硬中断处理程序的负担:硬中断处理程序需要在尽可能短的时间内完成,以便让系统能够响应其他中断。然而,某些中断处理任务可能相对耗时,如果直接在硬中断处理程序中执行这些任务,可能会导致系统响应变慢。Tasklet允许将这些耗时任务推迟到稍后执行,从而降低硬中断处理过程中的延迟。
避免资源竞争:Tasklet运行在中断上下文中,不会被其他Tasklet或中断处理程序抢占。这使得Tasklet在处理共享资源时能够避免资源竞争,提高系统的稳定性和可靠性。
优化中断处理:Tasklet提供了一种简单且高效的方式来处理软中断任务。开发人员可以将中断处理任务分解为硬中断处理程序和Tasklet,从而简化中断处理过程并提高系统性能。
提高系统性能:Tasklet的执行是在中断上下文中进行的,而不是在进程上下文中。这意味着Tasklet的调度和执行开销相对较小,从而提高系统性能。
异步处理:Tasklet使得内核可以在适当的时机异步执行一些任务,而不是立即执行。这有助于平衡系统负载,并确保系统在高负载情况下仍能保持响应。
Linux内核中的Tasklet主要用于处理一些需要在软中断上下文中执行的任务,这些任务通常是由硬中断处理程序触发的。以下是一些典型的在Tasklet上运行的任务:
网络接收处理:当网络设备接收到数据包时,它会触发一个硬中断。硬中断处理程序会对数据包进行初步处理,然后将数据包放入接收队列。接着,它会调度一个Tasklet来处理接收队列中的数据包,包括协议解析、路由等操作。
网络发送处理:网络设备在发送数据包时,需要处理一系列操作,例如:将数据包放入发送队列、更新设备寄存器等。这些操作可以在Tasklet中完成,以避免在硬中断处理程序中执行耗时任务。
块设备I/O处理:块设备(如硬盘、SSD等)在执行I/O操作时,也会触发硬中断。硬中断处理程序通常负责处理I/O完成事件,然后将I/O请求放入完成队列。接下来,它会调度一个Tasklet来处理完成队列中的I/O请求,包括通知上层文件系统、更新缓存等操作。
USB设备事件处理:USB设备在插入或拔出时会触发硬中断。硬中断处理程序负责检测设备状态变化,并调度一个Tasklet来执行设备配置、资源分配等操作。
其他硬件设备事件处理:许多其他硬件设备(如声卡、视频卡等)也会触发硬中断。这些设备的事件处理程序通常会将一些耗时任务放入Tasklet中执行,以减轻硬中断处理程序的负担。
需要注意的是,Tasklet只是Linux内核中的一种底层软中断处理机制。内核开发人员也可以选择使用其他机制,如工作队列(workqueues)或线程化中断处理程序,来完成类似的任务。具体采用哪种机制取决于具体的应用场景和性能需求。
tasklet
在Linux内核中是以tasklet_struct
的结构体定义。
struct tasklet_struct {
struct tasklet_struct *next;
unsigned long state;
atomic_t count;
void (*func)(unsigned long);
unsigned long data;
};
DECLARE_PER_CPU_SHARED_ALIGNED(struct tasklet_head, tasklet_vec);
#define tasklet_schedule(t) \
__tasklet_schedule((t), &tasklet_vec)
#define tasklet_hi_schedule(t) \
__tasklet_schedule((t), &tasklet_hi_vec)
#define tasklet_disable_nosync() local_irq_disable()
#define tasklet_enable() local_irq_enable()
#define tasklet_init(t, f, data) \
do { \
(t)->next = NULL; \
(t)->state = 0; \
atomic_set(&(t)->count, 0); \
(t)->func = (f); \
(t)->data = (data); \
} while (0)
struct tasklet_struct
是 tasklet 描述符结构体,包括了下一个 tasklet 的指针、状态、引用计数、回调函数以及回调函数的参数等成员。
tasklet_struct结构体包含了四个成员变量:next指针、count计数器、func函数指针和data数据。其中,next指针用于将多个tasklet链接在一起形成一个链表;count计数器用于表示tasklet是否已经被调度,防止同一tasklet被多次调度;func函数指针用于存储tasklet需要执行的函数地址;data数据则用于传递给tasklet需要使用的参数。
DECLARE_PER_CPU_SHARED_ALIGNED(struct tasklet_head, tasklet_vec)
定义了每个 CPU 上 tasklet 列表的头指针,tasklet_vec
数组的每个元素是一个 tasklet_head
结构体,指向一个 tasklet 链表,即一个 CPU 上的所有 tasklet 都储存在这个链表中。
tasklet_schedule(t)
宏用于将指定的 tasklet 添加到当前 CPU 的 tasklet 列表中,等待执行。tasklet_hi_schedule(t)
则是将指定的 tasklet 添加到高优先级的 tasklet 列表中,等待执行。
tasklet_schedule()函数首先对tasklet的count计数器加1,表示tasklet需要被调度执行;然后调用tasklet_hi_schedule()函数将其加入到内核的高优先级任务队列中,等待执行。
当tasklet被调度执行时,内核会将其放入TASKLET_SOFTIRQ中断处理程序中进行处理。在TASKLET_SOFTIRQ中断处理程序中,内核会依次执行所有需要被调度执行的tasklet,并将其从任务队列中移除。
tasklet_disable_nosync()
宏用于禁用当前 CPU 的中断,以确保在执行 tasklet 时不会被打断。tasklet_enable()
则是启用中断。
tasklet_init(t, f, data)
宏用于初始化一个 tasklet 描述符结构体,指定回调函数和参数,并将引用计数初始化为 0。
可以看到,tasklet_init()函数将tasklet的next指针置为空,将count计数器清零,将func函数指针指向所需执行的函数地址,将data数据设置为需要传递的参数。
当tasklet被调度执行时,内核会将其放入TASKLET_SOFTIRQ中断处理程序中进行处理。在TASKLET_SOFTIRQ中断处理程序中,内核会依次执行所有需要被调度执行的tasklet,并将其从任务队列中移除。
TASKLET_SOFTIRQ中断处理程序的定义如下:
asmlinkage __visible void __do_softirq(void)
{
unsigned long flags;
struct softirq_action *h;
local_irq_save(flags);
__this_cpu_inc(softirq_count);
h = softirq_vec;
while (h < softirq_vec + NR_SOFTIRQS) {
__u32 pending;
pending = local_softirq_pending() & h->mask;
if (pending)
h->action(pending);
h++;
}
__this_cpu_dec(softirq_count);
local_irq_restore(flags);
}
当内核执行TASKLET_SOFTIRQ
中断处理程序时,会首先禁用本地中断,以避免中断重入和竞争条件。然后,内核会遍历tasklet_vec
数组,依次检查每个CPU上是否有已经排队等待执行的tasklet,如果有,则将tasklet添加到TASKLET_SOFTIRQ
处理程序的执行队列中。
当TASKLET_SOFTIRQ
中断处理程序完成后,内核会重新启用本地中断,并立即调用处理程序的执行队列中的所有tasklet。此时,这些tasklet将以FIFO(先进先出)的顺序被执行,一直到执行队列为空为止。
需要注意的是,如果在执行TASKLET_SOFTIRQ
中断处理程序期间有更高优先级的中断发生,那么内核会立即停止执行TASKLET_SOFTIRQ
中断处理程序并转而处理更高优先级的中断。当更高优先级的中断处理完成后,内核会恢复TASKLET_SOFTIRQ
中断处理程序的执行。
虽然Tasklet和Softirq都属于Linux内核中的软中断处理机制,但它们之间存在一些关键区别,这使得Tasklet在某些情况下更适合用于处理特定类型的任务。以下是Tasklet和Softirq之间的一些区别,以及为什么Tasklet仍然存在的原因:
简单性:Tasklet提供了一种简单的软中断处理方法。相比Softirq,Tasklet的API更简洁,使得内核开发人员更容易理解和使用。对于一些简单的软中断任务,使用Tasklet可能更加方便。
顺序执行:Tasklet是顺序执行的,同一时刻只有一个Tasklet实例在运行。这有助于避免在处理共享资源时出现竞争条件。相反,Softirq可以在多个CPU上并行执行,这可能导致资源竞争,需要额外的同步机制来解决。
动态创建:Tasklet可以在运行时动态创建和销毁,而Softirq在编译时就固定了数量。这使得Tasklet更灵活,能够根据实际需要创建和销毁。
优先级:Tasklet和Softirq具有不同的优先级。Tasklet的优先级相对较低,它们会在Softirq处理完成后执行。这使得内核可以在需要时优先处理Softirq任务。
尽管Softirq在某些方面具有更高的性能,但Tasklet仍然存在的原因是它们在简单性、顺序执行和动态创建等方面的优势。实际上,Tasklet是基于Softirq实现的,它们共享相同的底层软中断处理基础设施。具体选择使用Tasklet还是Softirq取决于具体的应用场景和性能需求。