一、什么是下半部
中断是一个很霸道的东西,处理器一旦接收到中断,就会打断正在执行的代码,调用中断处理函数。如果在中断处理函数中没有禁止中断,该中断处理函数执行过程中仍有可能被其他中断打断。出于这样的原因,大家都希望中断处理函数执行得越快越好。
另外,中断上下文中不能阻塞,这也限制了中断上下文中能干的事。
基于上面的原因,内核将整个的中断处理流程分为了上半部和下半部。上半部就是之前所说的中断处理函数,它能最快的响应中断,并且做一些必须在中断响应之后马上要做的事情。而一些需要在中断处理函数后继续执行的操作,内核建议把它放在下半部执行。
拿网卡来举例,在linux内核中,当网卡一旦接受到数据,网卡会通过中断告诉内核处理数据,内核会在网卡中断处理函数(上半部)执行一些网卡硬件的必要设置,因为这是在中断响应后急切要干的事情。接着,内核调用对应的下半部函数来处理网卡接收到的数据,因为数据处理没必要在中断处理函数里面马上执行,可以将中断让出来做更紧迫的事情。
可以有三种方法来实现下半部:软中断、tasklet和等待队列。
二、软中断
软中断一般很少用于实现下半部,但tasklet是通过软中断实现的,所以先介绍软中断。字面理解,软中断就是软件实现的异步中断,它的优先级比硬中断低,但比普通进程优先级高,同时,它和硬中断一样不能休眠。
软中断是在编译时候静态分配的,要用软中断必须修改内核代码。
在kernel/softirq.c中有这样的一个数组:
static struct softirq_action softirq_vec[NR_SOFTIRQS] __cacheline_aligned_in_smp;
内核通过一个softirq_action数组来维护的软中断,NR_SOFTIRQS是当前软中断的个数,待会再看他在哪里定义。
先看一下softirq_action结构体:
struct softirq_action
{
void (*action)(struct softirq_action *);
};
结构体里面就一个软中断函数,他的参数就是本身结构体的指针。之所以这样设计,是为了以后的拓展,如果在结构体中添加了新成员,也不需要修改函数接口。在以前的内核,该结构体里面还有一个data的成员,用于传参,不过现在没有了。
接下来看一下如何使用软中断实现下半部
一、要使用软中断,首先就要静态声明软中断:
enum
{
HI_SOFTIRQ=0,
TIMER_SOFTIRQ,
NET_TX_SOFTIRQ,
NET_RX_SOFTIRQ,
BLOCK_SOFTIRQ,
BLOCK_IOPOLL_SOFTIRQ,
TASKLET_SOFTIRQ,
SCHED_SOFTIRQ,
HRTIMER_SOFTIRQ,
RCU_SOFTIRQ, /* Preferable RCU should always be the last softirq */
NR_SOFTIRQS
};
上面通过枚举定义了NR_SOFTIRQS
(10)个软中断的索引号,优先级最高是0(HI_SOFTIRQ
)
二、定义了索引号后,还要注册处理程序。
通过函数open_softirq
来注册软中断处理函数,使软中断索引号与中断处理函数对应。该函数在kernel/softirq.c中定义:
void open_softirq(int nr, void (*action)(struct softirq_action *))
{
/* softirq_vec是个struct softirq_action类型的数组 */
softirq_vec[nr].action = action;
}
系统一般使用open_softirq()
函数进行软中断描述符的初始化,主要就是将action函数指针指向该软中断应该执行的函数。在tart_kernel()
进行系统初始化中,就调用了softirq_init()
函数对HI_SOFTIRQ
和TASKLET_SOFTIRQ
两个软中断进行了初始化
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;
}
/* 开启常规tasklet */
open_softirq(TASKLET_SOFTIRQ, tasklet_action);
/* 开启高优先级tasklet */
open_softirq(HI_SOFTIRQ, tasklet_hi_action);
}
/* 开启软中断 */
void open_softirq(int nr, void (*action)(struct softirq_action *))
{
softirq_vec[nr].action = action;
}
可以看到,TASKLET_SOFTIRQ
的action操作使用了tasklet_action()
函数,HI_SOFTIRQ
的action操作使用了tasklet_hi_action()
函数,这两个函数我们需要结合tasklet进行说明。我们也可以看看其他的软中断使用了什么函数:
open_softirq(TIMER_SOFTIRQ, run_timer_softirq);
open_softirq(NET_TX_SOFTIRQ, net_tx_action);
open_softirq(NET_RX_SOFTIRQ, net_rx_action);
open_softirq(BLOCK_SOFTIRQ, blk_done_softirq);
open_softirq(BLOCK_IOPOLL_SOFTIRQ, blk_iopoll_softirq);
open_softirq(SCHED_SOFTIRQ, run_rebalance_domains);
open_softirq(HRTIMER_SOFTIRQ, run_hrtimer_softirq);
open_softirq(RCU_SOFTIRQ, rcu_process_callbacks);
除了TASKLET_SOFTIRQ
和HI_SOFTIRQ
,其他的软中断更多地是用于特定的设备和环境,对于我们普通的IO驱动和设备而已,使用的软中断几乎都是TASKLET_SOFTIRQ
和HI_SOFTIRQ
,而系统为了对这些不同IO设备进行统一的处理,就在TASKLET_SOFTIRQ
和HI_SOFTIRQ
的action函数中使用到了tasklet。
对于每个CPU,都有一个irq_cpustat_t
的数据结构,里面有一个__softirq_pending
变量,这个变量很重要,用于表示该CPU的哪个软中断处于挂起状态,在软中断处理时可以根据此值跳过不需要处理的软中断,直接处理需要处理的软中断。内核使用local_softirq_pending()
获取此CPU的__softirq_pending
的值。
当使用open_softirq
设置好某个软中断的action指针后,该软中断就会开始可以使用了,其实更明了地说,从中断初始化完成开始,即使所有的软中断都没有使用open_softirq()
进行初始化,软中断都已经开始使用了,只是所有软中断的action都为空,系统每次执行到软中断都没有软中断需要执行罢了。
在每个CPU上一次软中断处理的一个典型流程是:
1, 硬中断执行完毕,开中断。
2, 检查该CPU是否处于嵌套中断的情况,如果处于嵌套中,则不执行软中断,也就是在最外层中断才执行软中断。
3, 执行软中断,设置一个软中断执行最多使用时间和循环次数(10次)。
4, 进入循环,获取CPU的__