Linux 内核中断下半部机制的设计
一、Linux 内核中断处理机制概述
ARMv8 的中断控制器称为通用中断控制器(Generic Interrupt Controller,GIC)
,负责对设备的中断信号进行处理,并将其发送给 CPU 执行。
GIC 有两种接口:
分发器接口(distributor interface):负责汇总收到的中断请求,并根据中断的优先级进行排序和分发。
CPU接口(CPU interface):直接与 CPU 相连,将中断派发给 CPU
工作机制简述:GIC 收到中断请求后,分发器接口会将优先级最高的中断传递给 CPU 接口,进而由 CPU 接口派发给 CPU 核(Who marks,Who runs),根据中断类型,触发 CPU 核上的中断处理程序。其中,GIC 支持中断优先级抢占。即一个新到达的高优先级中断可以抢占一个处于 active 状态的低优先级中断。
当一个中断发生后,内核会执行一个特定函数来响应该中断,这个函数通常称为中断处理程序(interrupt handler)
,运行在中断上下文上(和进程上下文不同)。
进程上下文和中断上下文的区别:
进程上下文:上下文简单说来就是一个环境,当一个进程在执行时,CPU的所有寄存器中的值、进程的状态以及堆栈中的内容被称为该进程的上下文。
中断上下文:硬件通过触发信号,导致内核调用中断处理程序,进入内核空间。所谓的“中断上下文”,其实也可以看作就是硬件传递过来的这些参数和内核需要保存的一些其他环境。
处理器总处于以下状态中的一种:
a. 用户态,运行于用户空间。此时进程的切换也就是上下文切换。
b. 内核态,运行于进程上下文,内核代表进程运行于内核空间。例如 CPU 执行系统调用时,进程转换为内核态执行,但依旧是服务于用户进程。若内核运行在进程上下文中需要等待资源和设备时,系统可以阻塞当前进程。
c. 内核态,运行于中断上下文,内核代表硬件运行于内核空间。硬件通过触发信号,导致内核调用中断处理程序,进入内核空间。中断时,内核不代表任何进程运行,它一般只访问系统空间,而不会访问进程空间,且内核在中断上下文中执行时一般不会阻塞。
二、中断处理区分上下半部的原因
中断(可能是异步)随时都可能发生,故必须保证中断处理程序能够快速执行,这样才能保证尽可能快的恢复中断代码的执行。而且对于硬件而言,让中断处理程序在尽可能短的时间内完成运行很重要。但同时,中断不仅仅只是执行中断处理程序那么简单,有可能一个中断需要耗费大量的时间来完成。此时,就陷入了“又不让马儿吃草,又想马儿跑得好”的窘境。
因此,Linux 引入了上下半部机制来处理中断,目的就是实现中断处理函数的快进快出。
在 Linux 中,中断具有最高的优先级,上半部(top-half)即狭义的中断处理程序,接收到一个中断,立即执行,此时的中断是不会被抢占的 ,即相当于说:此时的计算机是瞎子,它无法对外界的中断进行响应。考虑到计算机的响应能力和性能,上半部只做最少的工作,例如响应中断表明中断已经被软件接收,做一些简单的数据处理(如 DMA,Direct Memory Access),并且在中断处理完成时发送 EOI(End of Interrupt) 信号给 GIC 等,核心思想就是为了迅速将独占的 CPU 资源开放。Linux 内核并没有严格约束什么任务会被放到下半部执行,这主要依赖驱动开发者来决定,中断任务的划分对系统性能有比较大的影响。
至于余下的工作,即下半部(bottom-half),会在之后的合适的时间完成。至于何时是合适的时间,没有确切的答案。下半部执行的关键点是允许响应所有的中断。Linux 提供了多种下半部的实现方式,下半部属于具有较高优先级的内核任务。
三、Linux 中的两种下半部处理机制
1 软中断(softirq)
1.1 软中断初始化
软中断是 Linux 内核很早引入的机制,预留给系统中对时间要求较严格和重要的下半部使用。目前驱动中只有块设备和网络子系统使用了软中断。
系统通过枚举类型(enum
)静态定义了若干种软中断类型,并且每一种软中断都使用索引号来表示优先级,索引号越小,优先级越高。
<include/linux/interrupt.h>
enum
{
HI_SOFTIRQ=0, // 优先级为0,用于实现最高优先级的软中断
TIMER_SOFTIRQ, // 优先级为1,定时器软中断
NET_TX_SOFTIRQ, // 优先级为2,用于网络数据包的发送
NET_RX_SOFTIRQ, // 优先级为3,用于网络数据包的接收
BLOCK_SOFTIRQ, // 优先级为4,块设备
BLOCK_IOPOLL_SOFTIRQ, // 优先级为5,块设备
TASKLET_SOFTIRQ, // 优先级为6,用于实现 tasklet 软中断
SCHED_SOFTIRQ, // 优先级为7,用于进程调度、负载均衡
HRTIMER_SOFTIRQ, // 优先级为8,高精度定时器软中断
RCU_SOFTIRQ, // 优先级为9,专门用于 RCU 服务
NR_SOFTIRQS //表示软中断枚举类型中系统最多支持的软中断类型的数量
};
同时系统定义了一个用于描述软中断的数据结构 struct softirq_action
<include/linux/interrupt.h>
struct softirq_action
{
void (*action)(struct softirq_action *); // action 的函数指针,指向软中断请求的服务函数。若触发该软中断,就调用 action 回调函数处理
};
并且定义了软中断描述符——数组 softirq_vec[NR_SOFTIRQS]
,NR_SOFTIRQS
表示最大的软中断类型数量。每个软中断类型对应一个描述符,其中软中断的序号就是该数组的索引,具体描述为软中断向量i(0≤i≤9)所对应的软中断请求描述符就是 softirq_vec[i]
,简单理解为第 i 种软中断类型的解决办法就存放在softirq_vec[i]
中。需要强调的是,该数组被所有 CPU 共享,即虽然各个 CPU 有他们各自的触发和控制机制,但是他们所执行的软中断服务例程却是相同的。
<kernel/softirq.c>
static struct softirq_action softirq_vec[NR_SOFTIRQS] __cacheline_aligned_in_smp;
其中,__cacheline_aligned_in_smp
是 Linux 中的一个宏,用于防止在多核系统中伪共享现象的发生,提高性能。
伪共享
与多核系统下的缓存一致性有关。
CPU 从内存中读取数据到 Cache 的时候,是一块一块地读取数据的,这每一块的数据被称为 Cache Line(缓存行),所以 Cache Line 是 CPU 从内存读取数据到 Cache 的最小单位。
想象这样一种场景:数据 A 和 B 被存在同一块 Cache Line 中,当 核心甲需要访问 A,而核心乙需要访问 B 时,即核心甲和核心乙企图同时访问同一个 Cache Line ,此时该 Cache Line 状态为共享。再假设甲和乙轮流对 A 和 B 进行修改 ,则两个核心对该 Cache Line 的状态在独占修改和失效间反复跳转,且每次都需要进行访存操作,耗费大量时间,则缓存失去它的意义。
因此,对于多个线程共享的热点数据,应该避免这些数据刚好在同一个 Cache Line 中,否则就会出现伪共享的问题。
对于一个在多核间竞争比较严重的 Cache Line ,可以通过
__cacheline_aligned_in_smp
宏,使得变量在 Cache Line 中是对齐的,从而避免伪共享问题。具体可参考该文章:https://www.sohu.com/a/435070592_120052431
此外,系统通过一个 irq_cpustat_t
数据结构来描述软中断状态信息,可以理解为软中断状态寄存器。__softirq_pending
是一个32位无符号的变量,每一 bit 对应Linux中32种软中断类型(其中系统定义了10种)是否被上半部触发了。
<include/asm-generic/hardirq.h>
typedef struct {
unsigned int __softirq_pending;
} ____cacheline_aligned irq_cpustat_t;
同时,系统定义了一个 irq_stat[NR_CPUS]
数组(NR_CPUS
指 CPU 数量),为每个 CPU 分配了一个软中断状态寄存器,对于第 i 个 CPU ,它只能操作它自己的中断处理信息 irq_stat[i]
,从而使各个 CPU 间互不影响。
<kernel/softirq.c>
DEFINE_PER_CPU_ALIGNED(irq_cpustat_t, irq_stat);
DEFINE_PER_CPU_ALIGNED
是一条个 Per-CPU 变量,用以解决 lock bus 带来的性能问题首先什么是 lock bus ?
read-modify-write
的问题,本质上是一个保持对内存 read 和 write 访问的原子性的问题,也就是说对内存的读和写的访问不能被打断。例如
SWP
这个汇编指令就是原子的,读写之间不会被打断,这是通过加锁实现的。具体地,硬件在进行 memory 操作的时候设定 lock signal,禁止总线打断,直到完成了SWP
需要进行的两次内存访问之后再 clear lock signal。而这种机制会对多核系统的性能造成严重的的影响(系统中其他的进程对那条被 lock 的 memory bus 的访问都被阻塞了)。Per-CPU 变量
既然加锁会影响多核系统的性能,那索性不使用锁,而是给每一个 CPU 都分配一个内存,这就是 Per-CPU 变量的思想。
当一个在多个 CPU 之间共享的变量变成每个 CPU 都有属于自己的一个私有的变量的时候,我们就不必考虑来自多个 CPU 上的并发,仅仅考虑本 CPU 上的并发就可以了。需要注意的一点是:该任务不能调度到其他 CPU 上去!简单地说,每个 CPU 只能访问属于自己的部分,这样就不存在同步的问题了。幸运的是,软中断的触发恰好秉持着 Who marks,Who runs 的规则,不用担心不同核心会互相影响。
具体到该条语句
DEFINE_PER_CPU_ALIGNED(type,name)
,无论 SMP(多核)或者 UP (单核),都需要对齐到 L1 cache Line 。具体可参考该文章:http://www.wowotech.net/kernel_synchronization/per-cpu.html
通过调用 open_softirq()
函数注册一个软中断,其中 nr
是软中断的序号。注意到,softirq_vec[]
是一个多 CPU 共享的数组。
<kernel/softirq.c>
void open_softirq(int nr, void (*action)(struct softirq_action *))
{
softirq_vec[nr].action = action;
}
软中断的初始化通常在系统启动时完成。通过在源码中搜索 open_softirq
可以发现不同类型的软中断在不同位置被注册。
例如通过调用 softirq_init()
完成,而 softirq_init()
内部又调用了 open_softirq()
,为不同类型的软中断指定服务函数。
<kernel/softirq.c>
void __init softirq_init(void)
{
int cpu;
for_each_possible_cpu(cpu) {
per_cpu(tasklet_vec, cpu).tail =
&per_cpu(tasklet_vec, cpu).head;
per_cpu(tasklet_hi_vec, cpu).tail =
&per_cpu(tasklet_hi_vec, cpu).head;
for (i = 0; i < NR_SOFTIRQS; i++)
INIT_LIST_HEAD(&per_cpu(softirq_work_list[i], cpu));
}
register_hotcpu_notifier(&remote_softirq_cpu_notifier);
open_softirq(TASKLET_SOFTIRQ, tasklet_action); // 为 TASKLET_SOFTIRQ 类型设置软中断服务函数
open_softirq(HI_SOFTIRQ, tasklet_hi_action); // 为 HI_SOFTIRQ 类型设置软中断服务函数
}
又例如在 init_timers()
中,注册了 TIMER 类型的软中断
<kernel/time/timer.c>
void __init init_timers(void)
{
init_timer_cpus();
open_softirq(TIMER_SOFTIRQ, run_timer_softirq);
}
1.2 软中断触发机制
软中断的触发秉承“谁触发,谁执行”(Who marks,Who runs)的规则。
要触发软中断,有两个接口函数,分别是 raise_softirq()
和 raise_softirq_irqoff()
,唯一区别在于前者能主动关闭本地中断,因此 raise_softirq_irqoff()
允许在进程上下文中调用。
<kernel/softirq.c>
void raise_softirq(unsigned int nr)
{
unsigned long flags;
local_irq_save(flags); // 关闭当前处理器的中断响应
raise_softirq_irqoff(nr); // 间接调用了 __raise_softirq_irqoff(nr);
local_irq_restore(flags); // 打开中断响应
}
<kernel/softirq.c>
inline void raise_softirq_irqoff(unsigned int nr)
{
__raise_softirq_irqoff(nr); // 改变软中断寄存器状态
/*
* If we're in an interrupt or softirq, we're done
* (this also catches softirq-disabled code). We will
* actually run the softirq once we return from
* the irq or softirq.
*
* Otherwise we wake up ksoftirqd to make sure we
* schedule the softirq soon.
*/
if (!in_interrupt()) // 判断内核状态
wakeup_softirqd(); // 唤醒软中断内核线程
}
__raise_softirq_irqoff(nr);
跟踪代码路径:
__raise_softirq_irqoff(nr);
->or_softirq_pending(1UL << nr);
->#define or_softirq_pending(x) (__this_cpu_or(local_softirq_pending_ref, (x)))
->#define local_softirq_pending_ref irq_stat.__softirq_pending
由此可知最终代码是
__this_cpu_or(irq_stat.__softirq_pending, 1UL << nr)
,即将该处理器的软中断寄存器的__softirq_pending
第nr
位置为1。每当中断返回时,该 CPU 会检查__softirq_pending
成员的位,如果__softirq_pending
不为 0,说明有pending
的软中断需要处理。in_interrupt()
用于判断该内核当前的状态,若处于中断状态(硬中断、软中断、tasklet),则返回真,暂不唤醒
softirq
;反之则唤醒该softirq
以保证快速地处理。硬中断显然不行,若退出时为软中断,说明本次中断点在一个软中断处理过程中,那么中断返回会回到原来的软中断上下文中,这种情况下不允许嵌套调用另一个软中断(由于软中断在 CPU 中总是串行执行的)。
1.3 软中断调度点
软中断被触发后,处于 pending
状态,随时可能被 CPU 调度执行。而触发该动作有两种途径。
1)irq_exit()
当硬中断退出时,CPU 会通过 irq_exit()
检查当前 CPU 是否有正在等待的软中断。其中 local_softirq_pending()
用于判断本地 CPU 的 __softirq_pending
变量是否为 0,in_interrupt()
用于判断是否处于中断上下文(详解见1.2末)。
<kernel/softirq.c>
void irq_exit(void)
{
...
preempt_count_sub(HARDIRQ_OFFSET);
if (!in_interrupt() && local_softirq_pending())
invoke_softirq();
...
}
preempt_count_sub(HARDIRQ_OFFSET)
观察上方函数时,我产生了一个困惑:当 CPU 执行
irq_exit()
时,应该是属于硬中断上下文的,此时调用in_interrupt()
判断状态,岂不是invoke_softirq()
永远都不会被调用?事实上,程序处在哪个上下文并不是由执行函数来划定的,而是由一个标志位来确定,这个标志位就是
current_thread_info() -> preempt_count
,利用该变量的某些位用来指示上下文状态,包括硬中断、软中断、内核抢占使能标志等等。所以,在判断
in_interrupt()
之前调用了preempt_count_sub(HARDIRQ_OFFSET)
,该函数通过设置preempt_count
,表示当前退出硬中断上下文,所以!in_inerrupt()
函数通常为真。
<kernel/softirq.c>
static inline void invoke_softirq(void)
{
if (ksoftirqd_running(local_softirq_pending()))
return;
/*
* 首先判断负责处理 softirq 的内核线程是否正在运行
* 如果是,则返回,让内核线程继续处理
*/
if (!force_irqthreads) {
#ifdef CONFIG_HAVE_IRQ_EXIT_ON_IRQ_STACK
/*
* force_irqthreads 变量表示是否强制使用内核线程来处理 softirq
* 若果不是,就调用 __do_softirq()
*/
__do_softirq();
#else
/*
* 如果是,就唤醒内核线程,让线程来处理
* 内核线程最终也会调用 __do_softirq()
*/
do_softirq_own_stack();
#endif
} else {
wakeup_softirqd();
}
}
2)local_bh_enable_ip()
该函数通常表示重新使能下半部的时候,和其他同步机制屏蔽使能作用域是一样的,它只作用于本地 CPU。
<kernel/softirq.c>
void __local_bh_enable_ip(unsigned long ip, unsigned int cnt)
{
...
if (unlikely(!in_interrupt() && local_softirq_pending())) {
do_softirq();
}
...
}
1.4 软中断执行
do_softirq()
-> do_softirq_own_stack()
-> __do_softirq()
do_softirq()
同样判断内核线程是否在运行,然后调用 do_softirq_own_stack()
,最终会调用 __do_softirq()
函数。
<kernel/softirq.c>
asmlinkage __visible void do_softirq(void)
{
__u32 pending;
unsigned long flags;
if (in_interrupt())
return;
local_irq_save(flags);
pending = local_softirq_pending();
if (pending && !ksoftirqd_running(pending))
do_softirq_own_stack();
local_irq_restore(flags);
}
接下来具体分析 softirq
的处理过程
<kernel/softirq.c>
asmlinkage __visible void __softirq_entry __do_softirq(void)
{
struct softirq_action *h;
__u32 pending;
int softirq_bit;
...
pending = local_softirq_pending(); // 取出全局 softirq 标志位
restart:
set_softirq_pending(0); // 清除软中断寄存器
local_irq_enable(); // 打开本地中断
h = softirq_vec; // 获取数组向量的地址
//逐位地判断 softirq 标志位是否被置位,被置位的需要执行(顺序天然代表优先级)
while ((softirq_bit = ffs(pending))) {
unsigned int vec_nr;
int prev_count;
h += softirq_bit - 1;
vec_nr = h - softirq_vec;
//执行 softirq 的工作函数
h->action(h);
h++;
pending >>= softirq_bit;
}
...
local_irq_disable(); // 关闭本地中断
/*
* 再次检查是否有新增的软中断
* 因为软中断执行过程是开中断的,可能在这个过程中又发生了新中断
* 若是且满足以下三个条件,则跳转到 restart 标签处重复执行:
* 1. 软中断处理时间没有超过 2ms
* 2. !need_resched() 表示没有调度请求
* 3. 循环没有超过 10 次
* 否则唤醒 ksoftirq 内核线程来处理软中断
*/
pending = local_softirq_pending();
if (pending) {
if (time_before(jiffies, end) && !need_resched() &&
--max_restart)
goto restart;
wakeup_softirqd();
}
...
account_softirq_exit(current); // 表示离开软中断上下文
}
2 tasklet
2.1 tasklet 初始化
tasklet 机制是一种特殊的软中断,本质是软中断的一个变体,运行在软中断上下文中。
tasklet & softirq
tasklet 代码在某一时刻只能在一个 CPU(当初提交它的 CPU) 上运行,无需考虑并行处理所带来的线程安全问题;一般的软中断服务函数(action)在同一时刻可以被多个 CPU 并发地执行。
软中断类型内核中都是静态分配,不支持动态分配;而 tasklet 支持动态和静态分配
Linux 用数据结构 tasklet_struct
来描述一个 tasklet,每个结构代表一个独立的小任务。
<include/linux/interrupt.h>
struct tasklet_struct
{
struct tasklet_struct *next; // 指向下一个 tasklet 的指针,多个 tasklet 连成一个链表
unsigned long state; // 定义了该 tasklet 的当前状态。只使用了前两个 bit,其中bit0:TASKLET_STATE_SCHED = 1 时表示该 tasklet 已经被调度等待执行;bit1:TASKLET_STATE_RUN = 1 时表示该 tasklet 正在被执行,从而防止一个 tasklet 被多个 CPU 调用
atomic_t count; // 对该 tasklet 的引用计数值。只有当 count = 0 时才能被执行
void (*func)(unsigned long); // 指向以函数形式表示的可执行 tasklet 代码段,类似于 action 函数指针
unsigned long data; // 传递参数给 tasklet 处理函数,具体含义可由 func 函数自行解释
};
每个 CPU 维护了两个 tasklet 链表,一个是普通优先级的 tasklet_vec
,另一个是高优先级的 tasklet_hi_vec
,两者都是 Per-CPU
变量。其中 tasklet_vec
使用 TASKLET_SOFTIRQ
类型,优先级为6;tasklet_hi_vec
使用 HI_SOFTIRQ
类型,优先级为0。两个链表在系统启动时被初始化,详见 1.1 中的 softirq_init()
函数。
<kernel/softirq.c>
struct tasklet_head {
struct tasklet_struct *head;
struct tasklet_struct **tail;
};
static DEFINE_PER_CPU(struct tasklet_head, tasklet_vec);
static DEFINE_PER_CPU(struct tasklet_head, tasklet_hi_vec);
Linux 中定义了两个宏用于静态声明一个 tasklet。二者的唯一区别在于 count
的值。其中 DECLARE_TASKLET
宏将 count
初始化为 0,表示该 tasklet 处于激活状态;而 DECLARE_TASKLET_DISABLED
宏则将 count
初始化为 1,表示该 tasklet 处于关闭状态。
<include/linux/interrupt.h>
#define DECLARE_TASKLET(name, func, data)
struct tasklet_struct name = { NULL, 0, ATOMIC_INIT(0), func, data }
#define DECLARE_TASKLET_DISABLED(name, func, data)
struct tasklet_struct name = { NULL, 0, ATOMIC_INIT(1), func, data }
同时,也可以调用 tasklet_init()
函数动态初始化 tasklet。
<kernel/softirq.c>
void tasklet_init(struct tasklet_struct *t,
void (*func)(unsigned long), unsigned long data)
{
t->next = NULL;
t->state = 0; // 未调用,未执行
atomic_set(&t->count, 0); // 激活态
t->func = func;
t->data = data;
}
通过调用 tasklet_kill()
可以清除掉一个 tasklet 成员变量,功能与 tasklet_init()
相反
<kernel/softirq.c>
void tasklet_kill(struct tasklet_struct *t)
{
if (in_interrupt())
pr_notice("Attempt to kill tasklet from interrupt\n");
while (test_and_set_bit(TASKLET_STATE_SCHED, &t->state))
wait_var_event(&t->state, !test_bit(TASKLET_STATE_SCHED, &t->state));
tasklet_unlock_wait(t);
tasklet_clear_sched(t); // 清除掉 TASKLET_STATE_SCHED 标志位
}
2.2 tasklet 调度
Linux 中用 tasklet_schedule()
调度 tasklet,即将初始化好的 tasklet 挂上链表尾部。
<include/linux/interrupt.h>
static inline void tasklet_schedule(struct tasklet_struct *t)
{
if (!test_and_set_bit(TASKLET_STATE_SCHED, &t->state))
__tasklet_schedule(t);
}
test_and_set_bit()
该函数原子地设置
tasklet_struct -> state -> TASKLET_STATE_SCHED
为 1,然后返回该state
的旧值,若为真,说明该 tasklet 已被挂载;否则调用__tasklet_schedule() -> __tasklet_schedule_common()
将该 tasklet 挂入链表中。
__tasklet_schedule_common()
在关闭中断的情况下,首先把 tasklet 挂载到 tasklet_vec
链表中,然后触发一个 TASKLET_SOFTIRQ
类型的软中断。
<kernel/softirq.c>
static void __tasklet_schedule_common(struct tasklet_struct *t,
struct tasklet_head __percpu *headp,
unsigned int softirq_nr)
{
struct tasklet_head *head;
unsigned long flags;
local_irq_save(flags); // 关闭本地中断
head = this_cpu_ptr(headp); // 挂载到链表上
t->next = NULL;
*head->tail = t;
head->tail = &(t->next);
raise_softirq_irqoff(softirq_nr); // 触发软中断调度
local_irq_restore(flags); // 打开本地中断
}
2.3 使能 & 禁止一个 tasklet
tasklet_disable()
用于禁止一个 tasklet 被执行,tasklet_enable()
用于使能一个 tasklet 被执行。
<include/linux/interrupt.h>
static inline void tasklet_disable(struct tasklet_struct *t)
{
tasklet_disable_nosync(t);
tasklet_unlock_wait(t);
smp_mb();
}
static inline void tasklet_enable(struct tasklet_struct *t)
{
smp_mb__before_atomic();
atomic_dec(&t->count);
}
使能 & 禁止
tasklet_disable()
->tasklet_disable_nosync()
->atomic_inc(&t->count)
atomic_inc()
是原子增量操作,所以本质就是将count ++
,由上面的讨论可知此时该 tasklet 无法被执行。
atomic_dec(&t->count)
功能相反,原理类似。
2.4 tasklet 的执行
在 __tasklet_schedule_common
被执行之后,tasklet 被成功挂载到执行链表中。与 softirq 类似地,只有在被 CPU 调度后,tasklet 才会开始执行。需要强调的是,tasklet 被挂上了哪个 CPU 的 tasklet_vec
链表,就由哪个 CPU 的软中断来执行。我们可以观察到:当某个 tasklet 被某一 CPU 首次调度时,tasklet_schedule()
会将 TASKLET_STATE_SCHED
置位,而此后任一 CPU 企图再次调度该 tasklet 时,会因为 TASKLET_STATE_SCHED
的原因无法执行 __tasklet_schedule()
,无法完成调度,因此上述功能得以实现。
同样地,在 SMP 系统中,当某个 CPU 企图执行一个 tasklet 时,也要进行类似加锁操作。我们在 2.2 中已经分析过 test_and_set_bit()
的作用。简单地,该函数原子地将 TASKLET_STATE_RUN
置位,并返回其旧值。
<include/linux/interrupt.h>
static inline int tasklet_trylock(struct tasklet_struct *t)
{
return !test_and_set_bit(TASKLET_STATE_RUN, &(t)->state);
}
软中断执行会按照软中断状态 __softirq_pending
来依次执行 pending
状态的软中断。当执行 TASKLET_SOFTIRQ
类型的软中断时,回调函数 tasklet_action()
会被调用。
tasklet_action()
-> tasklet_action_common
<kernel/softirq.c>
static void tasklet_action_common(struct softirq_action *a,
struct tasklet_head *tl_head,
unsigned int softirq_nr)
{
struct tasklet_struct *list;
local_irq_disable(); // 关闭本地中断
/*
* 将 tasklet_vec 链表头加载到临时局部链表 list 中,
* 重置 tasklet_vec 链表
*/
list = tl_head->head;
tl_head->head = NULL;
tl_head->tail = &tl_head->head;
local_irq_enable(); // 打开本地中断
/*
* 遍历链表 list,逐个处理 tasklet
* 整个过程开中断,可能进入嵌套中断
*/
while (list) {
struct tasklet_struct *t = list;
list = list->next;
if (tasklet_trylock(t)) { // 企图加锁,并设置 TASKLET_STATE_RUN 标志位
if (!atomic_read(&t->count)) { // 原子读 count,确定是否可执行
if (tasklet_clear_sched(t)) { // 清除 TASKLET_STATE_SCHED 标志位,取消调度状态
if (t->use_callback)
t->callback(t);
else
t->func(t->data); // 调用处理函数
}
tasklet_unlock(t); // 清除 TASKLET_STATE_RUN 位
continue;
}
tasklet_unlock(t); // 当 count != 0 的特殊情况,说明该 tasklet 需要清除
}
local_irq_disable();
t->next = NULL;
*tl_head->tail = t;
tl_head->tail = &t->next;
__raise_softirq_irqoff(softirq_nr);
local_irq_enable();
}
}
tasklet_trylock(t) 有可能返回 false 吗?
初次分析上述代码时我产生了疑惑:假设在 CPU A 下运行上述代码 ,在 CPU A 的 tasklet 链表上挂载了一个 t,当 CPU A 企图执行该 t 时检查出
tasklet_trylock(t)
的返回值是false
,即该 tasklet 已在其他 CPU 中执行了。但是我们知道,在 CPU A 上挂载的 tasklet ,理论上是不会在其他 CPU 中重复挂载的,这意味着该 tasklet 不会出现在其他 CPU 的tasklet_vec
中,就更不可能在其他 CPU 中执行了,这怎么解释上述代码?难道tasklet_trylock(t)
是冗余的?永远都不会是false
?以下举例说明:
假设 CPU 0 在执行设备甲的回调函数时(
TASKLET_STATE_SCHED = 0
,TASKLET_STATE_RUN = 1
)(显然此时可以嵌套中断),设备乙发生了中断并且被安排给 CPU 0 处理,则 CPU 0 暂停 tasklet 处理转而处理设备乙的硬件中断(关中断状态下)。此时设备甲又产生了新的中断,GIC 将该中断派发给 CPU 1 处理。CPU 1快速处理完硬中断,并调用tasklet_schedule()
,此时 CPU 1 发现TASKLET_STATE_SCHED = 0
(说明可以添加该中断),所以该 tasklet 被添加到 CPU 1 的tasklet_vec
链表中。但当其执行到tasklet_action()
的tasklet_trylock(t)
时发现TASKLET_STATE_RUN = 1
,无法获取该锁,于是 CPU 1 跳过了此次 tasklet。直到 CPU 0 中断返回,将TASKLET_STATE_RUN
标志位置为 0 之后,CPU 1 才能在下一次软中断执行中继续执行该 tasklet。上述情景说明,在
SMP
系统中,可能有同一类型 tasklet 同时存在不同 CPU 的情况出现,故tasklet_trylock(t)
的检查是有意义的,不可或缺的。
四、参考文章
- https://blog.csdn.net/myarrow/article/details/9287169
- https://blog.csdn.net/myarrow/article/details/9287169
- https://blog.csdn.net/KUNPLAYBOY/article/details/121109583
- https://www.sohu.com/a/435070592_120052431
- http://www.wowotech.net/kernel_synchronization/per-cpu.html
- https://blog.csdn.net/Joe_KingKiller/article/details/4408110
- https://www.bilibili.com/video/av334145952
- https://blog.csdn.net/jasonLee_lijiaqi/article/details/82721576
- https://zhuanlan.zhihu.com/p/363225717
- https://blog.csdn.net/lx123010/article/details/106791416