Linux中断子系统(三)-softirq和tasklet
背景
-
Read the fucking source code!
--By 鲁迅 -
A picture is worth a thousand words.
--By 高尔基
说明:
-
Kernel版本:4.14
-
ARM64处理器,Contex-A53,双核
-
使用工具:Source Insight 3.5, Visio
1. 概述
中断子系统中有一个重要的设计机制,那就是 Top-half和Bottom-half
,将紧急的工作放置在 Top-half
中来处理,而将耗时的工作放置在 Bottom-half
中来处理,这样确保 Top-half
能尽快完成处理,那么为什么需要这么设计呢?看一张图就明白了:
-
ARM处理器在进行中断处理时,处理器进行异常模式切换,此时会将中断进行关闭,处理完成后再将中断打开;
-
如果中断不分上下半部处理,那么意味着只有等上一个中断完成处理后才会打开中断,下一个中断才能得到响应。当某个中断处理处理时间较长时,很有可能就会造成其他中断丢失而无法响应,这个显然是难以接受的,比如典型的时钟中断,作为系统的脉搏,它的响应就需要得到保障;
-
中断分成上下半部处理可以提高中断的响应能力,在上半部处理完成后便将中断打开(通常上半部处理越快越好),这样就可以响应其他中断了,等到中断退出的时候再进行下半部的处理;
-
Bottom-half softirq tasklet workqueue tasklet softirq
在中断处理过程中,离不开各种上下文的讨论,了解不同上下文的区分有助于中断处理的理解,所以,还是来一张老图吧:
-
task_struct thread_info.preempt_count context
-
PREEMPT_BITS
:用于记录禁止抢占的次数,禁止抢占一次该值就加1,使能抢占该值就减1; -
SOFTIRQ_BITS
:用于同步处理,关掉下半部的时候加1,打开下半部的时候减1; -
HARDIRQ_BITS
:用于表示处于硬件中断上下文中;
前戏结束了,直奔主题吧。
2. softirq
2.1 初始化
softirq
不支持动态分配,Linux kernel提供了静态分配,关键的结构体描述如下,可以类比硬件中断来理解:
/* 支持的软中断类型,可以认为是软中断号, 其中从上到下优先级递减 */
enum
{
HI_SOFTIRQ=0, /* 最高优先级软中断 */
TIMER_SOFTIRQ, /* Timer定时器软中断 */
NET_TX_SOFTIRQ, /* 发送网络数据包软中断 */
NET_RX_SOFTIRQ, /* 接收网络数据包软中断 */
BLOCK_SOFTIRQ, /* 块设备软中断 */
IRQ_POLL_SOFTIRQ, /* 块设备软中断 */
TASKLET_SOFTIRQ, /* tasklet软中断 */
SCHED_SOFTIRQ, /* 进程调度及负载均衡的软中断 */
HRTIMER_SOFTIRQ, /* Unused, but kept as tools rely on thenumbering. Sigh! */
RCU_SOFTIRQ, /* Preferable RCU should always be the last softirq, RCU相关的软中断 */
NR_SOFTIRQS
};
/* 软件中断描述符,只包含一个handler函数指针 */
struct softirq_action {
void (*action)(struct softirq_action *);
};
/* 软中断描述符表,实际上就是一个全局的数组 */
static struct softirq_action softirq_vec[NR_SOFTIRQS] __cacheline_aligned_in_smp;
/* CPU软中断状态描述,当某个软中断触发时,__softirq_pending会置位对应的bit */
typedef struct {
unsigned int __softirq_pending;
unsigned int ipi_irqs[NR_IPI];
} ____cacheline_aligned irq_cpustat_t;
/* 每个CPU都会维护一个状态信息结构 */
irq_cpustat_t irq_stat[NR_CPUS] ____cacheline_aligned;
/* 内核为每个CPU都创建了一个软中断处理内核线程 */
DEFINE_PER_CPU(struct task_struct *, ksoftirqd);
来一张图吧:
-
softirq_vec[] irq_desc[] handler tasklet_action handler
-
软中断可以在不同的CPU上并行运行,在同一个CPU上只能串行执行;
-
irq_cpustat_t __softirq_pending 1UL << XXX_SOFTIRQ
2.2 流程分析
2.2.1 软中断注册
中断处理流程中设备驱动通过 request_irq/request_threaded_irq
接口来注册中断处理函数,而在软中断处理流程中,通过 open_softirq
接口来注册,由于它实在是太简单了,我忍不住想把代码贴上来:
void open_softirq(int nr, void (*action)(struct softirq_action *))
{
softirq_vec[nr].action = action;
}
也就是将软中断描述符表中对应描述符的 handler
函数指针指向对应的函数即可,以便软中断到来时进行回调。
那么,问题来了,什么时候进行软中断函数回调呢?
2.2.2 软中断执行之一:中断处理后
先看第一种情况,用图片来回答问题:
-
《Linux中断子系统(二)-通用框架处理》
文章中讲述了整个中断处理流程,在接收到中断信号后,处理器进行异常模式切换,并跳转到异常向量表处进行执行,关键的流程为:el0_irq->irq_handler->handle_arch_irq(gic->handle_irq)->handle_domain_irq->__handle_domain_irq
; -
__handle_domain_irq irq_enter irq_exit preempt_count_add/preempt_count_sub HARDIRQ_OFFSET
-
!in_interrupt() && local_softirq_pending !in_interrupt() in_nmi in_irq in_softirq(Bottom-half disable) in_serving_softirq local_softirq_pending
软中断执行的入口就是 invoke_softirq
,继续分析一波:
-
invoke_softirq threadirqs wakeup_softirqd __do_softirq
-
ksoftirqd smpboot_register_percpu_thread run_ksoftirqd __do_softirq
上图中的逻辑可以看出,最终的核心处理都放置在 __do_softirq
函数中完成:
-
local_softirq_pending
函数用于读取__softirq_pending
字段,可以类比于设备驱动中的状态寄存器,用于判断是否有软中断处理请求; -
软中断处理时会关闭
Bottom-half
,处理完后再打开; -
软中断处理时,会打开本地中断,处理完后关闭本地中断 Top-half Bottom-half Bottom-half Bottom-half
-
while(softirq_bit = ffs(pending))
,循环读取状态位,直到处理完每一个软中断请求; -
while restart wakeup_sotfirqd
-
time_before(jiffies, MAX_SOFTIRQ_TIME)
,软中断处理时间小于两毫秒; -
!need_resched
,当前没有进程调度的请求; -
max_restart = MAX_SOFTIRQ_RESTART restart trade-off
__do_softirq
既然可以在中断处理过程中调用,也可以在 ksoftirqd
中调用,那么 softirq
的执行可能有两种context,插张图吧:
让我们来思考最后一个问题:硬件中断触发的时候是通过硬件设备的电信号,那么软中断的触发是通过什么呢?答案是通过 raise_softirq
接口:
-
可以在中断处理过程中调用
raise_softirq
来进行软中断处理请求,处理的实际也就是上文中提到过的irq_exit
退出硬件中断上下文之后再处理; -
raise_softirq_irqoff or_softirq_pending irq_stat __softirq_pending
-
raise_softirq_irqoff
函数中,会判断当前的请求的上下文环境,如果不在中断上下文中,就可以通过唤醒内核线程来处理,如果在中断上下文中处理,那就不执行; -
多说一句,在软中断整个处理流程中,会经常看到
in_interrupt()
的条件判断,这个可以确保软中断在CPU上的串行执行,避免嵌套;
2.2.3 软中断执行之二:Bottom-half Enable后
第二种软中断执行的时间点,在 Bottom-half
使能的时候,通常用于并发处理,进程空间上下文中进行调用:
-
在讨论并发专题的时候,我们谈到过
Bottom-half
与进程之间能产生资源争夺的情况,如果在软中断和进程之间有临界资源(软中断上下文优先级高于进程上下文),那么可以在进程上下文中调用local_bh_disable/local_bh_enable
来对临界资源保护; -
图中左侧的函数,都是用于打开
Bottom-half
的接口,可以看出是spin_lock_bh/read_lock_bh/write_lock_bh
等并发处理接口的变种形式调用; -
__local_bh_enable_ip
函数中,首先判断调用该本接口时中断是否是关闭的,如果已经关闭了再操作BH接口就会告警; -
preempt_count_sub preempt_count_add thread_info->preempt_count __local_bh_enable_ip cnt preempt_count_sub(cnt-1) preempt_count_dec preempt_count_sub(cnt-1) thread_info->preempt_count do_softirq preempt_count_dec
-
Bottom-half Bottom-half Bottom-half
3. tasklet
从上文中分析可以看出, tasklet
是软中断的一种类型,那么两者有啥区别呢?先说结论吧:
-
软中断类型内核中都是静态分配,不支持动态分配,而
tasklet
支持动态和静态分配,也就是驱动程序中能比较方便的进行扩展; -
软中断可以在多个CPU上并行运行,因此需要考虑可重入问题,而
tasklet
会绑定在某个CPU上运行,运行完后再解绑,不要求重入问题,当然它的性能也就会下降一些;
3.1 数据结构
-
DEFINE_PER_CPU(struct tasklet_head, tasklet_vec) tasklet_head struct tasklet_struct tasklet tasklet_vec tasklet_vec_hi tasklet_vec
-
struct tasklet_struct tasklet next state func task_init()
3.2 流程分析
-
tasklet
本质上是一种软中断,所以它的调用流程与上文中讨论的软中断流程是一致的; -
tasklet tasklet_schedule tasklet tasklet raise_softirq_irqoff
-
tasklet_action softirq_init open_softirq
-
tasklet_action tasklet_vec list list t->func() continue tasklet tasklet_vec
3.3 接口
简单贴一下接口吧:
/* 静态分配tasklet */
DECLARE_TASKLET(name, func, data)
/* 动态分配tasklet */
void tasklet_init(struct tasklet_struct *t, void (*func)(unsigned long), unsigned long data);
/* 禁止tasklet被执行,本质上是增加tasklet_struct->count值,以便在调度时不满足执行条件 */
void tasklet_disable(struct tasklet_struct *t);
/* 使能tasklet,与tasklet_diable对应 */
void tasklet_enable(struct tasklet_struct *t);
/* 调度tasklet,通常在设备驱动的中断函数里调用 */
void tasklet_schedule(struct tasklet_struct *t);
/* 杀死tasklet,确保不被调度和执行, 主要是设置state状态位 */
void tasklet_kill(struct tasklet_struct *t);
收工!
如果觉得文章对您有帮助,那就点个 在看 吧,谢谢。