GICv3-4宏观视图

按照主题来讲,首先是GIC本身这个大主题的拆解。

从功能器件来看

GICv3-4已经开始对虚拟化提供大量支撑,从物理硬件上,按照功能器件可以拆解成:Distributor、ITS、Redistributor、CPU interface的集合。先将图给出来:

Distributor

其中Distributor连接外设承载SPI(Shared Pheripheral Interrupts)中断的优先级和分发,也承载SGI(Software Generated Interrupts)的中断优先级和分发(毕竟SGI主要还是为了从1个PE发到其他PE,Distributor才能完成这个级别的动作);外设中断信号也就是SPI进入Distributor,Distributor承载有对SPI路由规则的设置、优先级的设置、中断分组的设置以及对Distributor本身功能的设置,Distributor根据这些配置,在SPI到达时,决定将SPI送给哪个Redistributor,或是放弃。Distributor相比CPU是慢的,对于PE而言,对Distributor的控制寄存器的设置与对外设的寄存器的设置是相同的,将Distributor的寄存器映射到PE的内存视图,PE对Distributor的寄存器所在的内存段读读写写,就完成了对Distributor的操作。

Distributor的主要配置寄存器GICD_CTLR提供全局的配置:开关中断亲缘路由(ARE_NS 非Secure域,ARM_S Secure域)、开关Secure域、开关Secure/Non-secure域的Group1中断、开关Group0中断、开关SPI中断;Distributor其他配置寄存器GICD_*还包括对SPI的配置:配置每个SPI中断的优先级(如GICD_IPRIORITYR<n>对于中断m,n=m/4,其中第8*(m%4)-8*(m%4+1)位,即为此中断号对应的中断的优先级,0最高,255最低)、配置每个SPI的路由信息(如GICD_IROUTER<n>即指示中断号为n的SPI的中断路由方式,其IRM bit为0则配发到此寄存器中Affx指定的PE,其IRM bit为1则以此寄存器中Affx对应的PE为列表,选择其中一个PE,配发过去)、配置每个SPI中断是边沿触发还是电平触发(如GICD_ICFGR<n>中2x+1位,即控制中断号16n+x的中断,是电平触发(0)还是边沿触发(1))、触发基于消息的SPI(对寄存器写入,触发SPI中断,如向GICD_SETSPI_SR/NSR写入一个有效的SPI中断号,即可触发Secure域/非Secure域的中断,对应的GICD_CLRSPI_SR/NSR消除触发状态)、将每个SPI都联系到一个中断组(如GICD_IGROUPR<n>的bit x位,置为0/1,在GICD_CTLR.DS==1/0的情况下,分别可以将中断号n*32+x联系到Non-secure/Secure域的中断组0/1)、控制SPI中断的触发-活跃状态(如GICD_ISPENDR<n>通过对bit x置1,将中断号32n+x的中断从非活跃态转换到触发态或是从活跃态转换到触发并活跃态)。关于中断的分组以及掺入Non-secure域、Secure域的因素之后FIQ和IRQ又对应了不同的域、组,下面这个表给出了一个配置好的实例:

根据上表的配置意图,PE运行在Non-secure态时,Non-secure Group1中断直接进EL1,找IRQ Vector处理;Secure Group1中断和Group0中断则会直接进EL3,找FIQ Vector处理,EL3应该自己处理Group0的中断,但是对于Secure Group1中断,应该做context切换的事儿,使Secure Group1中断走上正路。PE运行在Secure态时,Secure Group1中断直接进Secure EL1,找IRQ Vector处理;而Non-Secure Group1和Group0中断则会进入EL3,同样的Group0中断还是EL3自己处理,而对于Non-Secure Group1中断EL3则会做些context切换的事儿,具体如下图所示:

ITS(Interrupt translation service)

在GICv3 ITS是选配的硬件机制,ITS用于路由LPI到合适的Redistributor;软件上,通过一个指令序列来配置ITS,内存中与这个ITS相关的表结构能将一个设备相关的EventID翻译成INTID,INTID就是PE能认识的中断号了。在GICv4中,ITS是必选的,并且可能不止一个。

ITS的配置寄存器都是GITS_*的样子,比如GITS_CTLR控制ITS的开关、是否可以断电以及一些GICv4才支持的虚拟化相关功能,在GITS_CTLR.Enable==1后,写入GITS_TRANSLATER即触发中断翻译并处理指令队列;比如GITS_CWRITER就是指定软件写入下一个ITS指令时,写入地址对CITS_CBASER的偏移量,相应的GITS_CBASER就是用于指定ITS指令队列的基地址和指令队列的大小以及指令队列的内存是否有效、缓存域、共享性等;再如GITS_CREADER看名字就知道是与GITS_CREADER相对的,用于指定读ITS指令队列里下一个ITS指令的地址偏移量(同样相对于GITS_CBASER)的偏移量;其他还有指示ITS本身版本信息的的GITS_IIDR、指示ITS支持的特性的GITS_TYPER。而除了这些寄存器,还有很多是直接在内存中的数据,比如翻译的表结构就存在内存中,然后表结构的基地址就放在GITS_BASER<n>,并且是Non-secure域的内存。

Redistributor

Redistributor是Distributor与CPU interface的中间器件,每个CPU Interface都对应有一个Redistributor(添加Redistributor也算是为了缓解高速运行的CPU与低速运行的外设的速度差),Redistributor一边承接SPI(来自Distributor)、LPI(Locality-specific Peripheral Interrupts)、SGI、PPI(Private Peripheral Interrupt),另一边总是将最高优先级的中断呈送给CPU interface。Redistributor的操作也是PE对Redistributor配置寄存器所在的内存段读读写写完成,也就是内存映射式的操作。每个PE都有自己的Redistributor和CPU interface两个逻辑器件,分别承载不同的任务,Redistributor负责提供的编程接口有:识别、控制、配置支持的功能以使能中断和中断路由;开关SGI/PPI,比如GICR_ICENABLER0相关bit写1,即可关闭对应中断号的中断;配置SGI/PPI的中断优先级;配置PPI/SGI的边沿触发或电平触发,如GICR_ICFGR1的相关位置1则此中断设为边沿出发、置0则设为电平触发,;将SGI/PPI关系到中断组;控制SGI/PPI的触发状态,如GICR_ICPENDR0相关bit写1则可将对应中断号的中断从触发态切换到不触发或是从触发并活跃态切换到活跃,不过非操作GICD_ISPENDR<n>产生的电平触发好像不能切除触发态;控制SGI/PPI的活跃状态,比如向GICR_ICACTIVER0的对应bit写1,即可使此中断号的中断进入不活跃状态;对连接的PE的电源管理支持;支持LPI的情况下,用于支持LPI相关中断属性和触发状态的数据结构所在的内存基地址也是由Redistributor的相关寄存器控制,如GICR_PROPBASER寄存器就用于存放LPI配置表的内存基地址及其缓存、共享特性,GICR_PENDBASER寄存器就用于存放LPI触发表的内存基地址及其缓存、共享特性;在支持GICv4的情况下,用于支持相关虚拟中断属性和触发状态的数据结构所在的内存基地址也是由Redistributor的相关寄存器控制,如GICR_VPROPBASER寄存器就用于存放当前调度运行的虚拟机的虚拟LPI配置表的基地址,GICR_VPENDBASER寄存器就用于存放当前调度运行的虚拟机的虚拟LPI触发状态表的基地址。

CPU interface

CPU interface是比较奇特的,它为了CPU快速访问而生,所有的CPU interface控制、操作寄存器(除了ICC_SRE_ELx)都有两种方式访问,一种是作为系统寄存器来访问,PE/vPE像是访问X0寄存器一样直接访问;另一种是作为映射好的外设内存来访问,PE访问IO地址来控制、操作CPU interface的寄存器;作为系统寄存器的访问只需要MRS/MSR操作,应该是更便捷的,不过电路实现应该会更复杂些吧。而为啥ICC_SRE_ELx单列出来没有说两种访问方式呢?因为ICC_SRE_ELx.SRE(SystemRegisterEnable)就是控制使用系统寄存器访问、或使用内存映射访问的,如果系统是内存映射访问的,MRS/MSR此寄存器就会产生exception,然后看处理代码的了。另外,CPU interface距离PE足够近,所以虚拟化的支持在CPU interface上要做的更多,ICH_*相关的寄存器就是hypervisor层次用于对virtual CPU interface进行控制、操作、保存状态的。CPU interface提供的编程接口主要有:通过常规控制和配置来使能中断处理以实现新Secure域与传统模式的兼容;感知中断;执行优先级降级;平息中断活跃状态;设置PE的中断优先级屏蔽;配置PE的抢占规则;决定最高优先级中断触发到PE。CPU interface还有一些模块,ICC_*支配的内核级物理中断控制;ICV_*支配的虚拟化内核级虚拟中断控制;ICH_*支配的Hypervisor级中断控制。

兼容引起上述器件的奇怪寄存器设定

表述GICv3-4的Distributor和CPU interface而不提一下对GICv2的兼容的话,似乎很多寄存器的存在都不合理,比如说ICC_SRE_ELx.SRE这个东西的存在就非常奇怪,为什么要支持内存映射和系统寄存器两种访问方式?再比如说触发软中断有ICC_ASGI1R_EL1、ICC_SGI0/1R_EL1三个寄存器,为啥GICD_SGIR也是用来触发软中断的?GICD_SPENDSGIR也是用来触发软中断的;GICR_ISPENDR0也是能用来触发软中断;软中断好热乎。而再加入GICv3-4对GICv2的兼容来看的话,GICD_SPENDSGIR与GICR_ISPENDR0就不再有冲突:在GICv2没有Redistributor,Distributor负责一切,所以GI亲缘路由关闭的情况下,也就是要兼容GICv2,Distributor负责软中断的分发,GICR_ISPENDR0不会使用;而打开亲缘路由时,也就是不再考虑GICv2的兼容时,Redistributor接管软中断的分发,GICR_ISPENDSGIR0即投入使用,同样GICD_SPENDSGIR<n>不再使用,GICD_SGIR也会禁用;其实说白了就是Redistributor分担了Distributor的部分业务,不光SGI,PPI也是,当ICC_SRE_ELx.SRE将CPU interface配成内存映射方式访问,GICD_CTLR.ARE_S/NS将亲缘路由关闭,GIC即配置为兼容模式。下图可能更直接的给出Non-secure域、Secure域、GICv3、GICv2综合后,兼容的情况:

从中断类型来看

从GICv2开始就有的PPI,就是PE私有的外设中断,比如说PE私有的timer之类的;PPI是私有的,触发后会给Redistributor信号(上图),其中断开关就受到Redistributor辖制:若当前Secure状态下的中断亲缘路由使能,写GICR_ISENABLER0(用于使能相关SGI/PPI到CPU interface)和GICR_ICENABLE0(关闭相关SGI/PPI到CPU interface)即可开关PPI中断;如果GIC支持且被配置在兼容模式,PPI会受Distributor辖制,即单个PPI通过写GICD_ISENABLER<0>(使能相关中断到CPU interface)和GICD_ICENABLER<0>(关闭相关中断到CPU interface)即可开关此PPI。

SPI经典共享外设中断,通过写GICD_ISENABLER<n>(见PPI使能/关闭,n=0操作PPI,n>0操作SPI)和GICD_ICENABLER<n>(同样n>0)开启关闭SPI中断。

SGI由PE们自己操作ICC_SGI1R_EL1(向Group1当前Secure状态)触发中断,操作ICC_ASGI1R_EL1(向Group1非本Secure状态)触发中断,操作ICC_SGI0R_EL1(向Group0)触发中断;SGI中断的开关方式与PPI一致。

LPI应该通过ITS(Interrupt Translation Service)来给到Redistributor,没有ITS也可以通过LPI INTID直接给到Redistributors(上图),而LPI本身是一类新的中断,与SGI/SPI是相提并论的,显著扩展了GIC能支撑的中断数量,它是边沿出发、基于消息的中断,不经Distributor分发,实现了LPI的GIC最少支持中断号到8192,Redistributor有相关寄存器来存放LPI中断触发状态表(状态表位于内存,其基地址会设入Redistributor的寄存器,Redistributor部分有提到),而运行过程中,LPI内会形成LPI配置表的缓存,通过GICR_INVLPIR或GICR_INVALLR可以失效指定中断号的LPI中断配置数据缓存,或是失效掉LPI中所有的配置缓存;而一般支持LPI的都有ITS,ITS更复杂也更自动一些且更能支持LPI的虚拟化。开关LPI是通过写入LPI配置表的使能位完成,以LPI为主题有很多东西要讲,比如这个LPI配置表是什么东西,Redistributor对这个表的缓存,LPI怎么触发(外设写寄存器触发),LPI触发后如何知道是哪个LPI触发了(有个触发的表),在有ITS的情况下,LPI又是怎么处理的;这些问题会再写一篇东西来讲,本文更着重于全局来看。

从各类中断的处理路线来看

PPI的触发路线是PE私有外设触发->Redistributor->CPU interface->PE

SGI的触发路线是PE触发->Distributor->Redistributor->CPU interface->PE

SPI的触发路线是外设/寄存器触发->Distributor->Redistributor->CPU interface->PE

LPI的触发路线是xx出发->(可能的ITS->)Redistributor->CPU interface->PE

从中断号INTIDx的分配来看

INTID 0-15 SGI:PE触发,传给其他PE,算是用于PE间通讯的中断;ARM推荐INTID 0-7给Non-secure域用,INTID 8-15给Secure域用;

INTID 16-31 PPI:每个PE私有外设;ARM推荐INTID 30作为EL1物理定时器,INTID 29作为EL3物理定时器,INTID 28作为EL3虚拟定时器,INTID 27作为虚拟定时器中断,INTID 26作为EL2物理定时器中断,INTID 25作为虚拟CPU interface维护中断,INTID 24作为Cross Trigger Interface中断,INTID 23作为性能监控计数器溢出中断,INTID 22作为Debug沟通通道中断;

INTID 32-1019 SPI

INTID 1020-1023用于GIC返回信息的中断ID;INTID 1020用于GIC反馈EL3读ICC_IAR0_EL1/ICC_HPPIR0_EL1,这标志着被感知的中断想要被Secure EL1处理,GIC只会在PE为EL3时返回INIID;INTID 1021于INTID 1020相对,虽然同样用于反馈EL3对ICC_IAR0_EL1/ICC_HPPIR0_EL1的读取,但是这标志着被读取的中断时想要被Non-secure域处理的;INTID 1022是GICv2的;INTID 1023用于表示有一个没有足够优先级的中断来过,或是最高优先级的中断与当前Secure状态/中断组不符。

INTID 0-1023是与GICv2兼容的。

INTID 1024-8191 保留

INTID 8192-具体实现 LPI

从中断的亲缘路由来看

PE的MPIDR_EL1定义了PE的亲缘信息,Aff3/Aff2/Aff1/Aff0逐级的降低亲缘等级,亲缘等级越低,范围越小;前面已经说过GICD_CTLR.ARE_S/NS用于打开Secure域/Non-secure域的亲缘路由,亲缘路由影响的主要是SPI和SGI,GICD_IROUTER<n>就定义了SPI的亲缘性,n为其INTID,也就是GICD_IROUTER32到GICD_IROUTER1019,此寄存器的IRM位的1/0对寄存器中Aff3-Aff0被解读为允许的PE范围或是指向某指定PE,也已经有过表述;ICC_(A)SGI0/1R_EL1分别向Group1隔壁Secure域(A),Group0,Group1当前Secure域,按照类似GICD_IROUTER对Aff和IRM位的控制方式向某PE或某组PE发SGI,不过Aff0在ICC_x发软中断的应用里是TargetList,用16bit代表Aff0级的16个PE,置1表示要发给对应PE。关于Affx之于Redistributor和Distributor,下图可能比较清晰,哦对,还有CPU interface:

从中断处理模式来看

目标分配式的中断处理,中断触发即注定目标PE;对于所有的PPI和LPI来说,都是中断发生时已经确定CPU处理中断的,也就是目标分配式;当GICD_IROUTE<n>.Interrupt_Routing_mode==0(也就是中断号n的这个SPI的中断路由设置条目:GICD_IROUTE<n>指定死了中断n的目标PE),这是的SPI n也是目标分配式;在兼容模式时,GICD_CTLR.ARE*==0(也就是Secure态和Non-secure态的中断的亲缘路由都关闭),且GICD_ITARGETSR<n>中(中断的亲缘路由关闭后,n=中断号/4,此处说的中断号%4对应的offset的那8个bit)只有一个bit为1(兼容模式下只支持8核,GICD_ITARGETSR<中断号/4>这个寄存器的32个bit里,中断号%4后对应的那8个bit,即为此中断可以送到的CPU interface表)。

目标列表式的中断处理,中断有明确的目标列表,并且都会触发,每个PE都要处理这个中断的触发状态,像是广播,仅SGI使用这种中断模式。

1对多式的中断处理,触发的中断同样有明确的处理列表,但是列表中的任意PE处理了这个中断,则其他PE就不会理会到这个中断;话是这样说,但是这是古代1对多式中断的处理方式,很明显这里面需要类似锁的东西让PE们来争,GICv3以上已经改成在1对多式中断触发即选择一个PE来处理,其他PE不会再受到干扰,就算选中的这个PE恰好屏蔽了这个中断,这个中断还是会被扔到CPU interface,CPU interface一查,发现没有足够的优先级才会放弃它。

用两个实例解开纷纷扰扰的Secure状态、中断分组、特殊中断号1020-1023、FIQ/IRQ的意义

关于中断的分组以及掺入Non-secure域、Secure域的因素之后FIQ和IRQ又对应了不同的域、组,下面这个表给出了一个配置好的实例:

根据上表的配置意图,PE运行在Non-secure态时,Non-secure Group1中断直接进EL1,找IRQ Vector处理;Secure Group1中断和Group0中断则会直接进EL3,找FIQ Vector处理,EL3应该自己处理Group0的中断,但是对于Secure Group1中断,应该做context切换的事儿,使Secure Group1中断走上正路。PE运行在Secure态时,Secure Group1中断直接进Secure EL1,找IRQ Vector处理;而Non-Secure Group1和Group0中断则会进入EL3,同样的Group0中断还是EL3自己处理,而对于Non-Secure Group1中断EL3则会做些context切换的事儿,具体如下图所示:

具体到过程上,在此配置下,当PE运行在Secure域,而Non-secure Group1发生了中断,中断信号会作为FIQ给入EL3(因为PE配置SCR_EL3.FIQ=1),而EL3的FIQ  Vector则会做Context切换,使PE进入Non-secure EL1,然后触发一个IRQ到Non-secure的EL1,于是中断就能正确的走到Non-secure被处理;而EL3如何识别这种类型的FIQ呢?那几个特殊中断号中的INTID 1021就是标志这种中断的发生,当EL3的FIQ Vector读ICC_IAR0_EL1寄存器得到1021时,即用于Non-secure域的中断触发时PE在Secure态。下图绘出了此情况的发生过程。

一点一点配置起GICv3-4

一个基于ARM的物理计算平台一般都会是多PE的,甚至对于Server来说,一般都会是多处理器的,而GIC作为其中断控制器件服务于全部PE,需要分别进行全局和每个PE独立的配置。全局上讲,主要是亲缘路由和中断组、Security域的使能,GICD_CTLR.ARE位就是控制亲缘路由的开关,打开之后GIC就是GICv3-4模式,不太考虑兼容GICv2;中断组分别由GICD_CTLR.EnableGrp1S作为Secure域Group1中断组的开关、GICD_CTLR.EnableGrp1NS作为Non-secure域Group1中断组的开关、GICD_CTLR.EnableGrp0作为Group0中断组的开关来控制。

对于每个PE,重置之后Redistributor都会认为PE是睡眠状态,清除GICR_WAKER.ProcessorSleep位,然后等待GICR_WAKER.ChildrenAsleep变为0,完成唤醒;CPU interface则需要先将ICC_SRE_ELn.SRE打开,ICC/V/H_*系列寄存器才能作为系统寄存器访问;然后配置ICC_PMR_EL1决定屏蔽某中断优先级以下的中断,ICC_PMR_EL1.Prioryty根据具体实现不同可以支持bit[7:0]8bit或最少到bit[7:4],支持的bit不同,决定能屏蔽的优先级的细度不同,比如说bit[7:0]能配置为屏蔽0-255个优先级中的任意一个,而bit[7:1]则只能配置为屏蔽0-254个优先级中的偶数个,bit[7:4]控制粒度是最粗的仅有0-240按跨度16取;之后配置ICC_BPR0/1_EL1(S/NS)三个寄存器,决定Group0、Group1(S)、Group1(NS)的优先级号的分位点,比如ICC_BPR0_EL1.BinaryPoint[2:0]为3时,8个bit的中断优先级就被分为[7:4]和[3:0],中断优先级[7:4]bit的数字决定中断抢占,[3:0]位与抢占无关,分位点细节在下面这个表说明的很清楚;

优先级屏蔽和中断优先级抢占分位点的配置是CPU interface配置的一部分,还要配置中断的EOI模式,ICC_CTLR_EL1和ICC_CTLR_EL3的EOImode位控制中断处理完毕后结束本次中断的方式,ICC_CTLR_ELx.EOImode配置为0则向ICC_EOIR0/1_EL1写入中断号即可结束本次中断,配置为1则向ICC_EOIR0/1_EL1写入中断号仅会降本次中断优先级,还需要再向ICC_DIR_EL1写入中断号来结束中断的触发状态。最后配置ICC_IGRPEN0_EL1使能中断组Group0、在PE Secure态配置Group1 Secure域的ICC_GRPEN1_EL1使能Group1 Secure域中断、在PE Non-secure态配置Group1 Non-secure域ICC_GRPEN1_EL1使能Group1 Non-secure域中断。

除了上面CPU interface的配置,PE本身SCR_EL3、HCR_EL2中控制中断路由到的异常等级的相关控制位也要做出配置;PSTATE也有中断屏蔽位,需要打开;而中断处理向量相关的寄存器VBAR_ELn也要做好配置,毕竟那里才有真正处理中断的代码。

对于SPI、PPI、SGI,主要需要配置其在优先级、中断组、边沿/电平触发、是否使能;其中优先级由GICD_IPRIORITY<n> n=0-254,根据中断号能算出优先级数值所在的8个bit,一点不浪费,另外亲缘路由开启也就是不兼容的情况下,n=0-7所属的中断号0-32个是没用的,这时候GICR_IPRIORITYR<n> n=0-7负责SGIhePPI的优先级。中断组由GICD_IGROUPR<n>和GICD_IGRPMODR<n> n=0-31中各挑出对应的1个位联合确定(亲缘路由开启状态),组合后对中断所属中断组的控制如下表:

同样的,对于亲缘路由打开,向后不兼容(ARE)的情况下,PPI及SGI的中断号也是不受GICD_系列这两组寄存器的控制,GICR_IGROUP0和GICR_IGROPMODR0来做联合确定中断组的归属。边沿触发的配置由GICD_ICFGR<n> n=0-63 控制,0表示电平、1表示边沿,而同样的ARE开启后GICR_ICFGR0会接管SGI、GICR_ICFGR1则会接管PPI的边沿触发配置。使能控制由GICD_ISENABLER<n>完成,而ARE开启后GICR_ISENABLER0则会接管SGI和PPI的使能。

另外,SPI与PPI/SGI的均为PE私有不同,SPI由Distributor分发到给PE,因此对于SPI需要指定其可能路由到的目标PE列表,这需要在GICD_IROUTER<n> n=32-1019 作出配置,前文已有GICD_IROUTER<n>的一些表述,包括IRM、Aff等重点命题,不再多言。

LPI仅支持亲缘路由开启的情况,配置LPI需要配置Redistributor和ITS两部分,而GICD_TYPER.LPIS标志LPI是否被当前GIC支持,而ITS在GICv3也可以不实现,而LPI更是可以不经ITS直接送到Redistributor,而这里要分析的是LPI支持且ITS也实现的情况,不过LPI+ITS的处理过程比较复杂,如果不介绍过程的话,仅提各种配置难免有些飘,因此先简要说明LPI+ITS的处理过程。外设触发LPI都是通过写GITS_TRANSLATER,这是一个只写寄存器,写入的是一个EventID,EventID指示外设发出了哪个中断,EventID之于ITS有点像INITD之于Distributor,EventID经过ITS翻译后会得到INTID。另外外设触发LPI时还会有一个DeviceID提供,具体提供方式是根据实现决定,像是AXI用户信号之类;不同的外设指定的EventID->INTID翻译方式不同,ITS根据DeviceID确定翻译方式。LPI的INTID是按集合分组的,同一个集合里的INTID会被路由到同样的Redistributor。ITS用设备表、中断翻译表和集合表来翻译并路由LPI,设备表映射DeviceID到中断翻译表,中断翻译表包含指定DeviceID的EventID->INTID映射和INTID所属的集合,集合表包含集合到Redistributor的映射,具体结构如下图所示:

当外设写GITS_TRANSLATER时,ITS用DeviceID从设备表中选择对应的中断翻译表,然后用EventID从中断翻译表中得到对应的INTID和集合ID,用集合ID到集合表中找到路由信息也就知道了到那个Redistributor,当然,加入虚拟化的GICv4还有个与集合表同级的vPE表来实现由vPEID找到Redistributor,总之,确定INTID之后就确定最后将中断发到目标Redistributor,这一过程如下图:

所以这一过程中是需要设备表、中断翻译表、集合表甚至vPE表(当然还有触发表,不过那是Redistributor部分),这4个表都是靠程序申请指定,然后GIC运行时使用。在为设备表、vPE表、集合表申请的内存(整页)后,要将页大小、申请了多少个页配置到GITS_BASER<n> n=0-7 中Type段为0b001、0b010、0b100的基地址寄存器的Page_size和Size段,然后将内存基地址放在Physicall_Address段;GITS_BASER<n>通过配置Indirect段,支持两级表和单级表,两级表当然需要按规则翻译,不过能降低对连续内存的占用,单级表则比较简单但需要连续内存来放表,单级页表会排布成这个样子:

而两级表会排布成这个样子:

这些表的排布都已经是老一套原理,都不必细说。中断翻译表与上述三个表不同,它是由设备ID查设备表确定地址的,因此其内存基地址不会在GITS_BASER<n>存放,而是会存放在设备表的条目中。

其中设备表的条目一般会是这样:

中断翻译表的条目一般会是这样:

而集合表一般都是这样的:

然后这是vPE表:

关于ITS还有一个要注意的就是它的控制指令是放在一个环形指令队列里,队列相关的三个寄存器GITS_CBASER、GITS_CREADER、GITS_CWRITER,具体含义上文有述,在这个指令队列里这三个寄存器的排布也可以参照下图:

而具体的ITS控制指令稍多,我把列表拉出来放在这儿算是个备忘:

还有一半儿:

简要描述完ITS的运行逻辑后,说一下ITS的配置也就不显得突兀了,具体的在系统启动时对ITS的配置主要再三个方面:首先是为设备表、中断翻译表和集合表甚至vPE表申请内存;然后要为指令队列申请内存;最后使能ITS,也就是GITS_CTLR.Enable置位,而ITS使能后,GITS_BASER<n>和GITS_CBASER就会变成只读状态。

要用LPI不光需要配置ITS,Redistributor也承载LPI的部分功能。Redistributor对LPI的配置信息放在内存的表里,并且所有的Redistributor都会共享同一组LPI配置,Redistributor中LPI的配置表寄地址寄存器在前面已经有提到过,就是GICR_PROPBASER,而LPI的触发状态信息表寄地址寄存器也在前面提到过就是GICR_PENDBASER,每Redistributor一份,像是下图这样:

初始化Redistributor的LPI部分功能需要申请LPI配置表内存并在表中配置好内容,在每个Redistributor中都把GICR_PROPBASER配置为这个配置表;然后每个Redistributor都申请LPI触发信息表的内存,并对各表中内存进行初始化,也就是置0,都不是触发态,然后将各个Redistributor的GICR_PENDBASER配置到各个表;最后对每个Redistributor的GICR_CTLR.EnableLPIs置1,同样的,GICR_PROPBASER和GICR_PENDBASER都会变为只读。

LPI的配置信息表对每个LPI INTID都用1个字节,8bit,如下表:

可以看到Priority只有6bit与SPI/PPI/SGI的8字节不同,而少的两个bit会作为0处理,并且LPI没有Group0/1以及Secure/Non-secure域之分,只会作为Non-secure Group1处理。另外,LPI的配置信息都在内存里,所以Redistributor会缓存LPI的配置,因此要修改LPI的配置,程序必须要在更新LPI配置表后确认此修改全局可见,并失效掉Redistributor中的缓存(ITS的INV指令可完成失效)。

GICv3-4对虚拟化的一些考量

虚拟化是对一个kernel做虚拟化,就算现代kernel对于被虚拟化有考量,也不会周全考量;特别在外设方面,除了网络、存储方面,很少有其他复杂设备能够较好支持虚拟化,比如GPU之类;就算是有所支持,host的外设大多在Guest中只是一个virtio设备。绝大多数情况下,内核操作外设都是通过外设地址访问、中断处理两个方面完成(DMA这种东西应该算是中断方面),一个提供通道,一个同步消息;对于地址,由于MMU、两阶段MMU的支持,虽然有映射方面的复杂度,但在映射完毕的实际运行中效率也是没有差的;但是对于中断,vPE只是一个EL1进程这个本质导致给到vPE的中断与用户态程序的信号有着差不多的性质;毕竟会被context切换休克掉的vPE不能保障中断的实时处理。

而作为中断的大总管,GIC,对中断方面的虚拟化又应该如何考量?靠近CPU的部分也就是CPU interface可能比较容易处理,那都是vPE的现场,在EL2偷偷做一些修改也就糊弄过去了,就像是信号一样;而Guest kernel认为的Redistributor又如何能正确处理呢,毕竟vPE时有时无,而Redistributor可是一个有着自己处理逻辑的物理硬件;Guest内核配置的自己的Redistributor又该如何得到伸张呢,会是到物理硬件还是有一段代码来模拟,100个虚拟机可是能配出100中不同的配置;而Distributor和ITS都会有同样的问题。

没有太多的遇到使用GICv3-4虚拟化特性的代码,所以对于上面的问题,我在最后给一些含糊的分析,等到真正用到再落实它们。GICv3为了虚拟化添加了CPU interface下ICV_*系列的寄存器,Guest kernel访问ICC_*时可以被映射到CV_*系列寄存器,当然,这可以通过HCR_EL2等寄存器控制;另外还添加了ICH_*系列寄存器 ,用来控制能提供的虚拟化特性如:开关虚拟CPU interface、访问虚拟寄存器状态来做context切换、配置维护中断和控制虚拟中断;CPU interface的虚拟化使用算是可以糊弄过去了,下面这个图可以看到ICH_VMCR_EL2如何取到当前vPE的ICV_*:

关于context切换,CPU interface的context主要包含:ICV_*系列寄存器的状态、虚拟优先级、触发/活跃/触发并活跃三种虚拟中断状态。关于维护中断,主要是用来在vPE操作CPU interface的配置时,向Hypervisor发一个物理中断以响应vPE对CPU interface操作的,是PPI的INTID 25。

然后GICv3为了虚拟化还添加了虚拟中断,也就是vINTID,也就是说要到vPE的pINTID的触发会转变成为vINTID,而在控制着当前vPE中vINTID到pINTID映射的是ICH_LR<n>_EL2 n=0-15这组寄存器,长这样:

给出一个物理中断递到vPE的过程,如下图:

最后关于LPI的虚拟化部分,GICv4支持vLPI对Redistributor的直接注入式使用,具体就是Redistributor关于LPI的两个关键寄存器都有其对应的虚拟版,也就是GICR_VPROPBASER和GICR_VPENDBASER,直接改这两个寄地址寄存器就能瞒天过海,而hypervisor对vPE的context切换时,修改这两个寄存器是比较慢的:先要GICR_VPENDBASER.Valid置零,然后听GICR_VPENDBASER.Dirty变0,最后才能更新GICR_VPROPBASER、更新GICR_VPENDBASER、Valid置1;另外context切换后Redistributor估计会重新cache,想来也会影响效率。最后,ITS将一个虚拟中断递给vPE或是Hypervisor的过程实例如下:

总的来讲,除了CPU interface部分,GICv3-4对于中断做虚拟化的考量还是不少的,但是似乎只有CPU interface的部分是高效的。GICD_*、GICR_*、GITS_*这三组寄存器都是需要Hypervisor第2阶段页表fault来承载起Guest kernel的修改,而实际功能还是需要在Hypervisor写一些代码来做,再加上context切换引入的开销,具体真的将外设给到单个或多个Guest会怎样还是不太好说的。


结束

上面的这些分析认认真真的消耗了我一个周的时间,写的东西也是零零碎碎,可以说我的文笔非常之差了;不过有些好处就是我记录的这些内容大概还是可信的,毕竟都是ARM官方relase的文档,我认为自己理解透了的,都会自由发挥的一通瞎扯,没有深刻理解的则跟着官方文档斟字酌句的翻译了过来,当然,翻译错、理解错的情况估计也会时有发生,仅能保障我智力范围内的精准度,虽然智力水平委实不高。所以。。。就这样,就到这里,反正我自己是觉着明了了。若有错漏,请怒斥我智商低,但请不要说我造谣。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章