minos 2.4 中斷虛擬化——中斷子系統

Rand_CS發表於2024-06-08
  • 首發公號: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

相關文章