linux kernel的中断子系统之(八):softirq
作者:linuxer 发布于:2014-10-24 11:53 分类:中断子系统
一、前言
对于中断处理而言,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(时间方面要求高)。本文的第二章讨论了softirq和tasklet这两种机制有何不同,分别适用于什么样的场景。第三章描述了一些context的概念,这是要理解后续内容的基础。第四章是进入softirq的实现,对比hard irq来解析soft irq的注册、触发,调度的过程。
注:本文中的linux kernel的版本是3.14
二、为何有softirq和tasklet
1、为何有top half和bottom half
中断处理模块是任何OS中最重要的一个模块,对系统的性能会有直接的影响。想像一下:如果在通过U盘进行大量数据拷贝的时候,你按下一个key,需要半秒的时间才显示出来,这个场景是否让你崩溃?因此,对于那些复杂的、需要大量数据处理的硬件中断,我们不能让handler中处理完一切再恢复现场(handler是全程关闭中断的),而是仅仅在handler中处理一部分,具体包括:
(1)有实时性要求的
(2)和硬件相关的。例如ack中断,read HW FIFO to ram等
(3)如果是共享中断,那么获取硬件中断状态以便判断是否是本中断发生
除此之外,其他的内容都是放到bottom half中处理。在把中断处理过程划分成top half和bottom half之后,关中断的top half被瘦身,可以非常快速的执行完毕,大大减少了系统关中断的时间,提高了系统的性能。
我们可以基于下面的系统进一步的进行讨论:
当网卡控制器的FIFO收到的来自以太网的数据的时候(例如半满的时候,可以软件设定),可以将该事件通过irq signal送达Interrupt Controller。Interrupt Controller可以把中断分发给系统中的Processor A or B。
NIC的中断处理过程大概包括:mask and ack interrupt controller-------->ack NIC-------->copy FIFO to ram------>handle Data in the ram----------->unmask interrupt controller
我们先假设Processor A处理了这个网卡中断事件,于是NIC的中断handler在Processor A上欢快的执行,这时候,Processor A的本地中断是disable的。NIC的中断handler在执行的过程中,网络数据仍然源源不断的到来,但是,如果NIC的中断handler不操作NIC的寄存器来ack这个中断的话,NIC是不会触发下一次中断的。还好,我们的NIC interrupt handler总是在最开始就会ack,因此,这不会导致性能问题。ack之后,NIC已经具体再次trigger中断的能力。当Processor A上的handler 在处理接收来自网络的数据的时候,NIC的FIFO很可能又收到新的数据,并trigger了中断,这时候,Interrupt controller还没有umask,因此,即便还有Processor B(也就是说有处理器资源),中断控制器也无法把这个中断送达处理器系统。因此,只能眼睁睁的看着NIC FIFO填满数据,数据溢出,或者向对端发出拥塞信号,无论如何,整体的系统性能是受到严重的影响。
注意:对于新的interrupt controller,可能没有mask和umask操作,但是原理是一样的,只不过NIC的handler执行完毕要发生EOI而已。
要解决上面的问题,最重要的是尽快的执行完中断handler,打开中断,unmask IRQ(或者发送EOI),方法就是把耗时的handle Data in the ram这个步骤踢出handler,让其在bottom half中执行。
2、为何有softirq和tasklet
OK,linux kernel已经把中断处理分成了top half和bottom half,看起来已经不错了,那为何还要提供softirq、tasklet和workqueue这些bottom half机制,linux kernel本来就够复杂了,bottom half还来添乱。实际上,在早期的linux kernel还真是只有一个bottom half机制,简称BH,简单好用,但是性能不佳。后来,linux kernel的开发者开发了task queue机制,试图来替代BH,当然,最后task queue也消失在内核代码中了。现在的linux kernel提供了三种bottom half的机制,来应对不同的需求。
workqueue和softirq、tasklet有本质的区别:workqueue运行在process context,而softirq和tasklet运行在interrupt context。因此,出现workqueue是不奇怪的,在有sleep需求的场景中,defering task必须延迟到kernel thread中执行,也就是说必须使用workqueue机制。softirq和tasklet是怎么回事呢?从本质上将,bottom half机制的设计有两方面的需求,一个是性能,一个是易用性。设计一个通用的bottom half机制来满足这两个需求非常的困难,因此,内核提供了softirq和tasklet两种机制。softirq更倾向于性能,而tasklet更倾向于易用性。
我们还是进入实际的例子吧,还是使用上一节的系统图。在引入softirq之后,网络数据的处理如下:
关中断:mask and ack interrupt controller-------->ack NIC-------->copy FIFO to ram------>raise softirq------>unmask interrupt controller
开中断:在softirq上下文中进行handle Data in the ram的动作
同样的,我们先假设Processor A处理了这个网卡中断事件,很快的完成了基本的HW操作后,raise softirq。在返回中断现场前,会检查softirq的触发情况,因此,后续网络数据处理的softirq在processor A上执行。在执行过程中,NIC硬件再次触发中断,Interrupt controller将该中断分发给processor B,执行动作和Processor A是类似的,因此,最后,网络数据处理的softirq在processor B上执行。
为了性能,同一类型的softirq有可能在不同的CPU上并发执行,这给使用者带来了极大的痛苦,因为驱动工程师在撰写softirq的回调函数的时候要考虑重入,考虑并发,要引入同步机制。但是,为了性能,我们必须如此。
当网络数据处理的softirq同时在Processor A和B上运行的时候,网卡中断又来了(可能是10G的网卡吧)。这时候,中断分发给processor A,这时候,processor A上的handler仍然会raise softirq,但是并不会调度该softirq。也就是说,softirq在一个CPU上是串行执行的。这种情况下,系统性能瓶颈是CPU资源,需要增加更多的CPU来解决该问题。
如果是tasklet的情况会如何呢?为何tasklet性能不如softirq呢?如果一个tasklet在processor A上被调度执行,那么它永远也不会同时在processor B上执行,也就是说,tasklet是串行执行的(注:不同的tasklet还是会并发的),不需要考虑重入的问题。我们还是用网卡这个例子吧(注意:这个例子仅仅是用来对比,实际上,网络数据是使用softirq机制的),同样是上面的系统结构图。假设使用tasklet,网络数据的处理如下:
关中断:mask and ack interrupt controller-------->ack NIC-------->copy FIFO to ram------>schedule tasklet------>unmask interrupt controller
开中断:在softirq上下文中(一般使用TASKLET_SOFTIRQ这个softirq)进行handle Data in the ram的动作
同样的,我们先假设Processor A处理了这个网卡中断事件,很快的完成了基本的HW操作后,schedule tasklet(同时也就raise TASKLET_SOFTIRQ softirq)。在返回中断现场前,会检查softirq的触发情况,因此,在TASKLET_SOFTIRQ softirq的handler中,获取tasklet相关信息并在processor A上执行该tasklet的handler。在执行过程中,NIC硬件再次触发中断,Interrupt controller将该中断分发给processor B,执行动作和Processor A是类似的,虽然TASKLET_SOFTIRQ softirq在processor B上可以执行,但是,在检查tasklet的状态的时候,如果发现该tasklet在其他processor上已经正在运行,那么该tasklet不会被处理,一直等到在processor A上的tasklet处理完,在processor B上的这个tasklet才能被执行。这样的串行化操作虽然对驱动工程师是一个福利,但是对性能而言是极大的损伤。
三、理解softirq需要的基础知识(各种context)
1、preempt_count
为了更好的理解下面的内容,我们需要先看看一些基础知识:一个task的thread info数据结构定义如下(只保留和本场景相关的内容):
struct thread_info {
……
int preempt_count; /* 0 => preemptable, <0 => bug */
……
};
preempt_count这个成员被用来判断当前进程是否可以被抢占。如果preempt_count不等于0(可能是代码调用preempt_disable显式的禁止了抢占,也可能是处于中断上下文等),说明当前不能进行抢占,如果preempt_count等于0,说明已经具备了抢占的条件(当然具体是否要抢占当前进程还是要看看thread info中的flag成员是否设定了_TIF_NEED_RESCHED这个标记,可能是当前的进程的时间片用完了,也可能是由于中断唤醒了优先级更高的进程)。 具体preempt_count的数据格式可以参考下图:
preemption count用来记录当前被显式的禁止抢占的次数,也就是说,每调用一次preempt_disable,preemption count就会加一,调用preempt_enable,该区域的数值会减去一。preempt_disable和preempt_enable必须成对出现,可以嵌套,最大嵌套的深度是255。
hardirq count描述当前中断handler嵌套的深度。对于ARM平台的linux kernel,其中断部分的代码如下:
void handle_IRQ(unsigned int irq, struct pt_regs *regs)
{
struct pt_regs *old_regs = set_irq_regs(regs);irq_enter();
generic_handle_irq(irq);irq_exit();
set_irq_regs(old_regs);
}
通用的IRQ handler被irq_enter和irq_exit这两个函数包围。irq_enter说明进入到IRQ context,而irq_exit则说明退出IRQ context。在irq_enter函数中会调用preempt_count_add(HARDIRQ_OFFSET),为hardirq count的bit field增加1。在irq_exit函数中,会调用preempt_count_sub(HARDIRQ_OFFSET),为hardirq count的bit field减去1。hardirq count占用了4个bit,说明硬件中断handler最大可以嵌套15层。在旧的内核中,hardirq count占用了12个bit,支持4096个嵌套。当然,在旧的kernel中还区分fast interrupt handler和slow interrupt handler,中断handler最大可以嵌套的次数理论上等于系统IRQ的个数。在实际中,这个数目不可能那么大(内核栈就受不了),因此,即使系统支持了非常大的中断个数,也不可能各个中断依次嵌套,达到理论的上限。基于这样的考虑,后来内核减少了hardirq count占用bit数目,改成了10个bit(在general arch的代码中修改为10,实际上,各个arch可以redefine自己的hardirq count的bit数)。但是,当内核大佬们决定废弃slow interrupt handler的时候,实际上,中断的嵌套已经不会发生了。因此,理论上,hardirq count要么是0,要么是1。不过呢,不能总拿理论说事,实际上,万一有写奇葩或者老古董driver在handler中打开中断,那么这时候中断嵌套还是会发生的,但是,应该不会太多(一个系统中怎么可能有那么多奇葩呢?呵呵),因此,目前hardirq count占用了4个bit,应付15个奇葩driver是妥妥的。
对softirq count进行操作有两个场景:
(1)也是在进入soft irq handler之前给 softirq count加一,退出soft irq handler之后给 softirq count减去一。由于soft irq handler在一个CPU上是不会并发的,总是串行执行,因此,这个场景下只需要一个bit就够了,也就是上图中的bit 8。通过该bit可以知道当前task是否在sofirq context。
(2)由于内核同步的需求,进程上下文需要禁止softirq。这时候,kernel提供了local_bh_enable和local_bh_disable这样的接口函数。这部分的概念是和preempt disable/enable类似的,占用了bit9~15,最大可以支持127次嵌套。
2、一个task的各种上下文
看完了preempt_count之后,我们来介绍各种context:
#define in_irq() (hardirq_count())
#define in_softirq() (softirq_count())
#define in_interrupt() (irq_count())#define in_serving_softirq() (softirq_count() & SOFTIRQ_OFFSET)
这里首先要介绍的是一个叫做IRQ context的术语。这里的IRQ context其实就是hard irq context,也就是说明当前正在执行中断handler(top half),只要preempt_count中的hardirq count大于0(=1是没有中断嵌套,如果大于1,说明有中断嵌套),那么就是IRQ context。
softirq context并没有那么的直接,一般人会认为当sofirq handler正在执行的时候就是softirq context。这样说当然没有错,sofirq handler正在执行的时候,会增加softirq count,当然是softirq context。不过,在其他context的情况下,例如进程上下文中,有有可能因为同步的要求而调用local_bh_disable,这时候,通过local_bh_disable/enable保护起来的代码也是执行在softirq context中。当然,这时候其实并没有正在执行softirq handler。如果你确实想知道当前是否正在执行softirq handler,in_serving_softirq可以完成这个使命,这是通过操作preempt_count的bit 8来完成的。
所谓中断上下文,就是IRQ context + softirq context+NMI context。
四、softirq机制
softirq和hardirq(就是硬件中断啦)是对应的,因此softirq的机制可以参考hardirq对应理解,当然softirq是纯软件的,不需要硬件参与。
1、softirq number
和IRQ number一样,对于软中断,linux kernel也是用一个softirq number唯一标识一个softirq,具体定义如下:
enum
{
HI_SOFTIRQ=0,
TIMER_SOFTIRQ,
NET_TX_SOFTIRQ,
NET_RX_SOFTIRQ,
BLOCK_SOFTIRQ,
BLOCK_IOPOLL_SOFTIRQ,
TASKLET_SOFTIRQ,
SCHED_SOFTIRQ,
HRTIMER_SOFTIRQ,
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的。这些具体使用情景分析会在各自的子系统中分析,本文只是描述softirq的工作原理。
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。
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)都是固定分配的,因此,不需要保护。
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的内容请参考下一个章节。
5、disable/enable softirq
在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了。barrier是优化屏障(Optimization barrier),会在内核同步系列文章中描述。
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了高优先级的任务,这里最好要检查一下,看看是否需要进行调度,确保高优先级的任务得以调度执行。
5、如何处理一个被触发的soft irq
我们说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);---标识下面的代码是正在处理softirq
cpu = 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。
原创文章,转发请注明出处。蜗窝科技
http://www.wowotech.net/linux_kenrel/soft-irq.html

评论:
2016-10-07 09:41
-----------
这里说的mask或者ack某个IRQ number的操作是通过操作中断控制器(例如GIC)的寄存器来完成的。
既然流程第一步已经进行了执行了mask,为什么rcu_irq_exit 还会进行关闭中断的操作啊?硬中断处理中关闭中断的操作是硬件实现的?
---------------
rcu_irq_exit中的中断操作是成对的,分别是local_irq_save和local_irq_restore,都是操作的CPU中断,而mask操作是disable某个外设的IRQ,操作的是中断控制器,是不一样的。
综上所述的情况,我们说softirq和tasklet运行在中断上下文是不是有些不恰当?
--------------
这是一个见仁见智的问题,不同的人对中断上下文理解不同,我对中断上下文的理解就是当前执行那一点的current thread和当前执行路径是没有任何的关系
关于第三个问题,你的描述是正确的。
2016-09-07 14:09
{
unsigned long flags;
local_irq_save(flags);
raise_softirq_irqoff(nr);
local_irq_restore(flags);
}
linuxer问个问题,既然该函数在中断处理函数中调用,这时候中断已经关闭了,那还在关闭中断,是不是多余的操作?
2016-09-07 17:39
2016-07-12 16:13
runtime PM resume的时候(__pm_runtime_resume),会关中断(spin_lock_irqsave),但是,在resume的过程中,调到devfreq_simple_ondemand_handler-->devfreq_monitor_resume了,而在devfreq_monitor_resume中,有mutex_lock的调用。就出现上面的警告了。你查一下为什么会有这个流程。
PS:mali是ARM的GPU,看情况,问题都出在相关的代码中,你可以检查一下代码。
2016-07-12 20:21
而根据打印出来的信息: in_atomic(): 0 和irqs_disabled(): 128
irqs_disabled(): 128,说明中断是被屏蔽了。 in_atomic(): 0说明目前处于原子操作上下文中。
而进入might_sleep的条件:
if ((preempt_count_equals(preempt_offset) && !irqs_disabled() &&
!is_idle_task(current)) || oops_in_progress)
return;
说明irqs_disabled(): 128 导致的。
这个问题出现概率极高,每次测试都会出现。目前还没有解决。根据整个执行流程来看,全部是kernel原生的代码,没有自己写的代码。
2016-07-04 15:13
c0 BUG: sleeping function called from invalid context at /space/builder/repo/sprdroid6.0_trunk_k318_dev/kernel/mm/slub.c:1240
c0 in_atomic(): 0, irqs_disabled(): 0, pid: 1, name: init
c0 Preemption disabled at:[<ffffffc0009cc934>] printk+0x7c/0x8c
根据看源代码,感觉进入sleep的dump流程是因为抢占被禁止了。还请楼主帮帮忙。
2016-07-04 16:38
可能是你再atomic区域(中断、softirq等)中,调用了内存分配的函数(最终会调到slab_alloc),而内存分配可能会睡眠。
2016-07-12 11:12
[01-02 07:45:29.883] <3>[ 8.510125] c0 BUG: sleeping function called from invalid context at /space/builder/repo/sprdroid6.0_trunk_k318_dev/kernel/mm/slub.c:1240
[01-02 07:45:29.883] <3>[ 8.510150] c0 in_atomic(): 0, irqs_disabled(): 0, pid: 1, name: init
[01-02 07:45:29.883] <3>[ 8.510170] c0 Preemption disabled at:[<ffffffc0009cc934>] printk+0x7c/0x8c
[01-02 07:45:29.883] <4>[ 8.510218] c0
[01-02 07:45:29.883] <4>[ 8.510242] c0 CPU: 0 PID: 1 Comm: init Tainted: G W O 3.18.12+ #1
[01-02 07:45:29.883] <4>[ 8.510263] c0 Hardware name
[01-02 07:45:29.884] <0>[ 8.510282] c0 Call trace:
[01-02 07:45:29.884] <38>[ 8.510987] c4 logd.auditd: policy loaded
[01-02 07:45:29.884] <4>[ 8.512944] c0 [<ffffffc00008b480>] dump_backtrace+0x0/0x164
[01-02 07:45:29.884] <4>[ 8.512958] c0 [<ffffffc00008b600>] show_stack+0x1c/0x28
[01-02 07:45:29.884] <4>[ 8.512972] c0 [<ffffffc0009cd570>] dump_stack+0x74/0xb8
[01-02 07:45:29.885] <4>[ 8.512988] c0 [<ffffffc0000e3414>] __might_sleep+0x198/0x1a0
[01-02 07:45:29.885] <4>[ 8.513003] c0 [<ffffffc0001e2e68>] __kmalloc_track_caller+0x220/0x2ac
[01-02 07:45:29.885] <4>[ 8.513017] c0 [<ffffffc0004a8fe0>] devm_kmalloc+0x38/0x84
[01-02 07:45:29.885] <4>[ 8.513032] c0 [<ffffffc000666954>] of_devfreq_cooling_register_power+0xfc/0x314
[01-02 07:45:29.885] <4>[ 8.513060] c0 [<ffffffbffc02b27c>] kbase_devfreq_init+0x268/0x2dc [mali_kbase]
[01-02 07:45:29.885] <4>[ 8.513092] c0 [<ffffffbffc018658>] kbase_release_device+0x38e0/0x3c2c [mali_kbase]
[01-02 07:45:29.885] <4>[ 8.513106] c0 [<ffffffc0004a7610>] platform_drv_probe+0x50/0xcc
[01-02 07:45:29.885] <4>[ 8.513120] c0 [<ffffffc0004a52c4>] driver_probe_device+0xb4/0x3c8
[01-02 07:45:29.885] <4>[ 8.513132] c0 [<ffffffc0004a56dc>] __driver_attach+0xa8/0xb0
[01-02 07:45:29.885] <4>[ 8.513145] c0 [<ffffffc0004a3150>] bus_for_each_dev+0x68/0xac
[01-02 07:45:29.885] <4>[ 8.513158] c0 [<ffffffc0004a4c90>] driver_attach+0x28/0x34
[01-02 07:45:29.885] <4>[ 8.513170] c0 [<ffffffc0004a4894>] bus_add_driver+0x16c/0x248
[01-02 07:45:29.885] <4>[ 8.513183] c0 [<ffffffc0004a6074>] driver_register+0x6c/0x138
[01-02 07:45:29.885] <4>[ 8.513196] c0 [<ffffffc0004a7528>] __platform_driver_register+0x74/0x84
[01-02 07:45:29.885] <4>[ 8.513222] c0 [<ffffffbffc04b01c>] init_module+0x1c/0x28 [mali_kbase]
[01-02 07:45:29.885] <4>[ 8.513235] c0 [<ffffffc000082acc>] do_one_initcall+0xcc/0x1e8
[01-02 07:45:29.885] <4>[ 8.513248] c0 [<ffffffc00013b1cc>] load_module+0x1728/0x1e08
[01-02 07:45:29.886] <4>[ 8.513259] c0 [<ffffffc00013b998>] SyS_init_module+0xec/0x148
[01-02 07:45:29.887] <6>[ 8.513381] c0 mali 60000000.gpu: Dynamic power table: 850 MHz @ 800 mV: 124 = 124 mW
[01-02 07:45:29.887] <6>[ 8.513396] c0 mali 60000000.gpu: Dynamic power table: 768 MHz @ 800 mV: 112 = 112 mW
楼主,在稳定性测试中经常会发现类似问题,不知道如何解决,还请帮忙看看。谢谢。
2016-07-12 13:42
另外可以试试在___might_sleep中加打印,查一下具体的原因。
if ((preempt_count_equals(preempt_offset) && !irqs_disabled() &&
!is_idle_task(current)) ||
system_state != SYSTEM_RUNNING || oops_in_progress)
return;
if (time_before(jiffies, prev_jiffy + HZ) && prev_jiffy)
return;
prev_jiffy = jiffies;
2016-07-12 13:59
使用GFP_KERNEL这个标志分配内存是可以睡眠的,但是irqs_disabled(): 0,
而preempt_count_equals(preempt_offset)=0说明是有抢占或者中断被禁止了。
关键我没有头绪, 不知从何入手。 求指点。谢谢
2016-07-12 15:10
中间没有insomd,rmmod。 而mail是什么东西,我也不清楚。估计是上层的什么应用吧。
在稳定性测试中还有类似的情况:
BUG: sleeping function called from invalid context at /space/builder/repo/sprdroid6.0mr1_prime_k318_dev/kernel/kernel/locking/mutex.c:97
in_atomic(): 0, irqs_disabled(): 128, pid: 3909, name: mali-renderer
[01-01 08:56:28.356] <0>[ 2881.732806] c0 Call trace:
[01-01 08:56:28.356] <4>[ 2881.735432] c0 [<ffffffc00008b480>] dump_backtrace+0x0/0x164
[01-01 08:56:28.356] <4>[ 2881.735441] c0 [<ffffffc00008b600>] show_stack+0x1c/0x28
[01-01 08:56:28.356] <4>[ 2881.735453] c0 [<ffffffc0009d121c>] dump_stack+0x74/0xb8
[01-01 08:56:28.356] <4>[ 2881.735464] c0 [<ffffffc0000e33d4>] __might_sleep+0x158/0x1a0
[01-01 08:56:28.356] <4>[ 2881.735473] c0 [<ffffffc0009d9d48>] mutex_lock+0x28/0x50
[01-01 08:56:28.356] <4>[ 2881.735483] c0 [<ffffffc000740c58>] devfreq_monitor_resume+0x20/0xb0
[01-01 08:56:28.356] <4>[ 2881.735494] c0 [<ffffffc000742548>] devfreq_simple_ondemand_handler+0x80/0xc4
[01-01 08:56:28.356] <4>[ 2881.735502] c0 [<ffffffc000740998>] devfreq_resume_device+0x34/0x5c
[01-01 08:56:28.356] <4>[ 2881.735521] c0 [<ffffffbffc014df0>] kbase_release_device+0x78/0x3b90 [mali_kbase]
[01-01 08:56:28.356] <4>[ 2881.735532] c0 [<ffffffc0004acdc4>] pm_generic_runtime_resume+0x38/0x58
[01-01 08:56:28.356] <4>[ 2881.735541] c0 [<ffffffc0004aec70>] __rpm_callback+0x70/0x9c
[01-01 08:56:28.356] <4>[ 2881.735550] c0 [<ffffffc0004aeccc>] rpm_callback+0x30/0xa0
[01-01 08:56:28.356] <4>[ 2881.735558] c0 [<ffffffc0004afff4>] rpm_resume+0x424/0x5f0
[01-01 08:56:28.356] <4>[ 2881.735566] c0 [<ffffffc0004b0224>] __pm_runtime_resume+0x64/0xa0
[01-01 08:56:28.356] <4>[ 2881.735582] c0 [<ffffffbffc01ea20>] kbase_platform_fake_register+0x18c/0xef8 [mali_kbase]
[01-01 08:56:28.356] <4>[ 2881.735597] c0 [<ffffffbffc02868c>] kbase_pm_clock_on+0x9c/0x1ac [mali_kbase]
[01-01 08:56:28.356] <4>[ 2881.735612] c0 [<ffffffbffc026dd4>] kbase_pm_do_poweron+0x24/0x3c [mali_kbase]
[01-01 08:56:28.356] <4>[ 2881.735627] c0 [<ffffffbffc0295f0>] kbase_pm_update_active+0xa0/0x18c [mali_kbase]
[01-01 08:56:28.356] <4>[ 2881.735642] c0 [<ffffffbffc027128>] kbase_hwaccess_pm_gpu_active+0x1c/0x2c [mali_kbase]
[01-01 08:56:28.376] <4>[ 2881.735656] c0 [<ffffffbffc00d860>] kbase_pm_context_active_handle_suspend+0xbc/0xc4 [mali_kbase]
[01-01 08:56:28.376] <4>[ 2881.735670] c0 [<ffffffbffc00b300>] kbase_js_sched+0x148/0x86c [mali_kbase]
[01-01 08:56:28.376] <4>[ 2881.735682] c0 [<ffffffbffc0074e8>] kbase_jd_submit+0x318/0x3b4 [mali_kbase]
[01-01 08:56:28.376] <4>[ 2881.735696] c0 [<ffffffbffc017318>] kbase_release_device+0x25a0/0x3b90 [mali_kbase]
[01-01 08:56:28.376] <4>[ 2881.735708] c0 [<ffffffc000246d20>] compat_SyS_ioctl+0xbc/0x1688
都是进入了sleep中。
2016-06-02 11:02
由上面我的描述我感觉系统调用中的软中断跟本文中描述的软中断区别很大啊,能否解释一下这两种软中断的区别呢?
2016-05-06 23:12
2、软中断打断往往不会直接打断进程的调用线路,总是借由中断来完成,一般是中断发生,在handler中raise softirq,在irq_exit中check softirq的pending状态。
3、你再仔细看看softirqd,并不是它来调度执行softirqd的,本身softirq仍然是由hardirq raise,然后再调度的,只有在一些特殊情况下,softirqd才启动执行。
4、我的意思是如果关闭了中断,那么不会有中断handler,也就不会raise新的softirq,当然,我那么说可能是有些不严谨,我的意思是如果关闭了中断,那么不会新触发的softirq来抢占当前进程的执行。
2016-05-06 09:55
1. 就网卡中断的例子来讲,假如软中断执行的时候被硬件中断打断,怎么回到之前软中断执行的代码
处?软中断可不像进程那样有进程上下文!
2. 软中断打断当前进程的调用路线是怎么样的?包括打断和恢复!
3. 软中断处于中断上下文,但是却是由softirqd这个内核线程调度执行的(内核上下文),这不矛盾么?
3.有个地方写错了吧:
关本地中断实际上是禁止了top half和bottom half抢占当前进程上下文的运行
关本地中断只是禁止了top half吧 ^_^
2016-05-06 15:34
软中断处于中断上下文,但是却是由softirqd这个内核线程调度执行的(内核上下文),这不矛盾么?
应该是:
软中断处于中断上下文,但是却是由softirqd这个内核线程调度执行的(进程上下文),这不矛盾么?
2016-02-29 23:22
功能
最新评论
- wangjing
写得太好了 - wangjing
写得太好了! - DRAM
圖面都沒辦法顯示出來好像掛點了。 - Simbr
bus至少是不是还有个subsystem? - troy
@testtest:只要ldrex-modify-strex... - gh
Linux 内核在 sparse 内存模型基础上实现了vme...
文章分类
随机文章
文章存档
- 2025年4月(5)
- 2024年2月(1)
- 2023年5月(1)
- 2022年10月(1)
- 2022年8月(1)
- 2022年6月(1)
- 2022年5月(1)
- 2022年4月(2)
- 2022年2月(2)
- 2021年12月(1)
- 2021年11月(5)
- 2021年7月(1)
- 2021年6月(1)
- 2021年5月(3)
- 2020年3月(3)
- 2020年2月(2)
- 2020年1月(3)
- 2019年12月(3)
- 2019年5月(4)
- 2019年3月(1)
- 2019年1月(3)
- 2018年12月(2)
- 2018年11月(1)
- 2018年10月(2)
- 2018年8月(1)
- 2018年6月(1)
- 2018年5月(1)
- 2018年4月(7)
- 2018年2月(4)
- 2018年1月(5)
- 2017年12月(2)
- 2017年11月(2)
- 2017年10月(1)
- 2017年9月(5)
- 2017年8月(4)
- 2017年7月(4)
- 2017年6月(3)
- 2017年5月(3)
- 2017年4月(1)
- 2017年3月(8)
- 2017年2月(6)
- 2017年1月(5)
- 2016年12月(6)
- 2016年11月(11)
- 2016年10月(9)
- 2016年9月(6)
- 2016年8月(9)
- 2016年7月(5)
- 2016年6月(8)
- 2016年5月(8)
- 2016年4月(7)
- 2016年3月(5)
- 2016年2月(5)
- 2016年1月(6)
- 2015年12月(6)
- 2015年11月(9)
- 2015年10月(9)
- 2015年9月(4)
- 2015年8月(3)
- 2015年7月(7)
- 2015年6月(3)
- 2015年5月(6)
- 2015年4月(9)
- 2015年3月(9)
- 2015年2月(6)
- 2015年1月(6)
- 2014年12月(17)
- 2014年11月(8)
- 2014年10月(9)
- 2014年9月(7)
- 2014年8月(12)
- 2014年7月(6)
- 2014年6月(6)
- 2014年5月(9)
- 2014年4月(9)
- 2014年3月(7)
- 2014年2月(3)
- 2014年1月(4)
2016-10-01 14:45
@linuxer,您好:
看了您的文章收获非常大,现有几个问题罗列出来希望指点一二,谢谢。
1. 本文中提到的这个流程图:关中断:mask and ack interrupt controller-------->ack NIC-------->copy FIFO to ram------>raise softirq------>unmask interrupt controller
=>最后一步unmask interrupt controller 这个是在中断处理程序执行完由硬件操作寄存器来进行操作的吗?
irq_exit
rcu_irq_exit
local_irq_save(flags);
local_irq_restore(flags);
既然流程第一步已经进行了执行了mask,为什么rcu_irq_exit 还会进行关闭中断的操作啊?硬中断处理中关闭中断的操作是硬件实现的?
2. 关于softirq的执行场景,目前我的理解:
1)是在中断上下文:
irq_exit
if (!in_interrupt() && local_softirq_pending()) //有pending softirq且没有disable_bh
invoke_softirq();
2)一个使用local_bh_disable和local_bh_enable来保护临界区的task,在local_bh_enable退出临界区时如有pending softirq则在该task的进程上下文执行。
3)由wakeup_softirqd 唤醒,在softirqd的进程上下文中执行。
综上所述的情况,我们说softirq和tasklet运行在中断上下文是不是有些不恰当?
3. 关于下面这段代码有个疑问想请教一下:
do_softirq(void)
local_irq_save(flags); //save本地中断状态,关中断 ----- a
pending = local_softirq_pending();
__do_softirq();
pending = local_softirq_pending();//获取 softirq pending 状态 ----- m
set_softirq_pending(0); //清除pending标志
local_irq_enable //开中断 ----- b
h->action(h); ----- c
local_irq_disable //关中断 ----- d
pending = local_softirq_pending();//重新获取 softirq pending 状态 ----- n
if (pending) {
if (time_before(jiffies, end) && !need_resched() &&
--max_restart)
goto restart;
wakeup_softirqd();
}
local_irq_restore(flags); //restore本地中断状态,开中断 ----- e
在c过程中又发生了中断打断了正在执行的softirq handler,又raise了softirq,那么中断结束又返回到被打断的softirq handler,继续执行m处获取的pending softirq,执行完毕后在n处获取新的
pending softirq 跳转到restart那里重新处理,我的描述正确吗?
谢谢!