- 首發公號: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