Linux中断管理机制

转自  https://www.cnblogs.com/arnoldlu/p/8659981.html

新的linux kernel 及 arm不支持 中断嵌套。 

 

 

关键词:GIC、IAR、EOI、SGI/PPI/SPI、中断映射、中断异常向量、中断上下文、内核中断线程、中断注册。

 

由于篇幅较大,简单梳理一下内容。

本章主要可以分为三大部分:

讲解硬件背景的1. ARM中断控制器

系统初始化的静态过程:GIC初始化和各中断的中断号映射2. 硬件中断号和Linux中断号的映射;每个中断的注册5. 注册中断

一个中断从产生到执行完毕的动态过程:ARM底层通用部分如何处理3. ARM底层中断处理;GIC部分的处理流程以及上层通用处理部分4. 高层中断处理

这里的高层处理,没有包括下半部。下半部在Linux中断管理 (2)软中断和taskletLinux中断管理 (3)workqueue工作队列中进行介绍。

1. ARM中断控制器

 

1.1 ARM支持中断类型

ARM GIC-v2支持三种类型的中断:

SGI:软件触发中断(Software Generated Interrupt),通常用于多核间通讯,最多支持16个SGI中断,硬件中断号从ID0~ID15。SGI通常在Linux内核中被用作IPI中断(inter-processor interrupts),并会送达到系统指定的CPU上。

PPI:私有外设中断(Private Peripheral Interrupt),是每个CPU私有的中断。最多支持16个PPI中断,硬件中断号从ID16~ID31。PPI通常会送达到指定的CPU上,应用场景有CPU本地时钟。

SPI:公用外设中断(Shared Peripheral Interrupt),最多可以支持988个外设中断,硬件中断号从ID32~ID1019。

 

1.2 GIC检测中断流程

GIC主要由两部分组成,分别是仲裁单元(Distributor)和CPU接口模块。

GIC仲裁单元为每一个中断维护一个状态机,分别是:inactive、pending、active and pending、active。

下面是来自IHI0048B GIC-V2规格书3.2.4 Interrupt handling state machine截图:

GIC检测中断流程如下:

(1) 当GIC检测到一个中断发生时,会将该中断标记为pending状态(A1)。

(2) 对处于pending状态的中断,仲裁单元回确定目标CPU,将中断请求发送到这个CPU上。

(3) 对于每个CPU,仲裁单元会从众多pending状态的中断中选择一个优先级最高的中断,发送到目标CPU的CPU Interface模块上。

(4) CPU Interface会决定这个中断是否可以发送给CPU。如果该终端优先级满足要求,GIC会发生一个中断信号给该CPU。

(5) 当一个CPU进入中断异常后,会去读取GICC_IAR寄存器来响应该中断(一般是Linux内核的中断处理程序来读寄存器)。寄存器会返回硬件中断号(hardware interrupt ID),对于SGI中断来说是返回源CPU的ID。

     当GIC感知到软件读取了该寄存器后,又分为如下情况:

     * 如果该中断源是pending状态,那么转改将变成active。(C)    

     * 如果该中断又重新产生,那么pending状态变成active and pending。(D)

     * 如果该中断是active状态,现在变成active and pending。(A2)

(6) 当处理器完成中断服务,必须发送一个完成信号EOI(End Of Interrupt)给GIC控制器。软件写GICC_EOIR寄存器,状态变成inactive。(E1)

补充:

(7) 对于level triggered类型中断来说,当触发电平消失,状态从active and pending变成active。(B2)

常用路径是A1->D->B2->E1。

1.2.1 GIC中断抢占

GIC中断控制器支持中断优先级抢占,一个高优先级中断可以抢占一个低优先级且处于active状态的中断,即GIC仲裁单元会记录和比较当前优先级最高的pending状态,然后去抢占当前中断,并且发送这个最高优先级的中断请求给CPU,CPU应答了高优先级中断,暂停低优先级中断服务,进而去处理高优先级中断。

GIC会将pending状态优先级最高的中断请求发送给CPU。

1.2.2 Linux对中断抢占处理

从GIC角度看,GIC会发送高优先级中断请求给CPU。

但是目前CPU处于关中断状态,需要等低优先级中断处理完毕,直到发送EOI给GIC。

然后CPU才会响应pending状态中优先级最高的中断进行处理。

所以Linux下:

1. 高优先级中断无法抢占正在执行的低优先级中断。

2.同处于pending状态的中断,优先响应高优先级中断进行处理。

1.3 GIC中断时序

 

借助GIC-400 Figure B-2 Signaling physical interrupts理解GIC内部工作原理。

M和N都是SPI类型的外设中断,且通过FIQ来处理,高电平触发,N的优先级比M高,他们的目标CPU相同。

(1) T1时刻:GIC的总裁单元检测到中断M的电平变化。

(2) T2时刻:仲裁单元设置中断M的状态为pending。

(3) T17时刻:CPU Interface模块会拉低nFIQCPU[n]信号。在中断M的状态变成pending后,大概需要15个时钟周期后会拉低nFIQCPU[n]信号来向CPU报告中断请求(assertion)。仲裁单元需要这些时间来计算哪个是pending状态下优先级最高的中断。

(4) T42时刻:仲裁单元检测到另外一个优先级更高的中断N。

(5) T43时刻:仲裁单元用中断N替换中断M为当前pending状态下优先级最高的中断,并设置中断N为pending状态。

(6) T58时刻:经过tph个时钟后,CPU Interface拉低你FIOCPU[n]信号来通知CPU。因为此信号在T17时刻已经被拉低,CPU Interface模块会更新GICC_IAR寄存器的Interrupt ID域,该域的值变成中断N的硬件中断号。

(7) T61~T131时刻:Linux对中断N的服务程序--------------------------------------------------------------中断服务程序处理段,从GICC_IAR开始到GICC_EOIR结束。

  T61时刻:CPU(Linux中断服务例程)读取GICC_IAR寄存器,即软件响应了中断N。这时仲裁单元把中断N的状态从pending变成active and pending。读取GICC_IAR

  T64时刻:在中断N被Linux相应3个时钟内,CPU Interface模块完成对nFIQCPU[n]信号的deasserts,即拉高nFIQCPU[n]信号。

  T126时刻:外设也deassert了该中断N。

  T128时刻:仲裁单元移出了中断N的pending状态。

  T131时刻:Linux服务程序把中断N的硬件ID号写入GICC_EOIR寄存器来完成中断N的全部处理过程。写GICC_EOIR

(8) T146时刻:在向GICC_EOIR寄存器写入中断N中断号后的tph个时钟后,仲裁单元会选择下一个最高优先级中断,即中断M,发送中断请求给CPU Interface模块。CPU Interface会拉低nFIQCPU[n]信号来向CPU报告外设M的中断请求。

(9) T211时刻:Linux中断服务程序读取GICC_IAR寄存器来响应中断,仲裁单元设置中断M的状态为active and pending。

(10) T214时刻:在CPU响应中断后的3个时钟内,CPU Interface模块拉高nFIOCPU[n]信号来完成deassert动作。

 

 

 

 

 

 

6. 一个中断的生命

经过上面的分析可以看出一个中断从产生、执行,到最终结束的流程。这里我们用树形代码路径来简要分析一下一个中断的生命周期。

vector_irq()->vector_irq()->__irq_svc()

  ->svc_entry()--------------------------------------------------------------------------保护中断现场

  ->irq_handler()->gic_handle_irq()------------------------------------------------具体到GIC中断控制器对应的就是gic_handle_irq(),此处从架构相关进入了GIC相关处理。

    ->GIC_CPU_INTACK--------------------------------------------------------------读取IAR寄存器,响应中断。

    ->handle_domain_irq()

      ->irq_enter()------------------------------------------------------------------------进入硬中断上下文

      ->generic_handle_irq()

        ->generic_handle_irq_desc()->handle_fasteoi_irq()--------------------根据中断号分辨不同类型的中断,对应不同处理函数,这里中断号取大于等于32。

          ->handle_irq_event()->handle_irq_event_percpu()

            ->action->handler()-----------------------------------------------------------对应到特定中断的处理函数,即上半部

              ->__irq_wake_thread()-----------------------------------------------------如果中断函数处理返回IRQ_WAKE_THREAD,则唤醒中断线程进行处理,但不是立即执行中断线程。

      ->irq_exit()---------------------------------------------------------------------------退出硬中断上下文。视情况处理软中断。

        ->invoke_softirq()-----------------------------------------------------------------处理软中断,超出一定条件任务就会交给软中断线程处理。

    ->GIC_CPU_EOI--------------------------------------------------------------------写EOI寄存器,表示结束中断。至此GIC才会接收新的硬件中断,此前一直是屏蔽硬件中断的。

  ->svc_exit-------------------------------------------------------------------------------恢复中断现场

 从上面的分析可以看出:

  • 中断上半部的处理是关硬件中断的,这里的关硬件中断是GIC就不接收中断处理。直到写EOI之后,GIC仲裁单元才会重新选择中断进行处理。
  • 软中断运行于软中断上下文中,但是仍然是关硬件中断的,这里需要特别注意,软中断需要快速处理并且不能睡眠。
  • 不是所有软中断都运行于软中断上下文中,部分软中断任务可能会交给ksoftirqd线程处理。
  • 包括IRQ_WAKE_THREAD、ksoftirqd、woker等唤醒线程的情况,都不会在中断上下文中进行处理。中断上下文中所做的处理只是唤醒,执行时机交给系统调度。
  • 如果要提高Linux实时性,有两个要点:一是将上半部线程化;另一个是将软中断都交给ksoftirqd线程处理

 

 

 

 

=================================================================================================

中断栈

内核栈


#define MIN_THREAD_SHIFT    (14 + KASAN_THREAD_SHIFT)
#define THREAD_SIZE     (UL(1) << THREAD_SHIFT)

union thread_union {
#ifndef CONFIG_THREAD_INFO_IN_TASK
    struct thread_info thread_info;
#endif
    unsigned long stack[THREAD_SIZE/sizeof(long)];
};

注意这里有一个宏来区分stack的实现方式 CONFIG_THREAD_INFO_IN_TASK ,如果定义了该宏,实际上会通过动态申请物理页来作为stack,并且把stack放到 task_struct 结构体中,用 task_struct->stack 来表示。
中断栈

    对于x86平台:

x86平台上,中断栈是独立于内核栈的存在,两者并不共享,如果是多处理器架构,那么每个CPU都对应有一个中断栈,本文不对x86做介绍。

    对于 ARM平台:

中断栈和内核栈则是共享的,中断栈和内核栈共享有一个负面因素,如果中断发生嵌套,可能会造成栈溢出,从而可能会破坏到内核栈的一些重要数据。

    对于ARM64平台:

中断栈的实现是独立的,并且区分两种情况,分别是vmap申请内存,还是直接静态定义,并且根据CPU个数,每个CPU对应单独的一个stack。


arch/arm64/kernel/irq.c:

#ifdef CONFIG_VMAP_STACK

static void init_irq_stacks(void)

{

    int cpu;

    unsigned long *p;

 

    for_each_possible_cpu(cpu) {

        /*

        * To ensure that VMAP'd stack overflow detection works

        * correctly, the IRQ stacks need to have the same

        * alignment as other stacks.

        */

        p = __vmalloc_node_range(IRQ_STACK_SIZE, THREAD_ALIGN,

                     VMALLOC_START, VMALLOC_END,

                     THREADINFO_GFP, PAGE_KERNEL,

                     0, cpu_to_node(cpu),

                     __builtin_return_address(0));

 

        per_cpu(irq_stack_ptr, cpu) = p;

    }

}

#else

/* irq stack only needs to be 16 byte aligned - not IRQ_STACK_SIZE aligned. */

DEFINE_PER_CPU_ALIGNED(unsigned long [IRQ_STACK_SIZE/sizeof(long)], irq_stack);

 

static void init_irq_stacks(void)

{

    int cpu;

 

    for_each_possible_cpu(cpu)

        per_cpu(irq_stack_ptr, cpu) = per_cpu(irq_stack, cpu);

}

#endif

 

 

 

IRQF_ONESHOT one shot本身的意思的只有一次的,结合到中断这个场景,则表示中断是一次性触发的,不能嵌套。对于primary handler,当然是不会嵌套,但是对于threaded interrupt handler,我们有两种选择,一种是mask该interrupt source,另外一种是unmask该interrupt source。一旦mask住该interrupt source,那么该interrupt source的中断在整个threaded interrupt handler处理过程中都是不会再次触发的,也就是one shot了。这种handler不需要考虑重入问题。
具体是否要设定one shot的flag是和硬件系统有关的,我们举一个例子,比如电池驱动,电池里面有一个电量计,是使用HDQ协议进行通信的,电池驱动会注册一个threaded interrupt handler,在这个handler中,会通过HDQ协议和电量计进行通信。对于这个handler,通过HDQ进行通信是需要一个完整的HDQ交互过程,如果中间被打断,整个通信过程会出问题,因此,这个handler就必须是one shot的。
IRQF_NO_SUSPEND 这个flag比较好理解,就是说在系统suspend的时候,不用disable这个中断,如果disable,可能会导致系统不能正常的resume。

 

 

内核里中断嵌套:

https://blog.csdn.net/juS3Ve/article/details/81437432

问:Linux的中断可以嵌套吗?

答:以前是可以嵌套的,现在不可以!    

个人补充: 以前可以嵌套也仅限于fiq 可以打断irq。

历史

早前的Linux内核版本,中断分为两种:

  1. 快中断,申请的时候带IRQF_DISABLED标记,在IRQ HANDLER里面不允许新的中断进来;

  2. 慢中断,申请的时候不带IRQF_DISABLED标记,在IRQ HANDLER里面允许新的其他中断嵌套进来。

老的Linux内核中,如果一个中断服务程序不想被别的中断打断,我们能看到这样的代码:

request_irq(FLOPPY_IRQ, floppy_interrupt,\
-					    IRQF_DISABLED, "floppy", NULL)

现在

在2010年如下的commit中,IRQF_DISABLED被作废了:

https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=e58aa3d2d0cc

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章