4.4_timer中断从Xenomai流向Linux

4.4 timer中断从Xenomai流向Linux

4.4.1 timer中断的3个逻辑中断号

以timer中断为例,来说明在引入了IPIPE Patch后,从Xenomai到Linux的中断处理流程。在之前的章节《4.2.1 通过DTS传递物理中断号给Linux》和《4.2.4 根据DTS完成timer初始化》,已经系统的讨论了timer的初始化,先简单总结一下。

DTS中timer节点定义的4个物理中断号,都转换成Linux virq存到数组arch_timer_ppi中。以我用的QEMU virt为例,arch_timer_of_init调用arch_timer_select_ppi()选择了ARCH_TIMER_VIRT_PPI,它的hwirq 等于27,它的virq等于3,存在arch_timer_ppi[ARCH_TIMER_VIRT_PPI]。

在这里插入图片描述

把IPIPE引入的Head域和Root域也加进来一起考虑,它们的XIRQ用与virq的相同的数值,来完成映射。如下图所示,展示了timer中断ARCH_TIMER_VIRT_PPI(hwirq 27)对应的3个逻辑中断号:

  • Linux virq: 3

  • Head domain XIRQ: 3

  • Root domain XIRQ: 3

在这里插入图片描述

对应上述3个逻辑中断号,分别注册了三个中断处理程序:

  1. Linux virq 3: 中断处理程序arch_timer_handler_virt

在这里插入图片描述

在timer驱动初始化过程中,arch_timer_register会调用API request_percpu_irq,注册中断处理程序arch_timer_handler_virt。根据《4.3 Linux的中断处理流程》,arch_timer_handler_virt实际上是注册到irqaction->handler。

在这里插入图片描述

  1. Root domain XIRQ 3: 中断处理程序__ipipe_do_IRQ

在这里插入图片描述

参考《3.5.2__ipipe_init()之完成中断处理程序设置》,调用了ipipe_request_irq,设置ipipe_root_domain.irqs[3].handler设置为__ipipe_do_IRQ。

在这里插入图片描述

在这里插入图片描述

  1. Head domain XIRQ 3: 中断处理程序xnintr_core_clock_handler

在这里插入图片描述

参考《4.1 Xenomai如何初始化》,调用了ipipe_request_irq,设置ipipe_head_domain.irqs[3].handler设置为xnintr_core_clock_handler。

在这里插入图片描述

接下来讨论timer中断在中断处理程序之间的流动。

4.4.2 timer中断的应答(__ipipe_dispatch_irq上半部)

在这里插入图片描述

先上图,一图抵千言!

基于《4.3 Linux的中断处理流程》,针对timer中断,加上IPIPE对Linux中断流程的改造以及如何从Xenomai传递到Linux的过程。同时,在图中用黄色高亮了3个中断处理程序。

IPIPE改变了物理中断的响应顺序,参考《2.3.5.3 handle_arch_irq_pipelined函数》,最终回到了gic_handle_irq:

irq_handler->handle_arch_irq_pipelined->handle_arch_irq->gic_handle_irq。

在gic_handle_irq中,IPIPE将gic_handle_irq->handle_domain_irq变成了gic_handle_irq->ipipe_handle_domain_irq,调用irq_find_mapping,从hwirq 27反向映射找出virq 3,传递给__ipipe_grab_irq。

在这里插入图片描述

进入__ipipe_grab_irq后,调用__ipipe_dispatch_irq,来到了IPIPE中断分发的核心函数,如下图函数调用堆栈所示。

在这里插入图片描述

第1506行,通过virq找出irq_desc。

第1507行,chained_irq等于0。

第1512行,ipd指针指向head域ipipe_head_domain。

第1514行,在head domain中,调用ipipe_request_irq注册XIRQ 3的中断处理程序时,会设置对应的ipd->irqs[3].control为IPIPE_HANDLE_MASK。所以此处判断为假,ipd变量仍然指向head域ipipe_head_domain。

第1517行,调用ipd->irqs[3].ackfn(desc),传入的参数是irq_desc。ackfn回调函数,指向__ipipe_ack_hrtimer_irq,它是什么时候设置的?

(1)__fixup_irq_handler函数初始化desc->ipipe_ack

如《4.2.4 根据DTS完成timer初始化》,在timer初始化过程中,会设置中断流控函数。如下图调用堆栈所示,IPIPE增加一个调用__fixup_irq_handler。在这个函数里,会根据已经注册的中断流控函数的类型,来设置irq_desc结构体中新增的两个成员:ipipe_ack和ipipe_end函数指针。

在__fixup_irq_handler的末尾,会把virq在root域的ackfn设为与desc->ipipe_ack相同。

注意,virq在head域的ackfn并没有设置,在哪里初始化呢?

在这里插入图片描述

(2)ipipe_request_irq设置ackfn

ipipe_request_irq向root域或head域注册中断处理程序的时候,还可以注册ackfn函数。如果没有指定ackfn,那么root域保持上述__fixup_irq_handler函数初始化的ackfn;而head域ackfn则和root域保持一致。

在这里插入图片描述

对于timer中断来说,在root域注册中断处理函数__ipipe_do_IRQ时,传入的ackfn为NULL,所以ipipe_root_domain->irqs[3].ackfn和__fixup_irq_handler初始化一致,是__ipipe_ack_fasteoi_irq。

而在head域注册中断处理函数xnintr_core_clock_handler时,指定了ackfn为__ipipe_ack_hrtimer_irq。

在这里插入图片描述

这个函数里面又回调了desc->ipipe_ack和ipipe_end,最终还是得搞清楚__fixup_irq_handler函数初始化的desc->ipipe_ack和ipipe_end。

(3)ackfn到底干啥了?

__ipipe_ack_hrtimer_irq这个函数里面又回调了desc->ipipe_ack和ipipe_end,它们是在__fixup_irq_handler函数初始化的,分别是__ipipe_ack_fasteoi_irq和__ipipe_end_fasteoi_irq。

void __ipipe_ack_fasteoi_irq(struct irq_desc *desc)
{
	desc->irq_data.chip->irq_hold(&desc->irq_data);
}

void __ipipe_end_fasteoi_irq(struct irq_desc *desc)
{
	if (desc->irq_data.chip->irq_release)
		desc->irq_data.chip->irq_release(&desc->irq_data);
}

__ipipe_ack_fasteoi_irq和__ipipe_end_fasteoi_irq分别调用了irq_hold和irq_release,这两个函数指针是IPIPE在irq_chip中新增的成员。在irq-gic-v3.c驱动中,进行了初始化。

static struct irq_chip gic_chip = {
    .name           = "GICv3",
    .irq_mask       = gic_mask_irq,
    .irq_unmask     = gic_unmask_irq,
    .irq_eoi        = gic_eoi_irq,
    .irq_set_type       = gic_set_type,
#ifdef CONFIG_IPIPE
    .irq_hold       = gic_hold_irq,
    .irq_release        = gic_release_irq,
#endif
    .irq_set_affinity   = gic_set_affinity,
    .irq_get_irqchip_state  = gic_irq_get_irqchip_state,
    .irq_set_irqchip_state  = gic_irq_set_irqchip_state,
    .flags          = IRQCHIP_SET_TYPE_MASKED |
                  IRQCHIP_SKIP_SET_WAKE |
                  IRQCHIP_PIPELINE_SAFE |
                  IRQCHIP_MASK_ON_SUSPEND,
};

gic_hold_irq和gic_release_irq做了什么?

在这里插入图片描述

第315行,gic_hold_irq调用gic_poke_irq写GICD_ICENABLER(GIC Distributor Interrupt Clear-Enable Register)寄存器,屏蔽timer中断hwirq 27.

第322行,gic_hold_irq调用gic_eoi_irq,写SYS_ICC_EOIR1_EL1寄存器,代表中断已处理完成。System Interrupt Controller End of Interrupt Register 1 at Exception Level 1,用于在 EOI 模式 0 下发送 End of Interrupt (EOI) 信号。这个寄存器允许 CPU 通知 GIC 当前中断已经处理完毕,从而使 GIC 可以释放资源并允许其他中断被处理。

第327行,gic_release_irq调用gic_poke_irq写GICD_ISENABLER(GIC Distributor Interrupt Set-Enable Register)寄存器,打开timer中断hwirq 27。

参考Documentation/ipipe.rst,指出了irq_hold和irq_release的实现原则。

在这里插入图片描述

从实际实现来看,timer中断属于percpu中断,,符合percpu->mask[+ack][eoi]的原则。上述第315行和第322行代码,分别对应mask和eoi,是对GIC V3中断控制器的操作。目前还缺少ack,对应是什么?

再次回到__ipipe_ack_hrtimer_irq中,第386行有个timer->ack,最终走到arch_timer_ack函数。

在这里插入图片描述

arch_timer_ack函数用于检查timer定时器控制寄存器中的中断状态位,并在必要时清除中断状态位以确认中断已被处理。

所以说,ack代表了对timer设备自身的中断相关寄存器的操作。

static int arch_timer_ack(const int access, struct clock_event_device *evt)
{
    unsigned long ctrl;

    // 从定时器控制寄存器中读取当前的控制值
    ctrl = arch_timer_reg_read(access, ARCH_TIMER_REG_CTRL, evt);

    // 检查定时器控制寄存器中的中断状态位
    if (ctrl & ARCH_TIMER_CTRL_IT_STAT) {
        // 如果中断状态位被设置,将中断状态位清除
        ctrl |= ARCH_TIMER_CTRL_IT_MASK;
        arch_timer_reg_write(access, ARCH_TIMER_REG_CTRL, ctrl, evt);
        return 1; // 返回 1,表示中断已被确认
    }

    // 如果中断状态位没有被设置,返回 0,表示没有中断需要确认
    return 0;
}

小结一下:

IPIPE引入了ack和end的概念,在irq_desc和ipipe_irqdesc中都定义了ack函数,默认情况下,irq_desc->ipipe_ack、root域和head域的ipipe_irqdesc->ackfn都是相同的,但是ipipe_irqdesc->ackfn又可以通过ipipe_request_irq单独指定。

在__ipipe_dispatch_irq中,无论中断没有在head域注册中断处理程序,则会调用root域ipd->irqs[irq].ackfn,对中断控制器进行响应。

补充一点:

什么时候调用irq_desc.ipipe_end?

  1. head domain的中断流控函数,xnintr_irq_handler,如果处理完毕,不再PROPAGATE给root domain,则会调用ipipe_end_irq函数,回调irq_desc->ipipe_end。这个地方有点奇怪,因为在__ipipe_ack_hrtimer_irq 已经调用过一次了irq_desc->ipipe_end。

  2. 假设中断到达了root domain

  • handle_percpu_devid_irq/handle_percpu_irq会显示的调用desc->ipipe_end(desc);

  • handle_fasteoi_irq: 调用cond_release_fasteoi_irq达到了desc->ipipe_end(desc)相同的效果。

4.4.3 timer中断流向Xenomai(__ipipe_dispatch_irq下半部)

在这里插入图片描述

第1543行,ipd在第1541行已经初始化为ipipe_head_domain。因已经注册了Xenomai为head domain,所以此处判断不成立,不会指向goto log。

第1550行,control在第1542行已经初始化为ipipe_head_domain->irqs[3].control。在《4.4.1 timer中断的3个逻辑中断号》,已经澄清了head domain的timer中断XIRQ和timer中断virq的数值是相同的,都是3。IPIPE_HANDLE_MASK的意义就是代表是否已经注册了handler!在ipipe_request_irq会对control进行赋值IPIPE_HANDLE_MASK,ipipe_free_irq会对control赋值0。针对head domain的timer中断XIRQ,因为之前已经调用ipipe_request_irq注册中断处理程序了,所以此处判断成立。

第1551行,flags是 __ipipe_dispatch_irq(unsigned int irq, int flags)的第2个入参。__ipipe_grab_irq调用 __ipipe_dispatch_irq(irq, 0)传入的flags为0,所以此处判断不成立,走到第1554行。但是也需要注意到,如果flags带IPIPE_IRQF_NOSYNC标记,即使是head domain的中断,也会记录到head domain的interrupt log。

第1554行,调用dispatch_irq_head函数。此函数的入参为virq 3,等同于Head domain的XIRQ 3,以3为数组下标,执行head domain已经注册的中断处理程序head->irqs[3].handler即xnintr_core_clock_handler。xnintr_core_clock_handler主体主要调用xnclock_tick(&nklock)更新Xenomai系统时钟,调用xnsched_run()执行调度操作,同时也会在合适的时机把timer中断传递给root domain。

在这里插入图片描述

第1555行,从dispatch_irq_head函数返回后,调用return退出__ipipe_dispatch_irq函数。所以,凡是中断已经在head domain注册了中断处理程序,在__ipipe_dispatch_irq中,是不会把此中断记录到root domain的interrupt log的。而timer中断是一个特例,timer中断必须传递给root domain,因为Linux需要时钟中断。

接下来讨论timer中断如何流向Linux.

4.4.4 timer中断流向root domain(记录到interrupt log)

timer中断在head domain中的中断处理程序xnintr_core_clock_handler,在不同的时机,会把timer中断号3记录到root domain的interrupt log。在《3.4.1.3 IPIPE interrupt log数据结构》中,已经讨论过interrupt log的记录与回放。

第一,xnintr_core_clock_handler起始

在这里插入图片描述

第205行,判断收到中断的CPU是否属于实时调度CPU,如果不属于则执行第207行,调用ipipe_post_irq_root->__ipipe_set_irq_pending(&ipipe_root, irq),将中断号XNARCH_HOST_TICK_IRQ记录到root domain的interrupt log中。

XNARCH_HOST_TICK_IRQ的定义在哪?它实际是读取ipipe_percpu.hrtimer_irq的值。在当前的例子中,ipipe_percpu.hrtimer_irq的值是3,所以中断号3会被记录到root domain的interrupt log中。

arch/arm64/xenomai/ipipe/include/asm/xenomai/machine.h
#define XNARCH_HOST_TICK_IRQ __ipipe_hrtimer_irq

include/linux/ipipe_tickdev.h
#define __ipipe_hrtimer_irq __ipipe_raw_cpu_read(ipipe_percpu.hrtimer_irq)

ipipe_percpu.hrtimer_irq在什么时候初始化?如下图,在Xenomai初始化的过程中,调用ipipe_select_timers为IPIPE选择一个timer。因为此时只有一个timer,所以必然会被选中,它的virq是3,ipipe_percpu.hrtimer_irq也等于3。

在这里插入图片描述

第二,xnintr_core_clock_handler中部

void xnintr_core_clock_handler(void)
{
    …………
    // 减少中断嵌套计数
    if (--sched->inesting == 0) {
        // 清除标志位表示不再处理中断
        sched->lflags &= ~XNINIRQ;
        // 执行调度操作
        xnsched_run();
        // 重新获取当前调度器实例
        sched = xnsched_current();
    }
    …………
}

沿着xnsched_run->__xnsched_run->pipeline_schedule->___xnsched_run,会有2处调用xnintr_host_tick。xnintr_host_tick本质上是调用ipipe_post_irq_root。如上面分析,ipipe_post_irq_root调用__ipipe_set_irq_pending(&ipipe_root, irq),将中断号XNARCH_HOST_TICK_IRQ(3)记录到root domain的interrupt log中。

在这里插入图片描述

第三,xnintr_core_clock_handler末尾

根据注释,可知在timer中断抢占root domain线程时,需调用xnintr_host_tick(sched)向root domain传递timer中断。

void xnintr_core_clock_handler(void)
{
    …………
    /*
     * 如果核心时钟中断抢占了一个实时线程,
     * 任何从实时线程到根线程的转换已经在 xnsched_run() 中触发了主机时钟中断传播。
     * 因此,在这里只需要在中断抢占根线程时传播主机时钟中断。
     */
    if ((sched->lflags & XNHTICK) &&  // 检查是否有主机时钟中断需要传播
        xnthread_test_state(sched->curr, XNROOT))  // 检查当前运行的线程是否是根线程
        // 传播主机时钟中断
        xnintr_host_tick(sched);
}

在head domain中的中断处理程序中,在不同的时机,会把timer中断号3记录到root domain的interrupt log。在《3.4.1.3 IPIPE interrupt log数据结构》中,已经讨论过interrupt log的记录与回放。

4.4.5 timer中断流向Linux(从interrupt log回放)

按上文所述,timer中断3已经记录到root domain的interrupt log。在《3.4.1.3 IPIPE interrupt log数据结构》中,已经讨论过interrupt log的记录与回放。本小结,讨论什么时机来回放interrupt log中的timer中断3。

从interrupt log中查询出未处理的中断,进行处理,为方便描述简称interrupt log回放。核心函数就是__ipipe_next_irq,它是__ipipe_set_irq_pending的反向操作。从下图的函数调用关系可知,IPIPE总是通过调用__ipipe_sync_stage来完成interrupt log回放。

在这里插入图片描述

第一,dispatch_irq_head中进行回放

当dispatch_irq_head中执行head domain已经注册的中断处理程序head->irqs[3].handler(即xnintr_core_clock_handler)返回后,有一个地方会回放root domain的interrupt log。

static void dispatch_irq_head(unsigned int irq) /* hw interrupts off */
{
   …………

    // 调用对应的中断处理程序
    head->irqs[irq].handler(irq, head->irqs[irq].cookie);

…………

    // 重新获取当前CPU的头域上下文
    p = ipipe_this_cpu_head_context();
    __clear_bit(IPIPE_STALL_FLAG, &p->status); // 清除IPIPE_STALL_FLAG标志位

    // 检查是否仍然在头域中运行
    if (likely(__ipipe_current_context == p)) {
        // 检查是否是从头域进入的代码
        if (old->domain == head) {
            // 是的,进行立即同步
            if (__ipipe_ipending_p(p))
                __ipipe_sync_stage();
            return;
        }
        // 切换回根域上下文
        __ipipe_set_current_context(ipipe_this_cpu_root_context());
    }

    /*
     * 此时我们一定是在根域中运行,在根域
     * 回放高优先级中断(慢路径)。
     */
    __ipipe_do_sync_pipeline(head);
}

在dispatch_irq_head,如果检测到当前的上下文已经是root domain,说明没有Xenomai实时任务需要调度,此时会调用__ipipe_do_sync_pipeline。经过层层调用,

__ipipe_do_sync_pipeline->__ipipe_sync_stage->__ipipe_do_sync_stage最终会执行root domain的irqs[3].handler,即之前注册的__ipipe_do_IRQ!

在这里插入图片描述

走到了__ipipe_do_IRQ之后,会回归到Linux中断子系统,调用__handle_domain_irq,顺着Linux的中断处理流程,最终回到Linux中virq 3对应的中断处理程序arch_timer_handler_virt!

在这里插入图片描述

第二,其它情况下进行回放

有很多地方都会进行root domain的中断 3的回放,这里只是贴一下图,暂时不分析。以后分析进程调度的时候,再做详细分析。

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述


对4.4章做一个小结:

timer中断是一个特殊的中断,必须在head domain/Xenomai和root domain/Linux中执行,所以中断发生后,会经过一个巧妙和复杂的过程,完成中断的流动。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值