这是spark架构代码分析的最后一篇,CPU初始化除了内存外的主要机制都已经分析到了,以后也不会再涉及spark相关的代码了。
一、T2平台中断硬件机制
要想对Linux里面关于T2中断的代码有深入理解,就要先对T2平台的硬件中断体系结构有所了解了。sparc平台的中断体系结构与其他很多平台有所不同,它没有专门的中断控制器,它的中断与CPU的体系架构是紧密相连的。
对T2 CPU有所了解的读者应该知道,T2 CPU是由8个物理处理器核,每个物理核心上又有8个线程,这里用线程这个术语并不准确,因为线程在汉语里面有很多的含义,容易引起误解,以后引用该术语时都用英文’strand’来代替,每个stand都可以看作是一个虚拟处理器。这样一个T2 CPU就总共有64个虚拟处理器。这里的虚拟处理器实际上是硬件上的概念,实际上在软件看来,这64个处理器都是物理上实实在在存在的处理器,因为它们每一个都有自己独立的寄存器集。但是T2 CPU并不是仅仅只有这64个虚拟处理器而已,在处理器外围还集成了许许多多的其他模块,以为CPU提供I/O能力,内存访问能力等。关于T2 CPU的详细组成可以参考相关文档,我们这里只关注其中与中断有关的模块。
T2 CPU中与中断处理相关的重要模块相互之间是有所联系的,我们以下图来表示出来:
上图只是一个逻辑连接图,并不是它们之间实实在在的物理连接,他们之间还存在这其他的模块。而且此图也并不十分精确,但是能大致反映T2 CPU的硬件中断流程,即PIU接收到外围设备的中断信号,然后传递给NCU处理,NCU会中断一个虚拟处理器,然后虚拟处理器处理中断。
左边的第一个模块就是64个虚拟处理器,其中的每一个虚拟处理器都可以接收中断。中断的软件处理流程就是从虚拟处理器接收到中断信号开始的。这里我们关注虚拟处理器怎么接收中断,以及接收到中断后怎么触发软件处理流程。
每个虚拟处理器都有4个与之相关联的环形队列,这些队列中的每一项都是一个64字节大小的数据包,称为interruput packets,这4个队列也叫做中断mondo队列。当然这些队列都是存在于内存中的。这4个中断mondo队列分别如下:
- Device mondos
- CPU mondos
- Resumable errors
- Nonresumable errors
其中Device mondos队列是用来处理I/O设备中断的,CPU mondos队列是用来处理处理器间中断的,Resumable errors队列是用来处理可回复错误的,Nonresumable errors队列是用来处理不可恢复错误的。
每个虚拟处理器上都有8个与中断队列相关的寄存器,分别如下:
寄存器名称 | ASI | Virtual address | Privilged mode | Hyperileged mode |
---|---|---|---|---|
CPU Mondo Queue Head | 0x25 | 3C016 | RW | R/W |
CPU Mondo Queue Tail | 0x25 | 3C816 | R or RW† | R/W |
Device Mondo Queue Head | 0x25 | 3D016 | RW | R/W |
Device Mondo Queue Tail | 0x25 | 3D816 | R or RW† | R/W |
Resumable Error Queue Head | 0x25 | 3E016 | RW | R/W |
Resumable Error Queue Tail | 0x25 | 3E816 | R or RW† | R/W |
Nonresumable Error Queue Head | 0x25 | 3F016 | RW | R/W |
Nonresumable Error Queue Tail | 0x25 | 3F816 | R or RW† | R/W |
上表中第一列是这个8个寄存器的名称,从名称可以看出,每个中断mondo队列有两个相关联的寄存器,分别是head和tail,这两个寄存器可以看作是两个指针,分别指向该队列的头和尾。至于这两个寄存器里的具体内容,不同的实现有着不同的内容,甚至在一些实现上可以由软件来定义。我们这里寄存器里面的值实际上代表了队列的头(队列中的第一个元素)或者尾(队列中的最后一个元素)在内存中的物理地址相对于该队列的物理基地址的偏移。
第二列是访问这些寄存器所使用的ASI(ASI请参考相关文档),第三列是访问这些寄存器所使用的虚拟地址,第4列是在寄存器privilged模式下的读写权限,最后一列是在hyper-privilged模式下的读写权限。可以看出这些寄存器在hyper-privilged模式下都是可读可写的,而head寄存器在privilged模式下是可读可写的,但是tail寄存器在privilged模式下是在一些实现里是可读可写的,但是在另外一下实现中是只读的。在T2中同样是只读的,如果试图在privilged模式下写tail寄存器,则会产生一个DAE_invalid_asi
异常。
当一个interrup packet 插入一个队列中时,它会插入队列的尾部,同时更新该队列的tail寄存器。(当然这个动作是由hypervisor来完成的,privilged software不用关心),而要取走一个interrupt packet则必须从队列的头部取走,同时privilged software负责更新head寄存器。
这4个中断队列都有一个与之相关联的trap(至于什么是trap,请参考相关文档),分别是:
名称 | Trap type | 优先级 | NP | Priv | HP |
---|---|---|---|---|---|
cpu_mondo | 0x07C | 16.08 | P (ie) | P (ie) | (pend) |
dev_mondo | 0x07D | 16.01 | P (ie) | P (ie) | (pend) |
resumable_error | 0x07E | 33.3 | P (ie) | P (ie) | (pend) |
nonresumable_error | 0x07F | — | — | — | — |
第二列的trap type实际上可以看作是该trap的ID,第4,5,6列分别是当处理器处于Nonprivilged,privileged,hyper-privileged模式下时,该trap发上时处理器的行为。P表示当trap发生时,处理器进入privilged模式通过privilged模式下的trap table处理器该trap,(ie)表示当trap发生时PSTATE寄存器中的中断允许位被清0时(即禁止中断时),该trap会保持pending状态直到系统重新允许中断,最后一列的(pend),当处理器处于hyper-privileged模式下时,若该trap发生,trap会保持pending状态直到处理器进入privilged模式。
对于上述4个中断mondo队列trap的前3种而言,任何时候只要相关队列的head和tail寄存器不相等,trap就会发生,然后当条件满足时(ie,处理器模式),虚拟处理器就会停止当前指令转而执行trap table中的trap hander。但是对于最后一个nonresumable_error trap,并不适用这一规则,该trap是由hyervisor 产生的。
到这里就有了一个疑问,这些interrupt pachket是怎么来的呢,其它的实现我不知到,但是T2中,我曾一度以为是由硬件自动产生的,但其实并不是这样,因为手册中说这些中断报文的格式是由软件定义的,其格式灵活性及大,硬件其实并没有这么智能能够产生设计之初并没有定义好的灵活性极大的报文。这些interrupt packets是由hypervisor 代码产生的。这里就要提到虚拟处理器中与中断相关的另一个trap了:
interrupt_vector_trap | 0x60 | 16.3 | H | H | H |
---|
从表可以看出,当interrupt_vector_trap 发生时,无论虚拟处理器当前处于什么模式,它都会进入hyper-priviled 模式,然后执行hypervisor trap table里的该trap的trap hander。在trap hander中,就会根据中断源的类型为相关的中断mondo队列在尾部添加intterrupt packet。
上述就是虚拟处理器接收到中断后的行为及相关硬件机制。但是这里还要补充一点,这是sparc手册里面的规定的规则,即上述的trap hander一般会通过设置软中断寄存器中的某一位来产生一个软中断,然后稍后在软中断处理函数(nterrupt_level_n (n = 1–15) trap hander)真正处理虚拟处理器所接收到的中断。至于为什么这样,手册中说软中断是用来使nucleus software与trap level=0时的privilged software交互的。所谓的nucleus software即是当trap level > 0时的代码。至于软中断trap为什么有这种功能,它和其他的trap有什么区别,在手册中并没有找到,我也就不知道了。
这样,虚拟处理器接收到中断信号后先产生interrupt_vector_trap,在hypervisor模式下为中断mondo队列添加interrupt packets,然后当处理器进入privilged模式时,就会产生相对应的中断mondo队列的trap,然后在该trap hander中设置软中断寄存器,稍后会产生软中断trap,最后在软中断trap中正式处理中断。从这个流程看来,sparc平台的中断体系是相当复杂的,这种中断体系有什么好处,其复杂性会不会带来性能损失,我就不评论了。
在T2 CPU中,虚拟处理器接收到的中断信号并不是直接来源于中断的中断源,而是来源于图1中的第二个模块NCU(Noncacheable Unit),在T2 CPU中,NCU位于虚拟处理器和I/O设备之间,它在为虚拟处理器和I/O设备提供路由服务。它的主要功能包括配置PCIe地址空间,中断管理以及非缓存访问I/O设备等。
当然NCU的其它功能我们这里不用关心,只需关心它的中断管理功能,它的中断管理功能就相当于一个中断控制器,它会确定中断源的中断向量。注意,以下我们的介绍都只关注正常的中断,关于错误处理的中断包括上面提到的resumable_error
trap和nonresumable_error trap后文都不再做专门介绍。
I/O设备中断:
NCU支持两种I/O设备中断,“internal” I/O中断以及“mondo”中断。“internal” I/O中断是由T2 CPU上集成的模块所产生的中断,包括SSI等I/O设备,以及一些 逻辑设备,和关于错误处理的中断等。每个”internal”中断都有一个硬连线的中断序号,这个中断序号是NCU上的INT_MAN 表的索引,而INT_MAN表中存放的是“internal”中断的中断向量。
这里有一个概念中断向量,实际上在sparc上privilged software的中断处理中是没有这一概念的。中断向量是hypervisor 代码用来识别中断源的。而在privilged software中有很多叫法,可以叫中断ID,sysno,cookie等,这些是privilged software用来识别中断源的,注意这和hypervisor及这里的中断向量不是一回事,也许一个中断向量可以对应多个外部中断,比如也许所有的“mondo”中断都只对应一个中断向量,而在该中断向量的处理函数中识别具体哪一个外部中断并依此产生interrupt packets,这就看hypervisor代码是怎么处理的了。实际上privilged software不仅需要中断ID来唯一标志一个中断源,还需要知道该中断id到底关联这哪一个设备以及设备上的哪一个中断,这样才能根据中断ID及设备的功能注册相应的中断处理程序。为此hypervisor会为每一个I/O设备都分配一个devhandle和一个或多个devino,devhandle代表它是哪一个I/O设备, devino则该设备上的哪一个中断,这样一个dev hander/devino 对就唯一的标志了一个中断源。至于这里的中断向量和privilged software里面的中断ID以及devhandle/devino 对是怎么关联的,就是hypervisor由hypervisor代码来完成的,我们这里同样不关注。
T2中到底有那些中断向量以及这些中断向量对应于那些中断源,这些细节问题请查看相关文档,我们这里不再赘述了。
注意,无论是I/O设备中断还是处理器间中断都是有中断向量的,两者是一样的。
当虚拟处理器接收到一个中断时,它需要知道该中断的中断向量,以在上文提到的
interrupt_vector_trap hander中根据中断向量来做相应的处理。为此,在每一个虚拟处理器上都有一个寄存器Incoming Vector Register 和寄存器ASI_INTR_RECEIVE,当虚拟处理接收到一个中断时,I而ASI_INTR_RECEIVE寄存器是64位,它的每一位都代表一种中断向量的状态,而Incoming Vector Register里面则保存有优先级最高的中断的中断向量,当读取此寄存寄存器时,就会返回该6位的中断向量,同时ASI_INTR_RECEIVE中与该中断相关的状态位会被清楚。
“internal”中断下文都不再介绍了,至于T2上集成了那些”internal”设备,有兴趣的话可以参考相关文档。
“mondo”中断则是由PCIe设备等外围设备产生的中断了,这些中断遵循标准的ACK/NACK 控制流。
在NCU上有一种名为MONDO_INT_BUSY的寄存器,它表征了“mondo”中断的状态,当接收到外部的mondo中断时,如果该寄存器中显示该mondo中断处于“IDLE”状态,则会接收中断会报并‘ACK’中断请求者,同时该中断状态变为”BUSY”,而同时接收到的中断附加的附载信息会保存到MONDO_INT_DATA0/1 table中,同时虚拟处理器中ASI_INTR_RECEIVE寄存器的相应位会被置位。而如果此时处于“BUSY”状态,则会拒绝接收中断并’NACK’中断请求者,这时中断请求者负责”pending”这个中断以便在稍后继续请求该中断。
虚拟处理器间中断:
虚拟处理器间中断是通过写Interrupt Vector Dispatch Register来产生,每个虚拟处理器都有一个Interrupt Vector Dispatch Register,这个寄存器是hyper-privilged模式下的寄存器,在privilged 和nonprivilegd模式下是不可见的,且在hyper-privilged模式下该寄存器是只写的,该寄存器各位描述如下表3所示:
Bit | Field | Initial Value | R/W | Description |
---|---|---|---|---|
63:14 | — | 0 | RO | Reserved |
13:8 | strand | X | W | Destination virtual processor |
7:6 | — | 0 | RO | Reserved. |
5:0 | vector | X | W | Interrupt Vector |
该寄存器的13:8位代表所要中断的目标虚拟处理器,5:0位代表中断目标处理器所使用的中断向量。当虚拟处理器写该寄存器时,目标虚拟处理器上的ASI_INTR_RECEIVE寄存器中代表该中断向量的相应位就会被置位。
上文中,处理器间中断,外部设备的mondo中断,内部设备中断都的硬件机制都已经讲过了,但是在图1中,NCU模块后面还跟了一个PIU模块。其实图1不是很准确,因为NCU不仅管理PIU的中断,它后面还跟有”internal”设备,这里之所以把PIU单独列出来,是因为”internal”设备的中断虽然没有深入细节,但基本机制已讲清楚,而PIU中断虽然属于“mondo”中断的范围内,但是它还对它所接收到的中断做了一些处理。而这些处理对privilged software不是透明的,所以我们这里需要关心它具体怎么处理的。
在T2处理器上集成了PIU,即PCIe接口单元,它向T2提供了PCIe总线的扩展接口,从而使系统中能够连接PCIe设备,当然它也负责接收其所扩展的PCIe总线上的设备所发出的中断。
当PIU产生一个中断,中断源可以是一下3种中的任意一种:
1,PIU的一个内部模块
2,PCI Express INTx Assertion Packet
3,one of PIU’s Event Queues
内部模块中断:
PIU可产生两种类型的内部模块中断:DMU内部中断和PEU内部中断。具体情形就不做介绍了。
PCI Express INTx Assertion Packet:
PCIe消息Assert_INTx/Deassert_INTx message模拟了4条虚拟的中断线,代表了传统PCI总线中的中断线INTA、INTB、 INTC和INTD。
当PIU接收到Assert_INTx message消息时,它就会产生相应的interrupt mondo,有4个专用的interrupt mondo,分别对应PCI中的INTA、INTB、INTC、INTD,这些interrupt mondo就会反映到上述NCU中“mondo”中断的相关寄存器以及虚拟处理器上的相关寄存器中从而触发中断。
Event Queue Interrupts:
PIU的上述两种类型的中断没有什么特殊之处,只是把接收到的中断转换为“mondo”中断。但是事件队列中断的处理就比较特殊了,正是我们需要关注的。
PIU为接收到的需要通知软件处理的消息(MSI消息或者电源管理,错误处理的消息)提供了事件队列,当接收到一个消息时,就把它加入到相应的队列中去。一个事件队列与一个特定的虚拟处理器绑定,而且每个队列都有一个与之对应的interrupt mondo,当在事件队列中写入一个或多个消息时只产生一个interrupt mondo,可以使用多个绑定在不同处理器的事件队列来提供硬件级别的中断分发。
在PIU内部,每当事件队列的head与tail不相等时,就会产生NOT_EMPTY
信号,这时如果这个事件队列被使能且该事件队列的中断状态处于空闲状态,就会产生一个interrupt mondo到那个特定的虚拟处理器。
每个事件队列都有一个专用的intterrupt mondo。
对PIU来说,所有事件队列都处于连续的内存空间中,每个事件队列都有一个固定的127个元素,即每个事件队列最多只能有127个消息。
由此可见对于Event Queue Interrupts来说,并不是接收到一个外围设备的中断(比如MSI中断)就产生一个intterrupt mondo,而是把接收到的MSI挂接到Event Queue中,由event queque产生intterrupt mondo。这里就可以把每个 Event Queue都看作是一个可以产生intterrupt mondo的虚拟设备。
纵上所述,I/O设备产生中断到NCU,NCU中断处理器。处理器先在hypervisor中处理,然后进入privilged模式下处理中断。
二、T2中断的Linux软件处理:
从上文的讲解可以知道,T2的虚拟处理器在接收到中断信号后会现在hypervisor中进行初步的处理,我们这里讲Linux里面的中断处理,hypervisor中的处理流程就不过多涉及了。我们直接从中断处理进入到privilged模式后开始。因为Linux内核就是运行在privilged模式下的。
中断进入Linux内核中处理是从哪里开始的呢,也许你已经知道了,正是那4个虚拟处理器的中断mondo队列的trap的trap hander开始的。其中关于错误处理的中断处理我们就略过了。这样就只剩下dev_mondo和cpu_mondo队列了。我们先看dev_mondo中断,是处理I/O设备的。
I/O设备中断处理:
这时Linux就要进入dev_mondo trap的处理函数中了,但是我们先不讲中断到这里后是怎么处理的,先在这里打住,讲点别的东西。讲什么呢,讲一讲中断初始化。在Linux中在正式进行中断处理之前都会先进行初始化。
中断初始化:
T2的中断初始化一个非常重要的工作就是为不同设备的中断分配中断请求号,即驱动程序注册中断时使用的中断号,这里之所以使用中断请求号而不用中断号这个概念是为了避免混淆,因为中断号的含义非常广泛。
从上文我们知道中断队列里面的interrupt patckets其实是一个大小64字节的内存段,即8个64位数据,它们是由hypervisor产生的,但是在T2的中断处理中只需要用到第一个64位数据,其他数据都是0。这里interrupt patckets其实就是我们所说的中断id或者sysno。系统就要根据该中断id找到为该设备分配的中断请求号。而中断初始化就要为中断id和中断请求号建立关联。
此外,设备驱动程序的中断请求号实际上是跟设备相关联的,即某个设备的驱动程序只会使用该设备的中断请求号,为设备分配中断请求号也是中断初始化的工作。
为了使读者能够对T2中断处理过程有清晰的了解,因此本文并不打算对其他的部分涉及过多,否则会引入过多的概念加大读者接收的难度,所以本文不会对中断初始化过程的每一个步骤都做详细的介绍,只介绍那些对理解中断处理过程不可或缺,我们我法略过的部分,有兴趣的读者可以参考T2系统启动过程的相关章节。
当系统启动后在hyper-privilged模式下执行时,hypervisor代码在扫描外围设备时,会为每一个设备分配一个唯一的ID号,叫做devhandle。devhandle会作为设备的一种资源放在设备资源的相关描述结构中(prom设备节点树的设备节点中),当Linux启动后,这些资源就会传递给Linux,这样在Linux中每一个设备都会有一个devhandle。
当linux系统扫描设备时,就会取到hypervisor扫描到的设备资源并为设备分配中断请求号,为设备扫描资源的函数就是arch/sparc/kernel/of_device_64.c 文件中的scan_one_device函数,该文件会调用build_one_device_irq函数为设备分配中断请求号,该函数会最终调用一个叫做irq_build的函数来分配这个中断请求号。这里我们不贴出build_one_device_irq的所有代码,只贴出调用irq_build函数的代码:
541 if (dp->irq_trans) {
542 irq = dp->irq_trans->irq_build(dp, irq,
543 dp->irq_trans->data);
544
545 if (of_irq_verbose)
546 printk("%s: direct translate %x --> %x\n",
547 dp->full_name, orig_irq, irq);
548
549 goto out;
550 }
第542行,第543行,实际上是通过函数指针调用irq_build函数的。这个函数指针是在之前的系统初始化过程中被赋值的,可参考相关章节。
而这个irq_build函数实际上最终会调用arch/sparc/kernel/irq_64.c文件中的sun4v_build_irq函数来为设备分配中断请求号。
这个函数我们呆会再讲,中断初始化除了分配中断请求号外,还会为Linux的中断处理准备一些基础设施,这就是arch/sparc/kernel/irq_64.c中的init_IRQ函数来完成的,该函数会在start_kernel系统初始化函数中调用。
该函数代码如下:
987 void __init init_IRQ(void)
988 {
989 unsigned long size;
990
991 map_prom_timers();
992 kill_prom_timer();
993
994 size = sizeof(struct ino_bucket) * NUM_IVECS;
995 ivector_table = kzalloc(size, GFP_KERNEL);
996 if (!ivector_table) {
997 prom_printf("Fatal error, cannot allocate ivector_table\n");
998 prom_halt();
999 }
1000 __flush_dcache_range((unsigned long) ivector_table,
1001 ((unsigned long) ivector_table) + size);
1002
1003 ivector_table_pa = __pa(ivector_table);
1004
1005 if (tlb_type == hypervisor)
1006 sun4v_init_mondo_queues();
1007
1008 init_send_mondo_info();
1009
1010 if (tlb_type == hypervisor) {
1011 /* Load up the boot cpu's entries. */
1012 sun4v_register_mondo_queues(hard_smp_processor_id());
1013 }
1014
1015 /* We need to clear any IRQ's pending in the soft interrupt
1016 * registers, a spurious one could be left around from the
1017 * PROM timer which we just disabled.
1018 */
1019 clear_softint(get_softint());
1020
1021 /* Now that ivector table is initialized, it is safe
1022 * to receive IRQ vector traps. We will normally take
1023 * one or two right now, in case some device PROM used
1024 * to boot us wants to speak to us. We just ignore them.
1025 */
1026 __asm__ __volatile__("rdpr %%pstate, %%g1\n\t"
1027 "or %%g1, %0, %%g1\n\t"
1028 "wrpr %%g1, 0x0, %%pstate"
1029 : /* No outputs */
1030 : "i" (PSTATE_IE)
1031 : "g1");
1032
1033 irq_desc[0].action = &timer_irq_action;
1034 }
第991,992行,关掉prom里面的定时器,这两行对我们理解中断处理没有什么帮助,就不深究了。
第994到999行,分配了NUM_IVECS个 元素的struct ino_bucket数据结构数组的空间,NUM_IVECS实际上是固定值2047。至于这个数组有什么用后面再说。
第1000,1001行,数据缓存,是为了保证在接着处理之前,刚才分配的struct ino_bucket结构能够位于内存中。
第1003行,把struct ino_bucket数组的物理基地址保存在变量 ivector_table_pa中。
第1005,1006行,根据tlb类型为4个中断mondo队列分配空间,之前的讲解我们已经知道中断mondo队列是位于内存中的,就是在这里分配的空间。我们看一看函数
sun4v_init_mondo_queues:
954 static void __init sun4v_init_mondo_queues(void)
955 {
956 int cpu;
957
958 for_each_possible_cpu(cpu) {
959 struct trap_per_cpu *tb = &trap_block[cpu];
960
961 alloc_one_queue(&tb->cpu_mondo_pa, tb->cpu_mondo_qmask);
962 alloc_one_queue(&tb->dev_mondo_pa, tb->dev_mondo_qmask);
963 alloc_one_queue(&tb->resum_mondo_pa, tb->resum_qmask);
964 alloc_one_queue(&tb->resum_kernel_buf_pa, tb->resum_qmask);
965 alloc_one_queue(&tb->nonresum_mondo_pa, tb->nonresum_qmask);
966 alloc_one_queue(&tb->nonresum_kernel_buf_pa,
967 tb->nonresum_qmask);
968 }
969 }
这里为每个虚拟处理器都分配了一些内存空间。其中第961,962行,就是为虚拟处理器中断mondo队列和dev中断mondo队列分配空间,剩余的4行是是为另外两个错误处理中断mondo队列分配空间的我们不关心。分配的空间的物理基地址保存在每个虚拟处理器的trap_block中。trap_block是一个 struct trap_per_cpu结构的数组,数组成员的个数就是虚拟处理器的数量,因此每个虚拟处理器都有数组中的一个成员与之对应。 struct trap_per_cpu的成员 cpu_mondo_qmask等这种成员在之前已经被初始化过了,它们的值就是相应队列的长度减1.
我们接着回到init_IRQ函数中来。
第1008行,init_send_mondo_info实际上是初始化处理器间中断的。上面为cpu mondo队列分配了空间,但是cpu mondo队列是CPU接收中断的。Cpu发送中断也要进行初始化:
971 static void __init init_send_mondo_info(void)
972 {
973 int cpu;
974
975 for_each_possible_cpu(cpu) {
976 struct trap_per_cpu *tb = &trap_block[cpu];
977
978 init_cpu_send_mondo_info(tb);
979 }
980 }
该函数实际上为每个虚拟处理器调用 init_cpu_send_mondo_info函数:
935 static void __init init_cpu_send_mondo_info(struct trap_per_cpu *tb)
936 {
937 #ifdef CONFIG_SMP
938 unsigned long page;
939
940 BUILD_BUG_ON((NR_CPUS * sizeof(u16)) > (PAGE_SIZE - 64));
941
942 page = get_zeroed_page(GFP_KERNEL);
943 if (!page) {
944 prom_printf("SUN4V: Error, cannot allocate cpu mondo page.\n");
945 prom_halt();
946 }
947
948 tb->cpu_mondo_block_pa = __pa(page);
949 tb->cpu_list_pa = __pa(page + 64);
950 #endif
951 }
该函数实际上为每个虚拟CPU的trap_block变量初始化了两个成员 cpu_mondo_block_pa, cpu_list_pa这两个成员实际上是与虚拟处理器发送中断相关的,它们的使用到我们用到的时候再说。
在回到init_IRQ函数中来,
第1011到1013行,为当前虚拟处理器注册它的4个中断mondo队列,sun4v_register_mondo_queues函数最后会调用hypervisor的服务来注册中断队列。
第1018行,清除软中断寄存器。第一节的讲解我们知道中断处理是需要用到软中断寄存器的。而在进入Linux之前的prom中,prom的定时器是使用软中断来实现的,而在我们禁止掉prom的定时器(init_IRQ函数的头两行),在软中断寄存器中可能有Pending状态的软中断,这里把它清掉。
第1026到1032行是一段内嵌汇编,它实际上就是打开虚拟处理器的中断允许位。
最后一行,irq_desc全局数组大家应该比较熟悉把吧。这里实际上是把中断请求号0默认分配给定时器了。
init_IRQ函数实际上做了4件事,分配struct ino_bucket结构数组,为虚拟处理器的中断mondo队列分配空间,为虚拟处理器发送中断分配空间以及把中断请求号0默认分配给定时器。
init_IRQ函数分析完了,中断处理的一些基础设施也准备好了,我们再回过头来看分配中断请求号的函数sun4v_build_irq。这里需要指出的sun4v_build_irq函数只是为一般的物理I/O设备分配中断请求号,而虚拟设备的中断请求号是sun4v_build_virq函数分配的,这个函数讲完sun4v_build_irq接着就讲,而使用msi中断的设备的中断请求号则要等到后面讲解msi中断的时候再讲了。看sun4v_build_irq:
615 unsigned int sun4v_build_irq(u32 devhandle, unsigned int devino)
616 {
617 unsigned long sysino = sun4v_devino_to_sysino(devhandle, devino);
618
619 return sun4v_build_common(sysino, &sun4v_irq);
620 }
sun4v_build_irq函数的参数是devhandle ,devino,devhandle在设备资源中可以得到,它标志了哪一个设备,devino则标志了个设备的哪一个中断。这就是我们前面降到的devhandle/devino对。
第617行,调用hypervisr服务通过 devhandle/devino对找到对应的中断id,这里就是sysino,就是中断mondo队列的interrupt packet中的第一个64位数据里面的内容。在设备资源初始化时得到设备的devhandle,通过devhandle得到设备的sysino,然后根据sysino分配中断请求号,把分配的中断请求号保存到设备资源列表中,这样就把中断 请求号,中断id和devhanle/dev对关联起来了,设备驱动程序初始化时取出资源列表里的中断请求号注册中断处理程序。当发生中断时,从中断mondo队列的interrupt packet中得到中断id,根据中断id就得到中断请求号,根据中断请求号调用中断处理程序。这就是T2中断处理的基本思想。
第619行,调用 sun4v_build_common函数根据sysino分配中断请求号。
我们来看sun4v_build_common函数:
574 static unsigned int sun4v_build_common(unsigned long sysino,
575 struct irq_chip *chip)
576 {
577 struct ino_bucket *bucket;
578 struct irq_handler_data *data;
579 unsigned int virt_irq;
580
581 BUG_ON(tlb_type != hypervisor);
582
583 bucket = &ivector_table[sysino];
584 virt_irq = bucket_get_virt_irq(__pa(bucket));
585 if (!virt_irq) {
586 virt_irq = virt_irq_alloc(0, sysino);
587 bucket_set_virt_irq(__pa(bucket), virt_irq);
588 set_irq_chip_and_handler_name(virt_irq, chip,
589 handle_fasteoi_irq,
590 "IVEC");
591 }
592
593 data = get_irq_chip_data(virt_irq);
594 if (unlikely(data))
595 goto out;
596
597 data = kzalloc(sizeof(struct irq_handler_data), GFP_ATOMIC);
598 if (unlikely(!data)) {
599 prom_printf("IRQ: kzalloc(irq_handler_data) failed.\n");
600 prom_halt();
601 }
602 set_irq_chip_data(virt_irq, data);
603
604 /* Catch accidental accesses to these things. IMAP/ICLR handling
605 * is done by hypervisor calls on sun4v platforms, not by direct
606 * register accesses.
607 */
608 data->imap = ~0UL;
609 data->iclr = ~0UL;
610
611 out:
612 return virt_irq;
613 }
第583行,ivector_table数组,它是struct ino_bucket结构的数组,还记得init_IRQ函数吗,这个数组就是那时分配的,当时这个数组是做什么用到绕过了没讲,这里就必须要讲了。struct ino_bucket结构的定义如下:
struct ino_bucket {
/*0x00*/unsigned long __irq_chain_pa;
/* Virtual interrupt number assigned to this INO. */
/*0x08*/unsigned int __virt_irq;
/*0x0c*/unsigned int __pad;
};
它有3个成员,第一个成员__irq_chain_pa是干什么的这里仍然不讲,到用到的时候再讲。第三个成员__pad,看名字就知道这个成员没什么实际作用,是用来补齐的。加入这个成员后该结构的大小就刚好是16字节,是2的整数次方。我们这里要讲的是第二个成员
__virt_irq,前面不是说中断处理要把从中断mondo队列的interrupt packet里得到的中断ID(即sysino)与虚拟中断号关联起来吗,它是怎么关联起来的?就是通过 struct ino_bucket结构的__virt_irq成员。对于一般的物理设备而言,得到的sysino就是这里ivector_table数组的索引,这里分配的中断请求号就保存在ivector_table[sysino]的__virt_irq成员里。当中断到来时,通过得到的sysino从ivector_table数组中取出中断请求号,然后调用中断处理程序。
第584行,得到ivector_table[sysino]的__virt_irq。bucket_get_virt_irq实际上是一段内嵌汇编,这段汇编的作用就是得到ivector_table[sysino]的__virt_irq成员,这里之所以没有用C语言引用__virt_irq,是为了排除cache的影响,即为了直接从内存中取得__virt_irq,而不是从D-cache中取用。
第585到591行,如果__virt_irq不为0,则表示已经为该sysino分配过__virt_irq,因此不再分配,直接返回,从这里可以看出不同的devhandel/devino对可能对应于同一个sysino,之所以会出现这种情况,我猜想可能是为了应对中断共享的情况,当两个不同的设备在硬件上共享一根中断线时,虽然devhandle/devino不同,但应该使用同一个中断请求号,也就应该使用同一个sysino。其中第 586行,调用 virt_irq_alloc分配中断请求号,我们看看这个函数:
117 unsigned char virt_irq_alloc(unsigned int dev_handle,
118 unsigned int dev_ino)
119 {
120 unsigned long flags;
121 unsigned char ent;
122
123 BUILD_BUG_ON(NR_IRQS >= 256);
124
125 spin_lock_irqsave(&virt_irq_alloc_lock, flags);
126
127 for (ent = 1; ent < NR_IRQS; ent++) {
128 if (!virt_irq_table[ent].in_use)
129 break;
130 }
131 if (ent >= NR_IRQS) {
132 printk(KERN_ERR "IRQ: Out of virtual IRQs.\n");
133 ent = 0;
134 } else {
135 virt_irq_table[ent].dev_handle = dev_handle;
136 virt_irq_table[ent].dev_ino = dev_ino;
137 virt_irq_table[ent].in_use = 1;
138 }
139
140 spin_unlock_irqrestore(&virt_irq_alloc_lock, flags);
141
142 return ent;
143 }
该函数的参数虽然第一个参数是dev_handle,dev_ino,但是对于一般物理设备而言,它不是这样用的,它使用这个函数时第一个参数是0,第二个参数是sysino。那么,中断请求号是按照什么规则分配的呢,即为哪个sysino分配哪个中断请求号呢。我们知道Linux中中断请求号的最大个数是NR_IRQS个,为了T2 Linux为了管理并分配中断请求号,定义了一个私有数组
virt_irq_table:
static struct {
unsigned int dev_handle;
unsigned i
nt dev_ino;
unsigned int in_use;
} virt_irq_table[NR_IRQS];
static DEFINE_SPINLOCK(virt_irq_alloc_lock);
可以看出数组成员的个数是 NR_IRQS个,每个成语管理一个中断请求号,即它第几个成员就管理中断请求号几。它有3个成员,分别保存该中断请求号分配给了哪个devhandle/devino对,但实际上这样说并不准确。因为对于一般物理设备而言,通过前面的分析我们知道,这里的dev_handle肯定是0,而dev_ino则是与devhandle/devino对所对应的sysino,它的第3个成员 in_use则管理该中断请求号是否已经被分配。该函数的第127行到129行就是分配具体的分配中断请求号的函数,可以看出它的分配规则实际上是按中断请求号从小到大顺序分配的,即Linux扫描一个设备就分配一个中断请求号,先扫描到的设备分配的中断请求号就小,后分配的就大。注意这里中断请求号是从1开始分配的,中断请求号0已经默认分配给定时器了。该函数第135到138行,如果该中断请求号分配出去,就保存下devhandle/devino,同时把in_use成员置1。
我们接着回到 sun4v_build_common函数中来,
第587行, bucket_set_virt_irq函数是 bucket_get_virt_irq函数相对应的函数,这里就是把为该sysino分配的中断请求号保存到ivector_table[sysino]的__virt_irq中。
第588行,set_irq_chip_and_handler_name函数并不是体系结构相关的函数,而是Linux里面为中断子系统提供的标准函数,该函数实际上是设置irq_desc[virq]的chip成员和handle_irq成员,至于这里设置了具体的设置成了什么,我们到遇到的时候再说。
本文档默认读者已经对Linux的中断子系统的有所了解,因此只关注Linux T2的体系结构相关的中断处理,如果你对Linux的标准的体系结构无关部分的中断处理过程不了解的话,建议先看看相关文档资料,了解之后再来看本文档。
第597行到609行,仍然是调用标准函数设置irq_desc[virq]的chip_data成员,至于具体设置成什么,以及有什么用,用到的时候再说。
到这里,sun4v_build_irq函数就分析完了,但是这个函数只能分析一般物理设备的中断请求号,而虚拟设备的中断请求号是由sun4v_build_virq函数分配的:
622 unsigned int sun4v_build_virq(u32 devhandle, unsigned int devino)
623 {
624 struct irq_handler_data *data;
625 unsigned long hv_err, cookie;
626 struct ino_bucket *bucket;
627 struct irq_desc *desc;
628 unsigned int virt_irq;
629
630 bucket = kzalloc(sizeof(struct ino_bucket), GFP_ATOMIC);
631 if (unlikely(!bucket))
632 return 0;
633 __flush_dcache_range((unsigned long) bucket,
634 ((unsigned long) bucket +
635 sizeof(struct ino_bucket)));
636
637 virt_irq = virt_irq_alloc(devhandle, devino);
638 bucket_set_virt_irq(__pa(bucket), virt_irq);
639
640 set_irq_chip_and_handler_name(virt_irq, &sun4v_virq,
641 handle_fasteoi_irq,
642 "IVEC");
643
644 data = kzalloc(sizeof(struct irq_handler_data), GFP_ATOMIC);
645 if (unlikely(!data))
646 return 0;
647
648 /* In order to make the LDC channel startup sequence easier,
649 * especially wrt. locking, we do not let request_irq() enable
650 * the interrupt.
651 */
652 desc = irq_desc + virt_irq;
653 desc->status |= IRQ_NOAUTOEN;
654
655 set_irq_chip_data(virt_irq, data);
656
657 /* Catch accidental accesses to these things. IMAP/ICLR handling
658 * is done by hypervisor calls on sun4v platforms, not by direct
659 * register accesses.
660 */
661 data->imap = ~0UL;
662 data->iclr = ~0UL;
663
664 cookie = ~__pa(bucket);
665 hv_err = sun4v_vintr_set_cookie(devhandle, devino, cookie);
666 if (hv_err) {
667 prom_printf("IRQ: Fatal, cannot set cookie for [%x:%x] "
668 "err=%lu\n", devhandle, devino, hv_err);
669 prom_halt();
670 }
671
672 return virt_irq;
673 }
在正式分析这个函数之前,先讲讲背景知识,分析这个函数要解决什么问题,对于物理I/O设备而言,是通过ivector_table数组来建立sysino号与所分配的中断请求号的关联的。而sysino号是通过设备的devhandle/devino得到的。其实我可以告诉你hypervisor同样也为虚拟设备分配了devhanle,即也有devhanlde/devino对,既然如此虚拟设备同样可以采用与物理设备一样的方式使用ivector_table来建立sysino号与所分配的中断请求号的关联。从原理上来看是没有任何问题的,但是这里为什么又把两者分开分别处理呢,hypervisor文档里给出的解释是ivector_table数组的元素个数是有限的,即只有2047个,而虚拟设备的通过devhandle/devino得到的sysino通常会大于2047,而ivector_table数组的元素个数已经够大了,再扩展它的大小的话很浪费内存,很不划算,因此他们采用了另外一种称作cookie的机制来解决,在cookie机制里面从中断mondo队列的interrupt packet里面得到的中断ID并不叫做sysino,而叫做cookie,cookie和sysino的区别就是sysino是由hypervisor通过某种规则有devhandle和devino映射来的,而cookie则是由privliged software即操作系统指定的。即Linux 中断初始化的时候告诉hypervisor该虚拟设备的中断使用什么cookie,而发生中断时,hypervior把该cookie写到中断mondo队列的interrupt packet中。任何Linux从interrupt packet中得到这个cookie,由于这个cookie是由Linux指定的,因此它知道是对应的哪个中断。
了解了上面的背景后,我们来分析 sun4v_build_virq函数。
第630行到635行,分配了struct ino_bucket结构,从这里看出虚拟设备仍然是使用struct ino_bucket结构来建立cookie与中断请求号之间的关联的,但是要注意,虚拟设备的 struct ino_bucket结构是这里单独分配的,而物理设备的struct ino_bucket结构位于ivector_table数组中。
第637行,调用virt_irq_alloc函数分配中断请求号,从这里虚拟设备和物理设备的中断请求号的分配方法是一样的,而且两者的中断请求号是不会重合的,两者之间的区别仅仅是中断ID与中断请求号建立关联的方式不同。
第638行,把分配的中断请求号保存到struct ino_bucket结构的__virt_irq成员中。
第640行到642行,在前面的sun4v_build_irq也有一样的过程,同样,后面遇到再分析。
第664行,虚拟设备和物理设备的不同在于cookie是由Linux指定的,这里cookie指定的是该中断相关的struct ino_bucket结构的物理地址取反。
第665行,调用hypervisor服务告诉hypervisor虚拟设备中断devhandle/devino所使用的cookile是我们这里指定的~__pa(bucket)。
当虚拟设备产生中断时,Linux从interrup packet中取出cookie,即得到了
struct ino_bucket结构的地址,然后从该结构中得到中断请求号。
到这里中断初始化过程就分析的差不多了,其中最中要的过程就是为设备分配中断请求号,当设备驱动程序从设备资源列表中得到中断请求号后,就可以使用该中断请求号注册中断处理程序了。
Linux T2中断处理:
好了我们接着回到中断处理中来,从前面的分析我们知道当设备中断处理从hypervisor进入Linux时,入口是虚拟处理器的dev mondo中断队列的dev mondo trap的trap handler,它定义于文件arch/sparc/kernel/sun4v_ivec.S文件中:
63 sun4v_dev_mondo:
64 /* Head offset in %g2, tail offset in %g4. */
65 mov INTRQ_DEVICE_MONDO_HEAD, %g2
66 ldxa [%g2] ASI_QUEUE, %g2
67 mov INTRQ_DEVICE_MONDO_TAIL, %g4
68 ldxa [%g4] ASI_QUEUE, %g4
69 cmp %g2, %g4
70 be,pn %xcc, sun4v_dev_mondo_queue_empty
71 nop
72
73 /* Get &trap_block[smp_processor_id()] into %g4. */
74 ldxa [%g0] ASI_SCRATCHPAD, %g4
75 sub %g4, TRAP_PER_CPU_FAULT_INFO, %g4
76
77 /* Get DEV mondo queue base phys address into %g5. */
78 ldx [%g4 + TRAP_PER_CPU_DEV_MONDO_PA], %g5
79
80 /* Load IVEC into %g3. */
81 ldxa [%g5 + %g2] ASI_PHYS_USE_EC, %g3
82 add %g2, 0x40, %g2
83
84 /* XXX There can be a full 64-byte block of data here.
85 * XXX This is how we can get at MSI vector data.
86 * XXX Current we do not capture this, but when we do we'll
87 * XXX need to add a 64-byte storage area in the struct ino_bucket
88 * XXX or the struct irq_desc.
89 */
90
91 /* Update queue head pointer, this frees up some registers. */
92 lduw [%g4 + TRAP_PER_CPU_DEV_MONDO_QMASK], %g4
93 and %g2, %g4, %g2
94
95 mov INTRQ_DEVICE_MONDO_HEAD, %g4
96 stxa %g2, [%g4] ASI_QUEUE
97 membar #Sync
98
99 TRAP_LOAD_IRQ_WORK_PA(%g1, %g4)
100
101 /* For VIRQs, cookie is encoded as ~bucket_phys_addr */
102 brlz,pt %g3, 1f
103 xnor %g3, %g0, %g4
104
105 /* Get __pa(&ivector_table[IVEC]) into %g4. */
106 sethi %hi(ivector_table_pa), %g4
107 ldx [%g4 + %lo(ivector_table_pa)], %g4
108 sllx %g3, 4, %g3
109 add %g4, %g3, %g4
110
111 1: ldx [%g1], %g2
112 stxa %g2, [%g4] ASI_PHYS_USE_EC
113 stx %g4, [%g1]
114
115 /* Signal the interrupt by setting (1 << pil) in %softint. */
116 wr %g0, 1 << PIL_DEVICE_IRQ, %set_softint
117
118 sun4v_dev_mondo_queue_empty:
119 retry
当中断在hypervior中处理时,已经把在interrup packet中保存了sysino/cookile,在dev mondo trap hander中会取出来。
第65,66行,读取dev mondo队列的head寄存器的值存放入g2寄存器中,其中的
ASI_QUEUE就是读取这些mondo队列寄存器的ASI标志符。
第67,68行,读取dev mondo队列的tail寄存器的值放入g4寄存器中。
第69行,比较head和tail寄存器的值
第70,71行,如果head和tail的值相等,则说明dev mondo队列中没有interrup packet,则说明没有中断,直接返回。
第74,75行,每个虚拟处理器上都有8个Scratchpad
(暂存)寄存器,他们是通过ASI标志符 ASI_SCRATCHPAD来访问的,其中地址为0的寄存器在虚拟处理器初始化时保存的是该虚拟处理器的trap_block变量的成员fault_info的虚拟地址。这里第74行是取出这个地址存放在g4中,第75行,该地址减去fault_info成员在trap_block变量中的偏移放到g4中,这样g4中存放的便是处理当前中断的虚拟处理器的trap_block变量的虚拟地址。
第78行,通过trap_block变量的虚拟地址得到起成员dev_mondo_pa的值放到g5中,
dev_mondo_pa成员是在init_IRQ函数中赋值的,它就是该虚拟处理器的dev mondo队列的物理基地址,这样g5中存放的就是dev mondo队列的物理基地址。
第81行, ASI_PHYS_USE_EC表示这条指令直接使用物理地址,g2是dev mondo队列的head寄存器的值,而head和tail寄存器里面存放的都是其指向的interrupt packet相对于队列基地址的偏移,而g5正好是dev mondo队列的物理基地址,两者相加则是队列中的最前面的head所指向的interrup packet的物理地址,这条语句就是取出该interrupt packet的前8个字节的数据存放在g3中,而这个值就是sysino或者cookie,即g3中存放的sysino或者cookie。
第82行,g2加0x40,实际上0x40就是64,正好是一个interrupt packet的大小,而g2,则是队列中head指向的interrupt packet的偏移,加64则是下一个interrupt packet的偏移。
第92,93行,取出当前虚拟处理器的trap_block变量的dev_mondo_qmask的值放在g4中,它实际上是dev_mondo队列的字节长度减1,第93行,该值与g2相与放在g2中,这里之所以这样处理是因为dev mondo队列是一个环形队列,如果g2的值大于该队列的长度的话(比如当head指向队列的最后一个元素时,其加64就超出队列范围),在这里相遇则时g2的偏移指向队列中的第一个元素了。
第95,96行,把g2的值存入head寄存器中,这里就是跟新head寄存器,从队列中取出一个interrupt packet后当然要更新head寄存器了。
第97行,进行指令流控制的,保证前面的内存访问指令都执行完。
第99行,是个宏TRAP_LOAD_IRQ_WORK_PA,该宏就不展开了,有兴趣的读者可以自己分析,这个宏执行完后,g1中保存的就是当前虚拟处理器的trap_block变量的成员
irq_worklist_pa的地址。
第102,是跳转语句,如果g3小于0就到1处执行,这条语句实际上是判断该中断是不是虚拟设备产生的。对于虚拟设备,g3里面存放的是cookie,实际上是 struct ino_bucket结构的物理地址的取反,由于T2地址空间是40位的,因此它的最高位必定是0,按位取反后,该值就变为负数了,而对于物理I/O设备而言,sysino一定是正数,据此就能判断处中断是有虚拟设备产生的还是有物理设备产生的,这也就是虚拟设备的cookie为什么要把 struct ino_bucket的物理地址按位取反的原因。
第103行,跳转语句中的命令槽宏的语句,实际上是把g3的值按位取反后保存在g4中,这样如果中断是虚拟设备产生的话,g4中就保存有该中断的 struct ino_bucket结构的物理地址。
而如果该中断是由物理设备产生的话,中断的 struct ino_bucket结构就要从ivector_table数组中得到了,这就是第106行到106行的作用了。
第106,107行,把变量ivector_table_pa的值存入g4中,ivector_table_pa里面是在
init_IRQ函数中赋值的,里面保存的是ivector_table的物理基地址。
第108,109行,g3左移4位后加g4,其中g3是sysino,g4是ivector_table的物理基地址,经过这两条语句后g4就是当前中断的struct ino_bucket结构的物理地址。
这样物理是虚拟设备还是物理设备,到这里g4存放的都是相关中断的struct ino_bucket结构的物理基地址。
第111行,标记1处,物理是虚拟设备还是物理设备,最后都会到这里来执行。这条语句是加载g1寄存器素所指向的内存地址的值到g2寄存器里面来,g1里面存放的是当前虚拟处理器的trap_block变量的成员irq_worklist_pa的地址,这条语句后g2里面存放的就是当前虚拟处理器的trap_block变量的成员irq_worklist_pa的值了。
第112行,把上面得到的irq_worklist_pa的值放到g4寄存器中,而g4寄存器中是当前中断的struct ino_bucket结构的物理地址,这样,irq_worklist_pa值就放到了当前中断的
struct ino_bucket结构的__irq_chain_pa的用法,还记得前面我们讲解 struct ino_bucket结构吗,当前只讲解的它的第二个成员virt_irq的用法,而__irq_chain_pa成员的用法当时所用到的时候再讲,这里就用到了。
第113行,把当前中断的struct ino_bucket结构的物理地址存放入当前虚拟处理器的trap_block变量的irq_worklist_pa中。
第116行,触发软中断,前面讲T2中断硬件机制的时候,不是说过mondo trap hander最终会触发软中断在稍后的时间真正处理中断了,这里就是触发软中断。
第119行,执行retry指令退出
好,现在来看看第111行到第113行这几条语句到底是干什么的,这几条语句实际上就是操作当前中断的struct ino_bucket结构的__irq_chain_pa成员,当前虚拟处理器的trap_block变量的irq_worklist_pa成员。这几条语句使用特殊的用法操作特殊的变量必然是为了针对性的解决特殊的问题,那么中断执行到这里会遇到什么问题呢?我们知道在dev mondo队列中可能不止一个interrupt packet,而从上面的分析我们知道这里的dev trap hander里面只是取出了其中的一个interrupt packet,那么可能队列中还剩下很多interrupt packet,所一当119行执行完retry指令退出trap时,dev mondo队列的head和tail寄存器仍然不想等,当前虚拟处理器会接着触发dev mondo trap处理下一个dev mondo trap。这样,这时就会有两个trap同时处于pending状态,一个是dev mondotrap,另外一个是第116行处附的软中断trap,那么会执行哪一个呢,这是根据trap的优先级来决定的,而dev mondo trap的优先级是高于软中断trap的,所以虚拟处理器会接着执行dev mondo trap,同理当下一个dev mondo trap执行完后还会接着执行,直到dev mondo队列变为空,而在此期间有最开始的interrupt packet的dev mondo trap所触发的软中断一直处于pending状态,因此后面接着的dev mondo trap中取设置软中断都是无效的。这样当所有的dev mondo trap都执行完后,实际上最终只触发了一个软中断。这样就要就要由一个软中断来处理所有的dev mondo队列中所有interrupt packiets所代表的中断,而在Linux中interrupt packiets是由struct ino_bucket结构代表的,所以就需要在真正处理中断时,找到dev mondo 队列中所有中断的struct ino_bucket结构,这就是这几条语句所要解决的问题,在第一个dev mondo trap中,第111条语句,把
当前虚拟处理器的trap_block变量的成员irq_worklist_pa放在第一个中断的struct ino_bucket结构的__irq_chain_pa成员中,实际上对第一个中断来说,irq_worklist_pa的值为0(当每次处理完中断时总会把该值置0),而当前虚拟处理器的trap_block变量的成员irq_worklist_pa存放第一个中断的struct ino_bucket结构的物理地址,在第二个dev mondo trap中,进行同样的过程,这样第二个中断的struct ino_bucket结构的__irq_chain_pa成员存放的实际上就是第一个中断的中断的struct ino_bucket结构的地址,而当前虚拟处理器的trap_block变量的成员irq_worklist_pa存放的就是第二个中断的struct ino_bucket的地址,以次类推,最终就形成了一个链表把所有中段的struct ino_bucket结构链接起来了,就形成了一个struct ino_bucket的链表,且链表的最后一个struct ino_bucket结构的__irq_chain_pa值是0。:
这样,当真正处理中断时,从trap_block[cpuid].irq_worklist_pa就可以找到所有pendding中断的 struct ino_bucket结构,然后依次处理
现在dev mondo trap分析完了,下面中断就会进入软中断trap中去处理,在trap table表中:
tl0_irq5: TRAP_IRQ(handler_irq, 5)
它是以参数handler_irq,和5的宏 TRAP_IRQ,该宏是在文件arch/sparc/include/asm/ttable.h中定义的:
145 #define TRAP_IRQ(routine, level) \
146 rdpr %pil, %g2; \
147 wrpr %g0, PIL_NORMAL_MAX, %pil; \
148 ba,pt %xcc, etrap_irq; \
149 rd %pc, %g7; \
150 mov level, %o0; \
151 call routine; \
152 add %sp, PTREGS_OFF, %o1; \
153 ba,a,pt %xcc, rtrap_irq;
这段汇编就不仔细分析了,它的主要功能是进入中断执行环境和在中断处理完后推出中断执行环境,它会调用routine即传入的hander_irq真正处理中断,我们直接取看handler_irq函数,这是个c语言函数,定义于文件arch/sparc/kernel/irq_64.c中:
707 void handler_irq(int irq, struct pt_regs *regs)
708 {
709 unsigned long pstate, bucket_pa;
710 struct pt_regs *old_regs;
711 void *orig_sp;
712
713 clear_softint(1 << irq);
714
715 old_regs = set_irq_regs(regs);
716 irq_enter();
717
718 /* Grab an atomic snapshot of the pending IVECs. */
719 __asm__ __volatile__("rdpr %%pstate, %0\n\t"
720 "wrpr %0, %3, %%pstate\n\t"
721 "ldx [%2], %1\n\t"
722 "stx %%g0, [%2]\n\t"
723 "wrpr %0, 0x0, %%pstate\n\t"
724 : "=&r" (pstate), "=&r" (bucket_pa)
725 : "r" (irq_work_pa(smp_processor_id())),
726 "i" (PSTATE_IE)
727 : "memory");
728
729 orig_sp = set_hardirq_stack();
730
731 while (bucket_pa) {
732 struct irq_desc *desc;
733 unsigned long next_pa;
734 unsigned int virt_irq;
735
736 next_pa = bucket_get_chain_pa(bucket_pa);
737 virt_irq = bucket_get_virt_irq(bucket_pa);
738 bucket_clear_chain_pa(bucket_pa);
739
740 desc = irq_desc + virt_irq;
741
742 if (!(desc->status & IRQ_DISABLED))
743 desc->handle_irq(virt_irq, desc);
744
745 bucket_pa = next_pa;
746 }
747
748 restore_hardirq_stack(orig_sp);
749
750 irq_exit();
751 set_irq_regs(old_regs);
752 }
第713行,清软中断
第715,716行,进入中断执行环境,这些就不细讲了,本文的主题在于Linux T2的中断处理流程,想这些与理解本文主题帮助不大,而又复杂难以理解的部分都粗略跳过。同样,第729行以及第748行到751行都跳过不提,这些关于进程执行上下文,中断执行上下文的东西都是复杂而难以理解的,如果想要理解它们,需要开辟专门的主题专门的章节来专门讲解它们。
第719到727行,又是一段内嵌汇编,在这之前遇到的所有内嵌汇编我们都没有讲解其中的汇编代码,直接说明那段汇编的作用,是因为之前的内嵌汇编大多只执行一个操作,但是这里的内嵌汇编确不得不讲里面的汇编代码了,否则读者无法理解这段汇编到底是做什么用的。
本来我们的分析中不应该有讲解编程语言语法的内容,但是考虑到大多数读者对内嵌汇编了解不多,因此这里简单的讲一下gcc内嵌汇编的语法。gcc内嵌汇编是由关键字__asm__引入c语言代码的,该该关键字声明后面括号中的代码为汇编代码,但是我们的这段内嵌汇编中还有一个关键字__volatile__,该关键字表示这段汇编涉及的变量直接操作内存而不使用cache或寄存器中保持的临时变量。
在括号内就是内嵌汇编的代码了,它分为4个部分,相邻两部分之间用冒号隔开,其中第一部分是汇编代码模板,第二部分是输出变量,第三部分是输入变量,第4部分是做什么用的我也不是很清楚,不过它对理解这段汇编并不重要。其中第一部分的汇编代码可以通过%n引用第二部分和第三部分的变量或立即数,其中n就是变量出现在代码中的顺序,比如这段汇编中%0引用的就是出现的第一个变量pstate,执行时把变量代入汇编模板,然后执行那段汇编模板,由于引用变量时使用了符号’%’,因此汇编模板中引用寄存器前面要加两个’%'符号了。
上面讲的内嵌汇编非常简略,如果想详细了解请自行查阅相关资料。
我们再来看这段汇编,
第719行,读取pstate寄存器的值放到变量pstate中
第720行,把PSTATE_IE的值与pstate异或后写入pstate寄存器,这两条语句的作用实际上就是清楚pstate的PSTATE_IE位,实际上就是禁止中断。
第721行,%2实际上是变量irq_work_pa(smp_processor_id()),它定义于同一文件中:
#define irq_work_pa(__cpu) &(trap_block[(__cpu)].irq_worklist_pa)
它实际上就是当前虚拟处理器的trap_block变量的 irq_worklist_pa成员的地址,还记得前面的分析吗,该变量保存了当前所有pending中断的 struct ino_bucket结构链表的物理地址,该条语句就是把该地址放入变量bucket_pa中
第722行,把虚拟处理器的trap_block变量的 irq_worklist_pa成员清0,前面的分析提到过。
第723行,允许中断
这段汇编的作用主要是取出虚拟处理器的trap_block变量的 irq_worklist_pa成员的值放入bucket_pa,并在此期间禁止中断,禁止中断的原因是如果这里把irq_worklist_pa取出来后,一个dev mondo trap发生,那么这个新发生的中断就会丢失。
第731行到746行,是一个while循环,该循环实际上是取出 struct ino_bucket结构链表的所有元素,然后依次处理每个中断,对每个中断的处理过程都是一样的。
第736行,通过当前的struct ino_bucket结构的_irq_chain_pa成员得到链表中下一个struct ino_bucket结构的地址,bucket_get_chain_pa是一个内嵌汇编函数,比较简单,就不分析了。
第737行,得到当前中断的中断请求号
第738行,取出中断请求号后,就要把对应中断的struct ino_bucket结构的_irq_chain_pa成员清零,以接收下一次中断。
第740行,得到中断请求号的中断描述符
第743行,调用中断描述符号hanle_irq函数指针处理中断。
第740,743行实际上已经是Linux中断处理的标准流程,这里中断描述符中的handle_irq指针是我们在分配中断请求号中初始化的,你可以看看前面的分析,当时调用set_irq_chip_and_handler_name函数为中断描述符初始化了两个成员,其中就包括这里的handle_irq函数指针,物理是虚拟设备还是物理设备,两者都赋值的是函数 handle_fasteoi_irq
,该函数是Linux中断子系统提供的标准函数。从这里开始就进入体系结构无关的标准的中断处理流程了,那些流程就不接着分析了,总之,最后会调用使用该中断请求号注册的所有中断处理例程为中断提供服务。
这里还要补充一点,set_irq_chip_and_handler_name处了设置了中断描述符的handle_irq成员外,还设置了chip成员,他们分别设置如下:
对于物理设备:
static struct irq_chip sun4v_irq = {
.typename = "sun4v",
.enable = sun4v_irq_enable,
.disable = sun4v_irq_disable,
.eoi = sun4v_irq_eoi,
.set_affinity = sun4v_set_affinity,
};
对于虚拟设备:
static struct irq_chip sun4v_virq = {
.typename = "vsun4v",
.enable = sun4v_virq_enable,
.disable = sun4v_virq_disable,
.eoi = sun4v_virq_eoi,
.set_affinity = sun4v_virt_set_affinity,
};
这个结构中的成员是在中断处理的过程中提供一些中断的设置功能的,比如你想禁止莫个中断,调用irq_disable函数的时候最终就会调用到这里的enable成员,
set_affinity成员是用来自动设置该中断发送到的cpu的,eoi是在中断处理结束后进行清中断ack操作的,这些都是体系结构相关的操作,在理解了T2的中断硬件机制后,看这些应该不难理解,这些函数读者可自行分析。
Msi中断:
我们的分析到这里,好像Linux T2的中断处理流程都已经分析完了,虚拟设备和物理设备的中断都分析了,可以松一口气了,但是很可以,我可以告诉你还有一类设备的中断并没有分析完,那就是使用msi中断的设备。
Msi中断初始化:
跟其他设备一样,msi中断的初始化要做的事情就是为使用msi中断的PCIe设备分配中断请求号,什么时候分配?当然是PCIe设备的时候了。不过一般设备在初始化时会调用前面分析过的sun4v_build_irq或sun4v_build_virq分配中断请求号,而使用msi中断的设备却不会调用那两个函数,原因与T2的中断体系机构有关,因为msi中断不会直接传递给虚拟处理器,它中间要经过PIU的event queue。
在PCIe设备的驱动程序中,如果设备具备MSI能力就会调用msi_capability_init函数初始化设备的msi,在msi_capability_init函数中调用arch_setup_msi_irq函数为msi分配中断请求号,arch_setup_msi_irq会调用arch/sparc/kernel/pci_msi.c中的sparc64_setup_msi_irq函数来分配中断请求号,而中断请求号实际上是根据msi消息包里面的内容(我们称之为msiid)来分配的:
123 static int sparc64_setup_msi_irq(unsigned int *virt_irq_p,
124 struct pci_dev *pdev,
125 struct msi_desc *entry)
126 {
127 struct pci_pbm_info *pbm = pdev->dev.archdata.host_controller;
128 const struct sparc64_msiq_ops *ops = pbm->msi_ops;
129 struct msi_msg msg;
130 int msi, err;
131 u32 msiqid;
132
133 *virt_irq_p = virt_irq_alloc(0, 0);
134 err = -ENOMEM;
135 if (!*virt_irq_p)
136 goto out_err;
137
138 set_irq_chip_and_handler_name(*virt_irq_p, &msi_irq,
139 handle_simple_irq, “MSI”);
140
141 err = alloc_msi(pbm);
142 if (unlikely(err < 0))
143 goto out_virt_irq_free;
144
145 msi = err;
146
147 msiqid = pick_msiq(pbm);
148
149 err = ops->msi_setup(pbm, msiqid, msi,
150 (entry->msi_attrib.is_64 ? 1 : 0));
151 if (err)
152 goto out_msi_free;
153
154 pbm->msi_irq_table[msi - pbm->msi_first] = *virt_irq_p;
155
156 if (entry->msi_attrib.is_64) {
157 msg.address_hi = pbm->msi64_start >> 32;
158 msg.address_lo = pbm->msi64_start & 0xffffffff;
159 } else {
160 msg.address_hi = 0;
161 msg.address_lo = pbm->msi32_start;
162 }
163 msg.data = msi;
164
165 set_irq_msi(*virt_irq_p, entry);
166 write_msi_msg(*virt_irq_p, &msg);
167
168 return 0;
169
170 out_msi_free:
171 free_msi(pbm, msi);
172
173 out_virt_irq_free:
174 set_irq_chip(*virt_irq_p, NULL);
175 virt_irq_free(*virt_irq_p);
176 *virt_irq_p = 0;
177
178 out_err:
179 return err;
180 }
第133行, virt_irq_alloc函数实际上是为该msi分配中断请求号的,这个函数在之前的已经分析过,这里是以参数devhanle=0,devino=0来调用这个函数的
第138行,这个函数也在之前分析过,初始化相应中断请求号的中断描述符的chip成员和handle_irq成员,其中chip成员中的函数可以做一些中断的设置工作, msi_irq是在同一文件中定义的,可自行去看,handlei_irq是体系结构无关的标准函数。
141行,为该分配msiid, alloc_msi函数定义如下:
96 static int alloc_msi(struct pci_pbm_info *pbm)
97 {
98 int i;
99
100 for (i = 0; i < pbm->msi_num; i++) {
101 if (!test_and_set_bit(i, pbm->msi_bitmap))
102 return i + pbm->msi_first;
103 }
104
105 return -ENOENT;
106 }
从这个函数中可以看出它是通过msi位图来分配msiid的,分配规则就是选择第一个未被分配的msiid,并同时在位图中把相关位置0,也就是说如果你是系统中第一个请求msiid的msi那么的的msiid就是0,一次类推,但是一个msi中断的msiid并不是已成不变的,它通常在驱动初始化的时候请求msiid,但是如果你卸载了驱动模块,那么就会调用相关pci标准函数清楚msi,而该函数会调用我们这里同一个文件立的free_msi函数清楚掉其msiid,比如系统中第一个请求msiid的设备卸载了驱动,那么0号msiid在位图中的相关位就会被清掉,当有下一个设备请求msiid时就会把0号msiid分配给它。
我们再回到 sparc64_setup_msi_irq函数,
第147行,为该msi中断选择要要关联的msi队列,即当PIU接收到msi消息时,会被加入到哪个PIU event队列中去:
75 static u32 pick_msiq(struct pci_pbm_info *pbm)
76 {
77 static DEFINE_SPINLOCK(rotor_lock);
78 unsigned long flags;
79 u32 ret, rotor;
80
81 spin_lock_irqsave(&rotor_lock, flags);
82
83 rotor = pbm->msiq_rotor;
84 ret = pbm->msiq_first + rotor;
85
86 if (++rotor >= pbm->msiq_num)
87 rotor = 0;
88 pbm->msiq_rotor = rotor;
89
90 spin_unlock_irqrestore(&rotor_lock, flags);
91
92 return ret;
93 }
这个函数我们也不一行行分析,只是解释选择的原理,当系统中第一个msi请求msiid之前,函数中的 pbm->msiq_rotor是0,因此就选择的是第一个msi 队列即msiq_first,
当第二个msi请求时,就会选择第二个msi队列,依次类推,当选择到最后一个msi队列时,在来msi请求的话,就会回过头来选择第一个msi队列,一直这样轮替下去,这样做的好处是,msi中断可以均衡的分布在每一个msi队列中而不会使某一个msi队列负载过大而其他的msi队列却很空闲。
我们再回到 sparc64_setup_msi_irq函数,
第149行,调用ops->msi_setup函数设置msi,当选择了msi队列时,要告诉hypervis,然后hypervisor会设置PIU,这样我们为msi中断才会真正关联到相应的msi队列上去。msi_setup指针调用的是同一目录下的pci_sun4v.c文件中的 pci_sun4v_msi_setup函数,实际上这个函数直接调用hyperviosr服务进行设置,就不贴出来了。
第154行,还记得前面分配的msi_irq_table吗,当时说他是用来来保存msi中断的中断请求号的,这里就是把分配的中断请求号保存到msi_irq_table,msi_irq_table中第msiid个元素里面保存到就是与那个msiid相关的msi中断的中断请求号。
到这里msi中断的中断请求号也分配保存了,当发生中断时根据该中断请求号调用中断处理例程就行了。但是还有最后一个问题,我们知道msiq队列中的元素里面有msi消息包,中断处理的时候肯定是根据msi消息里的内容来判断是选择哪一个中断请求号的。那么该怎么知道这个消息包关联的是哪一个中断请求号呢,这就是接下来要做的最后一个工作了,执行msi消息包的内容,实际上我们指定的就是msiid,当取出msi消息包中的msiid时,就可一根据msiid从msi_irq_table表中取出相应中断请求号来调用相关中断处理例程了。
第156行到166行就是设置msi消息的了。
Msi消息管理结构如下:
6 struct msi_msg {
7 u32 address_lo; /* low 32 bits of msi message address */
8 u32 address_hi; /* high 32 bits of msi message address */
9 u32 data; /* 16 bits of msi message data */
10 };
我们这里把它的data成员设置程msiid就行了
第165行,set_irq_msi函数把分配的中断请求号加入到msi管理结构中去,这样设备驱动程序就可以从该管理结构中取出中断请求号注册中断了。
第166行, write_msi_msg函数会设置PCIe设备的配置空间,设置之后PCIe设备程生中断时就会把msi_msg中的data,写到msi_msg中指定的地址,然后PIU会从该地址中取出msiid,与别的信息组成程msi queue队列中的成员。
到这里,msi中断的初始化就分析完了。
msi中断处理过程:
我们之前分析的中断处理流程都是虚拟处理器的dev mondo队列中有其interrupt packet的设备的中断,就是说不管是虚拟设备还是物理设备,NCU接收到你的中断后直接把它通过中断向量的方式告诉虚拟处理器,通过上面的流程都可以处理你的中断,但msi中断却并不满足这一条件。当PIU接收到msi包后,是把它加入到PIU的一个event queque中,而event queque会代表该队列中的所有msi产生一个中断mondo,该中断mondo会一中断向量的方式发送的虚拟处理器,然后进入上面讲解的中断处理流程,间言之,这一流程处理的是event queque虚拟设备的中断,而不是具体设备的msi中断,那么msi中断是怎么处理的呢。既然msi中断是加入的event queque中,哪我们就看看event queue虚拟设备的中断是具体怎么处理的吧。
在arch/sparc/kernel/pci_msi.c文件中,有一个函数sparc64_pbm_msi_init,这个函数就是event queue虚拟设备初始化的函数,这里需要指出的是event queue是PIU的event queue,PIU 是PCIe即可单元,它可以看作是PCIe总线的root complex,因此这里的产生msi中断的设备实际上是指PCIe设备。
我们来看sparc64_pbm_msi_init函数:
320 void sparc64_pbm_msi_init(struct pci_pbm_info *pbm,
321 const struct sparc64_msiq_ops *ops)
322 {
323 const u32 *val;
324 int len;
325
326 val = of_get_property(pbm->op->node, "#msi-eqs", &len);
327 if (!val || len != 4)
328 goto no_msi;
329 pbm->msiq_num = *val;
330 if (pbm->msiq_num) {
331 const struct msiq_prop {
332 u32 first_msiq;
333 u32 num_msiq;
334 u32 first_devino;
335 } *mqp;
336 const struct msi_range_prop {
337 u32 first_msi;
338 u32 num_msi;
339 } *mrng;
340 const struct addr_range_prop {
341 u32 msi32_high;
342 u32 msi32_low;
343 u32 msi32_len;
344 u32 msi64_high;
345 u32 msi64_low;
346 u32 msi64_len;
347 } *arng;
348
349 val = of_get_property(pbm->op->node, "msi-eq-size", &len);
350 if (!val || len != 4)
351 goto no_msi;
352
353 pbm->msiq_ent_count = *val;
354
355 mqp = of_get_property(pbm->op->node,
356 "msi-eq-to-devino", &len);
357 if (!mqp)
358 mqp = of_get_property(pbm->op->node,
359 "msi-eq-devino", &len);
360 if (!mqp || len != sizeof(struct msiq_prop))
361 goto no_msi;
362
363 pbm->msiq_first = mqp->first_msiq;
364 pbm->msiq_first_devino = mqp->first_devino;
365
366 val = of_get_property(pbm->op->node, "#msi", &len);
367 if (!val || len != 4)
368 goto no_msi;
369 pbm->msi_num = *val;
370
371 mrng = of_get_property(pbm->op->node, "msi-ranges", &len);
372 if (!mrng || len != sizeof(struct msi_range_prop))
373 goto no_msi;
374 pbm->msi_first = mrng->first_msi;
375
376 val = of_get_property(pbm->op->node, "msi-data-mask", &len);
377 if (!val || len != 4)
378 goto no_msi;
379 pbm->msi_data_mask = *val;
380
381 val = of_get_property(pbm->op->node, "msix-data-width", &len);
382 if (!val || len != 4)
383 goto no_msi;
384 pbm->msix_data_width = *val;
385
386 arng = of_get_property(pbm->op->node, "msi-address-ranges",
387 &len);
388 if (!arng || len != sizeof(struct addr_range_prop))
389 goto no_msi;
390 pbm->msi32_start = ((u64)arng->msi32_high << 32) |
391 (u64) arng->msi32_low;
392 pbm->msi64_start = ((u64)arng->msi64_high << 32) |
393 (u64) arng->msi64_low;
394 pbm->msi32_len = arng->msi32_len;
395 pbm->msi64_len = arng->msi64_len;
396
397 if (msi_bitmap_alloc(pbm))
398 goto no_msi;
399
400 if (msi_table_alloc(pbm)) {
401 msi_bitmap_free(pbm);
402 goto no_msi;
403 }
404
405 if (ops->msiq_alloc(pbm)) {
406 msi_table_free(pbm);
407 msi_bitmap_free(pbm);
408 goto no_msi;
409 }
410
411 if (sparc64_bringup_msi_queues(pbm, ops)) {
412 ops->msiq_free(pbm);
413 msi_table_free(pbm);
414 msi_bitmap_free(pbm);
415 goto no_msi;
416 }
417
418 printk(KERN_INFO "%s: MSI Queue first[%u] num[%u] count[%u] "
419 "devino[0x%x]\n",
420 pbm->name,
421 pbm->msiq_first, pbm->msiq_num,
422 pbm->msiq_ent_count,
423 pbm->msiq_first_devino);
424 printk(KERN_INFO "%s: MSI first[%u] num[%u] mask[0x%x] "
425 "width[%u]\n",
426 pbm->name,
427 pbm->msi_first, pbm->msi_num, pbm->msi_data_mask,
428 pbm->msix_data_width);
429 printk(KERN_INFO "%s: MSI addr32[0x%llx:0x%x] "
430 "addr64[0x%llx:0x%x]\n",
431 pbm->name,
432 pbm->msi32_start, pbm->msi32_len,
433 pbm->msi64_start, pbm->msi64_len);
434 printk(KERN_INFO "%s: MSI queues at RA [%016lx]\n",
435 pbm->name,
436 __pa(pbm->msi_queues));
437
438 pbm->msi_ops = ops;
439 pbm->setup_msi_irq = sparc64_setup_msi_irq;
440 pbm->teardown_msi_irq = sparc64_teardown_msi_irq;
441 }
442 return;
443
444 no_msi:
445 pbm->msiq_num = 0;
446 printk(KERN_INFO "%s: No MSI support.\n", pbm->name);
447 }
第326行,从pbm的资源中得到msi event queue的数量,pbm实际上代表的就是PCIe总线的根设备root complex,实际上就是PIU,因此event queue 相关的资源都可以从pbm及PIU所对应的设备资源中找到。得到的数目放在pbm结构的成员msiq_num中。
pbm结构及Linux T2 PCIe总线的初始化会专门开辟一个章节来讲解,这里略过不提。
第331行到347行,定义了3个结构体变量,至于是干什么用的,遇到的时候再说。
第349行,从pbm设备资源中得到msi event queue队列的大小,存放如pbm结构的
msiq_ent_count成员中
直到第395行,都是得到msi event queue相关的资源放到相关的结构中,这些怎么用的用到的时候再说。
第397行,为msi分配位图,即每个msi在其中占有一位,注意是系统中所有msi中断,而不是msi queque队列,系统中msi中断的个数是在前面的资源中得到的。
第400行,这个函数做的工作比较多,我们看一下这个函数:
239 static int msi_table_alloc(struct pci_pbm_info *pbm)
240 {
241 int size, i;
242
243 size = pbm->msiq_num * sizeof(struct sparc64_msiq_cookie);
244 pbm->msiq_irq_cookies = kzalloc(size, GFP_KERNEL);
245 if (!pbm->msiq_irq_cookies)
246 return -ENOMEM;
247
248 for (i = 0; i < pbm->msiq_num; i++) {
249 struct sparc64_msiq_cookie *p;
250
251 p = &pbm->msiq_irq_cookies[i];
252 p->pbm = pbm;
253 p->msiqid = pbm->msiq_first + i;
254 }
255
256 size = pbm->msi_num * sizeof(unsigned int);
257 pbm->msi_irq_table = kzalloc(size, GFP_KERNEL);
258 if (!pbm->msi_irq_table) {
259 kfree(pbm->msiq_irq_cookies);
260 pbm->msiq_irq_cookies = NULL;
261 return -ENOMEM;
262 }
263
264 return 0;
265 }
第243到254行,为每个msi queueq都分配一个 struct sparc64_msiq_cookie结构,并为该结构赋值,其中的msiqid成员是相关msiq队列的ID号,注意第一个msi queue队列的id(msiq_first)号并不是0,它是从设备资源中得到的,我猜测是由于PIU还有其他的event queue不只是msi queue的原因导致的。每个msi queque队列都有一个中断,这里的分配的struct sparc64_msiq_cookie实际上是传递给msi queque队列中断的中断处理例程的参数。
第256到261行,分配msi_irq_talbe,msi_irq_table中每个msi中断(注意不是msiq 中断)都占有4个字节,它是用来保存每个msi中断的中断请求号。
第405到309行,分配msi queue队列,从这里可以看出PIU的msi queue的event queue也是在privilged software中分配的,它实际上执行的函数是统一目录下的pci_sun4v.c文件中的pci_sun4v_msiq_alloc函数:
767 static int pci_sun4v_msiq_alloc(struct pci_pbm_info *pbm)
768 {
769 unsigned long q_size, alloc_size, pages, order;
770 int i;
771
772 q_size = pbm->msiq_ent_count * sizeof(struct pci_sun4v_msiq_entry);
773 alloc_size = (pbm->msiq_num * q_size);
774 order = get_order(alloc_size);
775 pages = __get_free_pages(GFP_KERNEL | __GFP_COMP, order);
776 if (pages == 0UL) {
777 printk(KERN_ERR “MSI: Cannot allocate MSI queues (o=%lu).\n”,
778 order);
779 return -ENOMEM;
780 }
781 memset((char *)pages, 0, PAGE_SIZE << order);
782 pbm->msi_queues = (void *) pages;
783
784 for (i = 0; i < pbm->msiq_num; i++) {
785 unsigned long err, base = __pa(pages + (i * q_size));
786 unsigned long ret1, ret2;
787
788 err = pci_sun4v_msiq_conf(pbm->devhandle,
789 pbm->msiq_first + i,
790 base, pbm->msiq_ent_count);
791 if (err) {
792
793 err);
794 goto h_error;
795 }
796
797 err = pci_sun4v_msiq_info(pbm->devhandle,
798 pbm->msiq_first + i,
799 &ret1, &ret2);
800 if (err) {
801 printk(KERN_ERR “MSI: Cannot read msiq (err=%lu)\n”,
802 err);
803 goto h_error;
804 }
805 if (ret1 != base || ret2 != pbm->msiq_ent_count) {
806 printk(KERN_ERR "MSI: Bogus qconf "
807 “expected[%lx:%x] got[%lx:%lx]\n”,
808 base, pbm->msiq_ent_count,
809 ret1, ret2);
810 goto h_error;
811 }
812 }
813
814 return 0;
815
816 h_error:
817 free_pages(pages, order);
818 return -EINVAL;
819 }
第772到782行,为msiq队列分配空间,其中pbm->msiq_ent_count是每个队列中元素的个数 , sizeof(struct pci_sun4v_msiq_entry)是每个队列的大小, pbm->msiq_num是msiq队列的个数。
第784到812行,调用hypervisor服务把分配的msiq队列注册到hypervisor并确认注册成功。
我们在回到sparc64_pbm_msi_init函数中来
第411行,调用sparc64_bringup_msi_queues函数设置msiq队列:
302 static int sparc64_bringup_msi_queues(struct pci_pbm_info *pbm,
303 const struct sparc64_msiq_ops *ops)
304 {
305 int i;
306
307 for (i = 0; i < pbm->msiq_num; i++) {
308 unsigned long msiqid = i + pbm->msiq_first;
309 unsigned long devino = i + pbm->msiq_first_devino;
310 int err;
311
312 err = bringup_one_msi_queue(pbm, ops, msiqid, devino);
313 if (err)
314 return err;
315 }
316
317 return 0;
318 }
该函数为每一个msiq队列调用bringup_one_msi_queue,其中参数devino就是相关msiq队列的中断的dervino号,它们与pbm的devhandle共同构devhandle/devino对。
276 static int bringup_one_msi_queue(struct pci_pbm_info *pbm,
277 const struct sparc64_msiq_ops *ops,
278 unsigned long msiqid,
279 unsigned long devino)
280 {
281 int irq = ops->msiq_build_irq(pbm, msiqid, devino);
282 int err, nid;
283
284 if (irq < 0)
285 return irq;
286
287 nid = pbm->numa_node;
288 if (nid != -1) {
289 cpumask_t numa_mask = *cpumask_of_node(nid);
290
291 irq_set_affinity(irq, &numa_mask);
292 }
293 err = request_irq(irq, sparc64_msiq_interrupt, 0,
294 "MSIQ",
295 &pbm->msiq_irq_cookies[msiqid - pbm->msiq_first]);
296 if (err)
297 return err;
298
299 return 0;
300 }
第281行,为该队列的中断分配纵段请求号,它实际上是调用pci_sun4v.c函数中的pci_sun4v_msiq_build_irq函数:
843 static int pci_sun4v_msiq_build_irq(struct pci_pbm_info *pbm,
844 unsigned long msiqid,
845 unsigned long devino)
846 {
847 unsigned int virt_irq = sun4v_build_irq(pbm->devhandle, devino);
848
849 if (!virt_irq)
850 return -ENOMEM;
851
852 if (pci_sun4v_msiq_setstate(pbm->devhandle, msiqid, HV_MSIQSTATE_IDLE))
853 return -EINVAL;
854 if (pci_sun4v_msiq_setvalid(pbm->devhandle, msiqid, HV_MSIQ_VALID))
855 return -EINVAL;
856
857 return virt_irq;
858 }
则个函数实际上是使用pbm的devhanle与相关队列中断的devino组成的devhandle/devino号调用sun4v_build_irq分配中断请求号,sun4v_build_irq函数前面讲过,忘记的可以回去看看。
我们再看 bringup_one_msi_queue函数,
其第293行,有了msiq的中断请求号后,调用request_irq为该中断注册中断处理例程,这样,当msiq队列接收到msi消息包后,就会产生中断到虚拟处理器,然后hypervsior发送interrupt packet到dev mondo队列中,dev mondo trap会从struct ino_bucket结构中取出msiq中断的中断请求号,调用这里的中断处理例程。
到这里我们可以想象,在msiq中断的中断处理例程中,会取出msi 队列中的msi数据包,然后根据msi数据包调用具体msi设备的中断处理程序。
我们看看msq中断的中断处理例程:
11 static irqreturn_t sparc64_msiq_interrupt(int irq, void *cookie)
12 {
13 struct sparc64_msiq_cookie *msiq_cookie = cookie;
14 struct pci_pbm_info *pbm = msiq_cookie->pbm;
15 unsigned long msiqid = msiq_cookie->msiqid;
16 const struct sparc64_msiq_ops *ops;
17 unsigned long orig_head, head;
18 int err;
19
20 ops = pbm->msi_ops;
21
22 err = ops->get_head(pbm, msiqid, &head);
23 if (unlikely(err < 0))
24 goto err_get_head;
25
26 orig_head = head;
27 for (;? {
28 unsigned long msi;
29
30 err = ops->dequeue_msi(pbm, msiqid, &head, &msi);
31 if (likely(err > 0)) {
32 struct irq_desc *desc;
33 unsigned int virt_irq;
34
35 virt_irq = pbm->msi_irq_table[msi - pbm->msi_first];
36 desc = irq_desc + virt_irq;
37
38 desc->handle_irq(virt_irq, desc);
39 }
40
41 if (unlikely(err < 0))
42 goto err_dequeue;
43
44 if (err == 0)
45 break;
46 }
47 if (likely(head != orig_head)) {
48 err = ops->set_head(pbm, msiqid, head);
49 if (unlikely(err < 0))
50 goto err_set_head;
51 }
52 return IRQ_HANDLED;
53
54 err_get_head:
55 printk(KERN_EMERG “MSI: Get head on msiqid[%lu] gives error %d\n”,
56 msiqid, err);
57 goto err_out;
58
59 err_dequeue:
60 printk(KERN_EMERG "MSI: Dequeue head[%lu] from msiqid[%lu] "
61 “gives error %d\n”,
62 head, msiqid, err);
63 goto err_out;
64
65 err_set_head:
66 printk(KERN_EMERG "MSI: Set head[%lu] on msiqid[%lu] "
67 “gives error %d\n”,
68 head, msiqid, err);
69 goto err_out;
70
71 err_out:
72 return IRQ_NONE;
73 }
中断处理例程的参数就是我们之前讲过的每个队列都有的 struct sparc64_msiq_cookie,这里会取出结构中的值,其中该结构的成员msiq表示该中断是由哪个msiq产生的。
第22行,get_head指针指向的具体函数就不贴出来了,它实际上也是直接调用hypervisor服务得到队列中的第一个包的地址(也就是队列的head寄存器所指向的包),与队列的首地址之间的字节偏移。
第27行到51行的for循环,取出队列中所有的包中的msiid然后根据msiid从 msi_irq_table取出中断请求号,然后根据中断请求号调用中断,这些就不细讲了,其中第30就是具体取出msiid的函数指针,它实际上取出msiid后还会调用hypervisor服务把相应的msi中断的状态清为空闲状态,以便接收下一次中断。
到这里Linux T2的中断代码就分析完了,其中也许有一些对我们的理解来说不是很重要的细节没有分析到,但详细看完本文档后,那些对你不再是问题。