首发于术道经纬
Linux的中断处理机制 [四] - softirq(1)

Linux的中断处理机制 [四] - softirq(1)

Linux的中断处理机制 [一] - 数据结构(1)

为了避免嵌套带来的不确定性,可以选择在中断函数执行期间关闭中断。

可是,如果关中断的时间长了,势必会影响对其他IRQ的响应,时间短了又处理不完。为此,上文提到的第二级的中断处理又被分成了两个部分,一个是俗称的“顶半部”(top half),在此期间,中断关闭,软件主要完成对硬件中断的应答,需要快速执行完毕。

另一个与之对应的就是“底半部”(bottom half) ,在底半部中,中断打开,内核继续完成在顶半部中未完成的中断处理。

底半部代表了中断处理中被延后(deferred)执行的部分,目前Linux支持的底半部机制有3种:softirq, tasklet和work queue。

其中,tasklet的实现是建立在softirq之上的,两者都是由内核2.3版本引入的,而作为后起之秀的work queue,则是伴随2.5版本首次亮相的。从概念匹配的角度,我们有时会把顶半部对应地称为"hardirq"。

软中断

softirq,有时会被人们称作是"software interrupt"。在Linux中,早期用来实现system call的"int 0x80",以及用于异步通信的信号(signal)机制常常也被叫做software interrupt。为了区分,中文里会把它们分别叫做“软中断”和“软件中断”啥的,弄的人一头雾水。

其实啊,把"int 0x80"叫做software interrupt来自于Intel的手册,在具体的实现中,它确实有和硬件中断类似的处理流程,都走IDT表什么的,但它的作用是由ring 3切换到特权级更高的ring 0,对应从操作系统的用户态切换到内核态(现在这种实现系统调用的方式已经被SYSENTER/SYSCALL指令取代,基本不再使用了)。

而signal机制呢,算是从软件层面对硬件中断的一种模拟,它的效果和硬件中断是差不多的,但其实现方式和硬件中断的处理流程可以说是完全没有关系。至于softirq,则本身就是属于硬件中断处理的一部分,只是它处理的是相对不那么紧急,显得"soft"一些的部分。

signal和softirq在内核的实现中有很多相似之处,你将在本文后面的叙述中慢慢看到。

softirq的安装

中断的来源很多,所以softirq的种类也不少。内核的限制是不能超过32个,目前实际用到的有9个。

其中两个用来实现tasklet(HI_SOFTIRQ和TASKLET_SOFTIRQ),两个用于网络的发送和接收操作(NET_TX_SOFTIRQ和NET_RX_SOFTIRQ),一个用于调度器(SCHED_SOFTIRQ),实现SMP系统上周期性的负载均衡。在启用高分辨率定时器时,还需要一个HRTIMER_SOFTIRQ。

为了有效地管理不同的softirq中断源,Linux采用的是一个名为softirq_vec[]的数组,数组的大小由NR_SOFTIRQS 表示,这是在编译时就确定了的,不能在系统运行过程中动态添加。每个数组元素代表一种softirq的种类,而数组里存放的内容则是其各自对应的执行函数。

struct softirq_action
{
    void (*action)(struct softirq_action *);
};
struct softirq_action softirq_vec[NR_SOFTIRQS];

softirq通过open_softirq()实现了和执行函数的绑定,这一过程和使用signal()来于安装信号处理函数十分相似。所不同的是,信号即便不安装处理函数,内核也会为其提供一个默认的操作,但如果softirq不安装的话,那就真的不会处理了。

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

在softirq初始化函数中,完成了HI_SOFTIRQ和TASKLET_SOFTIRQ的执行函数的注册:

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

其他的一些softirq,则是在各自模块里初始化的,比如TIMER_SOFTIRQ的执行函数是在init_timers()里实现注册的:

void __init init_timers(void)
{
    init_timer_cpus();
    open_softirq(TIMER_SOFTIRQrun_timer_softirq);
}
softirq的触发

在中断的top half处理完后,就会通过raise_softirq()设置softirq的pending位图,这个pending位图由一个名为"__softirq_pending"的per-CPU形式的变量表示。

阅读时点击图片可获得更清晰视图

而后通过local_softirq_pending()查看pending位图中是否有待处理的softirq,如果有,就调用invoke_softirq()来触发softirq的处理流程,这个过程类似于信号的递送

void irq_exit(void)
{
    if (!in_interrupt() && local_softirq_pending()) 		
        invoke_softirq();
    ...
}

在invoke_softirq()中,如果没有"force_irqthreads"限定的中断线程化,就调用__do_softirq()直接执行softirq,否则就通过wakeup_softirqd()来唤醒ksoftirqd(如上图左侧淡蓝色虚线所示)。

void invoke_softirq(void)
{
    if (!force_irqthreads) {
	__do_softirq();
    } else {
	wakeup_softirqd();
    }
}

ksoftirqd的触发不止这一处,等到它下次出现的时候,我们再来介绍它,先把注意力放在这个基础的__do_softirq()函数上面。

void __softirq_entry __do_softirq(void)
{
    struct softirq_action *h = softirq_vec;
	
    while ((softirq_bit = ffs(pending))) {
        h += softirq_bit - 1;
        h->action(h);
        h++;
        pending >>= softirq_bit;
    ...
}

当前pending位图中可能有多个待处理的softirq,__do_softirq()会按照它们在位图中的顺序,也就是用ffs()找到位图中从右(LSB)到左(MSB)第一个非0的bit,依次执行该bit对应的处理函数(如上图红色虚线所示)。

softirq在pending位图中的顺序同时也是它们在softirq_vec[]数组中编号的顺序,这里编号形成的优先级并不影响不同softirq的执行频率,只是定义了它们同时pending时的执行次序。

__do_softirq()是紧接着"hardirq"执行的,它也是运行在中断上下文,如果非要和“hardirq上下文”有所区分的话,可以认为这是“softirq上下文”,在softirq上下文中,也是不能睡眠的。

线程饥饿

在while循环中执行"pending"里所有置位bit对应的softirq处理函数的过程中,有可能产生新的softirq,所以按理执行完一轮后,应再检查一遍 pending 位图,再返回之前被中断打断的进程上下文(process context)。

可是,像网络收发这种高速数据通信中,可能会产生非常多的中断,这样系统将一直忙于执行softirq,从而让需要对网络报文进行进一步处理的用户线程得不到执行的机会,俗称"饥饿"(starvation)。

为了解决这一问题,__do_softirq()对softirq的处理时间和处理次数都有所限制,其中时间限定为2ms(用MAX_SOFTIRQ_TIME表示),次数限制为10次(用MAX_SOFTIRQ_RESTART表示)。如果时间超过了2ms或者次数超过了10次,就调用wakeup_softirqd(),交由ksoftirqd来处理。

restart:
pending = local_softirq_pending();
if (pending) {
    if (time_before(jiffies, end) && !need_resched() && --max_restart)
        goto restart;

	wakeup_softirqd();
}

这里ksofitrqd再次出现了,那这个ksofitrqd到底是什么呢?请看下文分解。


参考:


原创文章,转载请注明出处。

编辑于 2022-03-26 17:30