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的文檔,我認爲自己理解透了的,都會自由發揮的一通瞎扯,沒有深刻理解的則跟着官方文檔斟字酌句的翻譯了過來,當然,翻譯錯、理解錯的情況估計也會時有發生,僅能保障我智力範圍內的精準度,雖然智力水平委實不高。所以。。。就這樣,就到這裏,反正我自己是覺着明瞭了。若有錯漏,請怒斥我智商低,但請不要說我造謠。

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