前言
Intel手册更新了,出了一章叫做用户中断(User Interrupt)的东西。顾名思义,就是把中断直接发给用户模式,其理念可谓是越俎代庖,相当前卫了。
这么前卫的东西,Intel直接不给32位用了,我直接笑死。而且,兼容模式也用不了,必须是64位的程序才能用。
Linux已经推出User-Interrupt的支持了,目前主要用途其实也就是快速的通知其他用户线程罢了(据Benchmark,比Linux的eventfd
快九倍,具体可以看这份PPT)。
AMD暂时没有对User-Interrupt的支持,但早晚会有。
架构
Intel在支持用户中断的处理器上,新增了数个MSR寄存器,并定义了多个指令来支持用户中断。
在支持用户中断的处理器上,中断被分为:普通中断(Ordinary Interrupts)和用户中断(User Interrupt)。普通中断就是指普通的,发往内核经IDT接收的中断。
处理器的cr4
寄存器的第25位被定义为UINTR
位,置位则表示操作系统启用用户中断。
中断接收
用户中断不使用IDT来接,而是由且仅由MSR来指定地址。
发生用户中断时,处理器不更改cs
段选择子,仅修改rsp
和rip
,因此CPL
不会发生变化。
在支持用户中断的处理器上,多出了一个标识位,叫做UIF
,即User-Interrupt Flag,表示处理器当前是否能接收用户中断。置位表示能接,复位表示不能接。
用户中断不使用IDT接收,而是一个专门定义的MSR寄存器。
除UIF
置位外,接收用户中断还要满足以下条件:
- 处理器在中断阴影(Interrupt-Shadow,即
mov ss,xx
, pop ss
, sti
等造成的一条指令下临时中断屏蔽)之外。
- 处理器位于用户模式,即
CPL=3
。
- 处理器位于长模式,即
CS.L
置位。也就是说即便处理是64位的系统里,32位程序也用不了。
- 处理器位于飞地(Enclave,即SGX保护区)之外。倒也正常,SGX基本上就要被Intel废了。
用户中断一般发生在一条指令完成的时候。但如果被打断的指令带有rep(z/nz)
前缀,则中断会打断迭代,rip
停留在这条带rep
的指令之前,而rcx
寄存器的值表示未完成的迭代次数,rsi
,rdi
寄存器则表示下一次迭代的线性地址。这样可以使得从用户中断返回时,继续完成迭代。
用户中断可以唤醒因tpause
指令和umwait
指令造成的处理器休眠状态。注意这两条指令均可以在用户态里使处理器休眠。
而hlt
指令造成的休眠发生在内核态,故用户中断无法唤醒hlt
状态下的处理器。
同理,由INIT
信号造成的Wait-for-SIPI
睡眠状态的处理器也无法被唤醒。
当用户中断发生时,UIF
会被强制复位以防止用户中断重入。
当用户态影栈机制被启用时,用户中断会向影栈压入发生中断位置的rip
的值以防止ROP攻击。
指令
Intel定义了五个新指令:senduipi
, uiret
, clui
, stui
, testui
。
senduipi指令
senduipi
即Send User Inter-Processor Interrupt,用于发送一个处理器间的中断。其格式为:
senduipi reg64
只有一个寄存器操作数,恒定为64位。重置操作数大小的前缀(如66h
)会被忽略。
这个操作数表示一个发送UIPI时UITT的索引值。注意,这不是用户中断的向量。
GCC11里添加了_senduipi
这个内置宏来让C语言使用这条指令。
当发送的UIPI
无效时,会抛出#GP
异常。
uiret指令
uiret
即User-Interrupt Return,用于从用户中断处理中返回。其格式为:
uiret
没有操作数,其用途就是返回到用户中断发生的地方恢复执行,包括了rsp
,rip
和rflags
寄存器。
此外,uiret
会强制置位UIF
位以继续接收用户中断。
当用户态影栈机制被启用时,会从影栈里弹出发生中断位置的rip
的值,并判断栈上的返回值。若不匹配,则产生#CP
异常。
clui指令
clui
即Clear User Interrupt Flag,用于屏蔽用户中断。其格式为:
clui
没有操作数,它会将UIF
复位。
stui指令
stui
即Set User Interrupt Flag,用于解除对用户中断的屏蔽。其格式为:
clui
没有操作数,它会将UIF
置位。
但和sti
性质相反的是:sti
解除屏蔽时,要到sti
的下一条指令完成后才会完成对中断屏蔽的解除,而stui
指令会在该指令完成后立即解除中断。
testui指令
testui
即Test User Interrupt Flag,用于获取UIF
的值。其格式为:
testui
没有操作数,它会把UIF
的值写入RFLAGS.CF
位中。因此需要结合setc
,cmovc
,jc
或pushf+pop-reg64
指令使用。
此外它还会将rflags
的zf
,af
,of
,pf
,sf
位复位。因此如果在testui
后仍需要使用这些位,请先用pushf
指令保存之。
MSR寄存器
Intel定义了六个新MSR寄存器:IA32_UINTR_RR
(MSR-Index=0x985), IA32_UINTR_HANDLER
(MSR-Index=0x986), IA32_UINTR_STACKADJUST
(MSR-Index=0x987), IA32_UINTR_MISC
(MSR-Index=0x988), IA32_UINTR_PD
(MSR-Index=0x989) 和 IA32_UINTR_TT
(MSR-Index=0x98A)
UIRR寄存器
User-Interrupt Request Register,即IA32_UINTR_RR
(MSR-Index=0x985)这个MSR寄存器,是一个用来向当前处理器发送用户中断的MSR。
该寄存器是一个位图,置位的项表示要发出去的用户中断的向量号。比方说写入0x11这个值就表示发出去的用户中断的向量号是1和4。
处理器在准备发布用户中断时,就会向该MSR写入值。
UIHANDLER寄存器
User-Interrupt Handler,即IA32_UINTR_HANDLER
(MSR-Index=0x986)这个MSR寄存器,表示在接收用户中断时,rip
寄存器的值。
值得注意的是,如果写进去了一个超出处理器支持范围的合法线性地址,那就会报#GP异常。
UISTACKADJUST寄存器
User-Interrupt Stack Adjustment寄存器,即IA32_UINTR_STACKADJUST
(MSR-Index=0x987)这个MSR寄存器,表示在接收用户中断时,处理器如何修改rsp
寄存器的值。
位 |
含义 |
0 |
该位置位时,从该MSR里加载rsp 寄存器的值 |
1-2 |
保留不用 |
3-63 |
用户中断处理函数的rsp 的高61位;低3位复位以在8字节上对齐 |
虽然表格里说“低3位复位以在8字节上对齐”,但实则处理器会将低4位复位以在16字节上对齐。
UINV与UITTSZ寄存器
User-Interrupt Notification Vector和User-Interrupt Target Table Size寄存器以IA32_UINTR_MISC
(MSR-Index=0x988)寄存器表达。这一点和普通中断的性质一样,栈地址会放在16字节对齐的位置上。
位 |
含义 |
0-31 |
该32位表达UITTSZ 寄存器的值 |
32-39 |
该8位表达UINV 的值 |
40-63 |
保留不用 |
UITTSZ
表示UITT
的最大索引值,也就是senduipi
指令的操作数允许的最大值。
UINV
表示用户中断的通知向量。
UITT寄存器
User-Interrupt Target Table寄存器,即IA32_UINTR_TT
(MSR-Index=0x98A)这个MSR寄存器,表示UITT
这张表的基址。
每个UITT
表项占128位,结构如下:
位 |
含义 |
0 |
Valid 位,表示是否有效。无效时,senduipi 指令会抛出#GP 异常 |
1-7 |
保留不用 |
8-15 |
用户中断向量 |
16-63 |
保留不用 |
64-127 |
UPID 基址,但由于UPID 需要按64字节对齐,故64-69位必须复位 |
UPID寄存器
User Posted-Interrupt Descriptor,即IA32_UINTR_PD
(MSR-Index=0x989)这个MSR寄存器,用于描述如何发送和接收中断。
但UPID
仅占16个字节,实在是不明白为什么要按64字节对齐。
位 |
含义 |
0 |
ON 位,即Outstanding Notification位。若该位置位,则表示有一个或多个未完成的中断通知 |
1 |
SN 位,即Suppress Notification位。若该位置位,则表示中断通知应当被按下不表 |
2-15 |
保留不用 |
16-23 |
NV 位,即Notification Vector。该位表示通知目标处理器时使用的普通中断向量 |
24-31 |
保留不用 |
32-63 |
表示接收通知的逻辑处理器的x2APIC号。若处理器未启用x2APIC,则以40-47位表示APIC号 |
40-47 |
表示接收通知的逻辑处理器的APIC号。若处理器启用了x2APIC,则以32-63位表示x2APIC号 |
64-127 |
PIR 位图,即Posted-Interrupt Request,置位则表示该用户中断的向量号存在中断请求 |
不论UITT
还是UPID
,原则上OS应当把它们放置在内核内存里。
但由于熔断漏洞(即CVE-2017-5715)会打破页表的用户内存和内核内存的隔离能力,OS的熔断漏洞补丁会为一个进程建立两套页表,第一套页表完整映射了内核内存和进程的用户内存,而第二套页表仅映射了进程的用户内存和一些用户与内核交界处(如系统调用函数,中断处理函数等开头)的内存。
用户态代码运行时,则使用第二套页表;进入内核时,则使用第一套页表。确保用户态的代码运行时,内核内存不被映射,这样就修复了熔断漏洞。
很明显,UITT
和UPID
应当在第二套页表里也有所映射。
扩展状态
最初的时候,扩展状态只有x87 FPU,因此用f(n)save
和f(n)rstor
指令来保存恢复FPU的状态即可。
后来出了SSE,于是新增了fxsave
和fxrstor
指令引用一个512字节的内存区域实现保存与恢复。
再后来则出现了AVX,512个字节塞不下AVX拓展出来的ymm
寄存器,因此新增了xsave
和xrstor
指令来把所有ymm
寄存器的高128位塞到后面去。
同时也意识到以后还会增强SIMD,不能再为此新增指令了,故xsave
和xrstor
指令带了一个隐含的操作数:edx:eax
。这是一个64位的值,表示一个掩码。
当edx:eax & xcr0
的某一位被置位时,则xsave
和xrstor
会保存恢复该扩展状态。比如当这个逻辑与运算的结果是7的时候,就保存恢复FPU(第0位置位),SSE(第1位置位)和AVX(第2位置位)的状态。
效果不错,以至于出了AVX-512的时候,zmm
寄存器也能用xsave
和xrstor
指令来保存恢复。
但后来Intel出了Processor Trace,AMD出了Light-Weight Profiling,以及Control-Flow Enforcement,还有现在的User Interrupt这些新增了内核MSR的东西。
而xsave
和xrstor
在用户态就能用,允许这两条指令来保存恢复的话显然会越权。
但如果用rdmsr
和wrmsr
来一个一个保存恢复就效率太差了,故新增了xsaves
和xrstors
指令来保存恢复这些MSR。这两条指令仅在内核态可用。
为了xsaves
和xrstors
,处理器新增了Extended Supervisor State Mask,即IA32_XSS
(MSR-Index=0xDA0)这个寄存器。
而xsaves
和xrstors
的逻辑变成了当edx:eax & (xcr0 | xss)
的某一位置位时,保存恢复其状态。
其中的第14位就用于表示xsaves
和xrstros
指令会保存恢复用户中断的状态。这个状态包括新增的六个MSR的值,但不包括UIF
位的值。
总而言之,操作系统可以借用xsaves
和xrstors
这两条指令来负责进线程的上下文切换。
用户中断的发送与接收的逻辑
先说UIPI吧。
执行senduipi
时,处理器读取该指令指定的寄存器操作数,以该寄存器的值作为索引,访问UITT
这张表中某一项。
如果这个索引超过了限制(即IA32_UINTR_MISC
这个MSR中的UITTSZ
),或者指定的UITT
表项中的Valid
位被复位,就会抛出#GP
异常表示无法发送。
如果任意一个保留位(包括表项指定的UPID
)被置位,也会抛出#GP
异常。
senduipi
会在UPID
里的PIR
域里,根据UITT
表项的用户中断向量进行置位。(由此可以看出,多个UITT
表项可以共用一个UPID
)
若UPID
的SN
位和ON
位均被复位,则将UPID
的ON
位置位,并且确认要发送通知。
注意以上的内存操作全部以内核模式权限进行的原子操作,即便senduipi
这条指令是在用户态里完成的。
若确认了要发送通知,则以UPID
的32-63位作为目标处理器的x2APIC号,并以UPID
的NV
位作为中断向量,发送普通IPI。
接下来是接收。当另一个处理器的Local APIC
接收到一个外部中断时,会判断中断向量与接收者处理器上的IA32_UINTR_MISC
的UINV
是否相等。
若不等,则表示这就是纯粹一个外部中断,或者其他用途的IPI,处理器会走IDT来接收中断。
若相等,则表示这是个用户中断,此时接收者的Local APIC
会自动发出EOI
(即End-of-Interrupt)信号,表示普通中断已被处理。然后处理器走用户中断的专有途径来处理用户中断。
接收者识别出用户中断后,会接着对自己的UPID
里的ON
位进行一个复位操作,然后读取并清零UPID
里的PIR
位,并将读出来的PIR
位以逻辑或写入UIRR
寄存器中。
当前处理器里未处理的用户中断会反映在UIRR
里。当处理器具备接收用户中断的条件时,若UIRR
不为0,则产生用户中断。
硬件虚拟化
由于目前只有Intel提出了用户中断,故硬件虚拟化的相关支持目前也就只有Intel VT-x才有。
新增的VMCS字段
Intel在VMCS里新增了一个Guest UINV
字段,该字段是一个16位Guest状态字段(注意UINV
本身是8位的,但VMCS的最小粒度是16位)。
在VM-Entry控制字段里新增了Load UINV
字段,表示VM-Entry时,是否根据VMCS里新增的Guest UINV
字段加载UINV
状态。
在VM-Exit控制字段里新增了Clear UINV
字段,表示VM-Exit时,是否直接清掉UINV
状态。
当Load UINV
字段置位时,处理器在VM-Entry会检查Guest UINV
字段的高八位是否为零
外部中断
若VMCS指定拦截外部中断,则当Local APIC
接收到外部中断时,不识别其是否为用户中断,直接当成外部中断进行拦截并触发VM-Exit。
MSR位图
在MSR位图中拦截用户中断的MSR操作不会影响处理器本身访问这些MSR。就好比拦截LSTAR
这个MSR的读取不影响处理器syscall
指令的执行。
虚拟中断
当VMCS中启用了虚拟中断处理的时候,不由Local APIC
处理接收到的中断,而是由Intel VT-x定义的虚拟中断处理方式来负责识别处理用户中断。其具体过程大同小异。
值得一提的是,事件注入的行为也会发生变化。当虚拟机管理器向开启了用户中断的vCPU注入一个外部中断时,处理器会根据向量号识别该中断是否为用户中断,并进行相关处理。
EPT
当用户中断的行为自身触发了EPT相关VM-Exit(如EPT-Violation)时,Exit Qualification
字段的第16位会被置位,表示这是异步于指令执行的VM-Exit。
OS设计
本章简单谈一谈设计OS时,如何支持用户中断。
OS可以选定一个空闲的普通中断向量,该中断向量会被处理器视为用户中断处理。
OS可以给出一个系统调用的接口,用于创建删除“用户中断控制器”对象。
该对象有一个UPID
,一个线程在接收中断:即一个中断处理函数和一个中断处理栈。
OS可以提供一个接口让线程注册一个中断处理函数(即IA32_UINTR_HANDLER
),并注明是否使用隔离的栈,用多大的隔离栈(即IA32_UINTR_STACKADJUST
)。
每个控制器只可注册一个线程的中断处理,除非逻辑上senduipi
的接收者可以不是唯一的。
OS可以为每个线程创建一个UITT
表,并提供注册发送器的系统调用。线程根据控制器注册了发送器后,系统会在UITT
表上分配一个空闲的UITT
项,填入控制器的UPID
,并将该索引返回给线程。
线程就可以用senduipi
指令来给别的线程发UIPI了,操作数里填注册发送器时系统返回的值即可。
在每个线程的扩展状态(即xsaves
和xrstors
管理的状态)里:
IA32_UINTR_RR
, IA32_UINTR_HANDLER
, IA32_UINTR_STACKADJUST
, IA32_UINTR_TT
是独立的,每个线程都各管各的。
IA32_UINTR_PD
取决于线程接收用户中断的控制器。
IA32_UINTR_MISC
由系统设定,一般所有线程均使用同一个中断向量号,否则管理普通中断的向量号会极为混乱。最大UITT
的索引号可以视情况给予弹性设置。
在OS调度线程的时候,可以检测哪些非活动线程的UIRR
不是零,然后优先调度到CPU上来处理用户中断。
最后别忘了用testui
保存线程的UIF
位并且用stui
或clui
恢复之。
目前还没有外部设备给处理器发送用户中断的文档。猜测如下:
- 配置中断控制器(注意是I/O APIC之类的硬件中断控制器)时,需要把中断向量配置为
UINV
。
- 中断控制器需要能让IRQ选择
UPID
,否则接收对象会存在多义性。
总结
其实我并没有搞到支持用户中断的CPU,本文描述的一切都是根据阅读手册来的。因此不可避免地会出现错误,以后搞到一块支持用户中断的CPU再来勘误。
总之目前来看,用户中断的应用仅限让硬件来加速事件型的线程间通信。外源性的用户中断支持还得等等。