文章目录
- OUT-OF-BAND启用和禁用
- stage升级
- oob中断
- 通知oob irq进入/退出evl
- 禁用/启用CPU中断
- 禁用/启用oob中断
- oob的IPI(Inter-Processor Interrupt)
- 注入IRQ
- 拓展 IRQ work API
OUT-OF-BAND启用和禁用
在将中断送到带外处理程序之前,需要启用oob中断。enable_oob_stage用于在所有CPU上设置带外中断阶段,并启用该阶段。
int enable_oob_stage(const char *name)
{struct irq_event_map *map;struct irq_stage_data *p;int cpu, ret;// 检查是否已有带外中断阶段if (oob_stage_present()) return -EBUSY;/* Set up the out-of-band interrupt stage on all CPUs. *//* 遍历所有可能的CPU,初始化每个CPU的带外中断阶段数据 */for_each_possible_cpu(cpu) {p = &per_cpu(irq_pipeline.stages, cpu)[1];map = p->log.map; /* save/restore after memset(). */memset(p, 0, sizeof(*p));p->stage = &oob_stage;memset(map, 0, sizeof(struct irq_event_map));p->log.map = map;
#ifdef CONFIG_DEBUG_IRQ_PIPELINEp->cpu = cpu;
#endif}/* 调用架构特定的函数 arch_enable_oob_stage 启用带外中断阶段。*/ret = arch_enable_oob_stage();if (ret)return ret;/*设置带外中断阶段的名称和索引,并打印信息。*/oob_stage.name = name;smp_wmb();oob_stage.index = 1;pr_info("IRQ pipeline: high-priority %s stage added.\n", name);return 0;
}
disable_oob_stage()用于禁用带外(OOB)阶段,通常是因为evl和pipeline启用失败后调用:
void disable_oob_stage(void)
{const char *name = oob_stage.name;WARN_ON(!running_inband() || !oob_stage_present());oob_stage.index = 0;smp_wmb();pr_info("IRQ pipeline: %s stage removed.\n", name);
}
stage升级
为了运行特定的函数,需要从inband升级到oob,调用run_oob_call函数。run_oob_call()是一个轻量级操作,在调用期间将CPU切换到带外中断阶段,不切换上下文:
- 保存中断状态并切换到OOB阶段;
- 执行回调函数;
- 根据入口和出口状态,同步所有或当前阶段;
- 恢复中断状态并返回回调结果。
/**
* run_oob_call - escalate function call to the oob stage
* @fn: address of routine
* @arg: routine argument
*
* Make the specified function run on the oob stage, switching
* the current stage accordingly if needed. The escalated call is
* allowed to perform a stage migration in the process.
*/
int notrace run_oob_call(int (*fn)(void *arg), void *arg)
{struct irq_stage_data *p, *old;struct irq_stage *oob;unsigned long flags;int ret, s;flags = hard_local_irq_save();/* Switch to the oob stage if not current. */p = this_oob_staged();oob = p->stage;old = current_irq_staged;if (old != p)switch_oob(p);s = test_and_stall_oob();barrier();ret = fn(arg);hard_local_irq_disable();if (!s)unstall_oob();/** The exit logic is as follows:** ON-ENTRY AFTER-CALL EPILOGUE** oob oob sync current stage if !stalled* inband oob switch to inband + sync all stages* oob inband sync all stages* inband inband sync all stages** Each path which has stalled the oob stage while running on* the inband stage at some point during the escalation* process must synchronize all stages of the pipeline on* exit. Otherwise, we may restrict the synchronization scope* to the current stage when the whole sequence ran on the oob* stage.*/p = this_oob_staged();if (likely(current_irq_staged == p)) {if (old->stage == oob) {if (!s && stage_irqs_pending(p))sync_current_irq_stage();goto out;}switch_inband(this_inband_staged());}sync_irq_stage(oob);
out:hard_local_irq_restore(flags);return ret;
}
irq_switch_oob()函数开启或关闭指定中断切换到oob阶段处理。当一个中断已经被请求,但没有指定 IRQF_OOB 标志时,可以使用 irq_switch_oob() 来手动启用或禁用带外交付。这在某些情况下非常有用,例如,当系统需要动态调整中断处理的优先级时。
irq_switch_oob(DEVICE_IRQ, true); // 启用带外交付
irq_switch_oob(DEVICE_IRQ, false); // 禁用带外交付int irq_switch_oob(unsigned int irq, bool on)
{struct irq_desc *desc;unsigned long flags;int ret = 0;desc = irq_get_desc_lock(irq, &flags, 0);if (!desc)return -EINVAL;if (!desc->action)ret = -EINVAL;else if (on)irq_settings_set_oob(desc);elseirq_settings_clr_oob(desc);irq_put_desc_unlock(desc, flags);return ret;
}
oob中断
Dovetail引入新的中断类型标志,带有该标志的IRQ在带外阶段处理:
/* IRQF_OOB - Interrupt is attached to an out-of-band handler living- on the heading stage of the interrupt pipeline- (CONFIG_IRQ_PIPELINE). It may be delivered to the- handler any time interrupts are enabled in the CPU,- regardless of the (virtualized) interrupt state- maintained by local_irq_save/disable().*/
#define IRQF_OOB 0x00200000
oob中断同样使用linux kernel里的中断注册和注销函数。
linux kernel中常见的三种中断注册函数:
- setup_irq():用于早期注册特殊中断,通常在内核初始化阶段,用于注册那些需要在系统启动时就初始化的中断。
- request_irq() :用于注册设备中断,适用于大多数设备驱动程序。
- __request_percpu_irq():用于注册每个CPU中断,适用于那些需要为每个 CPU 分配独立中断处理程序的场景。
常见的两种注销中断函数:free_irq()和free_percpu_irq()。
若oob中断是共享的,同一中断通道上的所有其他处理程序都需要具有IRQF_OOB标志。
以下是注册oob中断流程:
#include <linux/interrupt.h>static irqreturn_t oob_interrupt_handler(int irq, void *dev_id)
{...return IRQ_HANDLED;
}init __init driver_init_routine(void)
{int ret;...ret = request_irq(DEVICE_IRQ, oob_interrupt_handler,IRQF_OOB, "Out-of-band device IRQ",device_data);if (ret)goto fail;return 0;
fail:/* Unwind upon error. */...
}
通知oob irq进入/退出evl
当中断发生和退出时,需要通知evl。evl要阻止中断结束时任何进一步的任务重新调度,由evl来进行调度。
当 Dovetail 收到硬件中断时,会在进入中断处理流程时调用 irq_enter_pipeline(),在退出中断处理流程时调用 irq_exit_pipeline()。这些函数作为钩子,允许evl对中断事件做出反应。例如,evl可能会在中断上下文中阻止进一步的任务调度,以确保实时任务的可预测性。
static inline void irq_enter_pipeline(void)
{
#ifdef CONFIG_EVLevl_enter_irq();
#endif
}static inline void irq_exit_pipeline(void)
{
#ifdef CONFIG_EVLevl_exit_irq();
#endif
}
禁用/启用CPU中断
由于Dovetail将linux的中断虚拟化了,所以要将对应的中断控制函数进行修改。当pipeline启用时,常规的 local_irq_*() 内核 API 只能控制带内(in-band)阶段的中断禁用, **hard_local_irq_*()**控制实际的硬件中断。具体函数如下:
虽然inband中断被虚拟化,但在开关带内中断时依然需要关闭硬件中断,然后开启inband中断后检查是否有挂起的中断且不在管道中,如果有则同步当前中断阶段并恢复中断,否则直接恢复中断。
#define local_irq_enable() do { raw_local_irq_enable(); } while (0)
#define raw_local_irq_enable() arch_local_irq_enable()
static inline notrace void arch_local_irq_enable(void)
{barrier();inband_irq_enable(); // 启用虚拟中断
}
/*** inband_irq_enable - enable interrupts for the inband stage** Enable interrupts for the inband stage, allowing interrupts to* preempt the in-band code. If in-band IRQs are pending for the* inband stage in the per-CPU log at the time of this call, they* are played back.** The caller is expected to tell the tracer about the change, by* calling trace_hardirqs_on().*/
notrace void inband_irq_enable(void)
{/** We are NOT supposed to enter this code with hard IRQs off.* If we do, then the caller might be wrongly assuming that* invoking local_irq_enable() implies enabling hard* interrupts like the legacy I-pipe did, which is not the* case anymore. Relax this requirement when oopsing, since* the kernel may be in a weird state.*/WARN_ON_ONCE(irq_pipeline_debug() && hard_irqs_disabled());__inband_irq_enable();
}static void __inband_irq_enable(void)
{struct irq_stage_data *p;unsigned long flags;check_inband_stage();flags = hard_local_irq_save();unstall_inband_nocheck();p = this_inband_staged();if (unlikely(stage_irqs_pending(p) && !in_pipeline())) {sync_current_irq_stage();hard_local_irq_restore(flags);preempt_check_resched();} else {hard_local_irq_restore(flags);}
}
禁用/启用oob中断
与inband中断类似,oob中断也有一个stall位标志。
- 当stall=1时,带外阶段的事件日志中可能挂起的中断不会被处理;
- 当stall=0时,挂起的带外处理程序会被触发。
static __always_inline void oob_irq_disable(void)
{hard_local_irq_disable(); // 禁用硬件中断stall_oob(); // 设置stall位
}
oob的IPI(Inter-Processor Interrupt)
带外 IPI 是一种特殊的中断机制,用于在多处理器系统中通知远程 CPU 执行特定的操作。这些操作通常与任务调度、定时器管理等实时性要求较高的任务相关。带外 IPI 与常规的中断处理不同,它们通过一个独立的通道发送,以确保高优先级的任务能够及时得到处理。
- RESCHEDULE_OOB_IPI:用于跨 CPU 的任务重新调度请求。例如,一个在CPU #1上休眠的任务可能会被从CPU #0发出的系统调用解除阻塞:在这种情况下,运行在CPU #0上的调度程序代码应该告诉CPU #1它应该重新调度。通常,EVL核心通过test_resched()进行IPI调度。
/* hard irqs off. */
static __always_inline bool test_resched(struct evl_rq *this_rq)
{bool need_resched = evl_need_resched(this_rq);#ifdef CONFIG_SMP/* Send resched IPI to remote CPU(s). */if (unlikely(!cpumask_empty(&this_rq->resched_cpus))) {irq_send_oob_ipi(RESCHEDULE_OOB_IPI, &this_rq->resched_cpus);cpumask_clear(&this_rq->resched_cpus);this_rq->local_flags &= ~RQ_SCHED;}
#endifif (need_resched)this_rq->flags &= ~RQ_SCHED;return need_resched;
}
- TIMER_OOB_IPI:用于跨 CPU 的定时器重新调度请求。当某个 CPU 上的定时器状态发生变化(例如,停止定时器或迁移定时器中断)时,可以通过发送 TIMER_OOB_IPI 通知其他 CPU 更新其硬件定时器设置。
- CALL_FUNCTION_OOB_IPI:用于在带外阶段调用smp_call_function_oob()执行特定的回调函数。这与常规的 smp_call_function_single() 函数类似,但回调函数在带外阶段执行。
使用irq_send_oob_ipi()发送带外 IPI 的函数,参数irq只能是 RESCHEDULE_OOB_IPI、TIMER_OOB_IPI 或 CALL_FUNCTION_OOB_IPI类型的IPI,cpumask指定哪些 CPU 应该接收该 IPI。为了接收这些 IPI,必须为它们设置带外处理程序,并指定 IRQF_OOB 标志。irq_send_oob_ipi() 函数在内部对调用者进行了序列化处理,因此可以从带内或带外阶段调用。
// arm架构下的irq_send_oob_ipi实现
void irq_send_oob_ipi(unsigned int irq,const struct cpumask *cpumask)
{unsigned int sgi = irq - ipi_irq_base;if (WARN_ON(irq_pipeline_debug() &&(sgi < OOB_IPI_OFFSET ||sgi >= OOB_IPI_OFFSET + OOB_NR_IPI)))return;/* Out-of-band IPI (SGI1-2). */__smp_cross_call(cpumask, sgi);
}
注入IRQ
irq_inject_pipeline() 函数用于在当前 CPU 上注入一个 IRQ 事件,模拟硬件中断的发生。这在某些特定情况下非常有用,例如需要在软件中触发中断处理程序以处理某些紧急任务。需要确保硬件中断禁用,以避免在记录过程中被其他中断打断。
除了模拟硬件中断外,还可以直接将 IRQ 事件记录到中断日志中进行处理。irq_post_inband()和irq_post_oob()用于直接记录 IRQ 事件的函数,在特定情况下直接将IRQ 事件标记为挂起,而不需要通过完整的中断处理流程。
- irq_post_inband:当前阶段是带外阶段,事件需要推迟到带内阶段处理时。当前阶段是带内阶段但关中断,事件需要标记为挂起,直到开中断时再处理。
- irq_post_oob:当前阶段是带外阶段但关中断,将 IRQ 事件直接标记为挂起在当前 CPU 的带外阶段日志中。
拓展 IRQ work API
由于不能直接在oob阶段调用inband函数,所以使用被pipeline拓展的irq_work_queue()和irq_work_queue_on()存储这些函数。当切换到inband后,从队列中取出执行。
pipeline将这种irq引入,称为合成中断(Synthetic IRQs)。这种中断完全是软件产生的,不涉及架构和硬件。由于通用管道流程适用于合成中断,因此可以将此类中断附加到带外(out-of-band)和/或带内(in-band)处理程序,就像设备中断一样。
合成中断和软中断(softirqs)本质上是不同的:后者仅存在于带内上下文中,因此无法触发带外活动。
合成中断向量从 synthetic_irq_domain 分配,使用 irq_create_direct_mapping() 函数。
void __init irq_pipeline_init(void)
{
...
/** So far, internally we need one sirq for the tick proxy and* another one to relay the inband work. The companion core* may need a few of them as well. Assume 1024 is (more than)* enough system-wide.** CAVEAT: __irq_domain_add() is a bit fragile, max_irq must* not translate to a negative value when coerced to a signed* int, otherwise the domain creation would fail.*/
synthetic_irq_domain = irq_domain_add_nomap(NULL, 1024,&sirq_domain_ops,NULL);
...
}
可以安装合成中断处理程序,用于根据来自带外环境的调度请求在带内上运行,如下所示:
#include <linux/irq_pipeline.h>static irqreturn_t sirq_handler(int sirq, void *dev_id)
{do_in_band_work();return IRQ_HANDLED;
}static struct irqaction sirq_action = {.handler = sirq_handler,.name = "In-band synthetic interrupt",.flags = IRQF_NO_THREAD,
};unsigned int alloc_sirq(void)
{unsigned int sirq;sirq = irq_create_direct_mapping(synthetic_irq_domain);if (!sirq)return 0;setup_percpu_irq(sirq, &sirq_action);return sirq;
}
也可以安装合成中断处理程序,用于在带内环境触发时从带外阶段运行,如下所示:
static irqreturn_t sirq_oob_handler(int sirq, void *dev_id)
{do_out_of_band_work();return IRQ_HANDLED;
}unsigned int alloc_sirq(void)
{unsigned int sirq;sirq = irq_create_direct_mapping(synthetic_irq_domain);if (!sirq)return 0;ret = __request_percpu_irq(sirq, sirq_oob_handler,IRQF_OOB,"Out-of-band synthetic interrupt",dev_id);if (ret) {irq_dispose_mapping(sirq);return 0;}return sirq;
}
可以从带外上下文以两种不同的方式调度(或发布)sirq_handler() 在带内上下文中的执行:
- 使用通用注入服务:
irq_inject_pipeline(sirq);
- 使用轻量级注入方法(需要在 CPU 中禁用中断):
unsigned long flags = hard_local_irqsave();
irq_post_inband(sirq);
hard_local_irqrestore(flags);
类似的,从带外阶段触发 SIRQ 也有上述两种方式。但如果使用irq_post_oob,并不会立即触发oob中断,因为只是将irq挂起在带外日志中,需要同步中断日志后才会执行。但irq_inject_pipeline模拟硬件中断,因此可以立即执行。