linux 内核软中断介绍

在介绍软中断之前,先来介绍一个概念:中断下半部:
为了避免处理复杂的中断嵌套,中断处理程序是在关闭中断的情况下执行的。可是,如果关闭中断的时间太长,可能导致中断请求丢失。例如周期时钟每隔 10 毫秒发送一个中断请求,如果执行某个中断处理程序花费的时间超过 10 毫秒,在这段时间里时钟发送了两个中断请求,但是处理器只认为收到一个时钟中断请求。
最激进的解决办法是中断线程化,但是常用的解决办法是:把中断处理程序分为两部分,上半部(top half, th)在关闭中断的情况下执行,只做对时间非常敏感、与硬件相关或者不能被其他中断打断的工作;下半部(bottom half,bh)在开启中断的情况下执行,可以被其他中断打断。
上半部称为硬中断(hardirq),下半部有 3 种:软中断(softirq)、小任务(tasklet)和工作队列(workqueue)。 3 种下半部的区别如下。
(1)软中断和小任务是工作在中断上下文不允许睡眠;工作队列是使用内核线程实现的是工作在进程上下文,处理函数可以睡眠。
(2)软中断的种类是编译时静态定义的,在运行时不能添加或删除;小任务可以在运行时添加或删除。
(3)同一种软中断的处理函数可以在多个处理器上同时执行,处理函数必须是可以重入的,需要使用锁保护临界区;一个小任务同一时刻只能在一个处理器上执行,不要求处理函数是可以重入的。

软中断(softirq)是中断处理程序在开启中断的情况下执行的部分,可以被硬中断抢占。
内核定义了一张软中断向量表,每种软中断有一个唯一的编号,对应一个 softirq_action实例, softirq_action 实例的成员 action 是处理函数。
kernel/softirq.c
static struct softirq_action softirq_vec[NR_SOFTIRQS] __cacheline_aligned_in_smp;

include/linux/interrupt.h
struct softirq_action
{
void (*action)(struct softirq_action *);
};

2.1    软中断的种类
目前内核定义了 10 种软中断,各种软中断的编号如下:
include/linux/interrupt.h
enum
{
    HI_SOFTIRQ=0,
    TIMER_SOFTIRQ,
    NET_TX_SOFTIRQ,
    NET_RX_SOFTIRQ,
    BLOCK_SOFTIRQ,
    IRQ_POLL_SOFTIRQ,
    TASKLET_SOFTIRQ,
    SCHED_SOFTIRQ,
    HRTIMER_SOFTIRQ, /* 没有使用,但是保留,因为有些工具依赖这个编号 */
    RCU_SOFTIRQ, /* RCU软中断应该总是最后一个软中断 */
    
    NR_SOFTIRQS
};
(1) HI_SOFTIRQ:高优先级的小任务。
(2) TIMER_SOFTIRQ:定时器软中断。
(3) NET_TX_SOFTIRQ:网络栈发送报文的软中断。
(4) NET_RX_SOFTIRQ:网络栈接收报文的软中断。
(5) BLOCK_SOFTIRQ:块设备软中断。
(6) IRQ_POLL_SOFTIRQ:支持 I/O 轮询的块设备软中断。
(7) TASKLET_SOFTIRQ:低优先级的小任务。
(8) SCHED_SOFTIRQ:调度软中断,用于在处理器之间负载均衡。
(9) HRTIMER_SOFTIRQ:高精度定时器,这种软中断已经被废弃,目前在中断处理程序的上半部处理高精度定时器。
(10) RCU_SOFTIRQ: RCU 软中断。
软中断的编号形成了优先级顺序,编号小的软中断优先级高。


  注册软中断的处理函数


函数 open_softirq()用来注册软中断的处理函数,在软中断向量表中为指定的软中断编号设置处理函数。
kernel/softirq.c
void open_softirq(int nr, void (*action)(struct softirq_action *))
{
    softirq_vec[nr].action = action;
}

同一种软中断的处理函数可以在多个处理器上同时执行,处理函数必须是可以重入的,需要使用锁保护临界区。


  触发软中断


函数 raise_softirq 用来触发软中断,参数是软中断编号。
void raise_softirq(unsigned int nr);

在已经禁止中断的情况下可以调用函数 raise_softirq_irqoff 来触发软中断。
void raise_softirq_irqoff(unsigned int nr);

函数 raise_softirq 在当前处理器的待处理软中断位图中为指定的软中断编号设置对应的位,如下所示:
raise_softirq() -> raise_softirq_irqoff() -> __raise_softirq_irqoff()
kernel/softirq.c
void __raise_softirq_irqoff(unsigned int nr)
{
    or_softirq_pending(1UL << nr);
}

把宏 or_softirq_pending 展开以后是:
irq_stat[smp_processor_id()].__softirq_pending |= (1UL << nr);


 执行软中断


内核执行软中断的地方如下。
(1)在中断处理程序的后半部分执行软中断,对执行时间有限制:不能超过 2 毫秒,并且最多执行 10 次。
(2)每个处理器有一个软中断线程,调度策略是 SCHED_NORMAL,优先级是 120。
(3)开启软中断的函数 local_bh_enable()。

如果开启了强制中断线程化的配置宏 CONFIG_IRQ_FORCED_THREADING,并且在引导内核的时候指定内核参数“ threadirqs”,那么所有软中断由软中断线程执行。

(1)中断处理程序执行软中断。
在中断处理程序的后半部分,调用函数 irq_exit()以退出中断上下文,处理软中断,其代码如下:
kernel/softirq.c
void irq_exit(void)
{
    …
    preempt_count_sub(HARDIRQ_OFFSET);
    if (!in_interrupt() && local_softirq_pending())
        invoke_softirq();
    …
}
如果 in_interrupt()为真,表示在不可屏蔽中断、硬中断或软中断上下文,或者禁止软中断。
这里我们提一下本地中断的开启与关闭,当中断上来时,为了防止中断的嵌套,硬件会自动关闭本地中断,那本地中断什么时候打开呢?分两种情况:
1.    退出中断上下文时若有待处理的软中断,在执行软中断前__do_softirq->local_irq_enable会打开本地中断,即软中可以被硬件中断的打断。
2.    没有要处理的软中断,那么在中断完全退出时,会恢复被中断进程的寄存器上下文,系统状态寄存器一但被恢复,本地中断自然也就开了。
如果正在处理的硬中断没有抢占正在执行的软中断,没有禁止软中断,并且当前处理器的待处理软中断位图不是空的,那么调用函数 invoke_softirq()来处理软中断。
函数 invoke_softirq 的代码如下:
kernel/softirq.c
1 static inline void invoke_softirq(void)
2 {
3       if (ksoftirqd_running())
4           return;
5       
6       if (!force_irqthreads) {
7           __do_softirq();
8       } else {
9           wakeup_softirqd();
10      }
11 }
第 3 行代码,如果软中断线程处于就绪状态或运行状态,那么让软中断线程执行软中断。
第 6 行和第 7 行代码,如果没有强制中断线程化,那么调用函数__do_softirq()执行软中断。
第 8 行和第 9 行代码,如果强制中断线程化,那么唤醒软中断线程执行软中断。

函数__do_softirq 是执行软中断的核心函数,其主要代码如下:
kernel/softirq.c
1 #define MAX_SOFTIRQ_TIME msecs_to_jiffies(2)
2 #define MAX_SOFTIRQ_RESTART 10
3 asmlinkage __visible void __softirq_entry __do_softirq(void)
4 {
5   unsigned long end = jiffies + MAX_SOFTIRQ_TIME;
6   unsigned long old_flags = current->flags;
7   int max_restart = MAX_SOFTIRQ_RESTART;
8   struct softirq_action *h;
9   bool in_hardirq;
10  __u32 pending;
11  int softirq_bit;
12
13  …
14  pending = local_softirq_pending();
15  …
16  __local_bh_disable_ip(_RET_IP_, SOFTIRQ_OFFSET);
17  …
18
19  restart:
20  set_softirq_pending(0);
21
22  local_irq_enable();
23
24  h = softirq_vec;
25
26  while ((softirq_bit = ffs(pending))) {
27      …
28      h += softirq_bit - 1;
29      …
30      h->action(h);
31      …
32      h++;
33      pending >>= softirq_bit;
34  }
35
36  …
37  local_irq_disable();
38
39  pending = local_softirq_pending();
40  if (pending) {
41      if (time_before(jiffies, end) && !need_resched() &&
42          --max_restart)
43          goto restart;
44
45      wakeup_softirqd();
46  }
47
48  …
49  __local_bh_enable(SOFTIRQ_OFFSET);
50  …
51}

第 14 行代码,把局部变量 pending 设置为当前处理器的待处理软中断位图。
第 16 行代码,把抢占计数器的软中断计数加 1。
第 20 行代码,把当前处理器的待处理软中断位图重新设置为 0。
第 22 行代码,开启硬中断。
第 26~34 行代码,从低位向高位扫描待处理软中断位图,针对每个设置了对应位的软中断编号,执行软中断的处理函数。
第 37 行代码,禁止硬中断。
第 40 行代码,如果软中断的处理函数又触发软中断,处理如下。
a)    第 41~43 行代码,如果软中断的执行时间小于 2 毫秒,不需要重新调度进程,并且软中断的执行次数没超过 10,那么跳转到第 19 行代码继续执行软中断。
b)    第 45 行代码,唤醒软中断线程执行软中断。
第 49 行代码,把抢占计数器的软中断计数减 1。

(2)软中断线程
每个处理器有一个软中断线程,名称是“ ksoftirqd/”后面跟着处理器编号,调度策略是 SCHED_NORMAL,优先级是 120。
软中断线程的核心函数是 run_ksoftirqd(),其代码如下:
kernel/softirq.c
static void run_ksoftirqd(unsigned int cpu)
{
    local_irq_disable();
    if (local_softirq_pending()) {
        __do_softirq();
        local_irq_enable();
        …
        return;
    }
    local_irq_enable();
}

(3)开启软中断时执行软中断。
当进程调用函数 local_bh_enable()开启软中断的时候,如果是开启最外层的软中断,并且当前处理器的待处理软中断位图不是空的,那么执行软中断。
local_bh_enable() -> __local_bh_enable_ip()
kernel/softirq.c
void __local_bh_enable_ip(unsigned long ip, unsigned int cnt)
{
    …
    preempt_count_sub(cnt - 1);
    if (unlikely(!in_interrupt() && local_softirq_pending())) {
        do_softirq();
    }
    preempt_count_dec();
    …
}


抢占计数器


在介绍“禁止/开启软中断”之前,首先了解一下抢占计数器这个背景知识。
每个进程的 thread_info 结构体有一个抢占计数器: int preempt_count,它用来表示当前进程能不能被抢占。
抢占是指当进程在内核模式下运行的时候可以被其他进程抢占,如果优先级更高的进程处于就绪状态,强行剥夺当前进程的处理器使用权。
但是有时候进程可能在执行一些关键操作,不能被抢占,所以内核设计了抢占计数器。如果抢占计数器为 0,表示可以被抢占;如果抢占计数器不为 0,表示不能被抢占。
当中断处理程序返回的时候,如果进程在被打断的时候正在内核模式下执行,就会检查抢占计数器是否为 0。如果抢占计数器是 0,可以让优先级更高的进程抢占当前进程。
虽然抢占计数器不为 0 意味着禁止抢占,但是内核进一步按照各种场景对抢占计数器的位进行了划分,如下图所示。
 
其中第 0~7 位是抢占计数,第 8~15 位是软中断计数,第 16~19 位是硬中断计数,第 20 位是不可屏蔽中断( Non Maskable Interrupt, NMI)计数。
include/linux/preempt.h
/*
* PREEMPT_MASK: 0x000000ff
* SOFTIRQ_MASK: 0x0000ff00
* HARDIRQ_MASK: 0x000f0000
* NMI_MASK: 0x00100000
*/
#define PREEMPT_BITS 8
#define SOFTIRQ_BITS 8
#define HARDIRQ_BITS 4
#define NMI_BITS 1
各种场景分别利用各自的位禁止或开启抢占。
(1)普通场景( PREEMPT_MASK):对应函数 preempt_disable()和 preempt_enable()。
(2)软中断场景( SOFTIRQ_MASK):对应函数 local_bh_disable()和 local_bh_enable()。
(3)硬中断场景( HARDIRQ_MASK):对应函数 __irq_enter()和__irq_exit()。
(4)不可屏蔽中断场景( NMI_MASK):对应函数 nmi_enter()和 nmi_exit()。
反过来,我们可以通过抢占计数器的值判断当前处在什么场景:
include/linux/preempt.h
#define in_irq() (hardirq_count())
#define in_softirq() (softirq_count())
#define in_interrupt() (irq_count())
#define in_serving_softirq() (softirq_count() & SOFTIRQ_OFFSET)
#define in_nmi() (preempt_count() & NMI_MASK)
#define in_task() (!(preempt_count() & \
(NMI_MASK | HARDIRQ_MASK | SOFTIRQ_OFFSET)))
#define hardirq_count() (preempt_count() & HARDIRQ_MASK)
#define softirq_count() (preempt_count() & SOFTIRQ_MASK)
#define irq_count() (preempt_count() & (HARDIRQ_MASK | SOFTIRQ_MASK \
| NMI_MASK))
in_irq()表示硬中断场景,也就是正在执行硬中断。
in_softirq()表示软中断场景,包括禁止软中断和正在执行软中断。
in_interrupt()表示正在执行不可屏蔽中断、硬中断或软中断,或者禁止软中断。
in_serving_softirq()表示正在执行软中断。
in_nmi()表示不可屏蔽中断场景。
in_task()表示普通场景,也就是进程上下文。


禁止/开启软中断


如果进程和软中断可能访问同一个对象, 那么进程和软中断需要互斥, 进程需要禁止软中断。
禁止软中断的函数是 local_bh_disable(),注意:这个函数只能禁止本处理器的软中断,不能禁止其他处理器的软中断。该函数把抢占计数器的软中断计数加 2,其代码如下:
include/linux/bottom_half.h
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();
}
include/linux/preempt.h
#define SOFTIRQ_DISABLE_OFFSET (2 * SOFTIRQ_OFFSET)
开启软中断的函数是 local_bh_enable(),该函数把抢占计数器的软中断计数减 2。
为什么禁止软中断的函数 local_bh_disable()把抢占计数器的软中断计数加 2, 而不是加1 呢?目的是区分禁止软中断和正在执行软中断这两种情况。执行软中断的函数__do_softirq()把抢占计数器的软中断计数加 1。如果软中断计数是奇数,可以确定正在执行软中断。
注意:local_bh_enable() 在硬中断或者关闭硬中断时使用有可能出现问题,会有警告提醒。


 挂起的软中断


另一个跟软中断相关的字段是每个CPU都有一个32位掩码的字段
typedef struct {
    unsigned int __softirq_pending;
    unsigned int ipi_irqs[NR_IPI];
} ____cacheline_aligned irq_cpustat_t;

irq_cpustat_t irq_stat[NR_CPUS] ____cacheline_aligned;
EXPORT_SYMBOL(irq_stat);

他描述挂起的软中断。每一位对应相应的软中断。比如0位代表HI_SOFTIRQ.一个注册软中断必须在被标记后才会执行。
宏local_softirq_pending();来获取该字段的值。
宏set_softirq_pending(0);来设置该字段的值。
使用函数raise_softirq()来激活软中断。即把响应的软中断号对应的__softirq_pending中的位置1。表示该软中断被挂起。如果当前CPU不在中断上下文中,唤醒内核线程ksoftirqd来检查被挂起的软中断,然后执行相应软中断处理函数。
内核在如下几个点上检查被挂起的软中断:
1、当do_IRQ() 完成硬中断处理时调用irq_exit()时调用do_softirq()来处理软中断。
2、当一个特殊内核线程ksoftirqd/n被唤醒时,处理软中断。
3、当调用local_bh_enable()函数激活本地CPU的软中断时。条件满足就调用do_softirq() 来处理软中断。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

一叶知秋yyds

分享是一种美德,感谢金主打赏

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值