xenomai4的dovetail学习(2)——oob和中断管理
文章目录
- 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_PIPELINE
p->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);
else
irq_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_EVL
evl_enter_irq();
#endif
}
static inline void irq_exit_pipeline(void)
{
#ifdef CONFIG_EVL
evl_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;
}
#endif
if (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模拟硬件中断,因此可以立即执行。