中断绑定——中断亲和力(IRQ Affinity)
在 SMP 体系结构中,我们可以通过调用系统调用和一组相关的宏来设置 CPU 亲和力(CPU affinity),将一个或多个进程绑定到一个或多个处理器上运行。中断在这方面也毫不示弱,也具有相同的特性。中断亲和力是指将一个或多个中断源绑定到特定的 CPU 上运行。中断亲和力最初由 Ingo Molnar 设计并实现。
在 /proc/irq 目录中,对于已经注册中断处理程序的硬件设备,都会在该目录下存在一个以该中断号命名的目录 IRQ# ,IRQ# 目录下有一个 smp_affinity 文件(SMP 体系结构才有该文件),它是一个 CPU 的位掩码,可以用来设置该中断的亲和力, 默认值为 0xffffffff,表明把中断发送到所有的 CPU 上去处理。如果中断控制器不支持 IRQ affinity,不能改变此默认值,同时也不能关闭所有的 CPU 位掩码,即不能设置成 0x0。
我们以网卡(eth1,中断号 44 )为例,在具有 8 个 CPU 的服务器上来设置网卡中断的亲和力(以下数据出自内核源码 Documentation\IRQ-affinity.txt):
[root@moon 44]# cat smp_affinity
ffffffff
[root@moon 44]# echo 0f > smp_affinity
[root@moon 44]# cat smp_affinity
0000000f
[root@moon 44]# ping -f h
PING hell (195.4.7.3): 56 data bytes
...
--- hell ping statistics ---
6029 packets transmitted, 6027 packets received, 0% packet loss
round-trip min/avg/max = 0.1/0.1/0.4 ms
[root@moon 44]# cat /proc/interrupts | grep 44:
44: 0 1785 1785 1783 1783 1 1 0 IO-APIC-level eth1
[root@moon 44]# echo f0 > smp_affinity
[root@moon 44]# ping -f h
PING hell (195.4.7.3): 56 data bytes
..
--- hell ping statistics ---
2779 packets transmitted, 2777 packets received, 0% packet loss
round-trip min/avg/max = 0.1/0.5/585.4 ms
[root@moon 44]# cat /proc/interrupts | grep 44:
44: 1068 1785 1785 1784 1784 1069 1070 1069 IO-APIC-level eth1
[root@moon 44]#
在上例中,我们首先只允许在 CPU0~3 上处理网卡中断,接着运行 ping 程序,不难发现在 CPU4~7 上并没有对网卡中断进行处理。然后只在 CPU4~7 上对网卡中断进行处理, CPU0~3 不对网卡中断进行任何处理,运行 ping 程序之后,再次查看 /proc/interrupts 文件时,不难发现 CPU4~7 上的中断次数明显增加,而 CPU0~3 上的中断次数没有太大的变化。
在探讨中断亲和力的实现原理之前,我们首先来了解 I/O APIC 中的组成。
I/O APIC 由一组 24 条 IRQ 线,一张 24 项的中断重定向表(Interrupt Redirection Table),可编程寄存器,以及通过 APIC 总线发送和接收 APIC 信息的一个信息单元组成。其中与中断亲和力息息相关的是中断重定向表, 中断重定向表表中的每一项都可以被单独编程以指明中断向量和优先级、目标处理器及选择处理器的方式 。
通过表 2,不难发现 8259A 和 APIC 中断控制器最大不同点在于 hw_interrupt_type 类型变量的最后一项。对于 8259A 类型,set_affinity 被置为 NULL,而对于 SMP 的 APIC 类型,set_affinity 被赋值为 set_ioapic_affinity。
在系统初始化期间,对于 SMP 体系结构,将会调用 setup_IO_APIC_irqs() 函数来初始化 I/O APIC 芯片,芯片中的中断重定向表的 24 项被填充。在系统启动期间,所有的 CPU 都执行 setup_local_APIC() 函数,完成本地的 APIC 初始化。当有中断被触发时,将相应的中断重定向表中的值转换成一条消息,然后,通过 APIC 总线把消息发送给一个或多个本地 APIC 单元,这样,中断就能立即被传递给一个特定的 CPU,或一组 CPU,或所有的 CPU,从而来实现中断亲和力。
当我们通过 cat 命令将 CPU 掩码写进 smp_affinity 文件时,此时的调用路线图为:write() ->sys_write() ->vfs_write() ->proc_file_write() ->irq_affinity_write_proc() ->set_affinity() ->set_ioapic_affinity() ->set_ioapic_affinity_irq() ->io_apic_write();其中在调用 set_ioapic_affinity_irq() 函数时,以中断号和 CPU 掩码作为参数,接着继续调用 io_apic_write(),修改相应的中断重定向中的值,来完成中断亲和力的设置。当执行 ping 命令时,网卡中断被触发,产生了一个中断信号,多 APIC 系统根据中断重定向表中的值,依照仲裁机制,选择 CPU0~3 中的某一个 CPU,并将该信号传递给相应的本地 APIC,本地 APIC 又中断它的 CPU,整个事件不通报给其他所有的 CPU。
新特性展望——中断线程化(Interrupt Threads)
在嵌入式领域,业界对 Linux 实时性的呼声越来越高,对中断进行改造势在必行。在 Linux 中,中断具有最高的优先级。不论在任何时刻,只要产生中断事件,内核将立即执行相应的中断处理程序,等到所有挂起的中断和软中断处理完毕后才能执行正常的任务,因此有可能造成实时任务得不到及时的处理。中断线程化之后,中断将作为内核线程运行而且被赋予不同的实时优先级,实时任务可以有比中断线程更高的优先级。这样,具有最高优先级的实时任务就能得到优先处理,即使在严重负载下仍有实时性保证。
目前较新的 Linux 2.6.17 还不支持中断线程化。但由 Ingo Molnar 设计并实现的实时补丁,实现了中断线程化。最新的下载地址为:
下面将对中断线程化进行简要分析。
在初始化阶段,中断线程化的中断初始化与常规中断初始化大体上相同,在 start_kernel() 函数中都调用了 trap_init() 和 init_IRQ() 两个函数来初始化 irq_desc_t 结构体,不同点主要体现在内核初始化创建 init 线程时,中断线程化的中断在 init() 函数中还将调用 init_hardirqs(kernel/irq/manage.c(已经打过上文提到的补丁)),来为每一个 IRQ 创建一个内核线程,最高实时优先级为 50,依次类推直到 25,因此任何 IRQ 线程的最低实时优先级为 25。
void __init init_hardirqs(void)
{
……
for (i = 0; i action && !(desc->status & IRQ_NODELAY))
desc->thread = kthread_create(do_irqd, desc, "IRQ %d", irq);
……
}
}
static int do_irqd(void * __desc)
{
……
/*
* Scale irq thread priorities from prio 50 to prio 25
*/
param.sched_priority = curr_irq_prio;
if (param.sched_priority > 25)
curr_irq_prio = param.sched_priority - 1;
……
}
如果某个中断号状态位中的 IRQ_NODELAY 被置位,那么该中断不能被线程化。
在中断处理阶段,两者之间的异同点主要体现在:两者相同的部分是当发生中断时,CPU 将调用 do_IRQ() 函数来处理相应的中断,do_IRQ() 在做了必要的相关处理之后调用 __do_IRQ()。两者最大的不同点体现在 __do_IRQ() 函数中,在该函数中,将判断该中断是否已经被线程化(如果中断描述符的状态字段不包含 IRQ_NODELAY 标志,则说明该中断被线程化了),对于没有线程化的中断,将直接调用 handle_IRQ_event() 函数来处理。
fastcall notrace unsigned int __do_IRQ(unsigned int irq, struct pt_regs *regs)
{
……
if (redirect_hardirq(desc))
goto out_no_end;
……
action_ret = handle_IRQ_event(irq, regs, action);
……
}
int redirect_hardirq(struct irq_desc *desc)
{
……
if (!hardirq_preemption || (desc->status & IRQ_NODELAY) || !desc->thread)
return 0;
……
if (desc->thread && desc->thread->state != TASK_RUNNING)
wake_up_process(desc->thread);
……
}
对于已经线程化的情况,调用 wake_up_process() 函数唤醒中断处理线程,并开始运行,内核线程将调用 do_hardirq() 来处理相应的中断,该函数将判断是否有中断需要被处理,如果有就调用 handle_IRQ_event() 来处理。handle_IRQ_event() 将直接调用相应的中断处理函数来完成中断处理。
不难看出,不管是线程化还是非线程化的中断,最终都会执行 handle_IRQ_event() 函数来调用相应的中断处理函数,只是线程化的中断处理函数是在内核线程中执行的。
并不是所有的中断都可以被线程化,比如时钟中断,主要用来维护系统时间以及定时器等,其中定时器是操作系统的脉搏,一旦被线程化,就有可能被挂起,这样后果将不堪设想,所以不应当被线程化。如果某个中断需要被实时处理,它可以像时钟中断那样,用 SA_NODELAY 标志来声明自己非线程化,例如:
static struct irqaction irq0 = {
timer_interrupt, SA_INTERRUPT | SA_NODELAY, CPU_MASK_NONE, "timer", NULL, NULL
};
其中,SA_NODELAY 到 IRQ_NODELAY 之间的转换,是在 setup_irq() 函数中完成的。
中断负载均衡—SMP体系结构下的中断
中断负载均衡的实现主要封装在 arch\ arch\i386\kernel\io-apic.c 文件中。如果在编译内核时配置了 CONFIG_IRQBALANCE 选项,那么 SMP 体系结构中的中断负载均衡将以模块的形式存在于内核中。
late_initcall(balanced_irq_init);
#define late_initcall(fn) module_init(fn) //include\linux\init.h
在 balanced_irq_init() 函数中,将创建一个内核线程来负责中断负载均衡:
static int __init balanced_irq_init(void)
{ ……
printk(KERN_INFO "Starting balanced_irq\n");
if (kernel_thread(balanced_irq, NULL, CLONE_KERNEL) >= 0)
return 0;
else
printk(KERN_ERR "balanced_irq_init: failed to spawn balanced_irq");
……
}
在 balanced_irq() 函数中,每隔 5HZ=5s 的时间,将调用一次 do_irq_balance() 函数,进行中断的迁徙。将重负载 CPU 上的中断迁移到较空闲的CPU上进行处理。