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函数的执行。

相关文章
|
14天前
|
安全 Linux 编译器
探索Linux内核的奥秘:从零构建操作系统####
本文旨在通过深入浅出的方式,带领读者踏上一段从零开始构建简化版Linux操作系统的旅程。我们将避开复杂的技术细节,以通俗易懂的语言,逐步揭开Linux内核的神秘面纱,探讨其工作原理、核心组件及如何通过实践加深理解。这既是一次对操作系统原理的深刻洞察,也是一场激发创新思维与实践能力的冒险。 ####
|
2天前
|
算法 Linux 开发者
深入探究Linux内核中的内存管理机制
本文旨在对Linux操作系统的内存管理机制进行深入分析,探讨其如何通过高效的内存分配和回收策略来优化系统性能。文章将详细介绍Linux内核中内存管理的关键技术点,包括物理内存与虚拟内存的映射、页面置换算法、以及内存碎片的处理方法等。通过对这些技术点的解析,本文旨在为读者提供一个清晰的Linux内存管理框架,帮助理解其在现代计算环境中的重要性和应用。
|
2天前
|
人工智能 算法 大数据
Linux内核中的调度算法演变:从O(1)到CFS的优化之旅###
本文深入探讨了Linux操作系统内核中进程调度算法的发展历程,聚焦于O(1)调度器向完全公平调度器(CFS)的转变。不同于传统摘要对研究背景、方法、结果和结论的概述,本文创新性地采用“技术演进时间线”的形式,简明扼要地勾勒出这一转变背后的关键技术里程碑,旨在为读者提供一个清晰的历史脉络,引领其深入了解Linux调度机制的革新之路。 ###
|
4天前
|
算法 Linux 定位技术
Linux内核中的进程调度算法解析####
【10月更文挑战第29天】 本文深入剖析了Linux操作系统的心脏——内核中至关重要的组成部分之一,即进程调度机制。不同于传统的摘要概述,我们将通过一段引人入胜的故事线来揭开进程调度算法的神秘面纱,展现其背后的精妙设计与复杂逻辑,让读者仿佛跟随一位虚拟的“进程侦探”,一步步探索Linux如何高效、公平地管理众多进程,确保系统资源的最优分配与利用。 ####
24 4
|
5天前
|
缓存 负载均衡 算法
Linux内核中的进程调度算法解析####
本文深入探讨了Linux操作系统核心组件之一——进程调度器,着重分析了其采用的CFS(完全公平调度器)算法。不同于传统摘要对研究背景、方法、结果和结论的概述,本文摘要将直接揭示CFS算法的核心优势及其在现代多核处理器环境下如何实现高效、公平的资源分配,同时简要提及该算法如何优化系统响应时间和吞吐量,为读者快速构建对Linux进程调度机制的认知框架。 ####
|
7天前
|
缓存 Linux
揭秘Linux内核:探索CPU拓扑结构
【10月更文挑战第26天】
23 1
|
7天前
|
缓存 运维 Linux
深入探索Linux内核:CPU拓扑结构探测
【10月更文挑战第18天】在现代计算机系统中,CPU的拓扑结构对性能优化和资源管理至关重要。了解CPU的核心、线程、NUMA节点等信息,可以帮助开发者和系统管理员更好地调优应用程序和系统配置。本文将深入探讨如何在Linux内核中探测CPU拓扑结构,介绍相关工具和方法。
9 0
|
13天前
|
缓存 算法 安全
深入理解Linux操作系统的心脏:内核与系统调用####
【10月更文挑战第20天】 本文将带你探索Linux操作系统的核心——其强大的内核和高效的系统调用机制。通过深入浅出的解释,我们将揭示这些技术是如何协同工作以支撑起整个系统的运行,同时也会触及一些常见的误解和背后的哲学思想。无论你是开发者、系统管理员还是普通用户,了解这些基础知识都将有助于你更好地利用Linux的强大功能。 ####
24 1
|
14天前
|
缓存 编解码 监控
深入探索Linux内核调度机制的奥秘###
【10月更文挑战第19天】 本文旨在以通俗易懂的语言,深入浅出地剖析Linux操作系统内核中的进程调度机制,揭示其背后的设计哲学与实现策略。我们将从基础概念入手,逐步揭开Linux调度策略的神秘面纱,探讨其如何高效、公平地管理系统资源,以及这些机制对系统性能和用户体验的影响。通过本文,您将获得关于Linux调度机制的全新视角,理解其在日常计算中扮演的关键角色。 ###
42 1
|
5天前
|
缓存 算法 Linux
Linux内核中的内存管理机制深度剖析####
【10月更文挑战第28天】 本文深入探讨了Linux操作系统的心脏——内核,聚焦其内存管理机制的奥秘。不同于传统摘要的概述方式,本文将以一次虚拟的内存分配请求为引子,逐步揭开Linux如何高效、安全地管理着从微小嵌入式设备到庞大数据中心数以千计程序的内存需求。通过这段旅程,读者将直观感受到Linux内存管理的精妙设计与强大能力,以及它是如何在复杂多变的环境中保持系统稳定与性能优化的。 ####
11 0