中断
中断是一个异步事件,由设备在需要时产生。 例如,当串口数据可用或者一个以太网包接受时,则会产生一个中断,。 中断使驱动能在事件发生时尽可能快的知道,但不需要驱动花时间来轮询(主动等待)它。
驱动的通用框架中,通常在驱动启动/绑定操作时创建一个后台中断处理线程(IHT)来使用中断。 这个线程等待中断发生,当中断发生时,执行一些服务动作。
例如,考虑一个串口驱动。 它可以在下述事件发生时,收到中断:
- 接收到一个或多个字符,
- 现在有空间传送一个或多个字符,
- 一条控制指令(例如
DTR
)改变状态时。
中断唤醒 IHT。 IHT 通常通过读取某些状态寄存器来确定事件的原因。 然后,运行一个相对应的服务函数处理事件。 运行结束后, IHT 回到睡眠状态,等待下一个中断发生。
例如,如何收到一个字符, IHT 唤醒,读取状态寄存器表示 “ 数据可用”,然后调用函数从驱动缓存中的串口 FIFO 中取出所有可用的字符。
无需内核层级代码
你可能会熟悉其他操作系统使用的中断服务程序( ISR)。 这是内核级别的处理函数,运行在特权模式和中断控制器硬件接口下。
在 Fuchsia 中,内核处理中断处理的特权部分,并提供给驱动使用线程级别的函数。
这样的区别在于 IHT 在线程级别下运行,而 ISR 在非常限制(有时候很脆弱)的内核级别环境下运行。 主要的优点是如果 IHT 发生崩溃,它只会影响驱动,而 ISR 的崩溃则会引起整个操作系统的失效。
绑定到一个中断上
现在,仅有的提供中断的总线是 PCI 总线。 它支持两种中断:旧有的和消息信号中断(MSI)。
因此,为了在 PCI 上使用中断:
- 确定你的设备支持什么类型(传统还是 MSI ),
- 设置匹配的中断模式,
- 获取你的设备中断向量句柄(通常为一个,但是也有可能多个),
- 启动 IHT 后台线程,
- 安排 IHT 线程等待中断(步骤3中句柄)。
步骤1
和2
经常都是几乎一起完成,例如:
// Query whether we have MSI or Legacy interrupts.
uint32_t irq_cnt = 0;
if ((pci_query_irq_mode(&edev->pci, ZX_PCIE_IRQ_MODE_MSI, &irq_cnt) == ZX_OK) &&
(pci_set_irq_mode(&edev->pci, ZX_PCIE_IRQ_MODE_MSI, 1) == ZX_OK)) {
// using MSI interrupts
} else if ((pci_query_irq_mode(&edev->pci, ZX_PCIE_IRQ_MODE_LEGACY, &irq_cnt) == ZX_OK) &&
(pci_set_irq_mode(&edev->pci, ZX_PCIE_IRQ_MODE_LEGACY, 1) == ZX_OK)) {
// using legacy interrupts
} else {
// an error
}
pci_query_irq_mode() 函数有三个参数:
zx_status_t pci_query_irq_mode(const pci_protocol_t* pci,
zx_pci_irq_mode_t mode,
uint32_t* out_max_irqs);
The third argument is a pointer to integer that returns how many interrupts of the specified type your device supports.
Having determined the kind of interrupt supported, you then call pci_set_irq_mode() to indicate that this is indeed the kind of interrupt that you wish to use.
Finally, you call pci_map_interrupt() to create a handle to the selected interrupt. Note that pci_map_interrupt() has the following prototype:
—->
第一个参数pci
,是上述我们在 BAR 文档中已经看过的绑定在你的设备上的 PCI 协议栈指针。
第二个参数mode
,是你关心的中断类型;
它是例子中显示的两个常数之一。
第三个参数是一个整数指针用来返回你的设备支持的特定类型中断数量。
决定支持的中断类型,接下来你可以调用 pci_set_irq_mode() 来指明你想要使用的中断类型。
最后,你可以调用pci_map_interrupt() 来创建选择的中断句柄。注意pci_map_interrupt()有以下的原型:
zx_status_t pci_map_interrupt(const pci_protocol_t* pci,
int which_irq,
zx_handle_t* out_handle);
第一个参数是和上述调用相同的,第二个参数which_irq
表明你想要的设备相关的中断数,第三个参数则是创建的中断句柄指针。
现在你已经有一个中断句柄了。
注意:大多数的设备仅有一个中断,所以通常可以很简单的给
which_irq
传递0
。 如果你的设备有多于一个的中断,通常的经验是在for
循环内运行pci_map_interrupt()函数,并对每个中断依次绑定句柄。
中断等待
在你的 IHT 中,调用zx_interrupt_wait() 来等待中断。 它的函数原型如下:
zx_status_t zx_interrupt_wait(zx_handle_t handle,
zx_time_t* out_timestamp);
第一个参数是你从调用pci_map_interrupt()函数后获取到的句柄,第二个参数可以为NULL
(通常来说),或者是一个时间戳的指针,来指明中断被触发的时间(单位为纳秒,和系统单调时钟源相关,使用zx_clock_get_monotonic()
来获取)。
因此,一个典型的 IHT 将有如下的示例:
static int irq_thread(void* arg) {
my_device_t* dev = arg;
for (;;) {
zx_status_t rc;
rc = zx_interrupt_wait(dev->irq_handle, NULL);
// do stuff
}
}
通常惯例,传递给 IHT 的为你的设备上下文块。
上下文块中包含你从pci_map_interrupt()函数中获取到的句柄(此处为irq_handle
)成员。
边沿 vs 水平中断模式
中断硬件可以处理两种模式中的一个;“边沿”或者“水平”。
在边沿模式下,中断在有效边沿上被激活(当硬件信号从非活动状态到活动状态),并仅触发一次。 这也就是说,信号必须在它们再次识别之前回到非活动状态。
在水平模式下,当硬件信号是活动状态时中断也保持有效。
通常来说,当中断是专用时使用边沿模式,而当中断是被多个设备共享时使用水平模式(因为你想要中断保持活动直到所有设备都失效他们的请求线)。
Zircon 内核根据情况自动屏蔽和取消屏蔽中断。 对于水平触发硬件中断来说, zx_interrupt_wait() 在返回之前屏蔽中断,并在下次调用时取消屏蔽。 对于边沿触发中断来说,中断保持非屏蔽状态。
IHT 中不应该运行任何长时间运行的任务。 对于驱动来说,运行长时间的任务,应该使用工作线程。
使用中断关闭驱动
为了使用中断完全地关闭驱动,你可以使用 zx_interrupt_destroy() 来退出 zx_interrupt_wait() 调用。
这样做的话,当前台线程确定驱动程序应该被关闭时,它只需要销毁中断句柄,就可以使 IHT 关闭:
static void main_thread() {
...
if (shutdown_requested) {
// destroy the handle, this will cause zx_interrupt_wait() to pop
zx_interrupt_destroy(dev->irq_handle);
// wait for the IHT to finish
thrd_join(dev->iht, NULL);
}
...
}
static int irq_thread(void* arg) {
...
for(;;) {
zx_status_t rc;
rc = zx_interrupt_wait(dev->irq_handle, NULL);
if (rc == ZX_ERR_CANCELED) {
// we are being shut down, do any cleanups required
...
return;
}
...
}
}
当请求关闭时,主线程销毁中断句柄。
这将引起 IHT 的
zx_interrupt_wait()
调用带着错误码被唤醒。
IHT 查看错误码(在这种使用场景中为ZX_ERR_CANCELED
),并决定结束。
同时,主线程使用调用thrd_join()来等待 IHT 的子线程完成。
一旦 IHT 退出后,thrd_join()函数返回,主线程可以完成它的其他处理。
更高阶的使用读者可以参考一些其他可用的中断相关函数。