minos 2.4 中断虚拟化——中断子系统

  • 首发微信公号:Rand_cs

前面讲述了 minos 对 GICv2 的一些配置和管理,这一节再往上走一走,看看 minos 的中断子系统

中断

中断描述符

C 复制代码
/*
 * 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

C 复制代码
// 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,一个设备的硬件中断号记录在设备树文件里面,比如说串口:

C 复制代码
        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 转换中断号

C 复制代码
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,所以将物理中断号和虚拟中断号直接映射来简化实现。

注册中断

C 复制代码
// 注册 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);使能该中断,中断的注册主要就是做这两件事

另外,对于某个状态的状态标志,虽然寄存器里面存有相关信息,但是我们一般在系统软件层面上也设置相关标志,那么每次获取状态信息直接读取变量就行了,不用再去从设备寄存器里面获取

中断处理

C 复制代码
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;
}

与前文联系起来:

C 复制代码
__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
            ......

异常

异常描述符

C 复制代码
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 补充链接

C 复制代码
#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)

异常处理

C 复制代码
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);
}

再与前文联系起来:

C 复制代码
__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
相关推荐
wowocpp20 分钟前
spring boot Controller 和 RestController 的区别
java·spring boot·后端
后青春期的诗go25 分钟前
基于Rust语言的Rocket框架和Sqlx库开发WebAPI项目记录(二)
开发语言·后端·rust·rocket框架
freellf31 分钟前
go语言学习进阶
后端·学习·golang
全栈派森3 小时前
云存储最佳实践
后端·python·程序人生·flask
CircleMouse3 小时前
基于 RedisTemplate 的分页缓存设计
java·开发语言·后端·spring·缓存
獨枭4 小时前
使用 163 邮箱实现 Spring Boot 邮箱验证码登录
java·spring boot·后端
维基框架4 小时前
Spring Boot 封装 MinIO 工具
java·spring boot·后端
秋野酱4 小时前
基于javaweb的SpringBoot酒店管理系统设计与实现(源码+文档+部署讲解)
java·spring boot·后端
☞无能盖世♛逞何英雄☜4 小时前
Flask框架搭建
后端·python·flask
进击的雷神4 小时前
Perl语言深度考查:从文本处理到正则表达式的全面掌握
开发语言·后端·scala