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

  • 首发微信公号:Rand_cs

Hypervisor 需要对每个虚机的虚拟中断进行管理,这其中涉及的一系列数据结构和操作就是虚拟中断子系统

VIRQ

虚拟中断描述符

C 复制代码
struct vcpu {
    uint32_t vcpu_id;
...........
    /*
     * member to record the irq list which the
     * vcpu is handling now
     */
    struct virq_struct *virq_struct;

...........
} __cache_line_align;

minos 对于每个物理 CPU 定义了 struct pcpu 来管理其相关状态,同样的,minos 对于每个 vm 的 vcpu,也定义了一个结构体来管理 struct vcpu。每个 vcpu 里面又有一个 virq_struct 字段来管理该 vcpu 关于中断的一些信息,具体来看:

C 复制代码
struct virq_struct {
    int nr_lrs;           // LR 寄存器个数
    int last_fail_virq;   // 上一个因分配 LR 失败的 virq
    atomic_t pending_virq;  // 有多少个 virq 处于 pending 状态
    uint32_t active_virq;   // 有多少个 virq 处于 active 状态
    struct virq_desc local_desc[VM_LOCAL_VIRQ_NR];  // virq 描述符(PPI+SGI)
    unsigned long *pending_bitmap;  // virq pending 位图(PPI+SGI+SPI)
    unsigned long *active_bitmap;  // virq active 位图
    unsigned long lrs_bitmap[BITS_TO_LONGS(VGIC_MAX_LRS)];  // LR 位图
};
//..........................
#define VM_SGI_VIRQ_NR      (CONFIG_NR_SGI_IRQS)   //16
#define VM_PPI_VIRQ_NR      (CONFIG_NR_PPI_IRQS)   //16
#define VM_LOCAL_VIRQ_NR    (VM_SGI_VIRQ_NR + VM_PPI_VIRQ_NR)

在 minos 中,每个物理中断都有一个 irq_desc 来描述,同样的每个虚拟中断都有一个 virq_desc 描述符,定义如下:

C 复制代码
struct virq_desc {
    int32_t flags;
    uint16_t vno;    // virq number
    uint16_t hno;    // 如果该 virq 关联了一个 hwirq,记录该 hwirq number
    uint8_t id;      // LR 寄存器编号
    uint8_t state;   // 状态
    uint8_t pr;      // 优先级
    uint8_t src;     // SGI 中断下,记录源 vcpu id
    uint8_t type;    // edge or level
    uint8_t vcpu_id; // vcpu id
    uint8_t vmid;    // vm id
    uint8_t padding;
} __packed;

SGI 和 PPI 可以看作是 percpu 中断,所以这里将这两种虚拟中断描述符定义在了 vcpu->virq_struct 结构体中,那 SPI 类型的中断呢,SPI 类型的中断时所有 CPU 共享的,在虚拟化的情况下就是所有 VCPU 共享,它定义在 struct vm 中:

C 复制代码
struct vm {
    int vmid;
..................
    uint32_t vspi_nr;     // 当前 vm spi 中断个数
    struct virq_desc *vspi_desc;  // spi 类型中断描述符数组
    unsigned long *vspi_map;   // 分配 virq_desc 时使用的位图
    struct virq_chip *virq_chip;   // vgic 指针
..................
} __align(sizeof(unsigned long));

初始化操作

上述涉及了很多的结构体及其字段信息,这里来看一下它们的初始化流程

C 复制代码
// 初始化 vm 的 virq 信息
static int virq_create_vm(void *item, void *args)
{
    uint32_t size, vdesc_size, vdesc_bitmap_size, status_bitmap_size;
    struct vm *vm = (struct vm *)item;
    struct virq_struct *vs;
    void *base;
    int i;

    /*
     * Total size:
     * 1 - sizeof(struct virq_desc) * vspi_nr
     * 3 - vitmap_size(spi_nr)
     * 2 - vcpu_nr * bitmap_size * (SGI + PPI + SPI) * 2
     */
    // 获取最大的 vspi 数量
    vm->vspi_nr = vm_max_virq_line(vm);
    // 计算需要的 virq_desc 大小
    vdesc_size = sizeof(struct virq_desc) * vm->vspi_nr;
    // 对齐
    vdesc_size = BALIGN(vdesc_size, sizeof(unsigned long));
    // 根据数量 vspi_nr 计算位图大小
    vdesc_bitmap_size = BITMAP_SIZE(vm->vspi_nr);
    // 计算一个位图大小
    // 包括所有类型的中断:PPI+SGI+SPI
    status_bitmap_size = BITMAP_SIZE(vm->vspi_nr + VM_LOCAL_VIRQ_NR);

    // 需要分配的总大小 = virq_descs + spi_bitmap + pending_bitmap + active_bitmap
    size = vdesc_size + vdesc_bitmap_size +
        (status_bitmap_size * vm->vcpu_nr * 2);
    size = PAGE_BALIGN(size);

    pr_notice("allocate 0x%x bytes for virq struct\n", size);
    // 根据大小分配内存
    base = get_free_pages(PAGE_NR(size));
    if (!base) {
        pr_err("no more page for virq struct\n");
        return -ENOMEM;
    }
    // 相关地址信息记录到 vm 对应字段
    memset(base, 0, size);
    vm->vspi_desc = (struct virq_desc *)base;
    vm->vspi_map = (unsigned long *)(base + vdesc_size);
    // 初始化 vcpu 的 pending 和 active 位图
    base = base + vdesc_size + vdesc_bitmap_size;
    for (i = 0; i < vm->vcpu_nr; i++) {
        vs = vm->vcpus[i]->virq_struct;
        ASSERT(vs != NULL);
        vs->pending_bitmap = base;
        base += status_bitmap_size;
        vs->active_bitmap = base;
        base += status_bitmap_size;
    }

    return 0;
}

在创建 vm 的时候会调用 virq_create_vm 来初始化 virq 的基本信息,就是上述所提到的一些值

C 复制代码
// virq_struct 初始化
void vcpu_virq_struct_init(struct vcpu *vcpu)
{
    struct virq_struct *virq_struct = vcpu->virq_struct;
    struct virq_desc *desc;
    int i;

    virq_struct->active_virq = 0;
    atomic_set(0, &virq_struct->pending_virq);

    // 所有 desc 重置清零,设置为 0
    memset(&virq_struct->local_desc, 0,
        sizeof(struct virq_desc) * VM_LOCAL_VIRQ_NR);

    // 初始化每个 desc,所欲字段设置为默认值
    for (i = 0; i < VM_LOCAL_VIRQ_NR; i++) {
        desc = &virq_struct->local_desc[i];
        virq_clear_hw(desc);
        virq_set_enable(desc);

        /* this is just for ppi or sgi */
        desc->vcpu_id = VIRQ_AFFINITY_VCPU_ANY;
        desc->vmid = VIRQ_AFFINITY_VM_ANY;
        desc->vno = i;
        desc->hno = 0;
        desc->id = VIRQ_INVALID_ID;
        desc->state = VIRQ_STATE_INACTIVE;
    }
}

// 重置 vm 中所有 virq_desc
void vm_virq_reset(struct vm *vm)
{
    struct virq_desc *desc;
    int i;

    /* reset the all the spi virq for the vm */
    for ( i = 0; i < vm->vspi_nr; i++) {
        desc = &vm->vspi_desc[i];
        virq_clear_enable(desc); //屏蔽该 virq
        desc->pr = 0xa0;   //优先级
        desc->type = 0x0;  
        desc->id = VIRQ_INVALID_ID;
        desc->state = VIRQ_STATE_INACTIVE;

        if (virq_is_hw(desc))  //如果是 hw interrupt
            irq_mask(desc->hno); //芯片级屏蔽
    }
}

还有一些如上所示的一些重置函数,比较简单了,这里不再详述,大家可以自行阅读相关代码

注册虚拟中断

C 复制代码
// 注册普通的 virq
int request_virq(struct vm *vm, uint32_t virq, unsigned long flags)
{   
    // hwirq 设置为 0,该 virq 没有与物理物理中断关联
    return request_hw_virq(vm, virq, 0, flags);
}

// 注册中断衍生函数
int request_hw_virq(struct vm *vm, uint32_t virq, uint32_t hwirq,
            unsigned long flags)
{
    if (virq >= vm_max_virq_line(vm)) {
        pr_err("invaild virq-%d for vm-%d\n", virq, vm->vmid);
        return -EINVAL;
    } else {
        // 默认将所有的中断都发送给 vcpu0
        return request_virq_affinity(vm, virq, hwirq, 0, flags);
    }
}

// 注册中断的一个衍生函数
int request_virq_affinity(struct vm *vm, uint32_t virq, uint32_t hwirq,
            int affinity, unsigned long flags)
{
    struct vcpu *vcpu;
    struct virq_desc *desc;

    // 获取 vcpu0,这里的 affinity 其实就是一个 vcpu_id
    vcpu = get_vcpu_in_vm(vm, affinity);
    if (!vcpu) {
        pr_err("request virq fail no vcpu-%d in vm-%d\n",
                affinity, vm->vmid);
        return -EINVAL;
    }

    // 获取对应的 desc
    desc = get_virq_desc(vcpu, virq);
    if (!desc) {
        pr_err("virq-%d not exist vm-%d", virq, vm->vmid);
        return -ENOENT;
    }

    // 注册中断
    return __request_virq(vcpu, desc, virq, hwirq, flags);
}

上述是注册虚拟中断的一系列衍生函数,有一个注意点:request_hw_virq 调用 request_virq_affinity 时,affinity 参数默认是 0,也就是说后续默认将 virq 发送到 vcpu0。以前看过 KVM 的代码,KVM 也是这么处理的,默认将 SPI 中断发送到 virq0,对于 MAC ARM 使用的 HVF,虽然闭源,但是从结果上来看也是这样处理的,这是为了方便处理?存疑

C 复制代码
// 根据 virq 获取对应的 virq_desc 结构体
// local virq(sgi ppi) 为每个 cpu 拥有的,所以将 local_desc 定义于 vcpu 中
// spi 为所有该 vm 中的 vcpu 中共享,所以将 vspi_desc 定义与 vm 中
struct virq_desc *get_virq_desc(struct vcpu *vcpu, uint32_t virq)
{
    struct vm *vm = vcpu->vm;

    // if virq < 32
    if (virq < VM_LOCAL_VIRQ_NR)
        // 直接返回对应的 virq_desc
        return &vcpu->virq_struct->local_desc[virq];

    // 如果 virq 大于了最大号数
    if (virq >= VM_VIRQ_NR(vm->vspi_nr))
        return NULL;
    
    // virq-32 即为对应的下标值
    return &vm->vspi_desc[VIRQ_SPI_OFFSET(virq)];
}

get_virq_desc 根据 virq 虚拟中断号获取对应的 virq_desc 虚拟中断描述符,如果是 percpu 中断,从 vcpu->virq_struct 获取,如果是 SPI 类型中断,从 vm->vspi_desc 中获取

C 复制代码
// 注册 virq
static int __request_virq(struct vcpu *vcpu, struct virq_desc *desc,
            uint32_t virq, uint32_t hwirq, unsigned long flags)
{
    if (test_and_set_bit(VIRQS_REQUESTED_BIT,
                (unsigned long *)&desc->flags)) {
        pr_warn("virq-%d may has been requested\n", virq);
        return -EBUSY;
    }

    // 设置 desc 字段值
    desc->vno = virq;  // virq
    desc->hno = hwirq; // hwirq
    desc->vcpu_id = get_vcpu_id(vcpu); // vcpu_id
    desc->pr = 0xa0; // 优先级
    desc->vmid = get_vmid(vcpu); // vmid
    desc->id = VIRQ_INVALID_ID; // LR 编号,send_virq 的时候分配
    desc->state = VIRQ_STATE_INACTIVE; //刚注册,inactive

    /* mask the bits in spi_irq_bitmap, if it is a SPI */
    // 如果大于 VM_LOCAL_VIRQ_NR,则为 SPI 类型的中断
    if (virq >= VM_LOCAL_VIRQ_NR)
        set_bit(VIRQ_SPI_OFFSET(virq), vcpu->vm->vspi_map);

    /* if the virq affinity to a hwirq need to request
     * the hw irq */
    // 如果有对应的 hwirq
    if (hwirq) {
        // 设置芯片级别的 cpu 亲和性
        irq_set_affinity(hwirq, vcpu_affinity(vcpu));
        // 设置 VIRQS_HW 标志
        virq_set_hw(desc);
        // 在 hyp 下注册 hwirq 中断
        request_irq(hwirq, guest_irq_handler, IRQ_FLAGS_VCPU,
                vcpu->task->name, (void *)desc);
        irq_mask(desc->hno);
    // 否则清除 desc 的 VIRQS_HW 标志
    } else {
        virq_clear_hw(desc);
    }

    // 更新 virq 的 一些标志信息
    update_virq_cap(desc, flags);

    return 0;
}

想想之前注册物理中断所做的事情,主要就是设置物理中断号 irq 对应的物理中断描述符 irq_desc,其重点设置了该中断对应的 handler。注册虚拟中断类似,但有一点不同,虚拟中断在 host 侧是没有 handler 的,所以上述只是在 virq_desc 中设置了该中断的中断号、状态、各个 id 等等信息,就是没有 handler。因为 host 只是负责向虚机注入虚拟中断,处理是虚机做的事情。

另外如果一个虚拟中断关联了一个物理中断,那么需要在 host 注册该物理中断,其 handler 为 guest_irq_handler:

C 复制代码
// hw 类型的 virq 将会注册到 hyp,注册时的 handler 便为此 guest_irq_handler,它会将中断路由到某具体的 vcpu
static int guest_irq_handler(uint32_t irq, void *data)
{
    struct vcpu *vcpu;
    struct virq_desc *desc = (struct virq_desc *)data;

    if ((!desc) || (!virq_is_hw(desc))) {
        pr_notice("virq %d is not a hw irq\n", desc->vno);
        return -EINVAL;
    }

    /* send the virq to the guest */
    // 如果 vmid 和 vcpu_id 都没有指定,很随便的话,那么就选择当前的 vcpu
    if ((desc->vmid == VIRQ_AFFINITY_VM_ANY) &&
            (desc->vcpu_id == VIRQ_AFFINITY_VCPU_ANY))
        vcpu = get_current_vcpu();
    else
        // 获取 desc 指定的 vcpu
        vcpu = get_vcpu_by_id(desc->vmid, desc->vcpu_id);

    // send virq 给某 vcpu
    return send_virq(vcpu, desc);
}

该函数主要做的事情就是调用 send_virq 来向目标 vcpu 发送虚拟中断。

中断注入

C 复制代码
// 发送 virq 给某个 vcpu
static int send_virq(struct vcpu *vcpu, struct virq_desc *desc)
{
.......
    ret = __send_virq(vcpu, desc);
.......
    return 0;
}

// send virq,主要就是设置 vcpu 中对应的 virq_struct pending 位图
static int inline __send_virq(struct vcpu *vcpu, struct virq_desc *desc)
{
    struct virq_struct *virq_struct = vcpu->virq_struct;

    /*
     * if the virq is already at the pending state, do
     * nothing, other case need to send it to the vcpu
     * if the virq is in offline state, send it to vcpu
     * directly.
     *
     * SGI need set the irq source.
     */
    //将 virq 设置到 pending_bitmap
    if (test_and_set_bit(desc->vno, virq_struct->pending_bitmap))
        return 0;
    
    // pending_virq ++
    atomic_inc(&virq_struct->pending_virq);
    // 如果是 sgi 类型的 virq,设置来源 cpu 为当前 cpu
    if (desc->vno < VM_SGI_VIRQ_NR)
        desc->src = get_vcpu_id(get_current_vcpu());

    return 0;
}

先暂时略去 send_virq 中一些复杂逻辑,中断注入的主要流程如上所示,会发现就是简单的设置 pending 位图操作。这样就完了吗,当然不是,前文 minos 4.5 中断虚拟化------vGIC 我们提到过,在 host 侧发送虚拟中断的操作实际上是获取并写一个 LR 寄存器,所以实际的中断注入操作如下:

C 复制代码
// 进入 guest 
int vgic_irq_enter_to_guest(struct vcpu *vcpu, void *data)
{
    struct virq_struct *vs = vcpu->virq_struct;
    struct vm *vm = vcpu->vm;
    struct virq_desc *virq;
    int id = 0, bit, flags = 0, size, old;

    bit = vs->last_fail_virq;
    size = vm_irq_count(vm) - bit;
    old = vs->last_fail_virq;
    vs->last_fail_virq = 0;

repeat:
    // 遍历该 vcpu 的 pending_map
    for_each_set_bit_from(bit, vs->pending_bitmap, size) {
        // 获取该 virq 对应的 virq_desc
        virq = get_virq_desc(vcpu, bit);
        if (virq == NULL) {
            pr_err("bad virq %d for vm %s\n", bit, vm->name);
            clear_bit(bit, vs->pending_bitmap);
            continue;
        }

        /*
         * do not send this virq if there is same virq in
         * active state, need wait the previous virq done.
         */
        // 如果它也存在于 active_map,continue
        // 说明这里处理 pending_and_active 中断的方式是不重复 trigger
        if (test_bit(bit, vs->active_bitmap))
            continue;

        /* allocate a id for the virq */
        // 分配一个 LR 寄存器
        id = find_first_zero_bit(vs->lrs_bitmap, vs->nr_lrs);
        // 分配失败
        if (id >= vs->nr_lrs) {
            pr_err("VM%d no space to send new irq %d\n",
                    vm->vmid, virq->vno);
            // 记录分配 LR 失败的 virq
            vs->last_fail_virq = bit;
            break;
        }

        /*
         * indicate that FIQ has been inject.
         */
        // 如果存在标志就说明该 FIQ 已经被注入了????????
        if (virq->flags & VIRQS_FIQ)
            flags |= FIQ_HAS_INJECT;
        flags++;
        virq->id = id;   // 设置刚分配的 lr_id
        set_bit(id, vs->lrs_bitmap);
        
        // 芯片级别 send_virq,核心是写 gich_lr 寄存器
        virqchip_send_virq(vcpu, virq);
        // 状态转移
        virq->state = VIRQ_STATE_PENDING;

        /*
         * mark this virq as pending state and add it
         * to the active bitmap.
         */
        // 设置为 active
        set_bit(bit, vs->active_bitmap);
        vs->active_virq++;

        /*
         * remove this virq from pending bitmap.
         */
        // pending_nr --
        atomic_dec(&vs->pending_virq);
        // 清除在 pending map 中的比特位
        clear_bit(bit, vs->pending_bitmap);
    }

    // old != 0 表示上次想要 send virq 时,但是没有空闲的 lr 寄存器了,且发送失败的 virq 号记录到了 bit
    // vs->last_fail_virq == 0 表示这次很可能有空闲的 lr 寄存器
    // 所以调整 size 为上次失败的 virq 号,让上面的循环能够触及该失败的 virq 号,且为其分配 lr 寄存器
    if ((old != 0) && (vs->last_fail_virq == 0)) {
        bit = 0;
        size = old;
        old = 0;
        goto repeat;
    }

    return flags;
}

该函数有点长,但同样的略去一些"无关紧要"的代码,主要逻辑就是遍历 pending 位图,为每一个 pending 状态的 virq 分配一个 LR 寄存器,然后将该 virq 的信息记录到 LR 寄存器,该 virq 的状态从 pending 变为 active。在分配 LR 寄存器的时候可能因为没有空闲的 LR 导致分配失败,处理方式是记录下来下次 enter guest 的时候处理。

C 复制代码
// 退出 guest
int vgic_irq_exit_from_guest(struct vcpu *vcpu, void *data)
{
    struct virq_struct *vs = vcpu->virq_struct;
    struct virq_desc *virq;
    int bit;

    // 遍历该 vcpu 所有的 active_bitmap,这也就是说,可能出现多个 active interrupt
    for_each_set_bit(bit, vs->active_bitmap, vm_irq_count(vcpu->vm)) {
        // 获取对应的 virq_desc
        virq = get_virq_desc(vcpu, bit);
        if (virq == NULL) {
            pr_err("bad active virq %d\n", virq);
            clear_bit(bit, vs->active_bitmap);
            continue;
        }

        /*
         * the virq has been handled by the VCPU, if
         * the virq is not pending again, delete it
         * otherwise add the virq to the pending list
         * again
         */
        // 获取状态
        virq->state = virqchip_get_virq_state(vcpu, virq);
        // 如果该 virq 已经是 inactive,说明已处理完成
        // 重置 virq 对应的 LR 为空闲状态
        if (virq->state == VIRQ_STATE_INACTIVE) {
            virqchip_update_virq(vcpu, virq, VIRQ_ACTION_CLEAR);
            clear_bit(virq->id, vs->lrs_bitmap);
            virq->id = VIRQ_INVALID_ID;
            vs->active_virq--;
            clear_bit(bit, vs->active_bitmap);
        }
    }

    return 0;
}

对应的,当虚机退出 guest 会执行上述函数检查中断处理的情况,具体的会遍历 active 位图,对每一个 active virq,获取其对应的 LR.state,查看其状态,如果是 inactive,表明该 virq 已经处理完成,那么重置 LR 为空闲状态

总结xxxxxxxxxxxxxxxxxxx

Exception

前面讲述了虚拟中断,这里讲述虚机异常,也就是在 EL0、EL1 状态下发生的异常处理流程。Exception 的处理流程前文 minos 4.2 中断虚拟化------异常处理流程 讲述过,流程没有变,只是对于虚拟化多了几种异常,定义如下:

C 复制代码
/* type defination is at armv8-spec 1906 */
DEFINE_SYNC_DESC(guest_ESR_ELx_EC_WFx, EC_TYPE_BOTH, wfi_wfe_handler, 1, 4);
DEFINE_SYNC_DESC(guest_ESR_ELx_EC_UNKNOWN, EC_TYPE_BOTH, unknown_handler, 1, 4);
DEFINE_SYNC_DESC(guest_ESR_ELx_EC_CP15_32, EC_TYPE_BOTH, mcr_mrc_cp15_handler, 1, 4);
DEFINE_SYNC_DESC(guest_ESR_ELx_EC_CP15_64, EC_TYPE_AARCH32, mcrr_mrrc_cp15_handler, 1, 4);
DEFINE_SYNC_DESC(guest_ESR_ELx_EC_CP14_MR, EC_TYPE_AARCH32, mcr_mrc_cp14_handler, 1, 4);
DEFINE_SYNC_DESC(guest_ESR_ELx_EC_CP14_LS, EC_TYPE_AARCH32, ldc_stc_cp14_handler, 1, 4);
DEFINE_SYNC_DESC(guest_ESR_ELx_EC_FP_ASIMD, EC_TYPE_BOTH, access_simd_reg_handler, 1, 4);
DEFINE_SYNC_DESC(guest_ESR_ELx_EC_CP10_ID, EC_TYPE_AARCH32, mcr_mrc_cp10_handler, 1, 4);
DEFINE_SYNC_DESC(guest_ESR_ELx_EC_CP14_64, EC_TYPE_AARCH32, mrrc_cp14_handler, 1, 4);
DEFINE_SYNC_DESC(guest_ESR_ELx_EC_ILL, EC_TYPE_BOTH, illegal_exe_state_handler, 1, 4);
DEFINE_SYNC_DESC(guest_ESR_ELx_EC_SYS64, EC_TYPE_AARCH64, access_system_reg_handler, 1, 4);
DEFINE_SYNC_DESC(guest_ESR_ELx_EC_IABT_LOW, EC_TYPE_BOTH, insabort_tfl_handler, 1, 4);
DEFINE_SYNC_DESC(guest_ESR_ELx_EC_PC_ALIGN, EC_TYPE_BOTH, misaligned_pc_handler, 1, 4);
DEFINE_SYNC_DESC(guest_ESR_ELx_EC_DABT_LOW, EC_TYPE_BOTH, dataabort_tfl_handler, 1, 4);
DEFINE_SYNC_DESC(guest_ESR_ELx_EC_SP_ALIGN, EC_TYPE_BOTH, stack_misalign_handler, 1, 4);
DEFINE_SYNC_DESC(guest_ESR_ELx_EC_FP_EXC32, EC_TYPE_AARCH32, floating_aarch32_handler, 1, 4);
DEFINE_SYNC_DESC(guest_ESR_ELx_EC_FP_EXC64, EC_TYPE_AARCH64, floating_aarch64_handler, 1, 4);
DEFINE_SYNC_DESC(guest_ESR_ELx_EC_SERROR, EC_TYPE_BOTH, serror_handler, 1, 4);
DEFINE_SYNC_DESC(guest_ESR_ELx_EC_HVC32, EC_TYPE_AARCH32, aarch64_hypercall_handler, 1, 0);
DEFINE_SYNC_DESC(guest_ESR_ELx_EC_HVC64, EC_TYPE_AARCH64, aarch64_hypercall_handler, 1, 0);
DEFINE_SYNC_DESC(guest_ESR_ELx_EC_SMC32, EC_TYPE_AARCH32, aarch64_smccall_handler, 1, 4);
DEFINE_SYNC_DESC(guest_ESR_ELx_EC_SMC64, EC_TYPE_AARCH64, aarch64_smccall_handler, 1, 4);

static struct sync_desc *guest_sync_descs[] = {
    [0 ... ESR_ELx_EC_MAX]  = &sync_desc_guest_ESR_ELx_EC_UNKNOWN,
    [ESR_ELx_EC_WFx]    = &sync_desc_guest_ESR_ELx_EC_WFx,
    [ESR_ELx_EC_CP15_32]    = &sync_desc_guest_ESR_ELx_EC_CP15_32,
    [ESR_ELx_EC_CP15_64]    = &sync_desc_guest_ESR_ELx_EC_CP15_64,
    [ESR_ELx_EC_CP14_MR]    = &sync_desc_guest_ESR_ELx_EC_CP14_MR,
    [ESR_ELx_EC_CP14_LS]    = &sync_desc_guest_ESR_ELx_EC_CP14_LS,
    [ESR_ELx_EC_FP_ASIMD]   = &sync_desc_guest_ESR_ELx_EC_FP_ASIMD,
    [ESR_ELx_EC_CP10_ID]    = &sync_desc_guest_ESR_ELx_EC_CP10_ID,
    [ESR_ELx_EC_CP14_64]    = &sync_desc_guest_ESR_ELx_EC_CP14_64,
    [ESR_ELx_EC_ILL]    = &sync_desc_guest_ESR_ELx_EC_ILL,
    [ESR_ELx_EC_SYS64]  = &sync_desc_guest_ESR_ELx_EC_SYS64,
    [ESR_ELx_EC_IABT_LOW]   = &sync_desc_guest_ESR_ELx_EC_IABT_LOW,
    [ESR_ELx_EC_PC_ALIGN]   = &sync_desc_guest_ESR_ELx_EC_PC_ALIGN,
    [ESR_ELx_EC_DABT_LOW]   = &sync_desc_guest_ESR_ELx_EC_DABT_LOW,
    [ESR_ELx_EC_SP_ALIGN]   = &sync_desc_guest_ESR_ELx_EC_SP_ALIGN,
    [ESR_ELx_EC_FP_EXC32]   = &sync_desc_guest_ESR_ELx_EC_FP_EXC32,
    [ESR_ELx_EC_FP_EXC64]   = &sync_desc_guest_ESR_ELx_EC_FP_EXC64,
    [ESR_ELx_EC_SERROR] = &sync_desc_guest_ESR_ELx_EC_SERROR,
    [ESR_ELx_EC_HVC32]  = &sync_desc_guest_ESR_ELx_EC_HVC32,
    [ESR_ELx_EC_HVC64]  = &sync_desc_guest_ESR_ELx_EC_HVC64,
    [ESR_ELx_EC_SMC32]  = &sync_desc_guest_ESR_ELx_EC_SMC32,
    [ESR_ELx_EC_SMC64]  = &sync_desc_guest_ESR_ELx_EC_SMC64,
};

这个就不一一解释了,主要来看几个比较重要的异常,看看它们是如何处理的,最后结合前面讲述的串一下总体流程

系统调用

为什么分特权等级,很重要的一个原因就是为了安全,底层总是对上层保持不信任的态度。底层的一些核心功能不能直接交由上层来执行。上层只能发出请求,然后底层调用相关 handler 来帮忙处理,将结果返回给上层。

ARM 有 4 种特权等级,因此有了 3 种系统调用:SVC(Supervisor Call)、HVC(Hypervisor Call) 、SMC(Secure Monitor Call) 。通过上图,以及从名字上来看,SVC 就是请求 EL1 的服务,只能 EL0 调用;HVC 请求 EL2,之恶能 EL1 调用;SMC 请求 EL3,只能 EL1 和 EL2 调用。一般情况下是这样,但 ARM 比较灵活,EL2 也可以使用 SVC 指令,但是并不是请求 EL1 的服务,而是请求 EL2 它自己的服务

C 复制代码
struct svc_desc {
    char *name;   // 服务名字
    uint16_t type_start;  // 服务号
    uint16_t type_end;    // 同服务号,目前 start、end 都只是表示服务号
    svc_handler_t handler; 
};


// 通过该宏定义一个 hvc 调用
#define DEFINE_HVC_HANDLER(n, start, end, h)    \
    static struct svc_desc __hvc_##h __used \
    __section(".__hvc_handler") = { \
        .name = n,  \
        .type_start = start, \
        .type_end = end, \
        .handler = h, \
    }

// 两个例子
DEFINE_HVC_HANDLER("vm_hvc_handler", HVC_TYPE_VM0,
        HVC_TYPE_VM0, vm_hvc_handler);

DEFINE_HVC_HANDLER("misc_hvc_handler", HVC_TYPE_MISC,
        HVC_TYPE_MISC, misc_hvc_handler);

每个系统调用都有一个 svc_desc 描述符,记录了服务名字、服务号、handler。DEFINE_HVC_HANDLER 该宏定义一个静态全局的 svc_desc,并将它放在 __hvc_handler节里面。

在 boot 阶段解析 __hvc_handler 节来动态的加载服务例程:

C 复制代码
// minos.ld.S
    __hvc_handler_start = .;
    .__hvc_handler : {
        *(.__hvc_handler)
    }
    __hvc_handler_end = .;

    . = ALIGN(8);

//.............................................
// svc_service.c
// 定义一系列的 svc_desc 指针,boot 阶段初始化
static struct svc_desc *smc_descs[SVC_STYPE_MAX];
static struct svc_desc *hvc_descs[SVC_STYPE_MAX];

static int __init_text svc_service_init(void)
{
    pr_notice("parsing SMC/HVC handler\n");

    parse_svc_desc((unsigned long)&__hvc_handler_start,
               (unsigned long)&__hvc_handler_end, hvc_descs);
    parse_svc_desc((unsigned long)&__smc_handler_start,
               (unsigned long)&__smc_handler_end, smc_descs);

    return 0;
}

// 解析 __hvc_handler/__smc_handler 节,注册服务例程到对应的 table
static void parse_svc_desc(unsigned long start, unsigned long end,
               struct svc_desc **table)
{
    struct svc_desc *desc;
    int32_t j;
    // 从 start 开始遍历每一个 svc_desc 描述符
    section_for_each_item_addr(start, end, desc) {
        BUG_ON((desc->type_start > desc->type_end) ||
               (desc->type_end >= SVC_STYPE_MAX));
        // 注册该 svc_desc 到 hvc_descs/smc_descs table
        for (j = desc->type_start; j <= desc->type_end; j++) {
            if (table[j])
                pr_warn("overwrite SVC_DESC:%d %s\n", j,
                    desc->name);
            table[j] = desc;
        }
    }
}

上述流程简述:

  1. 首先定义了一系列的 svc_desc 描述符,编译期间将它们存放在 __hvc_handler 节里面
  2. boot 阶段,解析 __hvc_handler 节,遍历每一个 svc_desc,将它们的信息记录到 hvc_descs table 里面

这样做的好处我能想到的就是动态加载,如果不需要某个服务例程,可以直接在编译期间就剔除,不将它们编译到镜像,但其实像 sync_descn 这种定义感觉也行呢。

__smc_handler 和 smc_descs 类似,它们的定义和初始化流程不再赘述。但有朋友可能疑惑,smc,secure monitor call,其服务例程应该定义在 EL3,minos 不是运行在 EL2 的 hypervisor 吗,为什么 EL2 也有 smc handler?这是因为 EL2 有能力 trap smc 指令,当 HCR_EL2.TSC = 1 时,smc 指令并不会 trap 到 EL3,而是 trap 到 EL2,由 EL2 处理。

了解了 svc_desc 定义之后,我们来看其处理流程:

C 复制代码
// __sync_exception_from_lower_el -> sync_exception_from_lower_el

void sync_exception_from_lower_el(gp_regs *regs)
{
#ifdef CONFIG_VIRT
    extern void handle_vcpu_sync_exception(gp_regs *regs);

    /*
     * check whether this task is a vcpu task or a normal
     * userspace task.
     * 1 - TGE bit means a normal task
     * 2 - current->flags
     */
    // HCR_EL2.TGE = 1 表示所有 trap 到 EL1 的异常都会 trap 到 EL2 处理
    if ((current->flags & TASK_FLAGS_VCPU) && !(read_hcr_el2() & HCR_EL2_TGE))
        handle_vcpu_sync_exception(regs);
    else
#endif
        handle_sync_exception(regs);
}

首先从 vector.S 中的 __sync_exception_from_lower_el 开始,表示从低特权等级来的一个异常。只有当前 cpu 上运行的是 vcpu 线程,cpu 上运行的是 guest os,一个异常才有可能来自低特权等级。HCR_EL2.TGE = 1 表示所有 trap 到 EL1 的异常都会 trap 到 EL2 处理,比如说用户态(EL0) 执行 svc 请求内核 (EL1) 的服务也会被 trap 到 EL2 处理,我们不应该是这种情况,trap 到 EL1 就直接让 EL1 自己处理,所以应该设置 HCR_EL2.TGE = 0

C 复制代码
void handle_vcpu_sync_exception(gp_regs *regs)
{
    int cpuid = smp_processor_id();
    uint32_t esr_value;
    int ec_type;
    struct sync_desc *ec;
    struct vcpu *vcpu = get_current_vcpu();

    if ((!vcpu) || (vcpu->task->affinity != cpuid))
        panic("this vcpu is not belong to the pcpu");

    // 读取 ESR.ec 值,可以当作 exception number
    esr_value = read_esr_el2();
    ec_type = (esr_value & ESR_ELx_EC_MASK) >> ESR_ELx_EC_SHIFT;

    if (ec_type >= ESR_ELx_EC_MAX) {
        pr_err("unknown sync exception type from guest %d\n", ec_type);
        goto out;
    }

    pr_debug("sync from lower EL, handle 0x%x\n", ec_type);
    // 获取该 exception 对应的描述符
    ec = guest_sync_descs[ec_type];
    if (ec->irq_safe)
        local_irq_enable();
    // 返回地址修正
    regs->pc += ec->ret_addr_adjust;
    // 处理该异常
    ec->handler(regs, ec_type, esr_value);
out:
    local_irq_disable();
}

该函数从 ESR.ec 里面取出异常号,然后执行相应的 handler,对于 hvc handler 为 aarch64_hypercall_handler

C 复制代码
int aarch64_hypercall_handler(gp_regs *reg, int ec, uint32_t esr_value)
{
    struct vcpu *vcpu = get_current_vcpu();
    struct arm_virt_data *arm_data = vcpu->vm->arch_data;

    if (arm_data->hvc_handler)
        return arm_data->hvc_handler(vcpu, reg, read_esr_el2());
    else
        return __arm_svc_handler(reg, 0);
}

static int __arm_svc_handler(gp_regs *reg, int smc)
{
    uint32_t id;
    unsigned long args[6];

    // 第一个参数是服务号
    id = reg->x0;
    args[0] = reg->x1;
    args[1] = reg->x2;
    args[2] = reg->x3;
    args[3] = reg->x4;
    args[4] = reg->x5;
    args[5] = reg->x6;

    if (!(id & SVC_CTYPE_MASK))
        local_irq_enable();
    // 执行服务号对应的 handler
    return do_svc_handler(reg, id, args, smc);
}

// 系统调用 handler
int do_svc_handler(gp_regs *regs, uint32_t svc_id, uint64_t *args, int smc)
{
    uint16_t type;
    struct svc_desc **table;
    struct svc_desc *desc;

    // 如果是 smc 调用,调用 smc handler,反之调用 hvc handler
    if (smc)
        table = smc_descs;
    else
        table = hvc_descs;

    // 获取服务号
    // bit[29:24]   : service call ranges SVC_STYPE_XX
    type = (svc_id & SVC_STYPE_MASK) >> 24;

    if (unlikely(type > SVC_STYPE_MAX)) {
        pr_err("Unsupported SVC type %d\n", type);
        goto invalid;
    }
    // 获取该服务号对应的 svc_desc
    desc = table[type];
    if (unlikely(!desc))
        goto invalid;

    pr_debug("doing SVC Call %s:0x%x\n", desc->name, svc_id);
    // 执行 handler
    return desc->handler(regs, svc_id, args);

invalid:
    SVC_RET1(regs, -EINVAL);
}

流程很简单,看注释应该能明白,这里不赘述,最后我们以 guest OS 调用 hvc 为例梳理一下系统调用的流程

C 复制代码
hvc #xx
    __sync_exception_from_lower_el
        sync_exception_from_lower_el
            handle_vcpu_sync_exception
                esr_value = read_esr_el2();  //异常原因
                ec_type = (esr_value & ESR_ELx_EC_MASK) >> ESR_ELx_EC_SHIFT; //异常号
                ec = guest_sync_descs[ec_type];   //异常描述符
                regs->pc += ec->ret_addr_adjust;  //返回地址修正
                ec->handler(regs, ec_type, esr_value);  //异常处理
                    __arm_svc_handler
                        id = reg->x0;   //服务号
                        do_svc_handler  
                            type = (svc_id & SVC_STYPE_MASK) >> 24;  //具体的服务号
                            desc = table[type];   //服务描述符
                            desc->handler(regs, svc_id, args);  //执行该服务例程

minos 中还有一个特殊的系统调用,在调度 sys_sched() 中,底层实际上就是一个 svc #0 系统调用,这就是前面所说的例外,并不是说只有 EL0 才能使用 svc 来请求 EL1 的服务。EL2 也能使用 svc 指令,来调用 EL2 自己的一个服务例程。有了异常、系统调用的了解,我们再来回顾一下之前提到的调度:

C 复制代码
svc #0    // EL2 下使用 svc 指令
    __sync_exception_from_current_el  // 来自当前特权级的异常
        mrs    x1, ESR_EL2         // 获取异常原因
        ubfx   x2, x1, #ESR_ELx_EC_SHIFT, #ESR_ELx_EC_WIDTH 
        cmp x2, #ESR_ELx_EC_SVC64      // 如果 x2 == ESR_ELx_EC_SVC64 表示一个 svc 指令
        b.eq   __sync_current_out     // 跳去 __sync_current_out
            exception_return
                exception_return_handler
                    __exception_return_handler
                        pick_next_task   // 调度策略挑选下一个 task
                        switch_to_task   // 切换 task

因为 minos 目前在 EL2 中使用 svc 只有这么一种情况,所以直接在 vector.S 里面做了判断,并没有为其定义 svc_desc 描述符等,直接判断如果是 svc 指令那么就异常返回,在异常返回中执行调度操作。

  • 首发微信公号:Rand_cs
相关推荐
Victor3562 小时前
MongoDB(2)MongoDB与传统关系型数据库的主要区别是什么?
后端
JaguarJack2 小时前
PHP 应用遭遇 DDoS 攻击时会发生什么 从入门到进阶的防护指南
后端·php·服务端
BingoGo2 小时前
PHP 应用遭遇 DDoS 攻击时会发生什么 从入门到进阶的防护指南
后端
Victor3562 小时前
MongoDB(3)什么是文档(Document)?
后端
牛奔4 小时前
Go 如何避免频繁抢占?
开发语言·后端·golang
想用offer打牌9 小时前
MCP (Model Context Protocol) 技术理解 - 第二篇
后端·aigc·mcp
KYGALYX11 小时前
服务异步通信
开发语言·后端·微服务·ruby
掘了11 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
爬山算法11 小时前
Hibernate(90)如何在故障注入测试中使用Hibernate?
java·后端·hibernate
Moment12 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端