软中断和硬中断

中断概述

Linux内核需要对连接到计算机上所有硬件设备进行管理,毫无疑问这是它分内的事情。其通过中断机制让管理的硬件设备主动通知,而不是其主动轮询。
中断是一种电信号,由硬件设备产生送入中断控制器的输入引脚,然后中断控制器会想处理器发出信号;处理器收到该信号后,停下当前正在处理的事情,跳到中断处理程序的入口点,进行中断处理。当然处理器会通知操作系统已经产生中断;操作系统也可能会进行适当的处理。

处理器通过中断向量识别产生的中断,linux系统下Intel X86支持256中断向量,中断编号0-255

0-31 异常 非屏蔽 固定不变
32-47 屏蔽中断(IO设备)
48-25 软中断

硬件中断

硬中断是外部设备对CPU的中断,硬中断可抢占软中断,优先级高执行较快。
硬中断的本质是接收到中断信号后,跳转到公共段代码执行do_IRQ,并切换到硬中断请求栈,执行中断回调函数。

硬件中断流程

硬中断的汇编处理->do_IRQ->handle_irq->handle_edge_irq(handle_level_irq)->handle_irq_event->具体设备的硬中断处理

嵌套

linux下硬件中断可以嵌套,且无优先级别;除同种中断外,一个中断可打断另一个中断。此种机制短时间内可以接受更多的中断,可以有大的设备控制吞吐量;无优先级可以简化内核。
同种中断处理机制可以描述为,中断数据结构会设置IRQD_IRQ_INPROGRESS中断不处理标识,本地CPU或者其它CPU如果检查到此种中断的该标记,会直接退出,置上IRQS_PENDING后续处理标记。

软中断

软中断是硬中断服务程序对内核的中断,软中断时一种推后执行的机制,软中断是bottom half,上半部在屏蔽中断的上下文中运行,软中断相对来讲不是非常紧急,通常还比较耗时,不会在中断上下文中执行系统会自行安排运行时机。软中断不会抢占另一个软中断。

原理概述

1.软中断通过open_softirq注册一个软中断处理函数,在软中断向量表softirq_vec数组中添加新的action函数

1
2
3
4
5
6
7
//定时器init_timers调用初始化软中断调用函数
open_softirq(TIMER_SOFTIRQ, run_timer_softirq);
...
void open_softirq(int nr, void (*action)(struct softirq_action *))
{
softirq_vec[nr].action = action;
}

2.调用raise_softirq软中断触发函数,即软中断标记为挂起状态

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
/*
* This function must run with irqs disabled!
*/
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(unsigned int nr)
{
unsigned long flags;
//关闭本地CPU中断
local_irq_save(flags);
raise_softirq_irqoff(nr);
local_irq_restore(flags);
}

内核会在一些位置检查是否有挂起状态的软中断,如果有的话调用do_softirq执行软中断处理action函数
3.do_softirq完成两件事情
(1)切换到软件请求栈,让其处于软中断上下文
(2)执行do_softirq
4.
do_softirq
(1)执行软中断处理函数
(2)如果软中处理函数超过10个,唤醒内核线程让其处理本地CPU软中断。

软中断本质就是内核在某些位置检查是否有挂起的软中断(local_software_pending()不为0指有挂起软中断),若有则调用do_softirq切换到软中断请求栈,调用__do_softirq。

进程角度看软中断执行过程

步骤1:将返回四值和CPU状态寄存器压栈
步骤2:修改特权级别(系统程序需要核心态特权才能运行,用户态函数只能通过软中断调用系统API),设置中断事务标记
步骤3:唤醒守护线程,检测中断状态寄存器,发现软中断事务
步骤4:根据中断号通过查找中断向量表,找到ISR中断服务历程地址,跳转执行
步骤5:中断服务程序执行完成后,返回压栈的函数执行点

嵌套

软中断不打断软中断,相同软中断可在所有CPU上同时执行

软中断触发时机

(1)调用do_IRQ完成I/O中断时调用irq_exit
irq_exit->invoke_softirq->do_softirq
(2)如果系统使用I/O APIC,在处理完本地时钟中断时
(3)local_bh_enable->do_softirq

1
2
3
4
void local_bh_enable(void)
{
_local_bh_enable_ip(_RET_IP_);
}

(4)在SMP中,当CPU处理完被CALL_FUNCTION_VECTOR处理器间中断所触发的函数时:

1
2
3
4
5
6
7
8
void smp_trace_call_function_interrupt(struct pt_regs *regs)
{
smp_entering_irq();
trace_call_function_entry(CALL_FUNCTION_VECTOR);
__smp_call_function_interrupt();
trace_call_function_exit(CALL_FUNCTION_VECTOR);
exiting_irq();
}

exiting_irq->irq_exit

__do_softirq

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
76
77
78
79
80
81
82
83
84
85
asmlinkage void __do_softirq(void)
{
struct softirq_action *h;
__u32 pending;
//软中断结束时间
unsigned long end = jiffies + MAX_SOFTIRQ_TIME;
int cpu;
unsigned long old_flags = current->flags;
//软中断执行次数10次
int max_restart = MAX_SOFTIRQ_RESTART;

/*
* 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;

//获得CPU的软中断掩码,这时候仍然是关中断,可安全获得掩码
pending = local_softirq_pending();
//统计信息:进程被中断使用时间
account_irq_enter_time(current);
//执行完该函数后,关闭软中断,后续即使硬件再次触发新的软中断,也不会重新进入__do_softirq
__local_bh_disable(_RET_IP_, SOFTIRQ_OFFSET);
lockdep_softirq_enter();//just for debugging

cpu = smp_processor_id();

restart:
/* Reset the pending bitmask before enabling irqs */
//中断掩码清0,当然局部变量pending已经存储下来了,开启硬件中断后,可设置上新的软中断了
set_softirq_pending(0);
//开硬件中断,由于软中断执行时间一般较长,这里将中断打开避免长时间关中断,这段处理时间硬件中断就不会丢失了
local_irq_enable();

h = softirq_vec;

do {
if (pending & 1) {//中断挂起
unsigned int vec_nr = h - softirq_vec;//获取中断号
//保存抢占计数,后续无法破坏该计数了
int prev_count = preempt_count();
//软中断在每个核上执行计数
kstat_incr_softirqs_this_cpu(vec_nr);

trace_softirq_entry(vec_nr);
//执行回调函数
h->action(h);
trace_softirq_exit(vec_nr);
//软中断回调函数破坏了抢占计数,打印高级别警告信息,并恢复抢占计数
if (unlikely(prev_count != preempt_count())) {
printk(KERN_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() = prev_count;
}

rcu_bh_qs(cpu);
}
//处理下一个软中断
h++;
pending >>= 1;
} while (pending);//无软中断循环结束
//处理完一轮软中断后,因为处理时候中断是开启的,可能发生了硬件中断重新触发了软中断
//我们就关中断保障中断掩码再被修改
local_irq_disable();

//如果没有超过10次,且处理时间也在合法范围内,继续处理,否则唤醒ksoftirqd守护线程处理软中断
pending = local_softirq_pending();
if (pending) {
if (time_before(jiffies, end) && !need_resched() &&
--max_restart)
goto restart;
//调用线程处理剩下的中断
wakeup_softirqd();
}

lockdep_softirq_exit();

account_irq_exit_time(current);
__local_bh_enable(SOFTIRQ_OFFSET);
tsk_restore_flags(current, old_flags, PF_MEMALLOC);
}

防止软中断嵌套的流程:关软中断中肯定有一句原子地加1的关键语句,如果当前内核路径A在该原子操作之前被另一个内核路径B打断,则B执行完硬中断和软中断后,返回到A的此处,A接着执行该原子操作,之后的软中断处理应该是空转,因为肯定已经被B处理完了。如果在该原子操作之后被B打断,则B执行完硬中断,不会执行自己的软中断而是会直接退出(因为软中断嵌套了),返回到A的此处,A接着执行,这次A除了处理自己软中断,还会额外地处理B的软中断。
对于preempt_count中的软中断位,由上述可以知道,它的作用有两个:防止软中断在单cpu上嵌套;保证了在执行软中断期间不被抢占。

ksoftirqd进程

run_ksoftirqd是ksoftirqd线程的核心处理函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
static void run_ksoftirqd(unsigned int cpu)
{
//1.把当前CPU中断中断关掉
local_irq_disable();
//2.当前CPU是否有软中断
if (local_softirq_pending()) {
//3.处理软中断
__do_softirq();
rcu_note_context_switch(cpu);
local_irq_enable();
cond_resched();
return;
}
local_irq_enable();
}

该内核线程的优先级较低,且采用关闭中断保护方式,而不是关闭抢占保护方式,让更多的软中断被其它人调用执行。达到ksoftirqd进程的辅助作用。
一旦开始执行中断就不允许抢占了,软中断和硬中断都是这个做法,在执行期间不允许调度。