通用概念
- 中断(Interrupt)
- 外部硬件设备所产生的信号
- 异步:产生原因和当前执行指令无关,如程序被磁盘读打断
- 异常(Exception)
- 软件的程序执行而产生的事件
- 包括系统调用
- 同步:产生和当前执行或试图执行的指令相关
- 系统调用
- 是一种特殊的异常
- 操作系统里面要主动实现的异常
- 用户程序请求操作系统提供服务
不同体系结构术语的对应关系
Aarch64的中断(异步异常)
- 重置(reset)
- 最高级别的异常,用以执行代码初始化CPU核心
- 由系统首次上电或控制软件、Watchdog等触发
- 中断(Interrupt)
- CPU外部的信号触发,打断当前执行
- 如计时器中断、键盘中断等
Aarch的(同步)异常
- 中止(Abort)
- 失败的指令获取或数据访问
- 如访问不可读的内存地址等
- 异常产生指令(Exception generating Instruments)
- SVC: 用户程序->操作系统
- HVC: 客户用户程序->虚拟机管理器
- SMC: Normal World->Secure World
X86-64术语
- 中断(设备产生、异步)
- 可屏蔽:设备产生的信号,通过中断控制器与处理器相连,可被暂时屏蔽(如键盘、网络事件)
- 不可屏蔽:一些关键硬件的崩溃(如内存校验错误)
- 异常(软件产生、同步)
- 错误(Fault):如缺页异常(可恢复)、段错误(不可恢复)等
- 陷阱(Trap): 无需恢复,如断点(int3)、系统调用(int 80)
- 中止(Abort):严重的错误,不可恢复(机器检查)
中断的产生
中断控制器
ARM SOC的结构,里面可以看到很重要的一块GIC(通用中断控制器)
GIC连出很多根线,和各种设备进行了连接,同时也有一根线和CPU(core)进行了连接
GIC在这里相当于是中断源的处理,它把各个设备发出的中断汇集到GIC这里,再有GIC决定这个中断是发给哪个CPU还是外设
- 中断控制器需要考虑的问题
- 如何指定不同中断的优先级
- 中断交给谁处理
- 如何与软件协同
AArch64中断的分类
不同ARM GIC的实现
- 早期的ARM中断控制器
- 厂商制定模型
- 向量中断控制器VIC
- ARM提出
- 放在AMBA告诉总线上
- 32种非向量中断(不同中断相同的处理入口)
- 16种向量中断(不同中断不同的处理入口)
- 现在->GIC:通用中断控制器
- 中断类型变多,将中断分发给不同的核(对称或非对称)进行处理
- 主要功能
- 分发:管理所有中断、决定优先级、路由
- CPU接口:给每个CPU核有对应的接口
GIC中断来源分类
GIC有很多版本,这里以下图这种举例
- SPI:共享外围中断
- 可以被路由到一个或多个核,找到可用的核进行处理
- Distributor可配置路由
- 如UART中断(可以配置由哪个核处理)
- 中断ID:32~1019
- PPI:私有外围中断
- 对每个CPU都有一个PPI,所以可以指定核处理(是私有的,每个CPU都可以有自己的PPI)
- Distributor可配置路由
- 如WatchingDog -> A5 core
- 中断ID:16~31
- SGI:软件产生中断
- 核间通信(如多处理器的情况下,一个CPU往另一个CPU发一个跨处理器的中断)
- 中断ID:0~15
- NT:虽然是由软件发出的,但也是异步的,因为发给另外一个处理器B后,处理器B什么时候处理它和中断处理的过程是有关系的
GIC的路由配置——以ChCore启用timer为例
#define GICD_ISENABLER (KBASE+0xE82B1100) //定义GIC enable对应的地址:Kernel base+偏移
void plat_interrupt_init(void) // 进行Interrupt的初始化
{
u32 cpuid = smp_get_cpu_id();
if(cpuid == 0)
gicv2_dist_init();
gicv2_cpu_init();
/* 启用timer */
put32(GICD_ISENABLER, 0x08000000);
// 往中断控制器中写一个enable的值(这里实际上是某一个位置成了1)
timer_init();
}
使用MMIO,设置GIC中寄存器,启用timer
put32对应指令:
BEGIN_FUNC(put32)
str w1, [x0]
ret
END_FUNC(put32)
GIC中断信息获取
如:如何handle irq
这里对应已经从中断向量表里跳到了这个函数
void plat_handle_irq(void)
{
u32 cpuid = 0;
unsigned int irq_src, irq;
cpuid = smp_get_cpu_id();
irq_src = get32(core_irq_source[cpuid]); //从中断向量表里获取信息
irq = 1 << ctzl(irq_src);
switch(irq){
case INT_SRC_TIMER3:
handle_timer_irq();
break;
default:
kinfo("Unsupported IRQ %d\n", irq);
}
return;
}
使用MMIO,从GIC中的寄存器里获得中断信息
内存映射输入输出
Memory-Mapped I/O,MMIO,是一种常见的CPU控制和访问设备的方式
原理:把输入输出设备和物理内存放到同一块地址空间,为设备内部的内存和寄存器也分配相应的地址
当CPU通过MMIO方式为一个设备分配了地址之后,CPU可以使用和访问物理内存一样的指令(ldr和str)来访问设备的地址;设备通过总线监听CPU分配给自己的地址,然后完成CPU访问请求
Aarch64将MMIO作为CPU访问设备的重要方式
轮询与中断
->CPU获取有输入事件发生
通过MMIO方式获取树莓派上的异步收发传输器(UART)为例,OS获取输入的可能方式:
- 轮询:CPU不断通过MMIO查看UART是否有输入
- 中断:UART收到输入后,打断CPU正常执行,CPU再从UART获取输入
中断机制除了使得设备能主动通知CPU外,还包括让一个CPU核心去通知另一个CPU核心
MMIO使CPU可以主动访问设备,中断使设备能主动通知CPU->CPU与设备之间交互的重要方式
中断处理不能做太多事情
做太多事情会使处理效率非常低
因为中断在处理时CPU是不能做其他事情的,且其他任务也在等着这个中断的处理
- 解决方案
- 将几个中断一起处理,减少整体中断占用时间(如网卡,一次中断将多个数据一起读上来)
- delay,推迟执行(但是要注意delay什么,尤其是硬件,随意delay会出问题)(比如delay了一个中断,时间有点长的话会丢失一部分数据,用户明显会感受到卡顿)
案例:Linux的中断处理理念
- 在中断处理中做尽量少的事情
- 推迟非关键行为
- 结构:Top half & Bottom half
- Top half:做最少得工作后返回(如在中断处理里面,收到中断后可能将相关的数据先放置,标记信号已经收到,先回复信号发出者信号已经收到了)(比如到达目的地后先报平安,再去找酒店)
- Bottom half:推迟处理(softirq, tasklets, 工作队列,内核线程)
- Top Half:马上做
- 最小的、公共行为
- 保存寄存器,屏蔽其他中断
- 恢复寄存器,返回原来场景
- 最重要:调用合适的由硬件驱动提供的中断处理handler
- 因为中断被屏蔽,所以不要做太多事情(时间、空间)
- 使用将请求放入队列,或者设置标志位将其他处理推迟到Bottom half
- 最小的、公共行为
…未完待更…