Linux 中断子系统之softirq

1 前言

对于中断处理而言,linux将其分成了两个部分,一个叫做中断handler(top half),是全程关闭中断的,另外一部分是deferable task(bottom half),属于不那么紧急需要处理的事情。在执行bottom half的时候,是开中断的。有多种bottom half的机制,例如:softirq、tasklet、workqueue或是直接创建一个kernel thread来执行bottom half(这在旧的kernel驱动中常见,现在,一个理智的driver厂商是不会这么做的)。本文主要讨论softirq机制。由于tasklet是基于softirq的,因此本文也会提及tasklet,但主要是从需求层面考虑,不会涉及其具体的代码实现。

在普通的驱动中一般是不会用到softirq,但是由于驱动经常使用的tasklet是基于softirq的,因此,了解softirq机制有助于撰写更优雅的driver。softirq不能动态分配,都是静态定义的。内核已经定义了若干种softirq number,例如网络数据的收发、block设备的数据访问(数据量大,通信带宽高),timer的deferable task(时间方面要求高)。

2 Softirq机制

2.1 Softirq number

enum
{HI_SOFTIRQ=0,TIMER_SOFTIRQ,NET_TX_SOFTIRQ,NET_RX_SOFTIRQ,BLOCK_SOFTIRQ,IRQ_POLL_SOFTIRQ,TASKLET_SOFTIRQ,SCHED_SOFTIRQ,HRTIMER_SOFTIRQ, /* Unused, but kept as tools rely on the numbering. Sigh! */RCU_SOFTIRQ,    /* Preferable RCU should always be the last softirq */NR_SOFTIRQS
};

HI_SOFTIRQ用于高优先级的tasklet;
TASKLET_SOFTIRQ用于普通的tasklet。TIMER_SOFTIRQ是for software timer的(所谓software timer就是说该timer是基于系统tick的);
NET_TX_SOFTIRQ和NET_RX_SOFTIRQ是用于网卡数据收发的;
BLOCK_SOFTIRQ和BLOCK_IOPOLL_SOFTIRQ是用于block device的;
SCHED_SOFTIRQ用于多CPU之间的负载均衡的;
HRTIMER_SOFTIRQ用于高精度timer的。RCU_SOFTIRQ是处理RCU的。

2.2 Softirq 描述符

softirq是静态定义的,也就是说系统中有一个定义softirq描述符的数组,而softirq number就是这个数组的index。这个概念和早期的静态分配的中断描述符概念是类似的。具体定义如下:

struct softirq_action
{void   (*action)(struct softirq_action *);
};
static struct softirq_action softirq_vec[NR_SOFTIRQS] __cacheline_aligned_in_smp;

系统支持多少个软中断,静态定义的数组就会有多少个entry。____cacheline_aligned保证了在SMP的情况下,softirq_vec是对齐到cache line的。softirq描述符非常简单,只有一个action成员,表示如果触发了该softirq,那么应该调用action回调函数来处理这个soft irq。对于硬件中断而言,其mask、ack等都是和硬件寄存器相关并封装在irq chip函数中,对于softirq,没有硬件寄存器,只有“软件寄存器”,定义如下:

typedef struct { unsigned int __softirq_pending;
#ifdef CONFIG_SMP unsigned int ipi_irqs[NR_IPI];
#endif
} ____cacheline_aligned irq_cpustat_t;irq_cpustat_t irq_stat[NR_CPUS] ____cacheline_aligned;

ipi_irqs这个成员用于处理器之间的中断,我们留到下一个专题来描述。__softirq_pending就是这个“软件寄存器”。softirq采用谁触发,谁负责处理的。例如:当一个驱动的硬件中断被分发给了指定的CPU,并且在该中断handler中触发了一个softirq,那么该CPU负责调用该softirq number对应的action callback来处理该软中断。因此,这个“软件寄存器”应该是每个CPU拥有一个(专业术语叫做banked register)。为了性能,irq_stat中的每一个entry被定义对齐到cache line

2.3 Softirq注册

通过调用open_softirq接口函数可以注册softirq的action callback函数,具体如下:

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

softirq_vec是一个多CPU之间共享的数据,不过,由于所有的注册都是在系统初始化的时候完成的,那时候,系统是串行执行的。此外,softirq是静态定义的,每个entry(或者说每个softirq number)都是固定分配的,因此,不需要保护。

2.4 Softirq触发

在linux kernel中,可以调用raise_softirq这个接口函数来触发本地CPU上的softirq,具体如下:

void raise_softirq(unsigned int nr)
{ unsigned long flags;local_irq_save(flags); raise_softirq_irqoff(nr); local_irq_restore(flags);
}

虽然大部分的使用场景都是在中断handler中(也就是说关闭本地CPU中断)来执行softirq的触发动作,但是,这不是全部,在其他的上下文中也可以调用raise_softirq。因此,触发softirq的接口函数有两个版本,一个是raise_softirq,有关中断的保护,另外一个是raise_softirq_irqoff,调用者已经关闭了中断,不需要关中断来保护“soft irq status register”。
所谓trigger softirq,就是在__softirq_pending(也就是上面说的soft irq status register)的某个bit置一。从上面的定义可知,__softirq_pending是per cpu的,因此不需要考虑多个CPU的并发,只要disable本地中断,就可以确保对,__softirq_pending操作的原子性。

具体raise_softirq_irqoff的代码如下:

inline void raise_softirq_irqoff(unsigned int nr)
{ __raise_softirq_irqoff(nr); ----------------(1)if (!in_interrupt()) wakeup_softirqd();------------------(2)
}

(1)__raise_softirq_irqoff函数设定本CPU上的__softirq_pending的某个bit等于1,具体的bit是由soft irq number(nr参数)指定的。

(2)如果在中断上下文,我们只要set __softirq_pending的某个bit就OK了,在中断返回的时候自然会进行软中断的处理。但是,如果在context上下文调用这个函数的时候,我们必须要调用wakeup_softirqd函数用来唤醒本CPU上的softirqd这个内核线程。具体softirqd的内容请参考下一个章节。
2.5 Softirq disable/enable
在linux kernel中,可以使用local_irq_disable和local_irq_enable来disable和enable本CPU中断。和硬件中断一样,软中断也可以disable,接口函数是local_bh_disable和local_bh_enable。虽然和想像的local_softirq_enable/disable有些出入,不过bh这个名字更准确反应了该接口函数的意涵,因为local_bh_disable/enable函数就是用来disable/enable bottom half的,这里就包括softirq和tasklet。

先看disable吧,毕竟禁止bottom half比较简单:

static inline void local_bh_disable(void)
{ __local_bh_disable_ip(_THIS_IP_, SOFTIRQ_DISABLE_OFFSET);
}static __always_inline void __local_bh_disable_ip(unsigned long ip, unsigned int cnt)
{ preempt_count_add(cnt); barrier();
}

看起来disable bottom half比较简单,就是讲current thread info上的preempt_count成员中的softirq count的bit field9~15加上一就OK了。

enable函数比较复杂,如下:

static inline void local_bh_enable(void)
{ __local_bh_enable_ip(_THIS_IP_, SOFTIRQ_DISABLE_OFFSET);
}void __local_bh_enable_ip(unsigned long ip, unsigned int cnt)
{ WARN_ON_ONCE(in_irq() || irqs_disabled());-----------(1) preempt_count_sub(cnt - 1); ------------------(2)if (unlikely(!in_interrupt() && local_softirq_pending())) { -------(3) do_softirq(); }preempt_count_dec(); ---------------------(4) preempt_check_resched();
}

(1)disable/enable bottom half是一种内核同步机制。在硬件中断的handler(top half)中,不应该调用disable/enable bottom half函数来保护共享数据,因为bottom half其实是不可能抢占top half的。同样的,soft irq也不会抢占另外一个soft irq的执行,也就是说,一旦一个softirq handler被调度执行(无论在哪一个processor上),那么,本地的softirq handler都无法抢占其运行,要等到当前的softirq handler运行完毕后,才能执行下一个soft irq handler。注意:上面我们说的是本地,是local,softirq handler是可以在多个CPU上同时运行的,但是,linux kernel中没有disable all softirq的接口函数(就好像没有disable all CPU interrupt的接口一样,注意体会local_bh_enable/disable中的local的含义)。

说了这么多,一言以蔽之,local_bh_enable/disable是给进程上下文使用的,用于防止softirq handler抢占local_bh_enable/disable之间的临界区的。

irqs_disabled接口函数可以获知当前本地CPU中断是否是disable的,如果返回1,那么当前是disable 本地CPU的中断的。如果irqs_disabled返回1,有可能是下面这样的代码造成的:

local_irq_disable();……
local_bh_disable();……local_bh_enable();
……
local_irq_enable();

本质上,关本地中断是一种比关本地bottom half更强劲的锁,关本地中断实际上是禁止了top half和bottom half抢占当前进程上下文的运行。也许你会说:这也没有什么,就是有些浪费,至少代码逻辑没有问题。但事情没有这么简单,在local_bh_enable—>do_softirq—>__do_softirq中,有一条无条件打开当前中断的操作,也就是说,原本想通过local_irq_disable/local_irq_enable保护的临界区被破坏了,其他的中断handler可以插入执行,从而无法保证local_irq_disable/local_irq_enable保护的临界区的原子性,从而破坏了代码逻辑。

in_irq()这个函数如果不等于0的话,说明local_bh_enable被irq_enter和irq_exit包围,也就是说在中断handler中调用了local_bh_enable/disable。这道理是和上面类似的,这里就不再详细描述了。

(2)在local_bh_disable中我们为preempt_count增加了SOFTIRQ_DISABLE_OFFSET,在local_bh_enable函数中应该减掉同样的数值。这一步,我们首先减去了(SOFTIRQ_DISABLE_OFFSET-1),为何不一次性的减去SOFTIRQ_DISABLE_OFFSET呢?考虑下面运行在进程上下文的代码场景:

……local_bh_disable……需要被保护的临界区……local_bh_enable……

在临界区内,有进程context 和softirq共享的数据,因此,在进程上下文中使用local_bh_enable/disable进行保护。假设在临界区代码执行的时候,发生了中断,由于代码并没有阻止top half的抢占,因此中断handler会抢占当前正在执行的thread。在中断handler中,我们raise了softirq,在返回中断现场的时候,由于disable了bottom half,因此虽然触发了softirq,但是不会调度执行。因此,代码返回临界区继续执行,直到local_bh_enable。一旦enable了bottom half,那么之前raise的softirq就需要调度执行了,因此,这也是为什么在local_bh_enable会调用do_softirq函数。

调用do_softirq函数来处理pending的softirq的时候,当前的task是不能被抢占的,因为一旦被抢占,下一次该task被调度运行的时候很可能在其他的CPU上去了(还记得吗?softirq的pending 寄存器是per cpu的)。因此,我们不能一次性的全部减掉,那样的话有可能preempt_count等于0,那样就允许抢占了。因此,这里减去了(SOFTIRQ_DISABLE_OFFSET-1),既保证了softirq count的bit field9~15被减去了1,又保持了preempt disable的状态。

(3)如果当前不是interrupt context的话,并且有pending的softirq,那么调用do_softirq函数来处理软中断。

(4)该来的总会来,在step 2中我们少减了1,这里补上,其实也就是preempt count-1。

(5)在softirq handler中很可能wakeup了高优先级的任务,这里最好要检查一下,看看是否需要进行调度,确保高优先级的任务得以调度执行。

2.6 Softirq的处理

我们说softirq是一种defering task的机制,也就是说top half没有做的事情,需要延迟到bottom half中来执行。那么具体延迟到什么时候呢?这是本节需要讲述的内容,也就是说soft irq是如何调度执行的。

在上一节已经描述一个softirq被调度执行的场景,本节主要关注在中断返回现场时候调度softirq的场景。我们来看中断退出的代码,具体如下:

void irq_exit(void)
{
…… if (!in_interrupt() && local_softirq_pending()) invoke_softirq();……
}

代码中“!in_interrupt()”这个条件可以确保下面的场景不会触发sotfirq的调度:

(1)中断handler是嵌套的。也就是说本次irq_exit是退出到上一个中断handler。当然,在新的内核中,这种情况一般不会发生,因为中断handler都是关中断执行的。

(2)本次中断是中断了softirq handler的执行。也就是说本次irq_exit是不是退出到进程上下文,而是退出到上一个softirq context。这一点也保证了在一个CPU上的softirq是串行执行的(注意:多个CPU上还是有可能并发的)

我们继续看invoke_softirq的代码:

static inline void invoke_softirq(void)
{ if (!force_irqthreads) {
#ifdef CONFIG_HAVE_IRQ_EXIT_ON_IRQ_STACK __do_softirq();
#else do_softirq_own_stack();
#endif } else { wakeup_softirqd(); }
}

force_irqthreads是和强制线程化相关的,主要用于interrupt handler的调试(一般而言,在线程环境下比在中断上下文中更容易收集调试数据)。如果系统选择了对所有的interrupt handler进行线程化处理,那么softirq也没有理由在中断上下文中处理(中断handler都在线程中执行了,softirq怎么可能在中断上下文中执行)。本身invoke_softirq这个函数是在中断上下文中被调用的,如果强制线程化,那么系统中所有的软中断都在sofirq的daemon进程中被调度执行。

如果没有强制线程化,softirq的处理也分成两种情况,主要是和softirq执行的时候使用的stack相关。如果arch支持单独的IRQ STACK,这时候,由于要退出中断,因此irq stack已经接近全空了(不考虑中断栈嵌套的情况,因此新内核下,中断不会嵌套),因此直接调用__do_softirq()处理软中断就OK了,否则就调用do_softirq_own_stack函数在softirq自己的stack上执行。当然对ARM而言,softirq的处理就是在当前的内核栈上执行的,因此do_softirq_own_stack的调用就是调用__do_softirq(),代码如下(删除了部分无关代码):

asmlinkage void __do_softirq(void)
{……pending = local_softirq_pending();----------获取softirq pending的状态__local_bh_disable_ip(_RET_IP_, SOFTIRQ_OFFSET);-标识下面的代码是正在处理softirqcpu = smp_processor_id();
restart: set_softirq_pending(0); ---------清除pending标志local_irq_enable(); ------打开中断,softirq handler是开中断执行的h = softirq_vec; -------获取软中断描述符指针while ((softirq_bit = ffs(pending))) {----寻找pending中第一个被设定为1的bit unsigned int vec_nr; int prev_count;h += softirq_bit - 1; ------指向pending的那个软中断描述符vec_nr = h - softirq_vec;----获取soft irq number h->action(h);---------指向softirq handler h++; pending >>= softirq_bit; }local_irq_disable(); -------关闭本地中断pending = local_softirq_pending();----------(注1) if (pending) { if (time_before(jiffies, end) && !need_resched() && --max_restart) goto restart;wakeup_softirqd(); }__local_bh_enable(SOFTIRQ_OFFSET);----------标识softirq处理完毕
}

(注1)再次检查softirq pending,有可能上面的softirq handler在执行过程中,发生了中断,又raise了softirq。如果的确如此,那么我们需要跳转到restart那里重新处理soft irq。当然,也不能总是在这里不断的loop,因此linux kernel设定了下面的条件:
(1)softirq的处理时间没有超过2个ms
(2)上次的softirq中没有设定TIF_NEED_RESCHED,也就是说没有有高优先级任务需要调度
(3)loop的次数小于 10次

因此,只有同时满足上面三个条件,程序才会跳转到restart那里重新处理soft irq。否则wakeup_softirqd就OK了。这样的设计也是一个平衡的方案。一方面照顾了调度延迟:本来,发生一个中断,系统期望在限定的时间内调度某个进程来处理这个中断,如果softirq handler不断触发,其实linux kernel是无法保证调度延迟时间的。另外一方面,也照顾了硬件的thoughput:已经预留了一定的时间来处理softirq。

Linux 中断子系统之softirq相关推荐

  1. Linux中断子系统 - softirq

    本文基于linux4.6.3内核版本代码来说明softirq机制,代码在kernel/softirq.c中,代码不算多也就近800行.在中断处理中,分上半部和下半部,有一些任务不是特别紧急的,没必要在 ...

  2. Linux中断子系统

    首先感谢原文作者 LoyenWang 的分享,可以点击章节阅读原作者原文,或者查看本文的转载地址,再次感谢原作者分享,已经在公众号上征得作者同意. 说明: Kernel版本:4.14 ARM64处理器 ...

  3. linux 中断子系统

    linux 中断子系统 1,异常和中断 1.1 中断引入的必要性 1.2 同步异常 1.2.1 同步异常 1.2.2 同步异常的种类 1.3 异步异常 1.3.1 异步异常 1.3.2 异步异常的种类 ...

  4. Linux中断子系统(三)之GIC中断处理过程

    Linux中断子系统(三)之GIC中断处理过程 备注:   1. Kernel版本:5.4   2. 使用工具:Source Insight 4.0   3. 参考博客: Linux中断子系统(一)中 ...

  5. Linux中断子系统-通用框架处理

    背景 Kernel版本:4.14 ARM64处理器,Contex-A53,双核 使用工具:Source Insight 3.5, Visio 1. 概述 <Linux中断子系统(一)-中断控制器 ...

  6. 漫画-Linux中断子系统综述

    1.中断引发的面试教训 2.什么是中断? 中断: (英语:Interrupt)指当出现需要时,CPU暂时停止当前程序的执行转而执行处理新情况的程序和执行过程. 即在程序运行过程中,系统出现了一个必须由 ...

  7. linux中断子系统(基于imx6ul arm32分析)

    0.说明 本文主要针对linux内核中断整个框架进行梳理,针对的是armv7架构,硬件平台是imx6ul,基于arm GIC控制器来分析. GIC是arm公司设计使用的中断控制器,全称Global I ...

  8. Linux中断子系统(一)中断控制器GIC架构

    Linux中断子系统(一)中断控制器GIC架构 备注:   1. Kernel版本:5.4   2. 使用工具:Source Insight 4.0   3. 参考博客: Linux中断子系统(一)中 ...

  9. Linux中断子系统(二)中断控制器GIC驱动分析

    Linux中断子系统(二)中断控制器GIC驱动分析 备注:   1. Kernel版本:5.4   2. 使用工具:Source Insight 4.0   3. 参考博客: Linux中断子系统(一 ...

最新文章

  1. 学计算机的好处处300字,学计算机的心得体会300字
  2. 批量修改table和index 的表空间
  3. Java通过泛型的模板类型实例化对象
  4. float与double类型参数区别_8大基本数据类型及包装类,不知道这些点别说自己是大佬...
  5. 黑科技揭秘:百种异常随机注入,专有云为何稳如泰山
  6. 漫步数学分析三十二——可微映射的连续性
  7. MFCC特征提取过程详解
  8. gvim的常用编辑快捷键
  9. ListView 复用学习
  10. 个人作业week7——前端开发感想总结
  11. 微pe工具箱 系统安装教程_微pe工具箱怎么装系统
  12. 【垂直切换】TD-SCDMA与TD-LTE异构网络垂直切换仿真
  13. java 幸运大转盘_幸运大转盘抽奖 抽奖算法 程序实现逻辑
  14. 机器学习系列5---偏差和方差分解
  15. java 二进制加减_二进制加法Java实现
  16. 厦门大学计算机研究生2020专业目录,报录比|厦门大学各院系专业2020年硕士生报考录取数据统计表...
  17. js设置北京时区_JavaScript 实现北京时间转其他时区时间,根据系统对对应时区转换...
  18. 研究杜比视界和HDR近两个月后的各种经验和故事
  19. 黑群晖NAS (ARPL引导)安装教程
  20. hibernate学习笔记(总结)

热门文章

  1. 蚂蚁金服人工智能部技术总监李小龙:智能金融实践
  2. 中软计算机培训怎样,在中软计算机培训之后的工资好不好?
  3. 吐槽一下讯飞R1电纸书阅读器
  4. 《Fast unfolding of communities in large networks》论文阅读
  5. office2010连接服务器响应慢,从网络位置打开文件时,Office 运行缓慢或停止响应 - Office | Microsoft Docs...
  6. 青龙面板APP: 1.7版本(2023-05-03)
  7. SaleSmartly(ss客服)怎么玩转Instagram自动化?
  8. 无net.exe 加用户vbs
  9. QT横板格斗小游戏——基于网编的重构
  10. Java找工作为什么越来越难,有什么技巧嘛?