当前位置: 首页 > 工具软件 > Tasklet > 使用案例 >

中断延迟处理机制之Tasklet,从理论到实践

红弘盛
2023-12-01


作为Linux内核中的下半部工作机制之一,tasklet有它存在的价值和意义,和其它的中断延迟工作机制互补。

Tasklet是在I/O驱动中实现延迟执行函数的首选方法。如前所述,Tasklet是建立在两个名为HI_SOFTIRQTASKLET_SOFTIRQ的软中断之上。多个Tasklet可以与同一个软中断相关联,每个Tasklet都包含自己的函数。实际上,这两个软中断并没有什么区别,只是在do_softirq()函数中,它先执行HI_SOFTIRQ的Tasklet,再执行TASKLET_SOFTIRQ的Tasklet。

Tasklet和高优先级Tasklet存储在tasklet_vectasklet_hi_vec数组中。两个数组都包括NR_CPUStasklet_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中断处理程序的执行。

总结

对比:softirq vs tasklet

虽然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取决于具体的应用场景和性能需求。

 类似资料: