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

简介: 前面讲述了 minos 对 GICv2 的一些配置和管理,这一节再往上走一走,看看 minos 的中断子系统
  • 首发微信公号: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
目录
相关文章
|
5月前
|
虚拟化
minos 2.5 中断虚拟化——vGIC
这一节开始讲述真正的中断虚拟化,首先来看硬件方面的虚拟化。前文 minos 2.3 中断虚拟化——GICv2 管理 主要讲述 GICv2 的 Distributor 和 CPU Interface,在 Hypervisor 存在的情况下,它们都是为 Hypervisor 服务的。现在有了 vm,vm 里面的内核也需要操作 GIC,怎么办?我们模拟一个 GIC 设备给 vm 使用。
86 5
minos 2.5 中断虚拟化——vGIC
|
5月前
|
虚拟化 芯片
minos 2.3 中断虚拟化——GICv2 管理
硬件肯定需要软件配合,这一节就来实战 GICv2 首先准备好 GICv2 手册:https://developer.arm.com/documentation/ihi0048/bb/?lang=en,对于硬件的管理,最底层的操作就是读写硬件的寄存器,所以这里准备好手册,随时查阅手册
99 5
minos 2.3 中断虚拟化——GICv2 管理
|
5月前
|
Linux 调度 虚拟化
minos 2.2 中断虚拟化——异常处理流程
上一节讲述了 ARMv8 异常模型,很多理论,这一节来看一个实际的例子,来看看 minos 中的异常处理流程
64 5
minos 2.2 中断虚拟化——异常处理流程
|
5月前
|
安全 调度 KVM
minos 4.6 中断虚拟化——虚拟中断子系统
Hypervisor 需要对每个虚机的虚拟中断进行管理,这其中涉及的一系列数据结构和操作就是虚拟中断子系统
99 5
|
3月前
|
存储 Linux 调度
OpenStack如何支持虚拟化技术?
【8月更文挑战第21天】
211 0
|
1月前
|
存储 分布式计算 分布式数据库
云计算和虚拟化技术
云计算是指把计算资源、存储资源、网络资源、应用软件等集合起来,采用虚拟化技术,将这些资源池化,组成资源共享池,共享池即是“云”。
140 64
|
18天前
|
存储 持续交付 虚拟化
|
2月前
|
KVM 虚拟化
虚拟化技术概述及KVM环境安装
关于虚拟化技术概述及KVM环境安装的教程,涵盖了虚拟化的定义、分类、管理工具,以及KVM的系统需求、安装步骤和使用指南。
76 11
虚拟化技术概述及KVM环境安装
|
3月前
|
存储 运维 虚拟化
Docker技术概论(1):Docker与虚拟化技术比较
Docker技术概论(1):Docker与虚拟化技术比较
138 17
Docker技术概论(1):Docker与虚拟化技术比较
|
3月前
|
Devops 虚拟化 Docker
DevOps 中的标准虚拟化技术
【8月更文挑战第27天】
57 5