linux 中断子系统分析

1540阅读 0评论2016-01-10 wjq1060
分类:LINUX

Linux内核的中断子系统

Linux中断子系统主要包括了三个部分,一部分和体系结构相关,位于最底层,主要负责在中断发生之后保护CPU现场,调用内核统一的中断处理入口函数,负责从中断处理过程恢复到中断之前的流程等比较底层的工作。第二部分是内核的中断系统框架层,这部分为内核中断处理提供了一个统一的框架,对于靠上层的驱动程序,它提供中断程序注册的接口。对于体系结构相关的底层,它提供一个统一的中断处理架构,底层只需要准备好相应的中断号等参数,调用统一的中断处理入口程序即可。第三层是驱动注册的中断处理函数,这些函数由相应驱动提供,完成具体的中断处理。本文主要从这三个方面进行叙述。

http://alloysystem.blog.chinaunix.net

Andy.yx.deng#gmail.com(#->@)

1. Linux中断概述

之前我已经分析过ARM体系的处理器在发生中断时,CPU状态的变化和linux系统的跳转处理。分析的内容主要还是和ARM体系结构相关的处理过程,实际上当CPU完成这部分工作之后会跳转到和体系结构不相关的linux内核中断子系统中,完成具体的中断事务。我将linux中断处理过程分成三个部分,如下图所示。

wps_clip_image-7213

第一个部分就是体系结构相关的代码,这部分代码和CPU对中断的响应密切相关。主要完成工作有:

1. 保存中断现场(为恢复CPU现场,恢复到被中断的工作做准备)

2. 切换到中断执行的环境(切换到内核堆栈中)

3. 获取中断号,并调用中断子系统中的中断例程

4. 最后,恢复CPU现场,切换执行原来被中断的工作

这部分内容可以在之前的ARM中断处理分析中有详细的描述,在此不在复述,这里关注的是linux内核相关的中断子系统的分析。

第二个部分就是linux内核的中断子系统。这个系统设计了一种中断处理的框架,对于下层(体系结构相关的代码)只需要遵照这个架构准备相关的环境并调用内核中断处理过程的入口函数。对于驱动程序,只需要向这个系统中注册中断处理程序。

第三个部分就是驱动注册的中断处理程序。这个部分是驱动为了完成相关中断事务向内核中断子系统注册的中断例程。也就是我们平时见到比较多的调用request_irq向内核注册一个中断处理函数。

2. 内核中断子系数据结构

Linux内核中断子系统的主要数据结构和组织如下图所示。主要涉及的数据结构有三个:

struct irq_desc

struct irq_chip

struct irqaction

在下面的文章中,主要围绕这三个数据结构分析内核的中断子系统

wps_clip_image-24437

首先内核中有一个名为irq_desc数组,数组的名字是“中断描述符”的意思,翻阅其它参考资料,好像中文中没有这样说的。但是,事实上数组中的每一个项就描述了对应中断号的一个硬件中断。以前听供职于百度的一个牛人说过,看技术书籍要直接看英文的,读中文的书籍得到的是第二手的信息,信息的准确性值得怀疑。我很支持这个看法,好比上面irq_desc这样的名字,只要看到这个英文的命名,我们就能联想理解这个数字的意思和作用,但是硬要把这个名称翻译成中文,实在找不到很好的翻译。

struct irq_desc
{
    unsigned int        irq;
#ifdef CONFIG_SPARSE_IRQ
    struct timer_rand_state *timer_rand_state;
    unsigned int            *kstat_irqs;
# ifdef CONFIG_INTR_REMAP
    struct irq_2_iommu      *irq_2_iommu;
# endif
#endif 
    irq_flow_handler_t    handle_irq;
    struct irq_chip        *chip;
    struct msi_desc        *msi_desc;
    void            *handler_data;
    void            *chip_data;
    struct irqaction    *action;    /* IRQ action list */ 
    unsigned int        status;        /* IRQ status */ 
 
    unsigned int        depth;        /* nested irq disables */ 
    unsigned int        wake_depth;    /* nested wake enables */ 
    unsigned int        irq_count;    /* For detecting broken IRQs */ 
    unsigned long        last_unhandled;    /* Aging timer for unhandled count */ 
    unsigned int        irqs_unhandled;
    spinlock_t        lock;
#ifdef CONFIG_SMP
    cpumask_t        affinity;
    unsigned int        cpu;
#endif 
#ifdef CONFIG_GENERIC_PENDING_IRQ
    cpumask_t        pending_mask;
#endif 
#ifdef CONFIG_PROC_FS
    struct proc_dir_entry    *dir;
#endif 
    const char        *name;
} ____cacheline_internodealigned_in_smp;
 

Irq_desc中最关键的几个域是handle_irqchipactionHandle_irq是一个函数指针,也就是处理不同类型中断的统一入口函数。Linux系统将各种中断抽象成几种类型,有电平触发的中断、上升/下降沿触发的中断、还有一种简单中断处理模型等等。根据这些中断类型,linux内核抽象出了一套中断处理流程。不同类型的中断,只需要在handle_irq上安装不同的处理函数,而不需要完全重新完成这个过程的编程。

打一个比方,如果我要完成一个wifi子卡的中断程序,首先我要搞清楚芯片上中断控制器对这个中断号配置了什么样的中断类型。如果是电平触发的中断,我就需要将handle_irq函数指针设置成&handle_level_irqhandle_level_irq函数是linux内核中断子系统中抽象出来的对电平触发的中断的统一处理流程,之后的文章中还会详细分析这个函数。

上文中我们还看到了struct irq_chip数据结构,这个结构体中大部分是一些函数指针的定义,不同的CPU会在其中安装一些和体系结构相关的函数,完成类似于开启、关闭中断这样的操作。实际上它提供了linux内核中断子系统控制CPU中断硬件的操作接口函数。

struct irq_chip
{
    const char    *name;
    unsigned int    (*startup)(unsigned int irq);
    void        (*shutdown)(unsigned int irq);
    void        (*enable)(unsigned int irq);
    void        (*disable)(unsigned int irq);
 
    void        (*ack)(unsigned int irq);
    void        (*mask)(unsigned int irq);
    void        (*mask_ack)(unsigned int irq);
    void        (*unmask)(unsigned int irq);
    void        (*eoi)(unsigned int irq);
 
    void        (*end)(unsigned int irq);
    void        (*set_affinity)(unsigned int irq,
                                const struct cpumask *dest);
    int        (*retrigger)(unsigned int irq);
    int        (*set_type)(unsigned int irq, unsigned int flow_type);
    int        (*set_wake)(unsigned int irq, unsigned int on);
 
    /* Currently used only by UML, might disappear one day.*/ 
#ifdef CONFIG_IRQ_RELEASE_METHOD
    void        (*release)(unsigned int irq, void *dev_id);
#endif 
    /*
     * For compatibility, ->typename is copied into ->name.
     * Will disappear.
     */ 
    const char    *typename;
};

还有一个数据需要介绍一下就是irqaction,我在前面的文章中说过linux中断处理分为三层,最上面的一层就是驱动程序注册的中断处理函数。事实上,中断函数注册中断函数,就是分配一个struct irqaction结构体,将相应的处理函数赋值到handler中,并将这个结构体挂接到对应中断号的irq_desc结构体中。

3. 中断子系统情景分析

上面已经将linux内核中断子系统相关的数据结构做了说明。有图有真相!下面会结合一个中断发生后,内核中断子系统的处理过程,详细剖析linux中断处理过程。还是按照上面行文的风格,首先放一张中断处理过程的函数调用关系图,然后对中间涉及到的函数做逐一的分析。从事技术研发的过程中,我发现图例传递的信息量是很大的,特别是在IT开发过程中,一张图有时候比几页的文字传递的信息要多得多。生活中也是这样吗,俗话说“有图有真相!”“No PPNO way!”,由此可见图片的重要性。

wps_clip_image-3525

中断子系统入口点

从上面的函数调用流程来分析,首先是体系结构相关的代码在获取了中断号,CPU寄存器集合之后,调用asm_do_IRQ函数。这个函数也就是内核的中断子系统提供给体系结构相关代码的一个中断处理统一过程的入口点。

asm_do_IRQ函数中,通过判断irq是不是大于最大的中断号,如果没有就直接调用generic_handle_irq,一般这个中断号是不会超过最大中断号的,所有我们只分析generic_handle_irq函数。在完成中断过程的处理后,调用irq_exit函数,该函数中调用了invoke_softirq函数触发软件中断的处理。

struct irqaction
{
    irq_handler_t handler;
    unsigned long flags;
    cpumask_t mask;
    const char *name;
    void *dev_id;
    struct irqaction *next;
    int irq;
    struct proc_dir_entry *dir;
};

generic_handle_irq函数很简单,只是generic_handle_irq函数的一个封装而已,只是通过irq中断号,从irq_desc数组中得到了中断的描述符。

#define irq_to_desc(irq) (&irq_desc[irq])

generic_handle_irq_desc也是比较简单,直接调用handle_irq函数指针,这个函数指针根据中断的种类,指向handle_level_irq等函数。

/*
* do_IRQ handles all hardware IRQ's.  Decoded IRQs should not
* come via this function.  Instead, they should provide their
* own 'handler'
*/ 
asmlinkage void __exception asm_do_IRQ(unsigned int irq, struct pt_regs *regs)
{
    struct pt_regs *old_regs = set_irq_regs(regs);
 
    irq_enter();
 
    /*
     * Some hardware gives randomly wrong interrupts.  Rather
     * than crashing, do something sensible.
     */ 
    if (irq >= NR_IRQS)
        handle_bad_irq(irq, &bad_irq_desc);
    else 
        generic_handle_irq(irq);
 
    /* AT91 specific workaround */ 
    irq_finish(irq);
 
    irq_exit();
    set_irq_regs(old_regs);
}

电平触发中断类型

handle_level_irq函数是对电平触发中断的处理函数。下面对这种中断类型的处理做详细分析。该函数首先屏蔽掉对应中断号的中断,同时清除该中断源的pendding位(ack操作)。然后设置中断线状态为正在处理(IRQ_INPROGRESS)。这个时候,可以调用handle_IRQ_event执行驱动注册的中断处理程序。等处理完成后,清除中断线状态IRQ_INPROGRESS,开启中断线的中断(关掉对该中断线的屏蔽)。

边缘触发中断处理过程

Handle_edge_irq函数处理边缘触发中断。在边缘触发中断处理函数中,不屏蔽掉中断,因此同源中断可能会在调用handle_IRQ_event函数处理驱动注册的中断的过程中在其它CPU被触发。在这种情况下,其它CPU上会进入handle_edge_irq函数中,其它CPU判断该中断正在处理过程中就会设置pending标志,并屏蔽掉中断。之后,Handle_edge_irq函数在while循环中,会检查是否设置了pending标志,也就判断是否有同源中断在handle_IRQ_event函数执行期间再次被触发。如果再次被触发,这个时候while循环会继续循环执行驱动注册的中断函数。

为什么在该中断第二次被触发的时候需要屏蔽掉中断呢?这是因为第二次被触发时,可以通过设置pending位来记录中断发生过,如果这个时候不屏蔽掉中断,有可能第三次同源中断也可能发生,当第三次中断发生时,无法再通过设置pending标志来记录该中断,所以需要屏蔽中断。当while循环为pending位执行过程中(由于设置pending标志再次循环),这时已经确定了上一次并发的中断肯定触发调用handle_IRQ_event后,可以重新开启中断,允许在执行handle_IRQ_event接受同源中断请求。

static inline void generic_handle_irq_desc(unsigned int irq, struct irq_desc *desc)
{
#ifdef CONFIG_GENERIC_HARDIRQS_NO__DO_IRQ
    desc->handle_irq(irq, desc);
#else 
    if (likely(desc->handle_irq))
        desc->handle_irq(irq, desc);
    else 
        __do_IRQ(irq);
#endif 
}

不管是边缘触发中断类型还是电平触发中断类型,最后都会调用handle_IRQ_event函数来处理驱动注册的中断处理函数。这个函数的逻辑很简单,只需要依次调用action中的函数指针,并依次记录函数调用的结果。

/**
*    handle_level_irq - Level type irq handler
*    @irq:    the interrupt number
*    @desc:    the interrupt description structure for this irq
*
*    Level type interrupts are active as long as the hardware line has
*    the active level. This may require to mask the interrupt and unmask
*    it after the associated handler has acknowledged the device, so the
*    interrupt line is back to inactive.
*/ 
void 
handle_level_irq(unsigned int irq, struct irq_desc *desc)
{
    struct irqaction *action;
    irqreturn_t action_ret;
 
    spin_lock(&desc->lock);
    /*屏蔽掉该中断,并清除掉pendding的中断位*/ 
    mask_ack_irq(desc, irq);
    desc = irq_remap_to_desc(irq, desc);
 
    if (unlikely(desc->status & IRQ_INPROGRESS))
        goto out_unlock;
    desc->status &= ~(IRQ_REPLAY | IRQ_WAITING);
    kstat_incr_irqs_this_cpu(irq, desc);
 
    /*
     * If its disabled or no action available
     * keep it masked and get out of here
     */ 
    action = desc->action;
    if (unlikely(!action || (desc->status & IRQ_DISABLED)))
        goto out_unlock;
 
    /*设置中断为正在处理状态*/ 
    desc->status |= IRQ_INPROGRESS;
    spin_unlock(&desc->lock);
 
    /*调用handle_IRQ_event处理驱动注册的中断处理函数*/ 
    action_ret = handle_IRQ_event(irq, action);
    if (!noirqdebug)
        note_interrupt(irq, desc, action_ret);
 
    spin_lock(&desc->lock);
    /*清除掉中断正在处理的标记*/ 
    desc->status &= ~IRQ_INPROGRESS;
    /*开启中断屏蔽,允许中断*/ 
    if (!(desc->status & IRQ_DISABLED) && desc->chip->unmask)
        desc->chip->unmask(irq);
out_unlock:
    spin_unlock(&desc->lock);
}
EXPORT_SYMBOL_GPL(handle_level_irq);

今天南京的气温出奇的低,坐在家里已经打摆子了。没有空调怎么办?

andy yixin Deng

上一篇:Linux中断处理体系结构分析(一)
下一篇:没有了