为什么 Linux 内核不会在 returns IRQ_HANDLED 的共享 IRQ 的第一个处理程序处停止?
Why does the Linux kernel not stop at the first handler for a shared IRQ that returns IRQ_HANDLED?
我确定这是有充分理由的,但我看不出它是什么。在 __handle_irq_event_percpu
内,内核遍历为特定 IRQ 行注册的所有处理程序并调用它。我不明白的是为什么当到达第一个返回 IRQ_HANDLED
的处理程序时不退出此循环?看似简单的性能提升,肯定有不明白的地方。
有人知道为什么吗?
在Linux源代码树中,__handle_irq_event_percpu()在kernel/irq/handle.c:
irqreturn_t __handle_irq_event_percpu(struct irq_desc *desc, unsigned int *flags)
{
irqreturn_t retval = IRQ_NONE;
unsigned int irq = desc->irq_data.irq;
struct irqaction *action;
record_irq_time(desc);
for_each_action_of_desc(desc, action) {
irqreturn_t res;
trace_irq_handler_entry(irq, action);
res = action->handler(irq, action->dev_id);
trace_irq_handler_exit(irq, action, res);
if (WARN_ONCE(!irqs_disabled(),"irq %u handler %pS enabled interrupts\n",
irq, action->handler))
local_irq_disable();
switch (res) {
case IRQ_WAKE_THREAD:
/*
* Catch drivers which return WAKE_THREAD but
* did not set up a thread function
*/
if (unlikely(!action->thread_fn)) {
warn_no_thread(irq, action);
break;
}
__irq_wake_thread(desc, action);
/* Fall through - to add to randomness */
case IRQ_HANDLED:
*flags |= action->flags;
break;
default:
break;
}
retval |= res;
}
return retval;
}
for_each_action_of_desc(desc, action)宏在IRQ描述符的action列表中运行:
#define for_each_action_of_desc(desc, act) \
for (act = desc->action; act; act = act->next)
[...]
struct irq_desc {
struct irq_common_data irq_common_data;
struct irq_data irq_data;
unsigned int __percpu *kstat_irqs;
irq_flow_handler_t handle_irq;
struct irqaction *action; /* IRQ action list */
[...]
struct irqaction {
irq_handler_t handler;
void *dev_id;
void __percpu *percpu_dev_id;
struct irqaction *next;
irq_handler_t thread_fn;
struct task_struct *thread;
struct irqaction *secondary;
unsigned int irq;
unsigned int flags;
unsigned long thread_flags;
unsigned long thread_mask;
const char *name;
struct proc_dir_entry *dir;
} ____cacheline_internodealigned_in_smp;
如果中断线路由多个设备共享,则操作列表中有多个条目。 因此,多个设备可能同时进入中断状态。因此,对所有共享线路的设备调用该动作以检查是否有事情要做。
N.B.:
- This answer 在这个问题上有更好的争论
- 这个blog article描述了Linux内核中中断处理的步骤。
Inside __handle_irq_event_percpu the kernel loops over all the handlers registered for a particular IRQ line and calls it. What I don't understand is why this loop isn't exited when the first handler returning IRQ_HANDLED is reached? It seems like a simple performance improvement, so there must be something I don't understand.
有 2 种情况需要考虑 - 共享边沿触发 IRQ 和共享电平触发 IRQ。
共享边缘触发的 IRQ
在这种情况下,2 个或更多设备可以同时或在相似的时间发送 IRQ。如果发生这种情况,并且当第一个处理程序 returns IRQ_HANDLED
时退出“for each driver”循环,那么其他设备 can/will 会陷入“等待 IRQ 处理程序的注意”状态(大多数可能导致设备永久锁定)。为避免这种情况,对于边沿触发的 IRQ,内核的“for each driver”循环必须通知所有驱动程序(并且不能在 returns IRQ_HANDLED
时立即停止)。
请注意,共享边沿触发的 IRQ 很少见。对于 80x86 PC,当有超过 2 个串行端口控制器时(这可以通过对所有串行端口控制器使用相同的驱动程序并在驱动程序中而不是在内核的 IRQ 管理代码中处理问题来解决),但除了共享边缘触发的 IRQ 根本不存在(在 80x86 PC 上)。
共享级别触发的 IRQ
在这种情况下,2个或更多设备可以同时或相近时间发送一个IRQ;但是如果发生这种情况并且当第一个处理程序 returning IRQ_HANDLED
时退出“for each driver”循环,那么其他 IRQ(来自其他设备)不会丢失。相反,中断控制器将看到“至少一个设备仍在触发级别”,并将重新发出 IRQ(并继续发送更多 IRQ,直到所有设备都得到满足)。
对于共享级别触发的 IRQ,这是一种性能妥协(与“正确性”无关)。更具体地说:
如果多个设备很可能同时或相似地需要关注;那么您可以通过继续循环来提高性能(当驱动程序 returns IRQ_HANDLED
时),因为这可能会避免中断控制器重新发出 IRQ 的成本。
如果多个设备不太可能同时或相似地需要关注;那么您可以通过在驱动程序 returns IRQ_HANDLED
时立即停止循环来提高性能,因为这可能会避免执行不必要的设备驱动程序的中断处理程序的成本。
请注意,这取决于调用设备驱动程序的 IRQ 处理程序的顺序。要理解这一点,请想象有 2 个设备共享一条 IRQ 线,并且几乎所有 IRQ 都来自第一个设备。如果第一个设备的驱动程序的 IRQ 处理程序首先被调用 returns IRQ_HANDLED
那么第二个设备不太可能同时发送一个 IRQ;但是如果第二个设备的驱动程序的 IRQ 处理程序首先被调用 returns IRQ_HANDLED
那么很可能第二个设备也同时发送了一个 IRQ。
换句话说;如果内核按照“设备发送 IRQ 的机会”的顺序对设备驱动程序列表进行排序;那么更有可能一旦驱动程序 returns IRQ_HANDLED
停止循环将提高性能(并且第一个调用的驱动程序更有可能 return IRQ_HANDLED
早点)。
但是跟踪统计数据和“变得更聪明”(确定如何根据这些统计数据动态优化性能)也会增加一些开销,并且(至少在理论上,特别是如果设备驱动程序的中断处理程序无论如何都非常快) 这可能会比您获得的性能成本更高。
本质上;量化和最大化潜在收益需要大量工作(研究、基准测试);而且不打扰要容易得多(并且总是调用所有设备驱动程序的中断处理程序”,即使情况更糟)。
我确定这是有充分理由的,但我看不出它是什么。在 __handle_irq_event_percpu
内,内核遍历为特定 IRQ 行注册的所有处理程序并调用它。我不明白的是为什么当到达第一个返回 IRQ_HANDLED
的处理程序时不退出此循环?看似简单的性能提升,肯定有不明白的地方。
有人知道为什么吗?
在Linux源代码树中,__handle_irq_event_percpu()在kernel/irq/handle.c:
irqreturn_t __handle_irq_event_percpu(struct irq_desc *desc, unsigned int *flags)
{
irqreturn_t retval = IRQ_NONE;
unsigned int irq = desc->irq_data.irq;
struct irqaction *action;
record_irq_time(desc);
for_each_action_of_desc(desc, action) {
irqreturn_t res;
trace_irq_handler_entry(irq, action);
res = action->handler(irq, action->dev_id);
trace_irq_handler_exit(irq, action, res);
if (WARN_ONCE(!irqs_disabled(),"irq %u handler %pS enabled interrupts\n",
irq, action->handler))
local_irq_disable();
switch (res) {
case IRQ_WAKE_THREAD:
/*
* Catch drivers which return WAKE_THREAD but
* did not set up a thread function
*/
if (unlikely(!action->thread_fn)) {
warn_no_thread(irq, action);
break;
}
__irq_wake_thread(desc, action);
/* Fall through - to add to randomness */
case IRQ_HANDLED:
*flags |= action->flags;
break;
default:
break;
}
retval |= res;
}
return retval;
}
for_each_action_of_desc(desc, action)宏在IRQ描述符的action列表中运行:
#define for_each_action_of_desc(desc, act) \
for (act = desc->action; act; act = act->next)
[...]
struct irq_desc {
struct irq_common_data irq_common_data;
struct irq_data irq_data;
unsigned int __percpu *kstat_irqs;
irq_flow_handler_t handle_irq;
struct irqaction *action; /* IRQ action list */
[...]
struct irqaction {
irq_handler_t handler;
void *dev_id;
void __percpu *percpu_dev_id;
struct irqaction *next;
irq_handler_t thread_fn;
struct task_struct *thread;
struct irqaction *secondary;
unsigned int irq;
unsigned int flags;
unsigned long thread_flags;
unsigned long thread_mask;
const char *name;
struct proc_dir_entry *dir;
} ____cacheline_internodealigned_in_smp;
如果中断线路由多个设备共享,则操作列表中有多个条目。 因此,多个设备可能同时进入中断状态。因此,对所有共享线路的设备调用该动作以检查是否有事情要做。
N.B.:
- This answer 在这个问题上有更好的争论
- 这个blog article描述了Linux内核中中断处理的步骤。
Inside __handle_irq_event_percpu the kernel loops over all the handlers registered for a particular IRQ line and calls it. What I don't understand is why this loop isn't exited when the first handler returning IRQ_HANDLED is reached? It seems like a simple performance improvement, so there must be something I don't understand.
有 2 种情况需要考虑 - 共享边沿触发 IRQ 和共享电平触发 IRQ。
共享边缘触发的 IRQ
在这种情况下,2 个或更多设备可以同时或在相似的时间发送 IRQ。如果发生这种情况,并且当第一个处理程序 returns IRQ_HANDLED
时退出“for each driver”循环,那么其他设备 can/will 会陷入“等待 IRQ 处理程序的注意”状态(大多数可能导致设备永久锁定)。为避免这种情况,对于边沿触发的 IRQ,内核的“for each driver”循环必须通知所有驱动程序(并且不能在 returns IRQ_HANDLED
时立即停止)。
请注意,共享边沿触发的 IRQ 很少见。对于 80x86 PC,当有超过 2 个串行端口控制器时(这可以通过对所有串行端口控制器使用相同的驱动程序并在驱动程序中而不是在内核的 IRQ 管理代码中处理问题来解决),但除了共享边缘触发的 IRQ 根本不存在(在 80x86 PC 上)。
共享级别触发的 IRQ
在这种情况下,2个或更多设备可以同时或相近时间发送一个IRQ;但是如果发生这种情况并且当第一个处理程序 returning IRQ_HANDLED
时退出“for each driver”循环,那么其他 IRQ(来自其他设备)不会丢失。相反,中断控制器将看到“至少一个设备仍在触发级别”,并将重新发出 IRQ(并继续发送更多 IRQ,直到所有设备都得到满足)。
对于共享级别触发的 IRQ,这是一种性能妥协(与“正确性”无关)。更具体地说:
如果多个设备很可能同时或相似地需要关注;那么您可以通过继续循环来提高性能(当驱动程序 returns
IRQ_HANDLED
时),因为这可能会避免中断控制器重新发出 IRQ 的成本。如果多个设备不太可能同时或相似地需要关注;那么您可以通过在驱动程序 returns
IRQ_HANDLED
时立即停止循环来提高性能,因为这可能会避免执行不必要的设备驱动程序的中断处理程序的成本。
请注意,这取决于调用设备驱动程序的 IRQ 处理程序的顺序。要理解这一点,请想象有 2 个设备共享一条 IRQ 线,并且几乎所有 IRQ 都来自第一个设备。如果第一个设备的驱动程序的 IRQ 处理程序首先被调用 returns IRQ_HANDLED
那么第二个设备不太可能同时发送一个 IRQ;但是如果第二个设备的驱动程序的 IRQ 处理程序首先被调用 returns IRQ_HANDLED
那么很可能第二个设备也同时发送了一个 IRQ。
换句话说;如果内核按照“设备发送 IRQ 的机会”的顺序对设备驱动程序列表进行排序;那么更有可能一旦驱动程序 returns IRQ_HANDLED
停止循环将提高性能(并且第一个调用的驱动程序更有可能 return IRQ_HANDLED
早点)。
但是跟踪统计数据和“变得更聪明”(确定如何根据这些统计数据动态优化性能)也会增加一些开销,并且(至少在理论上,特别是如果设备驱动程序的中断处理程序无论如何都非常快) 这可能会比您获得的性能成本更高。
本质上;量化和最大化潜在收益需要大量工作(研究、基准测试);而且不打扰要容易得多(并且总是调用所有设备驱动程序的中断处理程序”,即使情况更糟)。