searchusermenu
  • 发布文章
  • 消息中心
点赞
收藏
评论
分享
原创

CPU如何响应外部设备中断

2023-04-20 07:52:58
171
0

备注:本文说的中断旅途,均是物理机环境,不是虚拟机环境。

引言

有限经验:计算机系统在运行时能完成一项或者多项任务,受两个方面的驱使。一方面是CPU执行代码的自我驱使。这部分,CPU的物理环境准备完成,CPU就会自动按照客观顺序去执行代码。CPU的物理环境准备包括:硬件配置,供电,复位,时钟,固件加载,存储系统,以及可执行代码。这些环境准备的目的是为了让CPU能正常取指。这就好比Ctrl+V工程师早上睁眼,序列化地做:思考bug如何解决,解决bug,提交bug测试,梦里解决bug。

另一方面是CPU被VM-Exit与异常/中断驱使。这部分是事件驱使的,异步并非事先过程化的任务。这些事件比如:发生VM-Exit从root模式陷入non-root模式,发生#GP从任务上下文陷入异常上下文,外设引发中断导致从任务上下文陷入中断上下文。这就好比Ctrl+V工程师接收到微信消息事件,转眼看看微信消息。看微信消息这个动作并不是你序列化工作的一部分,接收到微信消息的时间点也不确定

主动的那种,就类似CPU主动触发事件一样。看起来是序列化的,但是,以CPU外部中断为例,中断脉冲/电平或者消息从外设触发时刻到中断上下文查询外设中断ISR寄存器时刻,以CPU时钟周期作为时间元参考,这是一个漫长的时间。以CPU外设中断route为参考,这又是一个漫长的过程,其中还不乏很多代码流程分支。

那么,外设触发事件到CPU在中断上下文查询外设ISR寄存器期间,这个过程,中断到底经历了什么?

 

第一节:CPU响应外设中断的环境介绍

外设触发事件的来源非常多,比如来自CPU总线下的设备(很多平台的SOC外设挂在CPU总线下,比如PPC的各种外设控制器,来自PCI/PCIe总线下的设备,来自RapidIO总线下的设备,来自Irq Pin下的设备。如果以触发中断的事件类型分类,本人接触到的包括四类:发送中断,接收中断,错误中断,测试中断。中断源千奇百怪,但是本质上主要是这四种。响应外部事件的平台也多,比如X86平台,PPC平台,ARM平台,等等。外设中断类型也不只一种,比如沿中断,电平中断,MSI中断,Doorbell中断,IPI中断等等。本文以Intel X86 某平台响应千兆网卡芯片组E1000e MSI中断为例,使用其测试中断,来说明外设中断的漫长旅途。

要搞清楚外设中断的route,就需要先搞清楚外设所在系统TOP。

如何查看X86平台某个外设在系统TOP的位置呢?对于非BIOS开发/非驱动或者非BSP开发者,借助BIOS提供的interface和linux的源码基本上就能满足需求。因为X86平台基本上在设计初期就考虑了纵向与横向兼容,不会像嵌入式那样随心所欲的搞。比如现在需要在本人的平台使用网络控制器的MSI功能,我们需要看X86的IO-datasheet手册,知道这个平台自带一个网络控制器。并且IO-datasheet里面明确告之这个网络控制器在PCI/PCIe在总线下(BDF信息为[0d:31d:6d])。

其中的GbE Controller就是网卡控制器。

因为IO-datasheet手册是针对所有X86平台的,所以我们并不知道这个网络控制器的具体型号与版本信息。既然在PCI/PCIe总线下,我们可以写一段裸机程序(简单的做法是采用grub引导这段裸机程序)枚举PCI/PCIe设备,找到网络控制器的verdor/device ID。而且BIOS已经做过一遍PCI/PCIe设备树的枚举,我们在代码里面实际上可以跳过桥片的枚举,直接获取桥片的bus num相关寄存器来判断桥片下有什么设备,省去了复杂的枚举代码。再将这个verdor/device ID通过linux的源码就能知道具体的控制器型号。另外,在成熟硬件平台上面讨论外设中断功能,还可以借助linux的lspci指令来获取网络控制器的具体信息,省去了裸机代码的编写。

当然,以上都是说的如何逆向获取X86外设控制器的型号。

先来看看X86某平台的网络控制器与CPU之间的物理链接关系。在开发板的linux下lspci,如下:

...省略...

00:1f.4 SMBus: Intel Corporation Sunrise Point-LP SMBus (rev 21)

00:1f.6 Ethernet controller: Intel Corporation Ethernet Connection I219-LM (rev 21)

...省略...

其中BDF信息为00:1f:6的就是网卡芯片组控制器。查看系统TOP,并没有像玩嵌入式系统那样去查看原理图,或许这就是X86领域通用性的优势。很多硬件细节一方面Intel按照统一的标准去做,另外一方面BIOS隔离了硬件层。当然,如果玩BIOS,可能需要查看原理图了。现在的X86平台大部分外设挂接在PCI/PCIe总线下,并不会像PowerPC那样很多设备挂接在系统总线下(比如MAC控制器),这样就进一步提高了通用性以及隔离硬件细节。通过:

00:1f.6 Ethernet controller: Intel Corporation Ethernet Connection I219-LM (rev 21)

信息,如何知道到底是什么型号的网络控制器呢?通过lspci -k可以获取PCI/PCIe设备驱动的信息:

00:1f.6 Ethernet controller: Intel Corporation Ethernet Connection I219-LM (rev 21)

DeviceName: Onboard - Ethernet

Subsystem: Intel Corporation Ethernet Connection I219-LM

Kernel driver in use: e1000e

Kernel modules: e1000e

通过这些信息可以发现,这个网络控制器实际上是MAC控制器与Phy控制器的组合:搭载的E1000e芯片组包括MAC控制器与版本为rev 21的Phy控制器。再通过lspci -x 查看E1000e的verdor/device ID信息如下:

00:1f.6 Ethernet controller: Intel Corporation Ethernet Connection I219-LM (rev 21)

00: 86 80 6f 15 06 04 10 00 21 00 00 02 00 00 00 00

10: 00 00 20 df 00 00 00 00 00 00 00 00 00 00 00 00

20: 00 00 00 00 00 00 00 00 00 00 00 00 86 80 70 20

30: 00 00 00 00 c8 00 00 00 00 00 00 00 0b 01 00 00

其verdor/device ID为156f8086H.

    如果切换到I5-8500查看E1000e芯片组的verdor/device ID为:

00:1f.6 Ethernet controller: Intel Corporation Ethernet Connection (7) I219-V (rev 10)

00: 86 80 bc 15 06 05 10 00 10 00 00 02 00 00 00 00

10: 00 00 18 d1 00 00 00 00 00 00 00 00 00 00 00 00

20: 00 00 00 00 00 00 00 00 00 00 00 00 28 10 5b 08

30: 00 00 00 00 c8 00 00 00 00 00 00 00 ff 04 00 00

其verdor/device ID为15bc8086H。

以上,同时就说明,我们通过E1000e芯片组的PCI verdor/device ID信息就可以获取到MAC控制器与Phy连接器的型号。

至此,物理TOP搞清楚了,就可以开始梳理网络控制器中断route了。PCI/PCIe设备提交中断给CPU,可以走INTx,也可以走MSI,还可以走MSIx,我们这里仅仅以MSI中断来展开说明。

那么,PCI/PCIe上的MSI报文,是如何达到内核的呢?

在X86平台,MSI中断能够提交到内核处理,需要经过这几个阶段:PCI/PCIe设备触发中断,设备请求MSI报文,RC响应MSI报文,RC将MSI请求转到localAPIC,localAPIC将中断提交到内核,内核响应中断。其TOP如下:

外设触发中断 → MSI报文请求 → X86 RC →  localAPIC → Core/SMT。所以下文将按照以下顺序来介绍:

E1000e设备如何才能触发中断,

E1000e设备该怎么触发中断

X86 PCI/PCIe RC如何感知到E1000e提交的中断

X86 localAPIC如何感知到来自PCI/PCIe RC的中断

X86 CPU Core如何感知到来自localAPIC的中断

X86 CPU Core如何响应中断信号

 

第二节: E1000e设备如何才能触发中断

E1000e系列网卡控制器,手册编号为82574。需要82574触发中断事件,可以采用收发数据中断,也可以注入错误产生中断。如果设备支持软件主动触发中断的情况,还可以使用测试中断。82574支持的设备中断如下:

收发中断相对来说比较复杂一些,特别是当遇到自认为设备初始化没问题,但是就是没有发送/接收中断的时候,这就需要确认设备是否真的发送或者接收到了数据。如果是这样,就需要把MAC – PHY之间的链路配置正确,这部分工作相对来说不好弄,又特别是这种CPU集成芯片组。

82574支持测试中断,这里仅仅是需要其触发一个MSI中断,所以选择了其测试中断来触发MSI中断。上文提到的82574支持的所有中断类型都支持软件触发。触发通过ICR寄存器,对ICS寄存器的对应位写入1,将触发对应中断类型的中断。ICS寄存器的描述如下:

    对于设备的中断控制来说,能够接触到的设备中断控制器,主要涉及这几个寄存器:中断mask寄存器,中断config寄存器,中断status寄存器,中断clear寄存器,软件cause寄存器。可能不同的芯片实现细节上面有差异,但是按照分类,主要包括这几个。比如mask寄存器,有的芯片手册上面介绍为enable寄存器;clear寄存器,有的与mask寄存器是同一个寄存器;软件cause寄存器并非所有设备都提供或在提供了也并非开放出来;config寄存器也并非所有设备都支持。

          mask/enable寄存器是用来开关设备中断功能,一般控制粒度为每一个中断类型,即一个bit控制一个中断的开关,当然也有支持所有中断开关的功能。

          config寄存器是用来配置中断信号类型,比如配置电平中断还是沿中断。如果电平中断,还可以配置高电平还是低电平;如果是沿中断,还可以配置上升沿还是下降沿,以及脉冲宽度。当然CPU PIC(泛指PIC,非特指X86 PIC)的congfig寄存器还支持更复杂的配置,比如中断route到多核的配置,delivery mode配置,等等。

          status寄存器是用来显示当前已经发生了的中断类型,比如发送完成,接收完成,某种错误发生,等等。

          clear寄存器用来清除当前已经pending的中断,当然这个寄存器可有可无,很多芯片的中断清除通过status寄存器读清,甚至CPU自清除。

        cause寄存器用于软件主动触发某种中断使用,一般都是用于测试或者防止中断丢失使用。

        82574控制器的中断控制也不例外,主要支持了中断mask寄存器IMS,中断clear寄存器IMC,中断status寄存器ICR,中断cause寄存器ICS,其他增强功能寄存器。其中断config寄存器放到了PCI MSI CAP寄存器的message-address与meassge-data寄存器,这部分放到【X86 RC响应MSI】说明。82574实现的中断控制寄存器如下:

按照手册介绍的套路,首先设置IMC寄存器所有bits,失能所有中断类型,之后选择性延迟us级别时间;再设置IMS寄存器,使能需要的中断类型bits,之后选择性延迟us级别时间。为了保险起见,应该读取一次ICR寄存器,清空所有潜在触发的中断。ICR寄存器是一个中断状态寄存器,82574实现为读清。接下来,就可以设置ICS寄存器的bits,触发需要的中断类型。当然,前提是IMS寄存器对应bits被设置。

至此,82574就能够产生测试中断了,通过读取ICR寄存器,就能获取到当前触发的中断类型。当然,前提是82574的复位与初始化正常。这部分超出了本文说明的范围,详细参考其RM。

这里提到的设置或在读取82574寄存器,那么怎么读写其寄存器?下文继续。

 

第三节:E1000e外设该怎么触发中断

回到上一节内容提出的疑问:82574的中断config寄存器放到了PCI MSI CAP寄存器的message-address与meassge-data寄存器,以及怎么读写其寄存器。只有回答了这两个问题,才能知道E1000e芯片组如何请求MSI报文,进而触发中断。

  1. 如何访问PCI/PCIe设备寄存器

访问PCI/PCIe设备的寄存器,包括两种寄存器类型的访问:PCI/PCIe config space寄存器与设备自定义寄存器。前者采用PCI/PCIe maintain方式读写,后者采用memory或IO读写。通常,前者携带PCI/PCIe协议规范的信息。少部分设备相关信息会在其内部的CAP寄存器,比如E1000e的MSI配置信息;设备的自定义信息都在后者实现,比如E1000e的中断寄存器就在这里面实现。

在X86上面,PCI/PCie的 PCI/PCIe config space读写,对于0-255B以内(256-4K使用MMIO访问,超出本文使用范围,不再提),使用IO的方式。分别提供了两个IO Port:CONFIG_ADDRESS与CONFIG_DATA,IO Port分别是0xCF8与0xCFC。访问 PCI/PCIe config space 某个寄存器,如果是读取,先通过IO outl指令往CONFIG_ADDRESS port写入设备BDF信息与寄存器offset。

 

然后使用in指令从CONFIG_DATA port获取寄存器值即可。in指令,具体使用inb/inw/inl需要根据被读取寄存器的位宽来决定。同时,CONFIG_DATA port内部偏移也需要根据被读取寄存器的地址与4Byts对齐的相对偏移来决定。如果是写入, 先通过IO outl指令往CONFIG_ADDRESS port写入设备BDF信息与寄存器offset,然后使用out指令往CONFIG_DATA IO port写入数据即可,与读取PCI/PCIe header寄存器类似。

实际上,X86的PCI/PCIe maintain使用的IO port与PowerPC使用的CFG_ADDR/CFG_DATA MEMIO地址映射类似。仅仅是在X86上面这两个端口实例化为IO Port对象,PowerPC上面实例化为MEM地址对象。

在X86上面知道了如何去access PCI/PCIe设备的PCI config space寄存器,那么如何去访问具体某个设备的某一个寄存器呢?

根据CONFIG_ADDR IO port的layout信息,要访问一个具体设备的某一个PCI config space寄存器,需要提供设备的BDF信息与寄存器offset。offset就是要访问的寄存器地址,那么BDF信息从哪里来?这里不会说明一个PCI/PCIe设备树枚举的重型流程,读者可以参考linux的PCI/PCie枚举代码,这里介绍一种最简单的方式。对于采用BIOS引导的X86平台,如果通过枚举设备的方式来probe某个设备的存在,可以借助BIOS的枚举结果来简化枚举流程。BIOS在枚举设备期间,已经配置了桥与交换的bus number相关的寄存器,所以可以利用这些信息来跳过桥片的枚举与配置,直接遍历[bus,device,function]来读取设备device id and vendor id信息,只要不是0xffff_ffff/0x0000_ffff,0xffff_0000/0x0000_0000就认为设备存在。事实上,本文使用的82574控制器属于X86 SOC内部PCI/PCIe集成设备,IO-datasheet里面都会告之其BDF信息,加之BIOS已经对SOC内部桥做了配置,对于BIOS之后的开发,验证阶段是可以跳过设备枚举,默认认为设备是存在的。关于PCI/PCIe枚举这一块,相对于嵌入式开发,是可以走一点捷径的,如果仅仅只是想验证一些功能或者非BIOS维护开发。比如E1000e在IO-datasheet里面明确说了BDF信息为[0,1fH,6],仅仅是为了验证其MSI功能,完全可以默认认为[0,1fH,6]节点设备存在,并且桥片或者交换芯片的bus相关寄存器配置正确。要读取E1000e的device id and vendor id,往x86的CONFIG_ADDR IO里面写入BDF信息为[0,1fH,6],offset为0,从CONFIG_DATA读取4Bytes即可,读取回来的就是8086156fH。

如何读取E1000e的某个PCI/PCIe config space寄存器解决了,加上前文提到了E1000e的中断config寄存器存放到了PCI/PCIe config space MSI CAP部分。接下来就可以去probe其MSI CAP了。

  1. 设备寄存器之MSI CAP寄存器(82574中断配置寄存器所在)介绍

        MSI寄存器属于CAP寄存器,其CAP ID为05H。CAP寄存器的首地址在34H,其在PCI/PCIe config space的位置如下:

34H寄存器是CAP寄存器的首地址,指向下一个CAP寄存器group,其map如下:

 

每一个CAP寄存器group里面存放什么寄存器结构,根据其CAP ID寄存器的类别不同而不同,但是至少有Next Pointer 与Capability ID寄存器。前者指向下一个CAP寄存器寄存器的首地址,后者指示当前CAP寄存器类型。比如MSI CAP的可能寄存器定义如下:

 

MSI CAP采用以上那种寄存器结构,由Message Control寄存器的值决定,比如决定MSI CAP为32/64-bit Message Address与Masking的Message Control bit如下:

当然,决定MSI CAP寄存器数据结构的并非Message Control寄存器,而是设备厂商,Message Control仅仅决定程序猿如何解析MSI CAP寄存器结构。

  1. 如何探测设备MSICAP寄存器

以上,知道了如何去probe MSI CAP寄存器,我们按照以下方法来probe E1000e CAP寄存器:

  1. 读取34H 4Bytes
  2. 判断LSB 1Byte CAP ID,如果是05H,说明找到MSI,否则需要下一步
  3. 判断secondary LSB 1Byte是否为00H,如果是就说明CAP寄存器链结束,否则下一步
  4. 以secondary LSB 1Byte的值作为间接寻址地址,读取其值代表的地址,从步骤b开始重复。

以上,直到找到MSI CAP寄存器。当然,前提是所有寄存器地址必须落在0x100以内。如果超出0x100都没有找到MSI CAP或在secondary LSB 1Byte为零之前未找到,就认为当前设备不支持MSI功能。实际probe E1000e MSI CAP过程如下:

[ INFO ]R reg[34H] = [c8H]

[ INFO ]R reg[c8H] = [c823d001H]

[ INFO ]R reg[d0H] = [80e005H]

[ INFO ]Did find MSI function at d0H

也就是说,34H寄存器指向了C8H地址,C8H就是下一个CAP寄存器的地址,C8H指向的CAP寄存器的值为:c823d001H,其CAP ID为01H,不是MSI(CAP ID 01H代表PWM电源管理CAP寄存器)。这个PWM CAP指向下一个CAP寄存器在D0H,D0H寄存器里面存放的是 80e005H。终于,在D0H里面找到了MSI CAP。当然,MSI CAP指向的下一个CAP寄存器在E0H,至于这里面是什么,谁知道呢!(本文不关心)。

结合MSI CAP寄存器结构,得到message-control寄存器的值为0080H。那么也就知道了其MSI CAP寄存器支持64bits meaasge-address,不支持masking capable功能(怎么的来的参考协议对MSI control 寄存器的layout规范)。进而,也就得到ID为156f8086H的E1000e芯片组MSI CAP寄存器map如下:

  1. MSI CAP寄存器如何配合E1000e配置/提交中断到CPU.

针对MSI CAP寄存器group的每一个寄存器展开说明,看看E1000e如何利用这些寄存器配置来实现MSI报文提交的。

在说明之前,先说一个结论。MSI CAP寄存器的作用,一方面是设备告诉CPU,它支持的MSI中断控制能力,比如上文提到的32/64bits message-address,比如是MSI还是MSI-X;另一方面,是CPU告诉设备,MSI报文提交到那里,如何提交。“MSI报文提交到那里”不好理解,这就牵涉到了PCI/PCIe的协议对MSI的规范,读者就认为CPU响应MSI中断需要设备提交一个以MSI报文形式的CPU内部系统总线上的某个地址的存储器带响应的写,往message-address指示的地址写messag data数据。CPU对MSI的响应就是上一句话,如果把这句话当成一个类,那么各家芯片厂商实现出来的对象有所不同,比如X86与PowerPC在实现上面有差异,但是本质还是那句话:“CPU响应MSI中断需要设备提交一个以MSI报文形式的CPU内部系统总线上的某个地址的存储器带响应的写”。这部分细节,放到[C]X86 RC响应MSI 里面说明。

先来说明:设备告诉CPU,它支持的MSI中断能力。这部分信息在message-control寄存器里面。Message-Control的配置信息如下:

 

        Bit0:MSI中断使能,CPU控制设备使能MSI中断功能。如果设备还主持MSI-X中断,使能MSI中断的同时,在MSI-X Meaasge Control寄存器里面(当然前提是设备支持MSI-X功能,其CAP ID为11H),需要失能MSI-X中断。

        Bit[3:1]:设备告之CPU支持的MSI中断向量个数。这个就是说明,设备有能力修改message Data(MSI数据寄存器,其bits[7:0]就是MSI vector)的LSB bit[3:0]。什么意思呢?PCI/PCIe设备,有的设备为了支持通过MSI中断发送不同的message Data(vector),但是MSI CAP寄存器里面只有一个message Data寄存器,怎么实现呢?所以部分设备可以在CPU配置message Data寄存器之后,主动修改message Data寄存器的LSB bit[3:0],达到发送多个MSI 中断vector的效果。但是,这样就引入一个问题:设备毕竟是受控设备,CPU怎么让你瞎搞呢,所以本寄存器的Bit[6:4]作用体现出来了,就是CPU告诉设备,实际可以支持多少个MSI中断vector,即:设备在发送MSI中断的时候,到底能修改message Data寄存器LSB的范围。比如CPU读取Bit[3:1]出来是3H,说明这个设备最多可以支持8个(不是3个)MSI vector;CPU将本寄存器Bit[6:4]设置为1H,就是告之设备只能提交这样的两个MSI vector:message Data[7:0] 与 message Data[7:0]+1,多的就不行了。当然了,有了Bits[3:1]与Bits[6:4]来规范设备支持多vector的MSI功能,还不够,所以有了MSI CAP寄存器组的MSI CAP Masking Bits与Pending Bits寄存器的价值。这两个寄存器,前者是CPU告之设备,MSI中断vector的那一个可以提交MSI报文,即“使能”。因为Bit[3:1]规范了最多可以支持32个MSI vector的潜力,所以Masking Bits寄存器的每一个bit对应一个MSI vector,比如Set Bit4 to 0,即CPU允许massega data[7:0] + 3提交MSI报文(如果设备支持4个以上MSI vector(Bits[3:1)标识并且被允许提交4个以上MSI vector(Bits[6:4]控制))。Pending Bits寄存器,设备标识实际的多vector MSI中断中的哪一个需要提交MSI中断。注意,即使Masking Bits寄存器的某个MSI vector配置为1,不允许设备提交对应vector的MSI报文,但是设备依然可以在其内部处理设备中断,比如将Masking pending寄存器的对应bit置位,一旦CPU把Masking Bits寄存器对应vector对应bit清除,设备立马提交一个MSI报文,携带对应MSI vector。

        Bit[6:4]:CPU告之设备当前能够使用的中断向量个数,这个值需要小于等于Bit[3:1]的值。

      Bit[7]:设备告诉CPU,当前设备的Message Address位宽,32bits还是64bits。Message Address寄存器的作用就是设备需要提交MSI中断的时候,将MSI报文提交到CPU的什么存储器域地址。

        Bit[8]:设备告之CPU,当前设备MSI CAP是否支持中断Masking功能。也就是指示设备是否具备Masking Bits寄存器与Pending Bits寄存器。我见过的PCI/PCIe设备,如果Bits[3:1]为0,即仅支持一个MSI vector的情况,通常Bit[8]为0,即告之CPU不支持Masking Bits寄存器与Pending Bits寄存器。

再来说明:CPU告诉设备,MSI报文提交到那里,如何提交。这部分信息在message-Address/message Upper Address寄存器里面。message Upper Address寄存器是否存在,就是上文提到的受到message Control 寄存器的Bit[7]控制,该位为1说明message Upper Address寄存器存在,即message Address 寄存器是一个64Bits位宽的寄存器。该寄存器layout如下:

 

程序员配置PCI/PCIe的设备的message Address寄存器什么数据呢,取决于CPU的MSI中断实现方案。也就是CPU收到这个MSI报文,并能正确转换为中断请求,取决于CPU对MSI报文转中断的实现方案,即MSI报文仅仅是一种PCI/PCIe报文,不等于中断,也不是中断的专属,CPU要把这个MSI报文转换为中断,那是CPU自己的事情,与PCI/PCIe协议或者设备没有关系。

比如,PowerPC需要驱动开发者将message Address寄存器配置为PowerPC PCI总线存储域下的某个MSIIR寄存器的物理地址。这里简单说一下PowerPC对MSI的对接方案。之所以“message Address寄存器配置为【PowerPC PCI总线存储域】下的某个MSIIR寄存器的物理地址”,请注意【PowerPC PCI总线存储域】。是因为PowerPC与X86在对待PCI/PCIe总线的待遇上面有差异。PowerPC把PCI/PCIe总线当成系统总线下的外部总线,比系统总线低一个级别,也就说系统总线与PCI/PCIe总线之间是有层级转换的。所以,在PowerPC上面,Message Address寄存器配置就不能配置为MSIIR在【系统存储域的物理地址】(即CCSRBAR所在物理映射空间+MSIIR offset),而是应该配置为MSIIR系统存储域地址在PCI/PCIe映射上的【PCI/PCIe存储域】的地址。那么,MSIIR系统存储域地址在PCI/PCIe映射上的PCI/PCIe存储域的地址在哪里呢,比如PPC E500系列就是可以选择性采用PEXCSRBAR将CCSRBAR映射到PCI/PCIe存储域(当然,浪费一个inbound来做映射也是可以的,如果CPU的inbound够多),然后在message Address寄存器配置为PEXCSRBAR映射后的PCI/PCIe地址+MSIIR offset即可。当然,在PowerPC上面那种将PCI/PCIe存储域与系统总线存储域作为1:1映射的,不要被它欺骗了本质。

但是X86把PCI/PCIe提升到系统总线层面(个人经验理解,方便记忆,不代表实际怎么玩的),系统总线存储域与PCI/PCIe存储域采用统一编址,即模糊了层级转换关系,并且X86为了提高兼容性与扩展性,直接约定message Address寄存器配置的部分值,而不是天马行空瞎搞。之所以说X86上面模糊了CPU在存储域与PCI/PCIe存储域的转换,是因为X86上面至少在BIOS之上并没有开放类似PowerPC那种ATMU地址转换interface,至于X86内部逻辑以及它的microcode实现到底有没有转换或者说1:1的转换,无从得知。

 

message Upper Address寄存器配置为0000_0000H。

        message Address Bit[31:20]约定为0FEEH,如果存在message Upper Address寄存器,约定为0000_0000H。0FEEH:它是FSB模块的基地址,即0FEE0_0000H。也就是说,X86上面需要设备把MSI报文提交到FSB所在的memory space,提交的地址在0FEEx_xxxxH。设备MSI报文对0FEEx_xxxx这个些地址的访问,实际上是对FSB的访问,由FSB解析MSI报文,并向内核提交中断。FSB的基地址为什么在0FEE0_0000,来源于MCH手册。如下:

同时,这里对message Address寄存器配置的地址就是FSB的系统存储域地址,不存在与PCI/PCIe存储域的转换。当然,X86内部逻辑实现到底有没有CPU存储域与PCI/PCIe存储域转换,还不能得出结论,至少基于BIOS之上的开发,不需要软件开发者去配置转换关系(类似于PowerPC ATMU那种转换)。来看看X86对message-address寄存器的其它约定:

            Destination:顾名思义,配置MSI中断提交到哪一个内核或者那一些内核。如果仅仅是多核CPU(比如I5,N系列等),这里指的是Core对应的LocalAPIC ID,如果是支持超线程并且开启了超线程技术的CPU(比如I7,Xeon等),这里既可以指Core对应的localAPIC ID,还可以指SMT对应的localAPIC ID。此外,这个字段,还可以是一个8bits的MDA信息,即:message destination address。这里面到底是使用localAPIC ID还是MDA受到RH/DM控制。

      RH/DM:RH=1 and DM=0,Destination ID使用X86 interrupt destination mode 之physical destination mode,即Destination字段指定的是localAPIC ID,但是0xFF不支持;RH=1 and DM=1,Destination使用X86 interrupt destination mode 之logical destination mode,即Destination字段指定的是MDA信息,但是0xFF不支持;RH=0,DM被忽略,Destination ID 使用的是localAPIC ID。

以上,假如local APIC配置正确的情况,并且为fixed模式,把destination配置为00H,RH=0,DM=0,就是告之设备,localAPIC ID为0的Core(或者SMT)将接收到MSI中断。localAPIC与X86 interrupt destination mode等等信息放到[D] and [E]小节说明,有可能读者这个时候关于message address在X86上面到底怎么使用是晕呼的。

5. 设备通过MSI报文要给CPU提交什么中断数据

提交MSI中断的数据存放在Message Data寄存器,其目的就是让CPU知道当前的MSI中断向量,如果CPU厂商仅仅将PCI/PCIe MSI报文作为MSI中断使用的话。X86与PowerPC在对外Message Data寄存器的处理方式上面,再一次不同。Message Data寄存器的本来面目是这样的:

 

这个寄存器的信息,静态的时候仅仅是一堆二进制而已,动态的时候也仅仅是一串电平序列。这些数据到底如何处理,那就是CPU的事情了,比如可以拿来让CPU hlt,假如CPU厂商愿意的话。就好比前面提到的“hello world”字样,拿来教学还是装diaoshi就是使用者的事情了。当然了,本人接触到的X86与PowperPC CPU厂商都用来作为MSI中断的中断数据使用,PowerPC是把Message Data作为写入MSIIR寄存器的数据,至于写什么具体的数据,就规范了开发者必须按照MSIIR寄存器的layout。MSIIR的layout这里就不展开了,读者自行参考PowerPC E500系列CPU的RM介绍。总之就是:Message Data就是按照MSIIR寄存器layout,以达到PowerPC能解析是哪一个MSI中断向量的目的,一共支持255个中断向量。

X86解析Message Data的目的,也是要知道其MSI中断向量号。采用了一种简单粗暴但是高效的方式,直接提供中断向量的方式,而不是像PowerPC那样需要MSIIR来翻译中断向量。其Message Data 需求layout如下:

 

Bit[7:0]就是MSI中断向量,Bit[15:14]还支持中断信号类型的配置:电平还是沿中断,如果是电平还有高地电平的区分。直观明了,有没有。

回到主题,E1000e MSI CAP寄存器就说完了,其核心就是:CPU响应MSI中断需要设备提交一个以MSI报文形式的CPU内部系统总线上的某个地址的存储器带响应的写。写那里?写到message-address寄存器里面地址;写什么数据?写message-data里面的数据。至于MSI CAP寄存器的Masking bit与Pending bit寄存器就不再展开了,读者参考PCI/PCIe协议看详细信息。这里仅仅说明:Masking bit与Pending bit是为MSI支持多中断向量而生。

 

第四节:X86 PCI/PCIe RC如何感知到E1000e提交的中断

上一节,回答了PCI/PCIe设备如何提交MSI中断这个疑问。但是,实际上PCI/PCIe设备压根就没有提交MSI中断,所谓的MSI中断仅仅是CPU的一厢情愿罢了。PCI/PCIe设备仅仅是往PCI/PCIe地址总线域的Message Address寄存器写入了带有响应的Message Data而已(这都还是翻译为人能理解的说法,实际上从协议角度出发更离谱)。这个MSI报文到了那里?是PCI/PCIe总线树上的CPU还是逻辑器件,PCI/PCIe设备都不关心;这个MSI报文被作为中断使用,还是作为玩hlt使用,PCI/PCIe设备也不关心。通常CPU都是拿来作为中断使用,所以才有了MSI中断的说法,本人是这么理解的。当CPU拿来作为MSI中断使用的时候,Message Address与Message Data寄存器的layout就需要按照CPU厂商设计的规则来使用。比如X86有X86的规则,PowerPC有PowerPC的规则,ARM有ARM的规则。为了这些规则更好的无缝对接,所以Message Contoller寄存器就是CPU与PCI/CPIe设备之间配置前交互的桥梁。作为程序猿嘛,就参考RM照着做就行了,工程应用型代码就不问为什么了,有些事情当递归地问为什么的时候,会发现原来没有为什么。

现在已经知道了E1000e的MSI报文到达了CPU内部。对于X86,PCI/PCIe设备提交了MSI报文,实现了往FSB memory space 0FEEx_xxxxH写入Message Data数据。FSB再将0FEEx_xxxxH的存储器写翻译为中断请求,提交给内核对应的localAPIC。整个top,比如支持超线程技术的X86:

接下来,就看看MSI中断请求如何到了localAPIC。

 

第五节:X86 localAPIC如何感知到来自PCI/PCIe RC的中断

这部分内容实际上在上文基本上说明的差不多了。MSI请求实际上并没有直接到达localAPIC,而是先到达了X86内部的FSB memory space。由FSB将MSI请求转换为localAPIC的中断提交。对于多核CPU,或者支持多线程并开启多线程技术的CPU,每一个Core或者SMT都有一个localAPIC。FSB根据message Address寄存器指定的Destination ID以及DM/RH来决定把中断提交给哪一个localAPIC,以及如何提交。提交的数据,在X86上面即中断向量,由Message Data寄存器Bit[7:0]指定;提交的中断信号类型也是通过Message Data寄存器的Bit[15:14]指定。

这里面,关键是Message Address寄存器的Destination ID与RH/DM如何联动起来。以决定中断如何由FSB提交到localAPIC。

Destination ID在X86上面,分为的是physical localAPIC ID(Core或在SMT的physical ID)与logic localAPIC ID(逻辑上指定的 Core或者SMT ID)。physical localAPIC ID还是logic localAPIC ID受到RH/DM的影响;这个ID还可以代表一些ID的Mask值,也受到RH/DM。RH决定DM是否有效,DM决定中断delivery采用physical localAPIC ID还是logic localAPIC ID。

physical localAPIC ID模式指的是:Destination ID代表的某一个物理core或逻辑core(SMT)的physical ID,这个ID CPU出厂就被固化了(软件可以强制修改,BIOS可选修改);physical localAPIC ID大有来头,是一个physical package ID与core ID与SMT ID组合的结果。这个ID值怎么组合而来的,超出了这里的说明范畴,就不展开了。总之这么认为就行,X86的一颗芯片,如果仅仅支持多核技术的intel CPU,内部封装了至少一个physical package,每一个physical package有其ID;每一个physical package里面封装了至少一个core,每一个core有其ID。如果支持超线程的intel CPU,每一个core里面封装了至少两个SMT,并且开启了超线程技术的情况下,每一个SMT有其ID;通过这些ID的组合就能组合出physical localAPIC ID。大致TOP关系如下(支持超线程的情况):

 

事实上,程序猿不需要知道各种ID,直接通过读取localAPIC的ID寄存器就行了,这是一个组合之后的ID值,管它怎么来的。

logic localAPIC ID模式指的是:Destination ID代表一些core或者一些逻辑core(SMT)的logic localAPIC ID。也就是说,此时的Destination ID一个mask值,只要Core或者SMT的logic localAPIC ID &mask 为true,那么中断就可能会(注意,是可能,不是一定)delivery到这个Core或者SMT的localAPIC。Core/SMT的logic localAPIC ID怎么得来的呢?受到X86内部寄存器LDR的控制,intel CPU为每一个localAPIC都装备一个LDR寄存器与之对应,LDR的bit[31:24]就是localAPIC的logic localAPIC ID值。LDR的bit[31:24]一共8bit,也就说最多可以提供8个logic localAPIC ID值,每一个localAPIC可以独立占用一个bit表明它的logic localAPIC ID,以标榜它与其他localAPIC的logic localAPIC ID不同。Destination ID是一些logic localAPIC ID的mask值,每一bit就决定了中断是否delivery到对应logic localAPIC ID的Core或者SMT。但是,中断是否delivery到对应logic localAPIC ID的Core或者SMT,还受到intel CPU内部的DFR寄存器影响。DFR寄存器用于配置当前中断logic localAPIC ID的匹配模式,这个寄存器配置为1111b的时候为flat模式,为0000b的时候为cluster模式。当为flat模式的时候,Destination ID & logic localAPIC ID 为ture,那么中断将提交到这个localAPIC ID,也就是说可以同时把中断提交到多个localAPIC ID。当为cluster模式的时候,说真的,本人也没有使用过,看上去很复杂的样子。

接下来,列举出FSB可能采用的提交方式(把MSI消息以中断的形式提交给localAPIC)。

[RH,DM] = [0,*]的情况:忽略DM的存在,即不区分localAPIC ID为physical localAPIC ID模式还是logic localAPIC ID模式,Destination ID “强制” 某一个localAPIC的physical localAPIC ID,中断将提交到这个localAPIC。

[RH,DM] = [1,0]的情况:当前localAPIC ID为physical localAPIC ID模式,Destination ID为某一个localAPIC的physical localAPIC ID,中断将提交到这个localAPIC。

[RH,DM] = [1,1]的情况:当前localAPIC ID为logic localAPIC ID模式,Destination ID为至少一个logic localAPIC ID的mask值。如果当前logic localAPIC ID匹配模式为flat模式,Destination ID & logic localAPIC ID 为ture,那么中断将提交到这个localAPIC ID;如果当前logic localAPIC ID匹配模式为cluster模式,本文黔驴技穷...

以上,把FSB给localAPIC提交中断的方式说明白了。那么,提交的是什么数据呢?一是,message-data寄存器的bit[7:0],也就是中断向量值;二是,中断类型为message-data寄存器的bit[15:14];三是,delivery模式为message-data寄存器的bit[10:8]。msi message-data寄存器的delivery模式就放到下一节说明了,这个寄存器的其它字段就不展开了。

先给出一个典型的E1000e的MSI CAP寄存器配置参考如下:

[ INFO ]Get localAPIC ID:0

[ INFO ]Config msi controller register[d2H] as [0080H]

[ INFO ]Config msi address register[d4H] as [fee00000H]

[ INFO ]Config msi address upper register[d8H] as [00000000H]

[ INFO ]Config msi data register[dcH] as [0040H]

[ INFO ]Config msi controller register[d2H] as [0081H]

msi controller register配置为0081H,使能MSI功能,是一个64Bits的MSI address register,不支持MSI Multiple Message功能。

msi message-address register配置为往FSB地址FEE00000H投递MSI消息,中断给physical localAPIC ID为00H的内核。

msi message-data register配置为0040H,告之FSB投递中断采用沿中断,以Fixed方式delivery中断,中断向量为40H。

 

第六节:X86 CPU Core如何感知到来自localAPIC的中断

综上,MSI中断已经到达了localAPIC,接下来就看localAPIC如何将中断投递给内核。X86的localAPIC功能很强大也很多,这里不会重点说明localAPIC的功能,而是仅仅说明localAPIC与MSI功能相关的部分。

为了方便读者有个对localAPIC在系统中的top有概念,给出localAPIC与PCI/PCIe外设的关系如下:

 

PCI/PCIe上面提交的MSI中断以外部中断的形式提交给localAPIC。

localAPIC控制器,说白了就是CPU的中断控制器,在功能本质上,与E1000e芯片组的中断控制器没有本质区别。都是为了给处理器提交中断。所以localAPIC中断控制器也需要实现这些寄存器:中断mask寄存器,中断config寄存器,中断status寄存器,中断clear寄存器,软件cause寄存器。备注:localAPIC还有额外的一些寄存器,如果与MSI提交无关的,这里忽略。这里给出localAPIC寄存器组:

 

中断mask寄存器:localAPIC中断使能并不全在localAPIC寄存器组里面。而是通过两个MSR寄存器来实现:MSR_IA32_APIC_BASE与MSR_IA32_EXT_APIC_SIVR。并且,前一个寄存器并不能使用传统的MMIO与IO port方式访问,只能使用X86特定指令wrmsr与rdmsr。指令使用方法参考intel SDM .vol2。MSR_IA32_APIC_BASE寄存器控制控制localAPIC的开启与关闭,寄存器layout如下:

 

        Bit[11]就是localAPIC的开关控制位,1使能,0失能。bit[10]为localAPIC开启x2APIC模式的控制位,localAPIC存在两种工作模式,xAPIC与x2APIC。bit[8]指示当前core是否为BSP,BSP在X86上面指的是引导内核(物理内核非linux kernel概念),类似于powerpc的P0。bit[N:12]指示localAPIC寄存器组所在的基地址(软件可修改),如果采用xAPIC模式,这个基地址有价值,采用x2APIC,就没有它什么事儿了(xAPIC与x2APIC对于软件开发者而言,主要差异是访问localAPIC寄存器组的方式存在差异。xAPIC模式,采用MMIO[内存读写]的方式读写;x2APIC需要使用X86的特殊指令:wrmsr与rdmsr;并且,当开启X86虚拟化功能之后,虚拟机管理程序虚拟localAPIC存在两种模式下虚拟方式的不同)。MSR_IA32_EXT_APIC_SIVR可以用来临时开关localAPIC的功能,bit[1]=1使能localAPIC。它在localAPIC寄存器组的内部,如何访问这个寄存器就取决于localAPIC的工作模式,如前文所说。

中断config寄存器:localAPIC与MSI中断处理相关的配置,除了在MSR_IA32_APIC_BASE与TPR寄存器以外,其它的配置寄存器都不要做额外的配置。MSR_IA32_APIC_BASE配置localAPIC采用什么模式,前文已经说了;TPR决定当前localAPIC可向内核提交中断的最低优先级门槛,通常配置为0,允许所有中断delivery。余下的配置,都是配置localAPIC的本地中断功能,比如软件通过localAPIC可以触发IPI中断,以及其它一些类型中断。

中断status寄存器:localAPIC的中断状态寄存器,软件几乎不参与维护,内核自行完成。包括IRR与ISR寄存器。这两个寄存器都是256bits宽的寄存器,每32bits给软件提供可访问的接口。其地址按照16bits对齐,在localAPIC寄存器组内部offset,如果采用xAPCI模式,分别是:200H与100H;如果采用x2APIC分别是:20H与10H(比如ISR0的MSR地址为00000810H)。IRR CPU设置与清除由CPU维护,ISR软件读清除。这两个寄存器每一个bit就代表一个中断pending状态。这里需要注意,X86上面的MSI寄存器地址并不在地址总线存储域,比如ISR0的MSR地址为00000810H,这个地址并不占用地址总线存储域空间,而且访问这种地址需要用特殊指令wrmsr与rdmsr。仅玩过ARM与PowerPC的程序猿要注意一下。X86里面这种MSR寄存器非常多,而且还在不推陈但是出新的状态。

中断clear寄存器:ISR软件读清除,同时为了告之CPU可以响应下一次中断,需要软件写EIO寄存器。

软件cause寄存器:这部分主要是用来触发localAPIC本地中断使用的,比如IPI中断。MSI中断用不上,所以这里就不废话了。读者可以参考intel SDM .vol3。

上一节提到了:“delivery模式就放到下一节说明”。所以,这里针对X86的dilivery模式说明一下。X86支持的中断delivery模式包括如下:

000 (Fixed) Delivers the interrupt specified in the vector field to the target processor or processors.这也是外部中断常用的模式。

001 (Lowest Priority)

Same as fixed mode, except that the interrupt is delivered to the processor executing at the lowest priority among the set of processors specified in the destination field. The ability for a processor to send a lowest priority IPI is model specific and should be avoided by BIOS and operating system software.

010 (SMI) Delivers an SMI interrupt to the target processor or processors. The vector field must be programmed to 00H for future compatibility.

011 (Reserved)

100 (NMI) Delivers an NMI interrupt to the target processor or processors. The vector information is ignored.

101 (INIT) Delivers an INIT request to the target processor or processors, which causes them to perform an INIT. As a result of this IPI message, all the target processors perform an INIT. The vector field must be programmed to 00H for future compatibility.

101 (INIT Level De-assert),BSP启动AP时刻使用,首次通过IPI中断发送INIT,delivery模式需要使用这个。

(Not supported in the Pentium 4 and Intel Xeon processors.) Sends a synchronization message to all the local APICs in the system to set their arbitration IDs (stored in their Arb ID registers) to the values of their APIC IDs(see Section 10.7, “System and APIC Bus Arbitration”). For this delivery mode, the level flag must be set to 0 and trigger mode flag to 1. This IPI is sent to all processors, regardless of the value in the destination field or the destination shorthand field; however, software should specify the “all including self” shorthand.

110 (Start-Up),BSP启动AP时刻使用,INIT之后,需要通过IPI中断发送SIPI,delivery模式需要使用这个。

Sends a special “start-up” IPI (called a SIPI) to the target processor or processors. The vector typically points to a start-up routine that is part of the BIOS boot-strap code (see Section 8.4, “Multiple-Processor (MP) Initialization”). IPIs sent with this delivery mode are not automatically retried if the source APIC is unable to deliver it. It is up to the software to determine if the SIPI was not successfully delivered and to reissue the SIPI if necessary.

综上,针对MSI中断,我们需要做的就是使能localAPIC,选择一种localAPIC工作模式,配置中断优先级过滤等级,即可。剩下的就是,静静等待中断的到来。实际上,localAPIC针对MSI中断,也仅仅是起到了一个传递的作用。至于delivery模式,实际上已经在MSI CAP的message-data register里面配置了,与localAPIC没有什么关系。事实上localAPIC需要触发本地中断,也需要按照X86支持的delivery模式做相应的配置。

 

第七节:X86 CPU Core如何响应中断信号

首先明确一个事实,这里是说的是中断,不是异常更不是VM-Exit。因为要打断CPU当前指令流,本人遇到的有三种方式:中断,异常,虚拟机VM-Exit。所以,我们这里先明确这一节说明的范围,是中断。(这一节本来会有非常多的信息量,但是很多信息由于与主题MSI中断没有直接关系,所以省略了很多细节。为此,阅读以下内容,需要读者具备一点X86背景知识)。

由于内核(CPU Core非linux kernel,全文同)响应中断是一个非常复杂的模型,软件要准备的环境也非常之多,而很多准备工作已经偏离了MSI中断本身。所以我们这里对这个模型做了简化。

  1. 内核打开中断需要访问flag寄存器,X86在实模式/32位保护模式/64位保护模式的flag寄存器有差异,所以这里仅仅针对64位保护模式,即IA32e模式。
  2. 内核响应MSI中断前,不能假设内核当前运行权限级别,中断上下文被限制在ring0,那么就可能存在权限级别的切换,所以这里仅仅针对没有权限级别切换的情况,即中断前后都运行在ring0。
  3. X86支持硬件辅助虚拟化功能,是否开启VMX功能以及如何配置中断虚拟化,对中断的响应流程有很大的影响,CPU的模式切换也有差异,所以这里仅针对关闭虚拟化技术的情况。
  4. 在中断上下文,通常除了处理内核的中断标记,还需要处理localAPIC的中断标记,更需要处理外设的中断标记,这就涉及数据地址空间的访问,存在linear address与logic address与physical address之前的转换,所以这里仅仅针对linear address:logic address:physical address = 1:1:1(整得像在给某花生油打广告式的)的情况,并且在IA32e模式里面。
  5. 内核响应外部中断需要大量的环境准备工作,这里仅仅描述了与MSI相关的主线部分。
  6. 所有E820标记的RAM类型的内存都可读写可执行

所以,以下的描述都是基于上面的限制下展开的。针对X86平台,内核响应外部中断,当localAPIC把中断delivery给内核,内核如何感知到中断并跳入中断入口,就是CPU厂商的事情了,程序猿要做的就是:delivery中断之前,打开内核中断,把中断入口准备好,并在入口函数里面做一些必要的中断处理。详细见下文。

  1. 打开内部中断功能

当准备好CPU响应中断的环境之后,使用sti指令,打开内核中断使能。这个指令在SDM .vol2的描述如下:

Set interrupt flag; external, maskable

interrupts enabled at the end of the next

instruction

In most cases, STI sets the interrupt flag (IF) in the EFLAGS register. This allows the processor to respond to maskable hardware interrupts

执行sti指令之后,rFlag寄存器的IF标记被置位,内核可以接收中断(X86的flag寄存器在64bit时候为rflag,32位为eflag,在real mode为flag)。

  1. 准备中断入口

X86在保护保护模式下,能转入中断入口需要一个IDT--中断描述符表。这个表在实模式下叫IVT--中断向量表。

IVT存放的就是各种异常与中断的入口地址信息(非函数地址)--中断服务程序的segment与offset,CPU或在中断入口图下:

IDT存放的是gate描述符,内核通过gate描述符间接获取中断服务程序的入口地址。内核又是通过IDTR获取到IDT的地址,IDTR寄存器软件通过lidt指令完成IDT地址加载到IDTR,CPU获取中断入口还需要借助GDT或者LDT的帮助,map如下:

 

对于gate描述符,又分为以下几种:

Interrupt-gate descriptor中断门描述符

Trap-gate descriptor陷阱门描述符

Task-gate descriptor任务门描述符

MSI中断需要使用的是Interrupt-gate descriptor。任务门描述符在CPU处于IA32e模式时不存在,但是切换到其它模式还是存在的(X86就是牛逼,像个CPU活化石)。

所以,为了能响应MSI中断,需要在内存中准备IDT,IDT里面至少需要存放一个针对MSI中断的Interrupt-gate descriptor。Interrupt-gate descriptor需要间接指向MSI中断的服务程序。

  • IDTR长什么样子(本图上半部分)

IDTR由Base Address与limit组成。上图的最高bit位是47实际上是不严谨的,这个最大值应该通过CPUID.8000_0008H leaf(补充,X86很多SPEC信息都是可以通过CPUID与部分MSR寄存器读取出来的,不像powerPC/ARM代码里面为了支持各种历史平台而加入各种条件编译)获取当前CPU支持的最大MAXPHYADDR,详细参考SDM .vol2。Base Address指定了IDT的基地值在那里,limit指定了IDT的length。有了IDTR,当发生中断的时候,内核就知道了去walk中断入口信息。比如MSI中断使用40H中断向量,当中断delivery成功之后,内核从IDTR里面指定去walk IDT,去获取IDT里面40H(index的意思)偏移的描述符。

  • IDT里面存放了些啥玩意儿

IDT本质上就是在内存里面存放的一堆二进制数据,需要按照以下格式存放(64bits下每个gate描述符扩展为16Bytes):

具体的每一个gate描述符格式如下(64bits下每个gate描述符扩展为16Bytes,参考SDM .vol3):

其中的interrupt Gate就是MSI中断需要的gate描述符类型,64bits下格式如下。

 

针对本文的简化环境,主要关心Segment Selector与offset。通过Segment Selector与offset,CPU就能进一步在GDT(或者LDT)里面walk,进而定位到中断服务程序的间接入口地址。Segment Selector就是指定中断函数的地址Base在那里去获取--GDT还是LDT?内部偏移是多少?;offset就是中断函数的偏移。只有知道了中断函数的Base地址与offset地址,那么内核也就得到了中断函数的入口地址。Segment Selector如下:

 

备注:TI指定当前需要选择的段描述符在GDT还是LDT里面walk.比如通常让内核walk的中断向量的base地址所在的代码段描述符在GDT里面。

  • GDT里面准备了什么

同IDT类似,X86 CPU要获取GDT,也需要借助一个GDTR寄存器,这个寄存器采用X86指令lgdt来设置,将GDT存放的物理内存加载到GDTR里面。这样,CPU到GDT里面walk之前,才能知道GDT在那里。

GDT里面存放了各种段描述符,比如代码段描述符,数据段描述符,门描述符,等等。我们需要walk中断入口函数的base所在段描述符,所以需要代码段描述符。接下来看看IA32e模式GTD里面的代码段描述符长什么样子。

  • 代码段描述符

在IA32e模式里面,上图的很多region已经退化了(如果把模式切换回非IA32e,还是存在的)。描述如下,节选自intel SDM:

Code segments continue to exist in 64-bit mode even though, for address calculations, the segment base is treated as zero. Some code-segment (CS) descriptor content (the base address and limit fields) is ignored; the remaining fields function normally (except for the readable bit in the type field).Code segment descriptors and selectors are needed in IA-32e mode to establish the processor’s operating mode and execution privilege-level. The usage is as follows:

IA-32e mode uses a previously unused bit in the CS descriptor. Bit 53 is defined as the 64-bit (L) flag and is used to select between 64-bit mode and compatibility mode when IA-32e mode is active (IA32_EFER.LMA = 1).

— If CS.L = 0 and IA-32e mode is active, the processor is running in compatibility mode. In this case, CS.D selects the default size for data and addresses. If CS.D = 0, the default data and address size is 16 bits. If CS.D = 1, the default data and address size is 32 bits.

— If CS.L = 1 and IA-32e mode is active, the only valid setting is CS.D = 0. This setting indicates a default operand size of 32 bits and a default address size of 64 bits. The CS.L = 1 and CS.D = 1 bit combination is reserved for future use and a #GP fault will be generated on an attempt to use a code segment with these bits set in IA-32e mode.

  • In IA-32e mode, the CS descriptor’s DPL is used for execution privilege checks (as in legacy 32-bit mode).

In 64-bit mode, the processor does not perform runtime limit checking on code or data segments. However, the processor does check descriptor-table limits.

比如本人准备的GDT如下:

gdt64:

.quad 0 //第一个unused

.quad 0x00af9b000000ffff //code segmentdai

.quad 0x00cf93000000ffff //data segment

...省略...

.quad 0

 

GDTR的layout为:Base Address扩展为64Bits,limit仍然为16Bits。前者存放的是GDT在内存中,limit存放的是GDT的length。有个GDTR的帮助,那么CPU就知道在什么内存去walk代码的入口地址,当中断来的时候。事实上,当中断来的时候,CPU每次都需要walk GDT是非常效率地下的,所以这个时候CS寄存器就派上了用场,它是当前代码段描述符的一个cache,在IA32e模式里面退化成:只有sel字段有效,其余字段无意义。有了CS寄存器的存在,CPU获取代码段,就不需要每次都walk GDT,而是从CS寄存器获取。

综上,当MSI中断来临的时候,CPU就能通过当前中断向量,到IDTR指向的IDT里面walk interrupt gate descriptor,进而通过这个interrupt gate描述符取出代码段的selector,再通过这个selector并借助GDTR(或在CS寄存器)从GDT里面walk到中断服务程序的基地值。获取到这个基地值,与IDT里面中断服务程序地址的offset与,得到最终的中断服务程序地址(以GDT为例)。

流程上是这样的,但是:在IA32e模式里面,由于段机制的退化,分页机制的强化,实际上无论是GDT里面的代码段描述符还是CS寄存器,其Base region都没有意义了,所以CPU获取中断服务程序的Base地址,不再通过GDT里面的代码段描述符。CPU获取中断服务程序的地址直接通过IDT里面的中断描述符的入口函数offset字段获得,IA32e模式的IDT中断描述符的offset直接指定为服务程序的入口地址。虽然Base地址不通过GDT获取了,以上的流程还是存在的,比如代码段的DPL等check位还是存在的,所以流程上还是这样的,仅仅是被弱化了。

因为本文限制做了环境限制,又因为中断前后的运行权限都在ring0,所以也不存在stack的切换,所以TSS的相关功能可以被忽略;并且没有开启虚拟化技术VMX功能,所以也不存在虚拟化相关的流程。

至此,CPU得到了中断服务程序的入口。

  1. 中断函数上下文处理

不存在stack的切换,所以CPU会在当前stack空间里面依次压入中断前的处理器状态信息:(SS),RSP,rflag,CS,RIP,error code(部分异常),其示例如下:

 

没有权限级别的切换,所以本文采用上图的”Stack Usage with No Privilege-Level Change”的情况。CPU压栈同时,将复位rflash的IF标记,以关闭当前内核的中断功能。当需要返回被中断的程序继续执行,在中断与异常上下文,必须使用iret指令回返,方便CPU弹出之前压栈的状态信息。

在中断上下文,涉及到任务上下文与中断上下文的切换。任务上下文切入中断上下文的过程,CPU仅仅在当前(或者新的)栈中保存了SS,RSP,rflag,CS,RIP信息;中断上下文切换回任务上下文,CPU也仅仅恢复这些寄存器。如果仅仅是这样,中断上下文文回到任务上下文,CPU runtime的过程数据--通用寄存器信息全未恢复了,可能不能继续执行或者得不到正确结果。所以,通常的做法都是进入中断上下文,保存当前CPU的runtime寄存器信息,回任务上下文前,恢复CPU的runtime寄存器信息。这部分工作,针对每一个异常/中断,都是一样的,所以一般不会为了某一个中断单独维护一份代码。而是作为一个通用接口,保存CPU上下文,转入执行中断回调,恢复被中断的上下文,返回。其套路如下:

 

所以,这部分分两步说明:软件出入栈部分,中断回调执行部分。另外,中断服务程序通常都是动态注册到IDT里面的,所以我们要把上图提到的中断/异常回调执行函数与IDT关联起来,还需要一个回调注册机制。当然,非要静态注册,也是可以的。

  1. 中断/异常回调注册

回调函数注册,实际上就是按照IDT的数据结构把IDT与回调函数关联起来。比如MSI中断采用40H向量,也就说需要在IDT里面的40H index上面创建一个中断描述符。这部分用C代码就能实现。但是这样也面临一个问题,在中断上下文里面需要软件实现被中断函数CPU runtime寄存器信息的压栈与出栈,任务与中断上下文切换的压栈出栈,C语言可是搞不定的(当然也是可以的,在C语言里面直接写机器码,比如这里的方式)。所以需要汇编语言实现。如果每一个中断/异常回调都实现一个这样的汇编,那么代码冗余就很多。所以一般的套路是搞一个公共的入口,在这个公共的入口函数里面实现任务与中断上下文切换的压栈,调用回调中断服务,然后出栈,返回到任务上下文。比如采用这样的方式:

void irq_connect(unsigned v, void (*func)(void *regs))

{

    u8 *p_entry = malloc(50);

 

/*将p_entry地址作为IDT的offset入口地址,在IDT以v作为index位置创建一个中断描述符*/

    idt_entry_set(v, p_entry, 0);

 

    /*sub $8, %rsp*/

    *p_entry++ = 0x48; *p_entry++ = 0x83; *p_entry++ = 0xec; *p_entry++ = 0x08;

    /* mov $func low, %(rsp) */

    *p_entry++ = 0xc7; *p_entry++ = 0x04; *p_entry++ = 0x24;

    *(u32 *)thunk = (ulong)func; p_entry += 4;

    /* mov $func high, %(rsp+4) */

    *p_entry++ = 0xc7; *p_entry++ = 0x44; *p_entry++ = 0x24; *p_entry++ = 0x04;

    *(u32 *)p_entry = (ulong)func >> 32; p_entry += 4;

    /* jmp irq_entry_demo */

    *p_entry ++ = 0xe9;

    *(u32 *)p_entry = (ulong)irq_entry_demo - (ulong)(p_entry + 4);

}

上面代码的思路是在堆里面申请一段内存(前面已经限制所有RAM内存可读可写可执行了),然后在这段内存里面放一部分所有中断异常回调的公共代码。这里采用的方式是直接把期望的指令的机器码赋值到堆里面。当然也可以采用C代码调用汇编的方式,不可采用C内嵌汇编的方式,后者编译器会自动加栈操作而得不到干净的汇编指令。

上文代码给地址p_entry赋值用的那些机械码,就是做了这些事情:先把中断回调放到当前栈指向的,然后跳入irq_entry_demo函数里面(这里面再取出回调入口func并执行)。这样,这个堆空间里面存放的就是指令了,和编译器编译出来的代码没有什么区别。

前面已经说了,中断入口函数是通过IDT找到到了,那么这里就模拟CPU在IDT里面walk流程,来顺藤摸瓜找到MSI的中断服务程序,这里的MSI中断向量使用40H(其它值也是可以的,但是不能是0-31内核保留部分)。

IDT我们在代码里面静态通过编译器申请了空间,存放在数组idt_dec[256]里面,反汇编信息如下:

0000000000432e90 <idt_dec>:

...

 

0000000000433e90 <idt_dec_end>:

...

这段数据存放在BSS段里面,说明IDT数据结构是运行时通过动态初始化的。我们运行时把IDT的地址打印出来看看是否一致:

[ INFO ] IDT des @ 432e90H

[ INFO ] MSI IDT des @ 433290H as follow:

[ INFO ] Byte[15:12]:res

[ INFO ] Byte[11:8]:offset2=0H

[ INFO ] Byte[7:4]:offset1=5aH p=1D,dpl=0D,type=eH,IST=0H

[ INFO ] Byte[3:0]:sel=8D,offset0=5010H

是一致的,因为MSI的中断向量为40H,所以MSI中断描述符被存放到了433290H(432e90H+0x40*16)位置。所以,当MSI中断来了之后,CPU就能在这个位置拿到MSI的中断描述符。在IA32e下面,正如前文说的,中断的入口就是中断描述符的offset,这里的offset=offset2<<32 + offset1 << 16 + offset0,结果就是5a5010H。CPU根据中断描述符sel字段选择GDT/LDT并作了code segment check并且通过之后,开始压栈(RSP,rflag,CS,RIP,error code),然后转入5a5010H开始执行中断上下文。5a5010H存放的是什么?不就是前文irq_connect内部那个堆空间的!这个堆里面我们使用机器码存放了部分中断服务程序的部分公共代码,并跳转到了irq_entry_demo(在irq_entry_demo里面完成:压栈CPU的runtim寄存器信息,然后转入中断回调函数执行,然后出栈,最后iretq返回被中断的上下文)。irq_entry_demo实现在下一节说明,这里先来验证irq_connect里面的堆空间是不是存放的所有中断服务程序的部分公共代码。把地址5a5010H的数据dump出来:

[ INFO ] IDT common entry @ 5a5010H:

48H 83H ecH 08H 

c7H 04H 24H

f6H ccH 41H 00H

c7H 44H 24H 04H

00H 00H 00H 00H 

e9H b7H c9H e7H ffH 0aH 42H 71H adH

...省略...

备注:汇编与机器码之间如何转换,读者参考intel手册,这里不烧脑了。

以上,确实得到了验证,的确存放了部分中断服务程序的部分公共代码,并跳转到了irq_entry_demo接口。并且,我们挂接的MSI中断服务回调地址在:f6H ccH 41H,即41ccf6H位置,通过反汇编可执行文件,也得到了验证(当调用irq_connect(0x40,msi_irq)之后)。

000000000041ccf6 <msi_irq>:

  41ccf6:55                   push   %rbp

  41ccf7:48 89 e5             mov    %rsp,%rbp

  41ccfa:53                   push   %rbx

  41ccfb:48 83 ec 08          sub    $0x8,%rsp

  41ccff:48 83 05 61 74 01 00 addq   $0x1,0x17461(%rip)

...省略...

irq_connect在堆里面存放的代码实际上就是把中断回调函数的入口放到了当前栈,然后跳转到函数irq_entry_demo执行(irq_entry_demo函数再从栈里面把中断回调函数取出来,并执行这个函数)。

接下来就开始说明irq_entry_demo的作用了:压栈CPU runtim寄存器信息,把irq_connect通过p_entry堆代码压栈的中断函数入口“取出”,然后调用中断回调函数执行,然后出栈,最后iretq返回被中断的上下文。

  1. 软件出入栈部分

这部分工作把程序员觉得需要保存的寄存器信息压栈就行了,想多保存什么信息都行,出栈的时候按照反向顺序把所有信息弹出来就行了。demo如下:

00000000004219df <irq_entry_demo>:

  4219df:41 57                push   %r15

  4219e1:41 56                push   %r14

  4219e3:41 55                push   %r13

  4219e5:41 54                push   %r12

  4219e7:41 53                push   %r11

  4219e9:41 52                push   %r10

  4219eb:41 51                push   %r9

  4219ed:41 50                push   %r8

  4219ef:57                   push   %rdi

  4219f0:56                   push   %rsi

  4219f1:55                   push   %rbp

  4219f2:54                   push   %rsp

  4219f3:53                   push   %rbx

  4219f4:52                   push   %rdx

  4219f5:51                   push   %rcx

  4219f6:50                   push   %rax

  4219f7:48 89 e7             mov    %rsp,%rdi

  4219fa:ff 94 24 80 00 00 00 callq  *0x80(%rsp)

  421a01:58                   pop    %rax

  421a02:59                   pop    %rcx

  421a03:5a                   pop    %rdx

  421a04:5b                   pop    %rbx

  421a05:5d                   pop    %rbp

  421a06:5d                   pop    %rbp

  421a07:5e                   pop    %rsi

  421a08:5f                   pop    %rdi

  421a09:41 58                pop    %r8

  421a0b:41 59                pop    %r9

  421a0d:41 5a                pop    %r10

  421a0f:41 5b                pop    %r11

  421a11:41 5c                pop    %r12

  421a13:41 5d                pop    %r13

  421a15:41 5e                pop    %r14

  421a17:41 5f                pop    %r15

  421a19:48 83 c4 08          add    $0x8,%rsp

  421a1d:48 cf                iretq//CPU硬件出栈并回到被中断程序 

这里看代码就明白了,先压栈CPU的runtim寄存器信息,然后转入中断回调函数执行,然后出栈,最后iretq返回被中断的上下文(这一步必须有,CPU需要硬件出栈,如果有权限切换还需要做栈切换等等)。

  1. MSI中断回调函数

MSI中断回调里面,用户需要做的就是首先清除E1000e的中断标记,通过读取ICR寄存器就行了,然后做中断相关的服务,最后写localAPIC的EIO寄存器结束本次中断服务。

MSI中断服务程序完成之后,回到irq_entry_demo里面,恢复CPU被中断函数的CPU runtime寄存器信息,iretq返回,CPU硬件出栈(SS),RSP,rflag,CS,RIP寄存器。其中rflag的IF标记在这里被恢复,中断被重新打开,CPU可以继续响应中断。

由于很多信息与本文的主题MSI中断路由没有直接关联,所以省略了很多细节,读者自行烧脑了。

 

总结

现在回头来看,本文的MSI中断的漫长旅途,就是如下流程:

 

中断的投递是由外向内的,中断的感知/响应是由内向外的。

本文以PCI/PCIe设备E1000e芯片组通过MSI中断向X86提交中断,来说明外设中断的漫长旅途。对于其它的外设提交中断的流程,本质上没有区别。这个中断流程,本质上就是:什么设备,通过什么链路,提交给谁中断,谁如何响应中断。把这些环节的每一个对象更换了,中断的流程还是这些。比如设备,可以换成别的PCI/PCIe设备,仅仅影响设备中断初始化的具体内容,但是设备初始化这个事情还是存在;链路可以换成Irq Pin或者8259或者RapidIO,只影响中断给X86提交的通路实例,通路必须存在,至于走什么通路,不重要了;X86可以换成ARM,还可以换成PowerPC,但是如何把MSI请求转换成中断请求的功能还是存在,任务与中断上下文的切换处理还是存在...所以,原理还是很重要,有助于理解具体的实例,不至于稀里糊涂。

 

参考文献:《外设中断的漫长旅途》

0条评论
0 / 1000