- 首发微信公号:Rand_cs
前面讲述了 minos 对 GICv2 的一些配置和管理,这一节再往上走一走,看看 minos 的中断子系统
中断
中断描述符
/*
* if a irq is handled by minos, then need to register
* the irq handler otherwise it will return the vnum
* to the handler and pass the virq to the vm
*/
struct irq_desc {
irq_handle_t handler; // 中断 handler 函数
uint16_t hno; // 物理中断号
uint16_t affinity; // cpu 亲和性
unsigned long flags;
spinlock_t lock;
unsigned long irq_count;
void *pdata;
void *owner;
struct kobject *kobj;
struct poll_event_kernel *poll_event;
};
由 minos(hypervisor) 处理的每一个中断,都有一个 irq_desc 描述符,其中主要记录了该中断对应的物理中断号 hno,以及对应的 handler
// SGI(Software Generated Interrupts)软件中断
// PPI(Private Peripheral Interrupts)私有外设中断
// SPI(Shared Peripheral Interrupts)共享外设中断
static struct irq_desc percpu_irq_descs[PERCPU_IRQ_DESC_SIZE] = {
[0 ... (PERCPU_IRQ_DESC_SIZE - 1)] = {
default_irq_handler,
},
};
static struct irq_desc spi_irq_descs[SPI_IRQ_DESC_SIZE] = {
[0 ... (SPI_IRQ_DESC_SIZE - 1)] = {
default_irq_handler,
},
};
static int default_irq_handler(uint32_t irq, void *data)
{
pr_warn("irq %d is not register\n", irq);
return 0;
}
全局定义了两个 irq_desc 数组,percpu_irq_descs 表示 per cpu 中断,SGI 是发送给特定 CPU(组) 的中断,PPI 是每个 CPU 私有中断,它们都可以看作为 percpu 中断,而 SPI 是所有 CPU 共享(GICD_ITARGETSR设置亲和性)的外部中断。
这里再具体说一下我理解的 percpu 中断,对于 PPI 来说比较好理解,比如说时钟中断,本身就有 NCPU 个的时钟中断源,每个 CPU 私人具有一个中断源,所以我们定义 NCPU 个的 irq_desc 来分别描述这 NCPU 个时钟中断。没什么问题,但是 SGI 呢,我们这样想,对于 CPU0 来说,其他 CPU 包括自己都有可能向 CPU0 发送 SGI,同理对于其他 CPU 也是这样,那么每一种 SGI,我们也定义 NCPU 个 irq_desc 来描述,很合理。
spi_irq_descs 的下标我们可以当做虚拟中断号 virq,一个设备的硬件中断号记录在设备树文件里面,比如说串口:
pl011@9000000 {
clock-names = "uartclk\0apb_pclk";
clocks = < 0x8000 0x8000 >;
interrupts = < 0x00 0x01 0x04 >;
reg = < 0x00 0x9000000 0x00 0x1000 >;
compatible = "arm,pl011\0arm,primecell";
};
interrupts = < 0x00 0x01 0x04 >;
对于设备树的 interrupts 语句,后面一般跟 3 个数或者 2 个数,倒数第二个表示硬件中断号,倒数第一个表示触发方式,倒数第三个表示中断域,比如说是 SPI?PPI?
从这里可以看出串口 pl011 的中断号为 0x01,但似乎这个数不太对,怎么会在 32 以内?那是因为获取了这个数之后还要进行转换,在设备树分析的时候,从 interrupts 获取到中断信息后,马上会调用 irq_xlate 转换中断号
int get_device_irq_index(struct device_node *node, uint32_t *irq,
unsigned long *flags, int index)
{
int irq_cells, len, i;
of32_t *value;
uint32_t irqv[4];
if (!node)
return -EINVAL;
value = (of32_t *)of_getprop(node, "interrupts", &len);
if (!value || (len < sizeof(of32_t)))
return -ENOENT;
irq_cells = of_n_interrupt_cells(node);
if (irq_cells == 0) {
pr_err("bad irqcells - %s\n", node->name);
return -ENOENT;
}
pr_debug("interrupt-cells %d\n", irq_cells);
len = len / sizeof(of32_t);
if (index >= len)
return -ENOENT;
value += (index * irq_cells);
for (i = 0; i < irq_cells; i++)
irqv[i] = of32_to_cpu(*value++);
return irq_xlate(node, irqv, irq_cells, irq, flags);
}
irq_xlate -> irq_chip->irq_xlate -> gic_xlate_irq
int gic_xlate_irq(struct device_node *node,
uint32_t *intspec, unsigned int intsize,
uint32_t *hwirq, unsigned long *type)
{
if (intsize != 3)
return -EINVAL;
// SPI 中断
if (intspec[0] == 0)
*hwirq = intspec[1] + 32;
// PPI 中断
else if (intspec[0] == 1) {
if (intspec[1] >= 16)
return -EINVAL;
*hwirq = intspec[1] + 16;
} else
return -EINVAL;
*type = intspec[2];
return 0;
}
通过上述代码我们可以知道,pl101 的中断实际上是 1 + 32 = 33,这是一个物理中断号,在 minos 中物理中断号与虚拟中断号是一样的,没有做什么复杂的映射。在 Linux 系统,因为要考虑各个平台,各个平台使用的中断控制器,向后兼容一系列复杂的原因,做不到物理中断号与虚拟中断号直接映射。但目前 minos 没有太多平台特性,只支持 ARM,所以将物理中断号和虚拟中断号直接映射来简化实现。
注册中断
// 注册 percpu 类型的 irq
int request_irq_percpu(uint32_t irq, irq_handle_t handler,
unsigned long flags, char *name, void *data)
{
int i;
struct irq_desc *irq_desc;
unsigned long flag;
unused(name);
if ((irq >= NR_PERCPU_IRQS) || !handler)
return -EINVAL;
// 遍历每个CPU,注册对应的 irq
for (i = 0; i < NR_CPUS; i++) {
// 获取 per cpu 类型中断对应的 irq_desc
irq_desc = get_irq_desc_cpu(i, irq);
if (!irq_desc)
continue;
// 初始化 irq_desc 结构体
spin_lock_irqsave(&irq_desc->lock, flag);
irq_desc->handler = handler;
irq_desc->pdata = data;
irq_desc->flags |= flags;
irq_desc->affinity = i;
irq_desc->hno = irq;
/* enable the irq here */
// 使能该中断
irq_chip->irq_unmask_cpu(irq, i);
// irq_desc 中也取消 masked 标志
irq_desc->flags &= ~IRQ_FLAGS_MASKED;
spin_unlock_irqrestore(&irq_desc->lock, flag);
}
return 0;
}
// 注册普通的 SPI 共享外设
int request_irq(uint32_t irq, irq_handle_t handler,
unsigned long flags, char *name, void *data)
{
int type;
struct irq_desc *irq_desc;
unsigned long flag;
unused(name);
if (!handler)
return -EINVAL;
// 获取该 irq 对应的 irq_desc
// irq < 32 返回 percpu_irq_descs
// irq >= 32 返回 spi_desc
irq_desc = get_irq_desc(irq);
if (!irq_desc)
return -ENOENT;
type = flags & IRQ_FLAGS_TYPE_MASK;
flags &= ~IRQ_FLAGS_TYPE_MASK;
// 设置 irq_desc 各个字段
spin_lock_irqsave(&irq_desc->lock, flag);
irq_desc->handler = handler;
irq_desc->pdata = data;
irq_desc->flags |= flags;
irq_desc->hno = irq;
/* enable the hw irq and set the mask bit */
// 使能该中断
irq_chip->irq_unmask(irq);
// 在 irq_desc 层级也取消屏蔽
irq_desc->flags &= ~IRQ_FLAGS_MASKED;
// 如果 irq < SPI_IRQ_BASE,要么是 SGI 软件中断,要么是 PPI 私有中断
// 都属于 percpu 中断,设置该 irq 的亲和性为当前 cpu
if (irq < SPI_IRQ_BASE)
irq_desc->affinity = smp_processor_id();
spin_unlock_irqrestore(&irq_desc->lock, flag);
// 设置触发类型
if (type)
irq_set_type(irq, type);
return 0;
}
minos 中有上述两个注册中断函数,看函数名称一个是注册 percpu 类型的中断,一个是注册其他(SPI) 类型的中断,但其实 request_irq 什么类型的中断都会注册,从代码 if (irq < SPI_IRQ_BASE)
就可以看出来
注册中断就是在中断号对应的 irq_desc 填写好 handler 等信息,然后 irq_chip->irq_unmask(irq);
使能该中断,中断的注册主要就是做这两件事
另外,对于某个状态的状态标志,虽然寄存器里面存有相关信息,但是我们一般在系统软件层面上也设置相关标志,那么每次获取状态信息直接读取变量就行了,不用再去从设备寄存器里面获取
中断处理
int do_irq_handler(void)
{
uint32_t irq;
struct irq_desc *irq_desc;
int cpuid = smp_processor_id();
while (1) {
// 循环调用 get_pending_irq 读取 IAR 寄存器来获取中断号
irq = irq_chip->get_pending_irq();
if (irq >= BAD_IRQ)
return 0;
// 根据中断号获取 irq_desc
irq_desc = get_irq_desc_cpu(cpuid, irq);
// 不太可能为空,如果为空可能是发生了伪中断
if (unlikely(!irq_desc)) {
pr_err("irq is not actived %d\n", irq);
irq_chip->irq_eoi(irq);
irq_chip->irq_dir(irq);
continue;
}
do_handle_host_irq(cpuid, irq_desc);
}
return 0;ec->handler
}
// 执行中断对应的 handler
static int do_handle_host_irq(int cpuid, struct irq_desc *irq_desc)
{
int ret;
if (cpuid != irq_desc->affinity) {
pr_notice("irq %d do not belong to this cpu\n", irq_desc->hno);
ret = -EINVAL;
goto out;
}
// 执行 handler
ret = irq_desc->handler(irq_desc->hno, irq_desc->pdata);
// drop priority
irq_chip->irq_eoi(irq_desc->hno);
out:
/*
* 1: if the hw irq is to vcpu do not DIR it.
* 2: if the hw irq is to vcpu but failed to send then DIR it.
* 3: if the hw irq is to userspace process, do not DIR it.
*/
// 除了上述三种情况,调用 irq_dir deactivate
if (ret || !(irq_desc->flags & IRQ_FLAGS_VCPU))
irq_chip->irq_dir(irq_desc->hno);
return ret;
}
与前文联系起来:
__irq_exception_from_current_el
irq_from_current_el
irq_handler
do_irq_handler
do_handle_host_irq
irq_desc->handler
__irq_exception_from_lower_el
irq_from_lower_el
irq_handler
......
异常
异常描述符
struct sync_desc {
uint8_t aarch; // 执行状态
uint8_t irq_safe; // 概念同 Linux,如果handler不会导致死锁竞争等,safe
uint8_t ret_addr_adjust; // 返回地址修正
uint8_t resv; // pad
sync_handler_t handler;
};
对于异常的处理,也类似中断,每一个异常都定义了一个 sync_desc 来描述,里面记录了 handler 等信息
其他都比较好理解,就这个返回地址修正什么意思呢?当发生异常的时候,是将发生异常的指令的地址保存到 ELR_EL2 寄存器里面,但是返回的时候不一定返回异常指令地址。比如说 svc 系统调用指令,当 svc 执行完成后肯定是返回 svc 下一条指令,这个 ret_addr_adjust 就是做这个事情的,记录对应异常是否需要返回地址的修正
手册里有个地方记录着每种异常的伪代码,其中记录了是否修正,以及修正值:TODO 补充链接
#define DEFINE_SYNC_DESC(t, arch, h, is, raa) \
static struct sync_desc sync_desc_##t __used = { \
.aarch = arch, \
.handler = h, \
.irq_safe = is, \
.ret_addr_adjust = raa, \
}
DEFINE_SYNC_DESC(trap_unknown, EC_TYPE_AARCH64, unknown_trap_handler, 1, 0);
DEFINE_SYNC_DESC(trap_kernel_da, EC_TYPE_AARCH64, kernel_mem_fault, 1, 0);
DEFINE_SYNC_DESC(trap_kernel_ia, EC_TYPE_AARCH64, kernel_mem_fault, 1, 0);
目前 minos 定义了上述几个异常描述符(还有一些与虚拟化相关,暂且不谈),实际就两个,一个是指令异常,一个是数据异常,其他的都处于未定义状态(都调用到 panic)
异常处理
static void handle_sync_exception(gp_regs *regs)
{
uint32_t esr_value;
uint32_t ec_type;
struct sync_desc *ec;
// 获取异常原因,ESR[31:26]记录了异常的种类,其值当做异常号
esr_value = read_esr();
ec_type = ESR_ELx_EC(esr_value);
if (ec_type >= ESR_ELx_EC_MAX)
panic("unknown sync exception type from current EL %d\n", ec_type);
/*
* for normal userspace process the return address shall
* be adjust
*/
// 获取该异常对应的异常描述符
ec = process_sync_descs[ec_type];
// 修正返回地址
regs->pc += ec->ret_addr_adjust;
// 处理该异常
ec->handler(regs, ec_type, esr_value);
}
再与前文联系起来:
__sync_exception_from_current_el
sync_exception_from_current_el
handle_sync_exception
ec->handler
__sync_exception_from_lower_el
sync_exception_from_lower_el
handle_sync_exception
ec->handler
- 首发微信公号:Rand_cs