Linux内核22-软中断和tasklet

简介: Linux内核22-软中断和tasklet

1 软中断和Tasklet介绍


在之前的文章中,讲解中断处理相关的概念的时候,提到过有些任务不是紧急的,可以延后一段时间执行。因为中断服务例程都是顺序执行的,在响应一个中断的时候不应该被打断。相反,这些可延时任务执行时,可以使能中断。那么,将这些任务从中断处理程序中剥离出来,可以有效地保证内核对于中断响应时间尽可能短。这对于时间苛刻的应用来说,这是一个很重要的属性,尤其是那些要求中断请求必须在毫秒级别响应的应用。

Linux2.6内核使用两种手段满足这项挑战:软中断和tasklet,还有工作队列。其中,工作队列我们单独在一篇文章中讲解。

软中断和tasklet这两个术语是息息相关的,因为tasklet是基于软中断实现的。事实上,出现在内核源代码中的软中断概念有时候指的就是这两个术语的统称。另一个广泛使用的术语是中断上下文:可以是内核正在执行的中断处理程序,也可以是一个可延时处理的函数。

软中断是静态分配好的(编译时),而tasklet是在运行时分配并初始化的(比如,加载内核模块的时候)。因为软中断的实现是可重入的,使用自旋锁保护它们的数据结构。所以软中断可以在多个CPU上并发运行。tasklet不需要考虑这些,因为它的处理完全由内核控制,也就是说,相同类型的tasklet总是顺序执行的。换句话说,不可能同时有2个以上的CPU执行相同类型的tasklet。当然了,不同类型的tasklet完全可以在多个CPU上同时执行。完全顺序执行的tasklet简化了驱动开发者的工作,因为tasklet不需要考虑可重入设计。

既然已经理解了软中断和tasklet的机制,那么实现这样的可延时函数需要哪些步骤呢?如下所示:

  1. 初始化
    定义一个可延时函数。这一步,一般在内核初始化自身或者加载内核模块时完成。
  2. 激活
    将上面定义的函数挂起。也就是等待内核下一次的调度执行。激活可以发生在任何时候。
  3. 禁止
    对于定义的函数,可以选择性的禁止执行。
  4. 执行
    执行定义的延时函数。对于执行的时机,通过软中断控制。

激活和执行是绑定在一起的,也就是说,那个CPU激活延时函数就在那个CPU上执行。但这并不是总能提高系统性能。虽然从理论上说,绑定可延时函数到激活它的CPU上更有利于利用CPU硬件Cache。毕竟,可以想象的是,正在执行的内核线程要访问的数据结构也可能是可延时函数使用的数据。但是,因为等到延时函数执行的时候,已经过了一段时间,Cache中的相关行可能已经不存在了。更重要的是,总是把一个函数绑定到某个CPU上执行是有风险的,这个CPU可能负荷很重而其它的CPU可能比较空闲。


2 软中断


Linux2.6内核中,软中断的数量比较少。对于多数目的,这些tasklet足够了。因为不需要考虑重入,所以简单易用。事实上,只使用了6类软中断,如下表所示:

表4-9 Linux2.6中使用的软中断

软中断 优先级 描述
HI_SOFTIRQ 0 处理高优先级的tasklet
TIMER_SOFTIRQ 1 定时器中断
NET_TX_SOFTIRQ 2 给网卡发送数据
NET_RX_SOFTIRQ 3 从网卡接收数据
SCSI_SOFTIRQ 4 SCSI命令的后中断处理
TASKLET_SOFTIRQ 5 处理常规tasklet

这里的优先级就是软中断的索引,数值越小,代表优先级越高。Linux软中断处理程序总是从索引0开始执行。


2.1 软中断使用的数据结构


软中断的主要数据结构是softirq_vec数组,包含类型为softirq_action的32个元素。软中断的优先级表示softirq_action类型元素在数组中的索引。也就是说,目前只使用了数组中的前6项。softirq_action包含2个指针:分别指向软中断函数和函数使用的数据。

另一个重要的数据是preempt_count,存储在进程描述符中的thread_info成员中,用来追踪记录内核抢占和内核控制路径嵌套层数。它又被划分为4部分,如下表所示:

表4-10 preempt_count各个位域

描述
0-7 内核抢占禁止计数(最大值255)
8-15 软中断禁用深度计数(最大值255)
16-27 硬中断计数(最大值4096)
28 PREEMPT_ACTIVE标志

关于内核抢占的话题我们还会再写一篇专门的文章进行阐述,故在此不再详述。

可以使用宏in_interrupt()访问硬中断和软中断计数器。如果这两个计数器都是0,则返回值为0;否则返回非0值。如果内核没有使用多个内核态堆栈,该宏查找的是当前进程的thread_info描述符。但是,如果使用了多个内核态堆栈,则查找irq_ctx联合体中的thread_info描述符。在此情况下,内核抢占肯定是禁止的,所以该宏返回的是非0值。

最后一个跟软中断实现相关的数据是每个CPU都有一个32位掩码,用来描述挂起的软中断。存储在irq_cpustat_t数据结构的__softirq_pending成员中。对其具体的操作函数是local_softirq_pending()宏,用来是否禁止某个中断。


2.2 处理软中断


软中断的初始化使用open_softirq()函数完成,函数原型如下所示:

void open_softirq(int nr, void (*action)(struct softirq_action *))
{
    softirq_vec[nr].action = action;
}

软中断的激活使用方法raise_softirq(),其参数是软中断索引nr,主要执行下面的动作:

  1. 执行local_irq_save宏保存eflags寄存器中的IF标志并且禁止中断。
  2. 通过设备CPU软中断位掩码的相应位将软中断标记为挂起状态。
  3. 如果in_interrupt()返回1,直接跳转到第5步。如果处于这种情况, 要么是当前中断上下文中正在调用raise_softirq()、或者软中断被禁止。
  4. 否则,调用wakeup_softirqd()唤醒ksoftirqd内核线程。
  5. 执行local_irq_restore宏恢复IF标志。

应该周期性地检查挂起状态的软中断,但是不能因此增加太重的负荷。所以,软中断的执行时间点非常重要。下面是几个重要的时间点:

  • 调用local_bh_enable()函数使能软中断的时候。
  • do_IRQ()函数完成I/O中断处理,调用irq_exit()宏时。
  • 如果系统中使用的是I/O-APIC控制器,当smp_apic_timer_interrupt()函数处理完一个定时器中断的时候。
  • 在多核系统中,当CPU处理完一个由CALL_FUNCTION_VECTORCPU间的中断引发的函数时。
  • 当一个特殊的ksoftirqd/n内核线程被唤醒时。


2.3 do_softirq函数


如果某个时间点,检测到挂起的软中断(local_softirq_pending()非零),内核调用do_softirq()处理它们。这个函数执行的主要内容如下:

  1. 如果in_interrupt()等于1,则函数返回。这表明中断上下文中正在调用do_softirq()函数,或者软中断被禁止。
  2. 执行local_irq_save来保存IF标志的状态,并在本地CPU上禁用中断。
  3. 如果thread_union等于4KB,如果有必要,切换到软IRQ堆栈中。
  4. 调用__do_softirq()函数。
  5. 如果在第3步切换到软IRQ堆栈,则恢复原来的堆栈指针到esp寄存器中,然后切换到之前使用的异常堆栈中。
  6. 执行local_irq_restore恢复中断标志。


2.4 __do_softirq()函数


__do_softirq()函数读取位掩码,如果某位被置1,则执行其对应的处理函数。但是,在执行的过程中,可能会有新的软中断发生,这样后面的软中断处理就会延时。为了保证位掩码所有的软中断处理及时,__do_softirq()函数一次处理完所有的软中断。但是,这种机制又引发了新的问题,__do_softirq()函数一次运行时间过长。基于这个原因,__do_softirq()函数每次运行固定数量的循环次数,如果还有没执行的软中断,交给内核线程ksoftirqd进行处理。下面是__do_softirq()这个函数所做的工作:

  1. 设置每次循环次数为10。
  2. 拷贝软中断的位掩码到局部变量中。
  3. 调用local_bh_disable()函数禁止软中断。
    为什么此时禁止软中断呢?因为执行那些可延时函数时,中断是处于使能状态的,意味着执行__do_softirq()函数的过程中,随时都会发生中断,那么立即响应中断,执行do_IRQ()函数。而do_IRQ()函数中,在最后会调用irq_exit()宏,这个宏会引发另一个调用 __do_softirq()的程序执行。这在Linux内核中是禁止的,因为其可延时函数的执行都是串行的。所以,在此需要禁止软中断。
  4. 清除正在执行的软中断对应掩码位。
  5. 执行local_irq_enable()使能中断。
  6. 执行软中断对应的函数。
  7. 执行local_irq_disable()禁止中断。
  8. 迭代次数减1,继续执行。


2.5 ksoftirqd内核线程


在较新的内核版本中,每个CPU都有自己的ksoftirqd内核线程。每个ksoftirqd内核线程调用ksoftirqd()函数,主要的执行内容如下所示:

for(;;) {
    set_current_state(TASK_INTERRUPTIBLE);
    schedule();
    /* 处于TASK_RUNNING状态中 */
    while (local_softirq_pending()) {
        preempt_disable();
        do_softirq();
        preempt_enable();
        cond_resched();
    }
}

该内核线程被唤醒后,会调用local_softirq_pending()检查软中断位掩码,如果必要检查do_softirq()函数。

也就是说,ksoftirqd内核线程是时间维度上的一种平衡策略。

软中断函数也可以重新激活自身。实际上,网络软中断和tasklet软中断就是这样做的。更重要的是,外部事件,比如网卡上的数据包泛滥也可以频繁地激活软中断。

连续大量的软中断会造成潜在的问题,引入内核线程也是为了解决这个问题。如果没有这个内核线程,开发者只能使用两种替代策略。

第一种策略就是正在执行软中断的时候忽略新的软中断。换言之,在执行do_softirq()函数的过程中,除了执行已经记录的挂起中的软中断之外,不会再检查是否还会发生软中断。这个方案有瑕疵,假设软中断函数在执行do_softirq()函数的过程中被重新被激活。最坏的情况就是,直到下一次定时器中断发生时,软中断不会被执行,即使当前处理器处于空闲状态。对于这种软中断延迟,网络开发者不可接受。

第二种策略就是do_softirq()函数持续地检查是否有挂起的软中断。只有当所有的软中断被处理完该函数才退出。这肯定满足了网络开发者的需求,但是对系统的普通用户却造成了很大的干扰:如果网卡的数据包流非常频繁,或者软中断函数保持自激活,do_softirq()函数就永远不会返回,用户态的程序实际上无法正常工作。

综上所述,ksoftirqd内核线程就是尝试解决这种很难抉择的问题。do_softirq()函数判断是否有软中断挂起。迭代一些次数后,如果还有软中断挂起,函数就会唤醒内核线程,自身终止,交给内核线程去处理后续的软中断。内核线程的优先级比较低,用户程序的执行不会受到影响。如果处理器处于空闲状态,挂起的软中断也会很快被执行。


3 Tasklet


Tasklet是I/O驱动中实现可延时处理函数的一种优选方法。Tasklet的实现基于两种软中断,分别为HI_SOFTIRQTASKLET_SOFTIRQ。多个tasklet可能对应相同的软中断,每个tasklet都有自己的处理函数。除了do_softirq()执行HI_SOFTIRQ的tasklet优先于 TASKLET_SOFTIRQ之外,这两种软中断没有实质上的差异。

Tasklet和高优先级的tasklet分别存储在 tasklet_vectasklet_hi_vec数组中。它们都包含与CPU(NR_CPUS)相同个数的元素,这些元素的类型是tasklet_head,也就是说tasklet描述符的管理还是通过链表的结构进行管理(由此可以看出,链表在Linux内核数据管理中的作用了)。tasklet描述符的数据结构是tasklet_struct,它的成员如下表所示:

表4-11 tasklet描述符的数据成员

名称 描述
next 指向下一个描述符
state tasklet的状态
count 锁计数
func 指向tasklet处理函数
data 一个tasklet函数可能使用的无符号长整形数

state包含两个标志:

  • TASKLET_STATE_SCHED
    置1,表明tasklet正在挂起(也就是准备执行)。这意味tasklet描述符已经被插入到tasklet_vectasklet_hi_vec数组中了。
  • TASKLET_STATE_RUN
    表明正在执行。对于单处理器系统,该标志没有使用。

假设你正在写一个设备驱动且想使用tasklet,需要做什么呢?

首先,你应该申请一个新的tasklet_struct数据结构并通过tasklet_init()完成初始化。tasklet_init()的参数为tasklet描述符地址,你的tasklet处理函数地址,还有可选的整数参数。

Tasklet可以通过tasklet_disable_nosync()tasklet_disable()禁止。这两个函数都是增加tasklet描述符的count值。但是,后者必须等到正在运行的tasklet函数终止后才会返回。重新使能tasklet,使用tasklet_enable()函数。

为了激活tasklet,可以根据优先级分别调用tasklet_schedule()函数或者tasklet_hi_schedule()函数。它们的工作内容类似,如下所示:

  1. 检查TASKLET_STATE_SCHED,如果设置,则返回(说明已经被调度过了)。
  2. 调用local_irq_save保存中断标志IF并禁止中断。
  3. 将tasklet描述符添加到tasklet_vec[n]tasklet_hi_vec[n]数组中对应的列表的开始处,在此,n表示CPU的逻辑编号。
  4. 调用raise_softirq_irqoff()激活TASKLET_SOFTIRQHI_SOFTIRQ软中断。
  5. 调用local_irq_restore恢复中断标志IF。

接下来,我们看看tasklet是如何执行的。其实,跟其它软中断的执行过程类似。软中断被激活,do_softirq()就会执行对应的软中断函数。HI_SOFTIRQ软中断对应的函数为tasklet_hi_action(),而TASKLET_SOFTIRQ对应的函数为tasklet_action()。它们的执行过程也是类似的,如下所示:

  1. 禁止中断。
  2. 获取CPU的逻辑编号n。
  3. 将tasklet描述符链表中的地址存储到局部变量链表中。
  4. 清除tasklet_vec[n]tasklet_hi_vec[n]数组中已经调度过的tasklet描述符列表。(赋值NULL即可)
  5. 使能中断。
  6. 遍历链表中的tasklet描述符:
  • 在多核处理器系统中,需要检查TASKLET_STATE_RUN标志。
  • 通过检查tasklet描述符中的count成员,判断是否被禁止。如果tasklet被禁止,清除TASKLET_STATE_RUN标志,重新将tasklet描述符插回到tasklet描述符链表中,然后再一次激活TASKLET_SOFTIRQHI_SOFTIRQ软中断。
  • 如果tasklet被使能,清除TASKLET_STATE_SCHED标志,然后执行tasklet对应的处理函数。

需要注意的是,除非tasklet函数激活自身。否则,一次激活只能触发一次tasklet函数的执行。

相关文章
|
7天前
|
算法 Linux 调度
深入理解Linux内核调度器:从基础到优化####
本文旨在通过剖析Linux操作系统的心脏——内核调度器,为读者揭开其高效管理CPU资源的神秘面纱。不同于传统的摘要概述,本文将直接以一段精简代码片段作为引子,展示一个简化版的任务调度逻辑,随后逐步深入,详细探讨Linux内核调度器的工作原理、关键数据结构、调度算法演变以及性能调优策略,旨在为开发者与系统管理员提供一份实用的技术指南。 ####
32 4
|
11天前
|
缓存 算法 Linux
深入理解Linux内核调度器:公平性与性能的平衡####
真知灼见 本文将带你深入了解Linux操作系统的核心组件之一——完全公平调度器(CFS),通过剖析其设计原理、工作机制以及在实际系统中的应用效果,揭示它是如何在众多进程间实现资源分配的公平性与高效性的。不同于传统的摘要概述,本文旨在通过直观且富有洞察力的视角,让读者仿佛亲身体验到CFS在复杂系统环境中游刃有余地进行任务调度的过程。 ####
31 6
|
9天前
|
缓存 资源调度 安全
深入探索Linux操作系统的心脏——内核配置与优化####
本文作为一篇技术性深度解析文章,旨在引领读者踏上一场揭秘Linux内核配置与优化的奇妙之旅。不同于传统的摘要概述,本文将以实战为导向,直接跳入核心内容,探讨如何通过精细调整内核参数来提升系统性能、增强安全性及实现资源高效利用。从基础概念到高级技巧,逐步揭示那些隐藏在命令行背后的强大功能,为系统管理员和高级用户打开一扇通往极致性能与定制化体验的大门。 --- ###
32 9
|
8天前
|
缓存 负载均衡 Linux
深入理解Linux内核调度器
本文探讨了Linux操作系统核心组件之一——内核调度器的工作原理和设计哲学。不同于常规的技术文章,本摘要旨在提供一种全新的视角来审视Linux内核的调度机制,通过分析其对系统性能的影响以及在多核处理器环境下的表现,揭示调度器如何平衡公平性和效率。文章进一步讨论了完全公平调度器(CFS)的设计细节,包括它如何处理不同优先级的任务、如何进行负载均衡以及它是如何适应现代多核架构的挑战。此外,本文还简要概述了Linux调度器的未来发展方向,包括对实时任务支持的改进和对异构计算环境的适应性。
28 6
|
9天前
|
缓存 Linux 开发者
Linux内核中的并发控制机制:深入理解与应用####
【10月更文挑战第21天】 本文旨在为读者提供一个全面的指南,探讨Linux操作系统中用于实现多线程和进程间同步的关键技术——并发控制机制。通过剖析互斥锁、自旋锁、读写锁等核心概念及其在实际场景中的应用,本文将帮助开发者更好地理解和运用这些工具来构建高效且稳定的应用程序。 ####
28 5
|
9天前
|
算法 Unix Linux
深入理解Linux内核调度器:原理与优化
本文探讨了Linux操作系统的心脏——内核调度器(Scheduler)的工作原理,以及如何通过参数调整和代码优化来提高系统性能。不同于常规摘要仅概述内容,本摘要旨在激发读者对Linux内核调度机制深层次运作的兴趣,并简要介绍文章将覆盖的关键话题,如调度算法、实时性增强及节能策略等。
|
10天前
|
存储 监控 安全
Linux内核调优的艺术:从基础到高级###
本文深入探讨了Linux操作系统的心脏——内核的调优方法。文章首先概述了Linux内核的基本结构与工作原理,随后详细阐述了内核调优的重要性及基本原则。通过具体的参数调整示例(如sysctl、/proc/sys目录中的设置),文章展示了如何根据实际应用场景优化系统性能,包括提升CPU利用率、内存管理效率以及I/O性能等关键方面。最后,介绍了一些高级工具和技术,如perf、eBPF和SystemTap,用于更深层次的性能分析和问题定位。本文旨在为系统管理员和高级用户提供实用的内核调优策略,以最大化Linux系统的效率和稳定性。 ###
|
9天前
|
Java Linux Android开发
深入探索Android系统架构:从Linux内核到应用层
本文将带领读者深入了解Android操作系统的复杂架构,从其基于Linux的内核到丰富多彩的应用层。我们将探讨Android的各个关键组件,包括硬件抽象层(HAL)、运行时环境、以及核心库等,揭示它们如何协同工作以支持广泛的设备和应用。通过本文,您将对Android系统的工作原理有一个全面的认识,理解其如何平衡开放性与安全性,以及如何在多样化的设备上提供一致的用户体验。
|
9天前
|
缓存 运维 网络协议
深入Linux内核架构:操作系统的核心奥秘
深入Linux内核架构:操作系统的核心奥秘
27 2
|
11天前
|
监控 网络协议 算法
Linux内核优化:提升系统性能与稳定性的策略####
本文深入探讨了Linux操作系统内核的优化策略,旨在通过一系列技术手段和最佳实践,显著提升系统的性能、响应速度及稳定性。文章首先概述了Linux内核的核心组件及其在系统中的作用,随后详细阐述了内存管理、进程调度、文件系统优化、网络栈调整及并发控制等关键领域的优化方法。通过实际案例分析,展示了这些优化措施如何有效减少延迟、提高吞吐量,并增强系统的整体健壮性。最终,文章强调了持续监控、定期更新及合理配置对于维持Linux系统长期高效运行的重要性。 ####