本篇是《Linux内核学习笔记》系列的第六篇。
中断为什么要有下半部?因为我们对中断处理程序有一些要求:
- 中断是异步的,它可能打断了其他代码(甚至可能是另一段中断处理代码)的执行。 因此,要快一点完成。
- 中断执行时,会视设置而定,屏蔽其他的一些中断。屏蔽中断后CPU和相关硬件无法通信,所以中断要快点完成。
- 中断处理程序不是在进程上下文中执行的,这一点在上一篇的中断源代码分析中有所体现。因此它不能被阻塞、被调度,需要即时响应。
从前两点来看,中断处理程序必须要快,这是显然的。那么第三点会使人产生一个疑问:内核能不能提供一个队列用以存放中断信息?这样就可以在有空的时候从队列中取出一个待处理的中断,进行处理。
实际上,这就是内核的实现方式。中断处理程序迅速地将某些可以延迟执行的中断的信息保存下来,加入到tasklet中进行调度。
强调一下,本文以中断处理程序表示上半部分,从而和下半部分明确区分开来。
1. 下半部概述
1.1. 约定俗成的划分原则
对于驱动程序的编写者而言,如何划分上下半部分,并没有明确的规定。但有一些是应该要放在中断处理程序的:
- 对时间高度敏感的任务。
- 和硬件相关的任务。操作硬件一般都需要即时完成。
- 不希望被打断的任务,例如不可重入的中断处理程序。利用中断机制,可以避免它和其他代码并发执行。
除此之外的代码最好还是放在下半部执行。
1.2. Linux 2.6下半部的环境
包含软中断、tasklet和工作队列三种机制。接下来我们会做大概的介绍。
2. 软中断
这里的软中断(softirq)可不是编程产生的那个软中断(应该叫软件中断)!它们是截然不同的东西。
接下来软中断的代码取自Linux 5.10版本的kernel/softirq.c。它是1992年编写的,我们应该可以期望该版本的软中断代码是2.6版本的超集。至少它和笔者正在阅读的教材是兼容的。
2.1. 软中断定义
这是软中断的定义,它是一个函数指针。可神奇的是,这个函数的参数竟然又是一个软中断指针。
struct softirq_action
{
void (*action)(struct softirq_action *);
};
如果my_softirq是一个指向某softirq_action的指针,那么这个函数的参数其实可以这么用:
my_softirq->action(my_softirq);
也就是传入软中断结构体本身的地址。这是一个高瞻远瞩的设计,可以保证未来软中断结构体加入新的域时,处理程序可以轻易地获得软中断的相关参数,同时保证和已有的软中断代码兼容。
2.2. 软中断数组
下面是软中断数组的定义。
static struct softirq_action softirq_vec[NR_SOFTIRQS] __cacheline_aligned_in_smp;
有什么软中断呢?我们来看看:
/* PLEASE, avoid to allocate new softirqs, if you need not _really_ high
frequency threaded job scheduling. For almost all the purposes
tasklets are more than enough. F.e. all serial device BHs et
al. should be converted to tasklets, not to softirqs.
*/
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, /* Preferable RCU should always be the last softirq */
NR_SOFTIRQS
};
我们看到了网络、计时器、调度程序这种特别经典的例子,以及之后要提到的tasklet。软中断处理程序的索引表示其优先级,即0号软中断优先级最高,9号是最低的。换言之,要添加一个新的软中断,不能只是简单地append,当然注释中建议还是尽量不要添加。
又是一个小技巧!
仔细看源代码中是如何表示枚举类型中元素数量的。
2.3. 软中断注册
软中断的注册是嵌在内核代码之中的,外部驱动程序不能进行动态注册。
以网络子系统为例,它在初始化子系统时进行软中断注册:
static int __init net_dev_init(void)
{
...
open_softirq(NET_TX_SOFTIRQ, net_tx_action);
open_softirq(NET_RX_SOFTIRQ, net_rx_action);
...
}
2.4. 软中断触发
例如调度子系统会在合适的时候调用如下代码来触发重量级的进程调度程序:
raise_softirq(SCHED_SOFTIRQ);
而网络子系统则调用调用如下代码进行数据处理:
raise_softirq_irqoff(NET_TX_SOFTIRQ);
两个函数不太一样,后者在中断已经被禁止的情况下才可调用,而前者则会在触发软中断前先行关中断,触发软中断后恢复中断现场。从下面的函数可以清楚地看到这种关系。
void raise_softirq(unsigned int nr)
{
unsigned long flags;
local_irq_save(flags);
raise_softirq_irqoff(nr);
local_irq_restore(flags);
}
2.5. 软中断处理
raise_softirq_irqoff将需要处理的软中断号添加到一个集合中。由于软中断最多只有32个,所以用一个u32变量就能将其全部存储下来。
中断处理程序触发软中断后就差不多可以退出了,随后内核即会调用do_softirq函数处理所有待处理的软中断。
3. tasklet
在选择应该将软中断还是tasklet作为中断下半部的实现方式时,绝大多数时候应该使用tasklet。它本质上也是软中断,但接口却更简单,更加易用。
4. 工作队列
TBD