图解Linux内核中断子系统

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/rikeyone/article/details/99648441

本文用到的缩写词汇:

SVC:Supervisor(SVC)
Hyp:Hypervisor(Hyp)
PC:Program Counter (PC)  
CPSR:Current Program Status Register (CPSR) 
SPSRs:Saved Program Status Registers (SPSRs)
LR:Link Register(LR)
Irq hwid:硬件中断号
Irq number:内核中断号

中断的概念

  • ARM工作模式
    ARM processor mode
 7 Processor modes:
        User mode
        SVC mode
        FIQ  mode
        IRQ  mode
        Undef mode
        Abort  mode
        Hyp mode

中断发生时芯片会进入IRQ mode,当FIQ快速中断发生的时候,ARM芯片也会进入FIQ mode, 只是很少有FIQ的外设,所以Linux中没有对FIQ进行处理。对于arm架构的IRQ中断处理分两种情况,一种是从 User mode 切换到 IRQ mode,表示中断打断了用户空间程序的运行;另一种是从SVC mode切换到IRQ mode,表示中断打断了内核的运行。

关于中断的两个问题:

如何区分以上这两种情况呢?

实际上ARM的模式切换是通过设置CPSR寄存器中的CPSR[3:0]区域来进行的,当寄存器设置后,硬件会进行模式的切换,其中包括把切换前的CPSR寄存器内容保存到切换后的SPSR寄存器中。这样当我们在中断模式下查看SPSR[3:0]中的值就可以判断出切换前的模式是什么了。从而区分对待这两种情况采用不同的irq handler去处理。ARM平台上的IRQ mode运行时间是很短暂的,中断处理函数并不是在这里执行的,而是在SVC mode上运行的,在IRQ mode在做完信息保存后会直接通过代码设置CPSR[3:0]切换到SVC mode来执行后面的irq handler处理。

既然中断处理程序每次都要在SVC mode下完成,为什么还要先跳到IRQ mode呢?
因为这是硬件决定的,ARM在处理中断到来时,芯片会做如下一些事情:

把cpsr寄存器的值保存到IRQ mode模式下的spsr寄存器(spsr_irq)
把pc保存到IRQ mode模式下的lr寄存器(lr_irq)
设置cpsr寄存器,切换到IRQ mode模式,并屏蔽中断
设置pc为相应异常处理程序的入口地址

可见,硬件设计决定中断到来时,都是先进入IRQ mode,软件没有办法略过它。

  • ARM64 Exception level
    ARM64 EL mode
    ARM64已经取消了很多模式,只存在EL0/EL1/EL2/EL3这几种异常级别,应用程序运行于EL0,操作系统运行于EL1,虚拟机监控程序运行于EL2,安全监控程序运行于EL3。操作系统只需要支持EL1和EL0这两种即可。

处理器异常类型有4种:

Synchronous Aborts(SVC/HVC/SMC)
FIQ
IRQ
SYSTEM Error (SError)

SError本质上是一种异步外部abort,通常都来自于硬件异常,外部存储、clock等问题导致,Linux内核中,对SError进行了捕获,但是无法修复的,所以最后都会导致终止内核。打印错误打印:

Bad mode in Error handler detected, code 0xbf000002 – SError

处理器的异常发生时又区分了4种场景:
(1)运行级别不发生切换,从ELx变化到ELx,使用SP_EL0,这种情况在Linux kernel都是不处理的,使用invalid handler。
(2)运行级别不发生切换,从ELx变化到ELx,使用SP_ELx。这种情况下在Linux中比较常见。
(3)异常需要进行级别切换来进行处理(EL0到EL1),并且使用aarch64模式处理,如64位用户态程序发生系统调用,CPU会从EL0切换到EL1,并且使用aarch64模式处理异常。
(4)异常需要进行级别切换来进行处理(EL0到EL1),并且使用aarch32模式处理。如32位用户态程序发生系统调用,CPU会从EL0切换到EL1,并且使用aarch32模式进行处理。

针对这4种类型对应的4种场景,一共有16种情况,所以内核需要区分出这16种情况并分别作出处理,我们在后面中断向量表一节介绍。

  • 中断触发方式
    中断方式
    中断分为边沿触发和电平触发方式,如上图所示,边沿触发方式是在高电平和低电平时一直触发,当我们在处理中断时需要对该中断控制器上对应的中断线进行ack和mask处理,不然中断会一直报给中断控制器;而对于边沿触发,只会进来一次通知,所以只需要对中断控制器上的该中断线进行ack应答即可,而不需要mask该中断线。

  • 中断级联
    中断级联
    中断支持级联,由多个中断控制器形成一个树状结构,通过级联可以扩展对中断设备的支持数量。

Linux中断框架

  • 中断框架
    irq_framework

这个图上只是描述了简单的场景,想象一下当下的SOC架构,都是多核SOC,这个时候你就会产生一个疑问,多核心的CPU,中断应该送给哪一个核心去处理呢?

对于单核SOC:
中断控制器全部把中断分发给单核CPU

对于多核SOC:
(1)静态分发
中断控制器可以把中断发送给单个CPU或者一组CPU
(2)动态分发
中断控制器可以对中断进行仲裁从而发送给某一个CPU,体现出来就是会轮流在不同CPU上执行

  • irq affinity

上面介绍了多核处理的中断分发问题,实际上现代操作系统都支持irq affinity功能,也就是说可以根据应用的需求和使用场景来决定当前中断应该被哪个CPU来处理,系统刚启动时是不做限制的,默认是所有CPU都可以接收中断,像上面介绍的采用动态分发机制那样,GIC轮流上报中断给不同的CPU处理,后面可以通过上层应用设置的绑定关系来决定需要哪几个CPU来处理中断,可以针对每个中断做配置。内核实现了irq affinity的相关接口:

cat /proc/irq/1/smp_affinity

实现代码如下:

 #if defined(CONFIG_SMP)
 static int __init irq_affinity_setup(char *str)
 {
     alloc_bootmem_cpumask_var(&irq_default_affinity);
     cpulist_parse(str, irq_default_affinity);
     /*
      * Set at least the boot cpu. We don't want to end up with
      * bugreports caused by random comandline masks
      */
     cpumask_set_cpu(smp_processor_id(), irq_default_affinity);
     return 1;
 }
 __setup("irqaffinity=", irq_affinity_setup);
 
 static void __init init_irq_default_affinity(void)
 {
     if (!cpumask_available(irq_default_affinity))
         zalloc_cpumask_var(&irq_default_affinity, GFP_NOWAIT);
     if (cpumask_empty(irq_default_affinity))
         cpumask_setall(irq_default_affinity);
 }

高通平台上提供的上层服务来进行中断的平衡操作:

root@m1971:/ # ps -ef | grep balance
root          1216     1 0 11:58:00 ?     00:00:04 msm_irqbalance -f /system/vendor/etc/msm_irqbalance.conf
  • 中断向量表(ARM64)
    中断向量表的设置如下:
/*
 * The following fragment of code is executed with the MMU enabled
 *
 *   x0 = __PHYS_OFFSET
 */
__primary_switched:
    adrp    x4, init_thread_union
    add sp, x4, #THREAD_SIZE
    adr_l   x5, init_task
    msr sp_el0, x5          // Save thread_info

    adr_l   x8, vectors         // load VBAR_EL1 with virtual
    msr vbar_el1, x8            // vector table address  //设置vectors异常向量表地址
    isb

    stp xzr, x30, [sp, #-16]!
    mov x29, sp

    str_l   x21, __fdt_pointer, x5      // Save FDT pointer

    ldr_l   x4, kimage_vaddr        // Save the offset between
    sub x4, x4, x0          // the kernel virtual and
    str_l   x4, kimage_voffset, x5      // physical mappings

    // Clear BSS
    adr_l   x0, __bss_start
    mov x1, xzr
    adr_l   x2, __bss_stop
    sub x2, x2, x0
    bl  __pi_memset
    dsb ishst               // Make zero page visible to PTW


这里是通过vbar_el1寄存器是用来保存向量表的寄存器,这里实际上传入的是虚拟地址,因为是在MMU已经是能后配置的寄存器。设置完向量表之后,CPU发生异常后就会跳转到对应的地址执行异常处理函数。

vectors中断向量表定义:

/*
 * Exception vectors.
 */
    .pushsection ".entry.text", "ax"

    .align  11
ENTRY(vectors)
     /*从EL1变化到EL1,使用SP_EL0 */
    kernel_ventry   1, sync_invalid         // Synchronous EL1t
    kernel_ventry   1, irq_invalid          // IRQ EL1t
    kernel_ventry   1, fiq_invalid          // FIQ EL1t
    kernel_ventry   1, error_invalid        // Error EL1t
     /*从EL1变化到EL1,使用SP_EL1 */
    kernel_ventry   1, sync             // Synchronous EL1h
    kernel_ventry   1, irq              // IRQ EL1h
    kernel_ventry   1, fiq_invalid          // FIQ EL1h
    kernel_ventry   1, error_invalid        // Error EL1h
	/* AARCH64模式,从EL0变化到EL1,使用SP_EL1 */
    kernel_ventry   0, sync             // Synchronous 64-bit EL0
    kernel_ventry   0, irq              // IRQ 64-bit EL0
    kernel_ventry   0, fiq_invalid          // FIQ 64-bit EL0
    kernel_ventry   0, error_invalid        // Error 64-bit EL0
	/* AACH32模式,从EL0变化到EL1,使用SP_EL1 */
#ifdef CONFIG_COMPAT
    kernel_ventry   0, sync_compat, 32      // Synchronous 32-bit EL0
    kernel_ventry   0, irq_compat, 32       // IRQ 32-bit EL0
    kernel_ventry   0, fiq_invalid_compat, 32   // FIQ 32-bit EL0
    kernel_ventry   0, error_invalid_compat, 32 // Error 32-bit EL0
#else
    kernel_ventry   0, sync_invalid, 32     // Synchronous 32-bit EL0
    kernel_ventry   0, irq_invalid, 32      // IRQ 32-bit EL0
    kernel_ventry   0, fiq_invalid, 32      // FIQ 32-bit EL0
    kernel_ventry   0, error_invalid, 32        // Error 32-bit EL0
#endif
END(vectors)

平台对应16个异常处理函数,分别对应16中场景,不仅仅表示中断处理,还有其他的一些CPU异常,这里的kernel_ventry是一个宏,它会按照如下的方式展开:

     .macro kernel_ventry, el, label, regsize = 64
     .align 7
......
     b   el\()\el\()_\label
  • 通用中断API
extern int __must_check
request_threaded_irq(unsigned int irq, irq_handler_t handler,
             irq_handler_t thread_fn,
             unsigned long flags, const char *name, void *dev);

static inline int __must_check
request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags,
        const char *name, void *dev)
{
    return request_threaded_irq(irq, handler, NULL, flags, name, dev);
}

extern int __must_check
devm_request_threaded_irq(struct device *dev, unsigned int irq,
              irq_handler_t handler, irq_handler_t thread_fn,
              unsigned long irqflags, const char *devname,
              void *dev_id);
 
static inline int __must_check
devm_request_irq(struct device *dev, unsigned int irq, irq_handler_t handler,
         unsigned long irqflags, const char *devname, void *dev_id)
{                 
    return devm_request_threaded_irq(dev, irq, handler, NULL, irqflags,
                     devname, dev_id);
}

这里需要特别提到一下 request_threaded_irq ,它是最底层的实现,其他的API都是基于它实现的,其中有两个参数,一个是在硬件中断上下文中执行的handler,相当于是上半部,而另一个是在thread上下文执行,用于中断线程化,属于中断的下半部。根据实际需要,我们当然可以只传入其中一个handler来处理中断:如果上半部handler为NULL,那么接口内部会使用默认的上半部handler去处理,如果下半部为NULL,那么将不会执行中断线程化的一些操作。

  • 中断线程化
setup_irq_thread(struct irqaction *new, unsigned int irq, bool secondary)
{
    struct task_struct *t;
    struct sched_param param = {
        .sched_priority = MAX_USER_RT_PRIO/2,
    };

    if (!secondary) {
        t = kthread_create(irq_thread, new, "irq/%d-%s", irq,
                   new->name);
    } else {
        t = kthread_create(irq_thread, new, "irq/%d-s-%s", irq,
                   new->name);
        param.sched_priority -= 1;
    }

    if (IS_ERR(t))
        return PTR_ERR(t);

    sched_setscheduler_nocheck(t, SCHED_FIFO, &param);

中断线程化操作,是在注册一个中断处理程序时,根据传入的handler来创建一个线程用于执行handler,它最终会调用上面的操作来创建一个线程,可以看到它以此创建线程,并且线程的优先级最低位50,其后每注册一个优先级优先级更高一级(减1处理)。并且设置线程的调度类为SCHED_FIFO,也就是属于RT实时调度器的一种,它的优先级比普通进程优先级都要高。

  • irq handler flags
/*
 * These flags used only by the kernel as part of the
 * irq handling routines.
 *
 * IRQF_SHARED - allow sharing the irq among several devices
 * IRQF_PROBE_SHARED - set by callers when they expect sharing mismatches to occur
 * IRQF_TIMER - Flag to mark this interrupt as timer interrupt
 * IRQF_PERCPU - Interrupt is per cpu
 * IRQF_NOBALANCING - Flag to exclude this interrupt from irq balancing
 * IRQF_IRQPOLL - Interrupt is used for polling (only the interrupt that is
 *                registered first in an shared interrupt is considered for
 *                performance reasons)
 * IRQF_ONESHOT - Interrupt is not reenabled after the hardirq handler finished.
 *                Used by threaded interrupts which need to keep the
 *                irq line disabled until the threaded handler has been run.
 * IRQF_NO_SUSPEND - Do not disable this IRQ during suspend.  Does not guarantee
 *                   that this interrupt will wake the system from a suspended
 *                   state.  See Documentation/power/suspend-and-interrupts.txt
 * IRQF_FORCE_RESUME - Force enable it on resume even if IRQF_NO_SUSPEND is set
 * IRQF_NO_THREAD - Interrupt cannot be threaded
 * IRQF_EARLY_RESUME - Resume IRQ early during syscore instead of at device
 *                resume time.
 * IRQF_COND_SUSPEND - If the IRQ is shared with a NO_SUSPEND user, execute this
 *                interrupt handler after suspending interrupts. For system
 *                wakeup devices users need to implement wakeup detection in
 *                their interrupt handlers.
 */
#define IRQF_SHARED     0x00000080
#define IRQF_PROBE_SHARED   0x00000100
#define __IRQF_TIMER        0x00000200
#define IRQF_PERCPU     0x00000400
#define IRQF_NOBALANCING    0x00000800
#define IRQF_IRQPOLL        0x00001000
#define IRQF_ONESHOT        0x00002000
#define IRQF_NO_SUSPEND     0x00004000
#define IRQF_FORCE_RESUME   0x00008000
#define IRQF_NO_THREAD      0x00010000
#define IRQF_EARLY_RESUME   0x00020000
#define IRQF_COND_SUSPEND   0x00040000

#define IRQF_TIMER      (__IRQF_TIMER | IRQF_NO_SUSPEND | IRQF_NO_THREAD)

在注册中断时需要传入对应的标志位,如上所示,注释已经解释的很清楚了,这里我只特别强调一下IRQF_ONESHOT标志位,它是用于在中断线程化时使用的。正常中断处理过程中,本地CPU中断是被禁止的,直到上半部执行完后,才重新使能本地中断,而这个标志是会一直禁止本地CPU上的中断,直到下半部线程中的handler处理后才重新使能。这个标记会被特定的驱动使用。举个例子:有一些设备我们只能通过i2c或者spi总线访问它,但是这些操作总线的API都可能睡眠,所以我们会把上半部handler传入NULL,而在中断线程化中处理该中断,那么就需要一直保持禁止中断,直到线程中的handler函数执行完才使能。

看过旧版本的内核可以还会对一个flags感兴趣:

IRQF_DISABLED

这个标志位什么含义呢?为什么后面又把它删掉了呢?

在旧版本内核中,中断被处理时只会在中断控制器上禁止当前处理的中断线,而不会把当前CPU上的中断禁止掉,也就是说除了本中断不会重复被CPU检测到,其他类型的中断依然会打断CPU的执行,形成中断嵌套,这在旧版本的内核中是被允许的,并且是大量存在的,那这个标志在旧版本中就是为了在执行上半部中断处理函数时,禁止本地CPU中断的,也就是说除了本中断,其他中断也不允许在当前CPU上执行。为什么后面又要把它删除,因为中断嵌套是有风险的,对于中断堆栈是一种风险,如果大量中断到来可能会导致中断栈的溢出,这种风险是不能被接受的,因此后面的内核中,已经不支持中断嵌套了,当一个中断被处理时,本地CPU中断会被一直禁止,因此这个标志位就没有存在的意义了。

IRQ Domain

irq_domain
内核中使用irq number来表示中断,并且绑定对应的中断处理函数handler,在比较老的硬件上,可能只存在一个中断控制器,这种情况比较简单,内核可以直接把硬件中断号作为irq number来使用就好,但是当下随着硬件越来越复杂,一个系统中可能并不仅仅存在一个中断控制器,而每个中断控制器内都有对应的硬件中断号,它们的硬件中断号可能都是从1开始计算的,为了应对这种越来越复杂的硬件架构,内核抽象出了irq domain,每个中断控制器都相当于一个irq domain,每个irq hwid在每个irq domain中都是唯一的,这就不存在冲突的情况了,内核依然会使用唯一的irq number来管理中断,但是要建立irq number到irqdomain和irq hwid之间的映射关系。每个Irq domain是由中断控制器驱动创建和负责维护的,内核使用一个链表来管理所有的irq domain,当我们外设注册中断时,需要先获取irq number,这个过程实际上是由irq domain负责完成的,它申请唯一的一个irq number,并跟irq hwid进行绑定,最后返回irq number。
中断控制器驱动注册irq domain:

static inline struct irq_domain *irq_domain_add_linear(struct device_node *of_node,
                     unsigned int size,
                     const struct irq_domain_ops *ops,
                     void *host_data)
{   
    return __irq_domain_add(of_node_to_fwnode(of_node), size, size, 0, ops, host_data);
}
static inline struct irq_domain *irq_domain_add_nomap(struct device_node *of_node,
                     unsigned int max_irq,
                     const struct irq_domain_ops *ops,
                     void *host_data)
{
    return __irq_domain_add(of_node_to_fwnode(of_node), 0, max_irq, max_irq, ops, host_data);
}
static inline struct irq_domain *irq_domain_add_legacy_isa(
                struct device_node *of_node,
                const struct irq_domain_ops *ops,
                void *host_data)
{
    return irq_domain_add_legacy(of_node, NUM_ISA_INTERRUPTS, 0, 0, ops,
                     host_data);
}
static inline struct irq_domain *irq_domain_add_tree(struct device_node *of_node,
                     const struct irq_domain_ops *ops,
                     void *host_data)
{
    return __irq_domain_add(of_node_to_fwnode(of_node), 0, ~0, 0, ops, host_data);
}

Irq domain实现的时候会创建一个table用于保存对应的irq number到irq hwid之间的映射关系,同时还会注册对应的map回调函数到domian中,以便外设驱动使用。

外设驱动调用map接口建立irq number和hwid之间的关系:

irq_of_parse_and_map
irq_create_of_mapping

Map的时候实际上会通过搜索dts关系,找到对应interrupt controller name,然后找到与之匹配的irq domain,并调用irq domian中的map回调函数来进行绑定操作的,所以对map的规则制定是由中断控制器驱动来实现的,这个过程会申请唯一的irq number,然后创建到hwid之间的绑定关系并保存起来

中断调用流程

  • CPU接收到中断,陷入EL2进行处理
  • 执行中断的主入口函数(gic_handle_irq)
  • 获取主中断控制器的irq domain
  • 获取该irq domain中的irq hwid
  • 获取到绑定的irq number(irq_find_mapping)
  • 根据irq number来执行irq handler( handle_irq_event )
  • 如果是中断控制器级联的情况,则在irq handler中进一步处理irq domain,相当于从第3步继续执行下一级的解析

中断处理方式

上半部:

足够快速
不可阻塞
不可睡眠 (中断上下文,没有对应的task_struct)
不会重入(执行期间整个CPU上中断是被禁止的)

下半部:

Softirq
Tasklet
Workqueue
Irq thread
  • softirq
static struct softirq_action softirq_vec[NR_SOFTIRQS] __cacheline_aligned_in_smp;
void open_softirq(int nr, void (*action)(struct softirq_action *)); //注册softirq
void raise_softirq(unsigned int nr);//设置pending位,触发对softirq的处理

软中断是静态分配的,代码在静态定以后不可更改,do_softirq在合适的实际被调用,它会循环遍历系统中所有软中断,如果发现有挂起的软中断待执行,那么它就会去执行该软中断处理程序。

  • tasklet
void tasklet_init(struct tasklet_struct *t, void (*func)(unsigned long), unsigned long data);
void tasklet_schedule(struct tasklet_struct *t);
void tasklet_disable(struct tasklet_struct *t);
void tasklet_enable(struct tasklet_struct *t);

基于softirq实现的一种下半部机制,因此它也会在中断上下文中运行,所以一样要遵循中断上下文的规定。

优势:不用关注多核CPU重入的问题,tasklet做了互斥处理,同一时间只会在一个CPU上执行,编程简单,除非不可避免,否则请选择tasklet而不是softirq。
劣势:自然性能不如softirq

  • workqueue
bool schedule_work(struct work_struct *work);
bool schedule_work_on(int cpu, struct work_struct *work);
void flush_scheduled_work(void);
bool schedule_delayed_work(struct delayed_work *dwork, unsigned long delay);
bool mod_delayed_work(struct workqueue_struct *wq, struct delayed_work *dwork, unsigned long delay)
bool cancel_work(struct work_struct *work);
bool cancel_work_sync(struct work_struct *work);

特别注意:schedule_delayed_work如果已经加入了一个work,并且时间没有到期,如果此时再次延长时间加入work,将加入失败,这种场景应该使用mod接口。workqueue的特点是运行于进程上下文中,可以睡眠,可以阻塞,编程最为简单。

  • irq thread(中断线程化)
    前文已经介绍过了,不在重述。

(完)

参考文章:
Linux kernel source code - Linux 4.14
《Understanding The Linux Kernel》 - Daniel P.Bovet & Marco Cesati
《Linux kernel development Third Edition》 - Robert Love
http://www.wowotech.net/sort/irq_subsystem - wowotech

展开阅读全文

没有更多推荐了,返回首页