Linux内核——软中断的特点以及注册和触发

跟着简叔学的,可以B站搜索 简说linux

1.软中断一般是哪些

硬件中断往往处理简单的操作,触发由外设发出。一般申请内存,保存数据一般由(底半部)来实现,在内核中有三种底半部BH,软中断、tasklet ,工作队列。工作队列是一个内核的线程,tasklet是基于软中断来实现。对于软中断来说,处理函数是可以同时运行在多个cpu之上,tasklet只能运行在唯一一个cpu上。

内核中定义了几种常用的软中断 /kernel/softirq.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//软中断处理函数
struct softirq_action
{
void (*action)(struct softirq_action *);
};

static struct softirq_action softirq_vec[NR_SOFTIRQS] __cacheline_aligned_in_smp;

DEFINE_PER_CPU(struct task_struct *, ksoftirqd);

const char * const softirq_to_name[NR_SOFTIRQS] = {
"HI", "TIMER", "NET_TX", "NET_RX", "BLOCK", "IRQ_POLL",
"TASKLET", "SCHED", "HRTIMER", "RCU"
};

2.软中断的使用

软中断的使用可以分为3步,软中断的注册,触发,与执行

2.1软中断的注册

其实就是给第nr个软中断的操作设为 action

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

很多地方都用到了软中断的注册。

1678178880618

在软中断的初始化环节,注册了两个软中断,tasklet与Hi

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
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;
}

open_softirq(TASKLET_SOFTIRQ, tasklet_action);
open_softirq(HI_SOFTIRQ, tasklet_hi_action);
}

//其中tasklet_action就是tasklet的处理逻辑
static __latent_entropy void tasklet_action(struct softirq_action *a)
{
struct tasklet_struct *list;

local_irq_disable();
list = __this_cpu_read(tasklet_vec.head);
__this_cpu_write(tasklet_vec.head, NULL);
__this_cpu_write(tasklet_vec.tail, this_cpu_ptr(&tasklet_vec.head));
local_irq_enable();

while (list) {
struct tasklet_struct *t = list;

list = list->next;

if (tasklet_trylock(t)) {
if (!atomic_read(&t->count)) {
if (!test_and_clear_bit(TASKLET_STATE_SCHED,
&t->state))
BUG();
t->func(t->data);
tasklet_unlock(t);
continue;
}
tasklet_unlock(t);
}

local_irq_disable();
t->next = NULL;
*__this_cpu_read(tasklet_vec.tail) = t;
__this_cpu_write(tasklet_vec.tail, &(t->next));
__raise_softirq_irqoff(TASKLET_SOFTIRQ);
local_irq_enable();
}
}

2.2软中断的触发

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
void raise_softirq(unsigned int nr)
{
unsigned long flags;
//首先禁止本机中断 就是所有中断
local_irq_save(flags);
raise_softirq_irqoff(nr);

local_irq_restore(flags);
}

//调用这个函数之前 一定是已经禁止了本机中断
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();
}

void __raise_softirq_irqoff(unsigned int nr)
{
trace_softirq_raise(nr);
or_softirq_pending(1UL << nr);
}

层层寻找下去之后,会有一个软件中断寄存器结构的定义。每个外设都会有一个硬件中断寄存器,硬件寄存器中每一个bit都代表着某一类型的硬件设备中断。对于软件中断来讲,也会有不同类型的软件中断,那么同理。软件中断寄存器也会将某一位置一从而判断哪一个类型的软件中断类型被触发。触发软中断时,会将中断号放到触发中断的cpu的软件寄存器里边。

2.3软中断的执行

1678185060977

2.3.1执行时机

2.3.1.1 ksoftirqd线程

软中断初始化时候回调用,spawn_ksoftirqd,创建一个线程,每个cpu都会创建这个线程。

1678185992489

去看看 run_ksoftirqd具体执行了什么

1678186119041

首先有一个中断打开的动作,执行软中断时候必须要打开中断。然后local_softirq_pending()可以判断软中段的发生,然后由__do_softirq()执行中断处理的逻辑。

2.3.1.2 硬件中断返回irq_exit()

kernel/softirq.c

1678186601545

ivoke_softirq会处理cpu上面待处理的软中断,

1678186695049

最终会调用 __do_softirq. 也就是最终的

2.3.1.3 直接调用,例如在网络子系统等

找到最后也是直接调用 _do_softirq,所以说都是调用 _do_softirq函数。

2.3.2执行过程

执行过程就是上文说的_do_softirq函数,如何保证触发软中断的cpu也去执行这个中断处理,如果实现中断优先级,

kernel/softirq.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
asmlinkage __visible void __softirq_entry __do_softirq(void)
{
unsigned long end = jiffies + MAX_SOFTIRQ_TIME;
unsigned long old_flags = current->flags;
int max_restart = MAX_SOFTIRQ_RESTART;
struct softirq_action *h;
bool in_hardirq;
__u32 pending;
int softirq_bit;

/*
* Mask out PF_MEMALLOC s current task context is borrowed for the
* softirq. A softirq handled such as network RX might set PF_MEMALLOC
* again if the socket is related to swap
*/
current->flags &= ~PF_MEMALLOC;

pending = local_softirq_pending(); //获取未决软中断, 这个是每个cpu都有自己的寄存器实现了谁触发就谁处理。
account_irq_enter_time(current);

__local_bh_disable_ip(_RET_IP_, SOFTIRQ_OFFSET); //把那个preemtycount软中断位置1
in_hardirq = lockdep_softirq_start();

restart:
/* Reset the pending bitmask before enabling irqs */
set_softirq_pending(0); //软中断寄存器清零

local_irq_enable(); //打开本机中断

h = softirq_vec; //softirq_vec 是数组 所以h是基地址

while ((softirq_bit = ffs(pending))) { //依次之执行未决软中断 位数高的先执行
unsigned int vec_nr;
int prev_count;

h += softirq_bit - 1; //这次要处理的软中断地址

vec_nr = h - softirq_vec;
prev_count = preempt_count();

kstat_incr_softirqs_this_cpu(vec_nr);

trace_softirq_entry(vec_nr);
h->action(h); //然后就可以调用action执行具体这个软中断类型的action
trace_softirq_exit(vec_nr);
if (unlikely(prev_count != preempt_count())) {
pr_err("huh, entered softirq %u %s %p with preempt_count %08x, exited with %08x?\n",
vec_nr, softirq_to_name[vec_nr], h->action,
prev_count, preempt_count());
preempt_count_set(prev_count);
}
h++;
pending >>= softirq_bit;
}

rcu_bh_qs();
local_irq_disable(); //关闭中断

//这里就是优先级实现如果当前还有没有处理的软中断
pending = local_softirq_pending();
if (pending) { //并且执行的时间较少,当前没有更高优先级的进程去抢占,重启次数<10次
if (time_before(jiffies, end) && !need_resched() &&
--max_restart) //那么
goto restart;

wakeup_softirqd();
}

//推出软中断上下文环境
lockdep_softirq_end(in_hardirq);
account_irq_exit_time(current);
__local_bh_enable(SOFTIRQ_OFFSET);
WARN_ON_ONCE(in_interrupt());
tsk_restore_flags(current, old_flags, PF_MEMALLOC);
}

那么软中断是可以被硬中断打断执行的,比如我们在软执行的时候,突然又来了硬件中断。,开始执行硬件中断。