Chinaunix首页 | 论坛 | 博客
  • 博客访问: 350540
  • 博文数量: 106
  • 博客积分: 0
  • 博客等级: 民兵
  • 技术积分: 861
  • 用 户 组: 普通用户
  • 注册时间: 2013-09-10 08:32
文章分类

全部博文(106)

文章存档

2016年(11)

2015年(93)

2013年(2)

分类: LINUX

2015-08-19 14:51:21

Linux kernel中断子系统之(五):驱动申请中断API

作者:linuxer 发布于:2014-9-22 18:33 分类:中断子系统

一、前言

本文主要的议题是作为一个普通的驱动工程师,在撰写自己负责的驱动的时候,如何向Linux Kernel中的中断子系统注册中断处理函数?为了理解注册中断的接口,必须了解一些中断线程化(threaded interrupt handler)的基础知识,这些在第二章描述。第三章主要描述了驱动申请 interrupt line接口API request_threaded_irq的规格。第四章是进入request_threaded_irq的实现细节,分析整个代码的执行过程。

 

二、和中断相关的linux实时性分析以及中断线程化的背景介绍

1、非抢占式linux内核的实时性

在遥远的过去,linux2.4之前的内核是不支持抢占特性的,具体可以参考下图:

sxw

事情的开始源自高优先级任务(橘色block)由于要等待外部事件(例如网络数据)而进入睡眠,调度器调度了某个低优先级的任务(紫色block)执行。该低优先级任务欢畅的执行,直到触发了一次系统调用(例如通过read()文件接口读取磁盘上的文件等)而进入了内核态。仍然是熟悉的配方,仍然是熟悉的味道,低优先级任务正在执行不会变化,只不过从user space切换到了kernel space。外部事件总是在你不想让它来的时候到来,T0时刻,高优先级任务等待的那个中断事件发生了。

中断虽然发生了,但软件不一定立刻响应,可能由于在内核态执行的某些操作不希望被外部事件打断而主动关闭了中断(或是关闭了CPU的中断,或者MASK了该IRQ number),这时候,中断信号没有立刻得到响应,软件仍然在内核态执行低优先级任务系统调用的代码。在T1时刻,内核态代码由于退出临界区而打开中断(注意:上图中的比例是不协调的,一般而言,linux kernel不会有那么长的关中断时间,上面主要是为了表示清楚,同理,从中断触发到具体中断服务程序的执行也没有那么长,都是为了表述清楚),中断一旦打开,立刻跳转到了异常向量地址,interrupt handler抢占了低优先级任务的执行,进入中断上下文(虽然这时候的current task是低优先级任务,但是中断上下文和它没有任何关系)。

从CPU开始处理中断到具体中断服务程序被执行还需要一个分发的过程。这个期间系统要做的主要操作包括确定HW interrupt ID,确定IRQ Number,ack或者mask中断,调用中断服务程序等。T0到T2之间的delay被称为中断延迟(Interrupt Latency),主要包括两部分,一部分是HW造成的delay(硬件的中断系统识别外部的中断事件并signal到CPU),另外一部分是软件原因(内核代码中由于要保护临界区而关闭中断引起的)。

该中断的服务程序执行完毕(在其执行过程中,T3时刻,会唤醒高优先级任务,让它从sleep状态进入runable状态),返回低优先级任务的系统调用现场,这时候并不存在一个抢占点,低优先级任务要完成系统调用之后,在返回用户空间的时候才出现抢占点。漫长的等待之后,T4时刻,调度器调度高优先级任务执行。有一个术语叫做任务响应时间(Task Response Time)用来描述T3到T4之间的delay。

 

2、抢占式linux内核的实时性

2.6内核和2.4内核显著的不同是提供了一个CONFIG_PREEMPT的选项,打开该选项后,linux kernel就支持了内核代码的抢占(当然不能在临界区),其行为如下:

pre

T0到T3的操作都是和上一节的描述一样的,不同的地方是在T4。对于2.4内核,只有返回用户空间的时候才有抢占点出现,但是对于抢占式内核而言,即便是从中断上下文返回内核空间的进程上下文,只要内核代码不在临界区内,就可以发生调度,让最高优先级的任务调度执行。

在非抢占式linux内核中,一个任务的内核态是不可以被其他进程抢占的。这里并不是说kernel space不可以被抢占,只是说进程通过系统调用陷入到内核的时候,不可以被其他的进程抢占。实际上,中断上下文当然可以抢占进程上下文(无论是内核态还是用户态),更进一步,中断上下文是拥有至高无上的权限,它甚至可以抢占其他的中断上下文。引入抢占式内核后,系统的平均任务响应时间会缩短,但是,实时性更关注的是:无论在任何的负载情况下,任务响应时间是确定的。因此,更需要关注的是worst-case的任务响应时间。这里有两个因素会影响worst case latency:

(1)为了同步,内核中总有些代码需要持有自旋锁资源,或者显式的调用preempt_disable来禁止抢占,这时候不允许抢占

(2)中断上下文(并非只是中断handler,还包括softirq、timer、tasklet)总是可以抢占进程上下文

因此,即便是打开了PREEMPT的选项,实际上linux系统的任务响应时间仍然是不确定的。一方面内核代码的临界区非常多,我们需要找到,系统中持有锁,或者禁止抢占的最大的时间片。另外一方面,在上图的T4中,能顺利的调度高优先级任务并非易事,这时候可能有触发的软中断,也可能有新来的中断,也可能某些driver的tasklet要执行,只有在没有任何bottom half的任务要执行的时候,调度器才会启动调度。

3、进一步提高linux内核的实时性

通过上一个小节的描述,相信大家都确信中断对linux 实时性的最大的敌人。那么怎么破?我曾经接触过一款RTOS,它的中断handler非常简单,就是发送一个inter-task message到该driver thread,对任何的一个驱动都是如此处理。这样,每个中断上下文都变得非常简短,而且每个中断都是一致的。在这样的设计中,外设中断的处理线程化了,然后,系统设计师要仔细的为每个系统中的task分配优先级,确保整个系统的实时性。

在Linux kernel中,一个外设的中断处理被分成top half和bottom half,top half进行最关键,最基本的处理,而比较耗时的操作被放到bottom half(softirq、tasklet)中延迟执行。虽然bottom half被延迟执行,但始终都是先于进程执行的。为何不让这些耗时的bottom half和普通进程公平竞争呢?因此,linux kernel借鉴了RTOS的某些特性,对那些耗时的驱动interrupt handler进行线程化处理,在内核的抢占点上,让线程(无论是内核线程还是用户空间创建的线程,还是驱动的interrupt thread)在一个舞台上竞争CPU。

 

三、request_threaded_irq的接口规格

1、输入参数描述

输入参数 描述
irq 要注册handler的那个IRQ number。这里要注册的handler包括两个,一个是传统意义的中断handler,我们称之primary handler,另外一个是threaded interrupt handler
handler primary handler。需要注意的是primary handler和threaded interrupt handler不能同时为空,否则会出错
thread_fn threaded interrupt handler。如果该参数不是NULL,那么系统会创建一个kernel thread,调用的function就是thread_fn
irqflags 参见本章第三节
devname  
dev_id 参见第四章,第一节中的描述。

2、输出参数描述

0表示成功执行,负数表示各种错误原因。

3、Interrupt type flags

flag定义 描述
IRQF_TRIGGER_XXX 描述该interrupt line触发类型的flag
IRQF_DISABLED 首先要说明的是这是一个废弃的flag,在新的内核中,该flag没有任何的作用了。具体可以参考:Disabling IRQF_DISABLED 
旧的内核(2.6.35版本之前)认为有两种interrupt handler:slow handler和fast handle。在request irq的时候,对于fast handler,需要传递IRQF_DISABLED的参数,确保其中断处理过程中是关闭CPU的中断,因为是fast handler,执行很快,即便是关闭CPU中断不会影响系统的性能。但是,并不是每一种外设中断的handler都是那么快(例如磁盘),因此就有 slow handler的概念,说明其在中断处理过程中会耗时比较长。对于这种情况,在执行interrupt handler的时候不能关闭CPU中断,否则对系统的performance会有影响。 
新的内核已经不区分slow handler和fast handle,都是fast handler,都是需要关闭CPU中断的,那些需要后续处理的内容推到threaded interrupt handler中去执行。
IRQF_SHARED

这是flag用来描述一个interrupt line是否允许在多个设备中共享。如果中断控制器可以支持足够多的interrupt source,那么在两个外设间共享一个interrupt request line是不推荐的,毕竟有一些额外的开销(发生中断的时候要逐个询问是不是你的中断,软件上就是遍历action list),因此外设的irq handler中最好是一开始就启动判断,看看是否是自己的中断,如果不是,返回IRQ_NONE,表示这个中断不归我管。 早期PC时代,使用8259中断控制器,级联的8259最多支持15个外部中断,但是PC外设那么多,因此需要irq share。现在,ARM平台上的系统设计很少会采用外设共享IRQ方式,毕竟一般ARM SOC提供的有中断功能的GPIO非常的多,足够用的。 当然,如果确实需要两个外设共享IRQ,那也只能如此设计了。对于HW,中断控制器的一个interrupt source的引脚要接到两个外设的interrupt request line上,怎么接?直接连接可以吗?当然不行,对于低电平触发的情况,我们可以考虑用与门连接中断控制器和外设。

IRQF_PROBE_SHARED IRQF_SHARED用来表示该interrupt action descriptor是允许和其他device共享一个interrupt line(IRQ number),但是实际上是否能够share还是需要其他条件:例如触发方式必须相同。有些驱动程序可能有这样的调用场景:我只是想scan一个irq table,看看哪一个是OK的,这时候,如果即便是不能和其他的驱动程序share这个interrupt line,我也没有关系,我就是想scan看看情况。这时候,caller其实可以预见sharing mismatche的发生,因此,不需要内核打印“Flags mismatch irq……“这样冗余的信息
IRQF_PERCPU 在SMP的架构下,中断有两种mode,一种中断是在所有processor之间共享的,也就是global的,一旦中断产生,interrupt controller可以把这个中断送达多个处理器。当然,在具体实现的时候不会同时将中断送达多个CPU,一般是软件和硬件协同处理,将中断送达一个CPU处理。但是一段时间内产生的中断可以平均(或者按照既定的策略)分配到一组CPU上。这种interrupt mode下,interrupt controller针对该中断的operational register是global的,所有的CPU看到的都是一套寄存器,一旦一个CPU ack了该中断,那么其他的CPU看到的该interupt source的状态也是已经ack的状态。 
和global对应的就是per cpu interrupt了,对于这种interrupt,不是processor之间共享的,而是特定属于一个CPU的。例如GIC中interrupt ID等于30的中断就是per cpu的(这个中断event被用于各个CPU的local timer),这个中断号虽然只有一个,但是,实际上控制该interrupt ID的寄存器有n组(如果系统中有n个processor),每个CPU看到的是不同的控制寄存器。在具体实现中,这些寄存器组有两种形态,一种是banked,所有CPU操作同样的寄存器地址,硬件系统会根据访问的cpu定向到不同的寄存器,另外一种是non banked,也就是说,对于该interrupt source,每个cpu都有自己独特的访问地址。
IRQF_NOBALANCING 这也是和multi-processor相关的一个flag。对于那些可以在多个CPU之间共享的中断,具体送达哪一个processor是有策略的,我们可以在多个CPU之间进行平衡。如果你不想让你的中断参与到irq balancing的过程中那么就设定这个flag
IRQF_IRQPOLL  
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。
IRQF_FORCE_RESUME 在系统resume的过程中,强制必须进行enable的动作,即便是设定了IRQF_NO_SUSPEND这个flag。这是和特定的硬件行为相关的。
IRQF_NO_THREAD 有些low level的interrupt是不能线程化的(例如系统timer的中断),这个flag就是起这个作用的。另外,有些级联的interrupt controller对应的IRQ也是不能线程化的(例如secondary GIC对应的IRQ),它的线程化可能会影响一大批附属于该interrupt controller的外设的中断响应延迟。
IRQF_EARLY_RESUME  
IRQF_TIMER  

 

四、request_threaded_irq代码分析

1、request_threaded_irq主流程

int request_threaded_irq(unsigned int irq, irq_handler_t handler, 
             irq_handler_t thread_fn, unsigned long irqflags, 
             const char *devname, void *dev_id) 
{  
    if ((irqflags & IRQF_SHARED) && !dev_id)---------(1) 
        return -EINVAL;

    desc = irq_to_desc(irq); -----------------(2) 
    if (!desc)         return -EINVAL;

    if (!irq_settings_can_request(desc) || ------------(3) 
        WARN_ON(irq_settings_is_per_cpu_devid(desc))) 
        return -EINVAL;

    if (!handler) { ----------------------(4) 
        if (!thread_fn) 
            return -EINVAL; 
        handler = irq_default_primary_handler; 
    }

    action = kzalloc(sizeof(struct irqaction), GFP_KERNEL);

    action->handler = handler; 
    action->thread_fn = thread_fn; 
    action->flags = irqflags; 
    action->name = devname; 
    action->dev_id = dev_id;

    chip_bus_lock(desc); 
    retval = __setup_irq(irq, desc, action); -----------(5) 
    chip_bus_sync_unlock(desc); 
}

(1)对于那些需要共享的中断,在request irq的时候需要给出dev id,否则会出错退出。为何对于IRQF_SHARED的中断必须要给出dev id呢?实际上,在共享的情况下,一个IRQ number对应若干个irqaction,当操作irqaction的时候,仅仅给出IRQ number就不是非常的足够了,这时候,需要一个ID表示具体的irqaction,这里就是dev_id的作用了。我们举一个例子:

void free_irq(unsigned int irq, void *dev_id)

当释放一个IRQ资源的时候,不但要给出IRQ number,还要给出device ID。只有这样,才能精准的把要释放的那个irqaction 从irq action list上移除。dev_id在中断处理中有没有作用呢?我们来看看source code:

irqreturn_t handle_irq_event_percpu(struct irq_desc *desc, struct irqaction *action) 
{

    do { 
        irqreturn_t res;  
        res = action->handler(irq, action->dev_id);

…… 
        action = action->next; 
    } while (action);

…… 
}

linux interrupt framework虽然支持中断共享,但是它并不会协助解决识别问题,它只会遍历该IRQ number上注册的irqaction的callback函数,这样,虽然只是一个外设产生的中断,linux kernel还是把所有共享的那些中断handler都逐个调用执行。为了让系统的performance不受影响,irqaction的callback函数必须在函数的最开始进行判断,是否是自己的硬件设备产生了中断(读取硬件的寄存器),如果不是,尽快的退出。

需要注意的是,这里dev_id并不能在中断触发的时候用来标识需要调用哪一个irqaction的callback函数,通过上面的代码也可以看出,dev_id有些类似一个参数传递的过程,可以把具体driver的一些硬件信息,组合成一个structure,在触发中断的时候可以把这个structure传递给中断处理函数。

(2)通过IRQ number获取对应的中断描述符。在引入CONFIG_SPARSE_IRQ选项后,这个转换变得不是那么简单了。在过去,我们会以IRQ number为index,从irq_desc这个全局数组中直接获取中断描述符。如果配置CONFIG_SPARSE_IRQ选项,则需要从radix tree中搜索。CONFIG_SPARSE_IRQ选项的更详细的介绍请参考IRQ number和中断描述符

(3)并非系统中所有的IRQ number都可以request,有些中断描述符被标记为IRQ_NOREQUEST,标识该IRQ number不能被其他的驱动request。一般而言,这些IRQ number有特殊的作用,例如用于级联的那个IRQ number是不能request。irq_settings_can_request函数就是判断一个IRQ是否可以被request。

irq_settings_is_per_cpu_devid函数用来判断一个中断描述符是否需要传递per cpu的device ID。per cpu的中断上面已经描述的很清楚了,这里不再细述。如果一个中断描述符对应的中断 ID是per cpu的,那么在申请其handler的时候就有两种情况,一种是传递统一的dev_id参数(传入request_threaded_irq的最后一个参数),另外一种情况是针对每个CPU,传递不同的dev_id参数。在这种情况下,我们需要调用request_percpu_irq接口函数而不是request_threaded_irq。

(4)传入request_threaded_irq的primary handler和threaded handler参数有下面四种组合:

primary handler threaded handler 描述
NULL NULL 函数出错,返回-EINVAL
设定 设定 正常流程。中断处理被合理的分配到primary handler和threaded handler中。
设定 NULL 中断处理都是在primary handler中完成
NULL 设定 这种情况下,系统会帮忙设定一个default的primary handler:irq_default_primary_handler,协助唤醒threaded handler线程

(5)这部分的代码很简单,分配struct irqaction,赋值,调用__setup_irq进行实际的注册过程。这里要罗嗦几句的是锁的操作,在内核中,有很多函数,有的是需要调用者自己加锁保护的,有些是不需要加锁保护的。对于这些场景,linux kernel采取了统一的策略:基本函数名字是一样的,只不过需要调用者自己加锁保护的那个函数需要增加__的前缀,例如内核有有下面两个函数:setup_irq和__setup_irq。这里,我们在setup irq的时候已经调用chip_bus_lock进行保护,因此使用lock free的版本__setup_irq。

chip_bus_lock定义如下:

static inline void chip_bus_lock(struct irq_desc *desc) 

    if (unlikely(desc->irq_data.chip->irq_bus_lock)) 
        desc->irq_data.chip->irq_bus_lock(&desc->irq_data); 
}

大部分的interrupt controller并没有定义irq_bus_lock这个callback函数,因此chip_bus_lock这个函数对大多数的中断控制器而言是没有实际意义的。但是,有些interrupt controller是连接到慢速总线上的,例如一个i2c接口的IO expander芯片(这种芯片往往也提供若干有中断功能的GPIO,因此也是一个interrupt controller),在访问这种interrupt controller的时候需要lock住那个慢速bus(只能有一个client在使用I2C bus)。

 

2、注册irqaction

(1)nested IRQ的处理代码

在看具体的代码之前,我们首先要理解什么是nested IRQ。nested IRQ不是cascade IRQ,在GIC代码分析中我们有描述过cascade IRQ这个概念,主要用在interrupt controller级联的情况下。为了方便大家理解,我还是给出一个具体的例子吧,具体的HW block请参考下图:

SOC-INT

上图是一个两个GIC级联的例子,所有的HW block封装在了一个SOC chip中。为了方便描述,我们先进行编号:Secondary GIC的IRQ number是A,外设1的IRQ number是B,外设2的IRQ number是C。对于上面的硬件,linux kernel处理如下:

(a)IRQ A的中断描述符被设定为不能注册irqaction(不能注册specific interrupt handler,或者叫中断服务程序)

(b)IRQ A的highlevel irq-events handler(处理interrupt flow control)负责进行IRQ number的映射,在其irq domain上翻译出具体外设的IRQ number,并重新定向到外设IRQ number对应的highlevel irq-events handler。

(c)所有外设驱动的中断正常request irq,可以任意选择线程化的handler,或者只注册primary handler。

需要注意的是,对root GIC和Secondary GIC寄存器的访问非常快,因此ack、mask、EOI等操作也非常快。

我们再看看另外一个interrupt controller级联的情况:

nested

IO expander HW block提供了有中断功能的GPIO,因此它也是一个interrupt controller,有它自己的irq domain和irq chip。上图中外设1和外设2使用了IO expander上有中断功能的GPIO,它们有属于自己的IRQ number以及中断描述符。IO expander HW block的IRQ line连接到SOC内部的interrupt controller上,因此,这也是一个interrupt controller级联的情况,对于这种情况,我们是否可以采用和上面GIC级联的处理方式呢?

不行,对于GIC级联的情况,如果secondary GIC上的外设1产生了中断,整个关中断的时间是IRQ A的中断描述符的highlevel irq-events handler处理时间+IRQ B的的中断描述符的highlevel irq-events handler处理时间+外设1的primary handler的处理时间。所幸对root GIC和Secondary GIC寄存器的访问非常快,因此整个关中断的时间也不是非常的长。但是如果是IO expander这个情况,如果采取和上面GIC级联的处理方式一样的话,关中断的时间非常长。我们还是用外设1产生的中断为例子好了。这时候,由于IRQ B的的中断描述符的highlevel irq-events handler处理设计I2C的操作,因此时间非常的长,这时候,对于整个系统的实时性而言是致命的打击。对这种硬件情况,linux kernel处理如下:

(a)IRQ A的中断描述符的highlevel irq-events handler根据实际情况进行设定,并且允许注册irqaction。对于连接到IO expander上的外设,它是没有real time的要求的(否则也不会接到IO expander上),因此一般会进行线程化处理。由于threaded handler中涉及I2C操作,因此要设定IRQF_ONESHOT的flag。

(b)在IRQ A的中断描述符的threaded interrupt handler中进行进行IRQ number的映射,在IO expander irq domain上翻译出具体外设的IRQ number,并直接调用handle_nested_irq函数处理该IRQ。

(c)外设对应的中断描述符设定IRQ_NESTED_THREAD的flag,表明这是一个nested IRQ。nested IRQ没有highlevel irq-events handler,也没有primary handler,它的threaded interrupt handler是附着在其parent IRQ的threaded handler上的。

具体的nested IRQ的处理代码如下:

static int __setup_irq(unsigned int irq, struct irq_desc *desc, struct irqaction *new) 
{

…… 
    nested = irq_settings_is_nested_thread(desc); 
    if (nested) { 
        if (!new->thread_fn) { 
            ret = -EINVAL; 
            goto out_mput; 
        } 
        new->handler = irq_nested_primary_handler;
 
    } else {  
…… 
    }

……

}

如果一个中断描述符是nested thread type的,说明这个中断描述符应该设定threaded interrupt handler(当然,内核是不会单独创建一个thread的,它是借着其parent IRQ的interrupt thread执行),否则就会出错返回。对于primary handler,它应该没有机会被调用到,当然为了调试,kernel将其设定为irq_nested_primary_handler,以便在调用的时候打印一些信息,让工程师直到发生了什么状况。

(2)forced irq threading处理

具体的forced irq threading的处理代码如下:

static int __setup_irq(unsigned int irq, struct irq_desc *desc, struct irqaction *new) 
{

…… 
    nested = irq_settings_is_nested_thread(desc); 
    if (nested) {  
…… 
    } else { 
        if (irq_settings_can_thread(desc)) 
            irq_setup_forced_threading(new);
 
    }

……

}

forced irq threading其实就是将系统中所有可以被线程化的中断handler全部线程化,即便你在request irq的时候,设定的是primary handler,而不是threaded handler。当然那些不能被线程化的中断(标注了IRQF_NO_THREAD的中断,例如系统timer)还是排除在外的。irq_settings_can_thread函数就是判断一个中断是否可以被线程化,如果可以的话,则调用irq_setup_forced_threading在set irq的时候强制进行线程化。具体代码如下:

static void irq_setup_forced_threading(struct irqaction *new) 

    if (!force_irqthreads)-------------------------------(a) 
        return; 
    if (new->flags & (IRQF_NO_THREAD | IRQF_PERCPU | IRQF_ONESHOT))-------(b) 
        return;

    new->flags |= IRQF_ONESHOT; -------------------------(d)

    if (!new->thread_fn) {-------------------------------(c) 
        set_bit(IRQTF_FORCED_THREAD, &new->thread_flags); 
        new->thread_fn = new->handler; 
        new->handler = irq_default_primary_handler; 
    } 
}

(a)系统中有一个强制线程化的选项:CONFIG_IRQ_FORCED_THREADING,如果没有打开该选项,force_irqthreads总是0,因此irq_setup_forced_threading也就没有什么作用,直接return了。如果打开了CONFIG_IRQ_FORCED_THREADING,说明系统支持强制线程化,但是具体是否对所有的中断进行强制线程化处理还是要看命令行参数threadirqs。如果kernel启动的时候没有传入该参数,那么同样的,irq_setup_forced_threading也就没有什么作用,直接return了。只有bootloader向内核传入threadirqs这个命令行参数,内核才真正在启动过程中,进行各个中断的强制线程化的操作。

(b)看到IRQF_NO_THREAD选项你可能会奇怪,前面irq_settings_can_thread函数不是检查过了吗?为何还要重复检查?其实一个中断是否可以进行线程化可以从两个层面看:一个是从底层看,也就是从中断描述符、从实际的中断硬件拓扑等方面看。另外一个是从中断子系统的用户层面看,也就是各个外设在注册自己的handler的时候是否想进行线程化处理。所有的IRQF_XXX都是从用户层面看的flag,因此如果用户通过IRQF_NO_THREAD这个flag告知kernel,该interrupt不能被线程化,那么强制线程化的机制还是尊重用户的选择的。

PER CPU的中断都是一些较为特殊的中断,不是一般意义上的外设中断,因此对PER CPU的中断不强制进行线程化。IRQF_ONESHOT选项说明该中断已经被线程化了(而且是特殊的one shot类型的),因此也是直接返回了。

(c)强制线程化只对那些没有设定thread_fn的中断进行处理,这种中断将全部的处理放在了primary interrupt handler中(当然,如果中断处理比较耗时,那么也可能会采用bottom half的机制),由于primary interrupt handler是全程关闭CPU中断的,因此可能对系统的实时性造成影响,因此考虑将其强制线程化。struct irqaction中的thread_flags是和线程相关的flag,我们给它打上IRQTF_FORCED_THREAD的标签,表明该threaded handler是被强制threaded的。new->thread_fn = new->handler这段代码表示将原来primary handler中的内容全部放到threaded handler中处理,新的primary handler被设定为default handler。

(d)强制线程化是一个和实时性相关的选项,从我的角度来看是一个很不好的设计(个人观点),各个驱动工程师在撰写自己的驱动代码的时候已经安排好了自己的上下文环境。有的是进程上下文,有的是中断上下文,在各自的上下文环境中,驱动工程师又选择了适合的内核同步机制。但是,强制线程化导致原来运行在中断上下文的primary handler现在运行在进程上下文,这有可能导致一些难以跟踪和定位的bug。

当然,作为内核的开发者,既然同意将强制线程化这个特性并入linux kernel,相信他们有他们自己的考虑。我猜测这是和一些旧的驱动代码维护相关的。linux kernel中的中断子系统的API的修改会引起非常大的震动,因为内核中成千上万的驱动都是需要调用旧的接口来申请linux kernel中断子系统的服务,对每一个驱动都进行修改是一个非常耗时的工作,为了让那些旧的驱动代码可以运行在新的中断子系统上,因此,在kernel中,实际上仍然提供了旧的request_irq接口函数,如下:

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); 
}

接口是OK了,但是,新的中断子系统的思路是将中断处理分成primary handler和threaded handler,而旧的驱动代码一般是将中断处理分成top half和bottom half,如何将这部分的不同抹平?linux kernel是这样处理的(这是我个人的理解,不保证是正确的):

(d-1)内核为那些被强制线程化的中断handler设定了IRQF_ONESHOT的标识。这是因为在旧的中断处理机制中,top half是不可重入的,强制线程化之后,强制设定IRQF_ONESHOT可以保证threaded handler是不会重入的。

(d-2)在那些被强制线程化的中断线程中,disable bottom half的处理。这是因为在旧的中断处理机制中,botton half是不可能抢占top half的执行,强制线程化之后,应该保持这一点。

 

(3)创建interrupt线程。代码如下:

if (new->thread_fn && !nested) { 
    struct task_struct *t; 
    static const struct sched_param param = { 
        .sched_priority = MAX_USER_RT_PRIO/2, 
    };

    t = kthread_create(irq_thread, new, "irq/%d-%s", irq,------------------(a)
               new->name);

    sched_setscheduler_nocheck(t, SCHED_FIFO, ?m);


    get_task_struct(t);---------------------------(b) 
    new->thread = t; 

    set_bit(IRQTF_AFFINITY, &new->thread_flags);---------------(c) 
}

if (!alloc_cpumask_var(&mask, GFP_KERNEL)) {----------------(d) 
    ret = -ENOMEM; 
    goto out_thread; 

if (desc->irq_data.chip->flags & IRQCHIP_ONESHOT_SAFE)-----------(e) 
    new->flags &= ~IRQF_ONESHOT;

(a)调用kthread_create来创建一个内核线程,并调用sched_setscheduler_nocheck来设定这个中断线程的调度策略和调度优先级。这些是和进程管理相关的内容,我们留到下一个专题再详细描述吧。

(b)调用get_task_struct可以为这个threaded handler的task struct增加一次reference count,这样,即便是该thread异常退出也可以保证它的task struct不会被释放掉。这可以保证中断系统的代码不会访问到一些被释放的内存。irqaction的thread 成员被设定为刚刚创建的task,这样,primary handler就知道唤醒哪一个中断线程了。

(c)设定IRQTF_AFFINITY的标志,在threaded handler中会检查该标志并进行IRQ affinity的设定。

(d)分配一个cpu mask的变量的内存,后面会使用到

(e)驱动工程师是撰写具体外设驱动的,他/她未必会了解到底层的一些具体的interrupt controller的信息。有些interrupt controller(例如MSI based interrupt)本质上就是就是one shot的(通过IRQCHIP_ONESHOT_SAFE标记),因此驱动工程师设定的IRQF_ONESHOT其实是画蛇添足,因此可以去掉。

 

(4)共享中断的检查。代码如下:

old_ptr = &desc->action; 
old = *old_ptr;

if (old) { 
    if (!((old->flags & new->flags) & IRQF_SHARED) ||-----------------(a) 
        ((old->flags ^ new->flags) & IRQF_TRIGGER_MASK) || 
        ((old->flags ^ new->flags) & IRQF_ONESHOT)) 
        goto mismatch;

    /* All handlers must agree on per-cpuness */ 
    if ((old->flags & IRQF_PERCPU) != (new->flags & IRQF_PERCPU)) 
        goto mismatch;

    /* add new interrupt at end of irq queue */ 
    do {------------------------------------(b) 
        thread_mask |= old->thread_mask; 
        old_ptr = &old->next; 
        old = *old_ptr; 
    } while (old); 
    shared = 1; 
}

(a)old指向注册之前的action list,如果不是NULL,那么说明需要共享interrupt line。但是如果要共享,需要每一个irqaction都同意共享(IRQF_SHARED),每一个irqaction的触发方式相同(都是level trigger或者都是edge trigger),相同的oneshot类型的中断(都是one shot或者都不是),per cpu类型的相同中断(都是per cpu的中断或者都不是)。

(b)将该irqaction挂入队列的尾部。

 

(5)thread mask的设定。代码如下:

if (new->flags & IRQF_ONESHOT) { 
        if (thread_mask == ~0UL) {------------------------(a) 
            ret = -EBUSY; 
            goto out_mask; 
        } 
        new->thread_mask = 1 << ffz(thread_mask);

    } else if (new->handler == irq_default_primary_handler && 
           !(desc->irq_data.chip->flags & IRQCHIP_ONESHOT_SAFE)) {--------(b) 
        ret = -EINVAL; 
        goto out_mask; 
    }

对于one shot类型的中断,我们还需要设定thread mask。如果一个one shot类型的中断只有一个threaded handler(不支持共享),那么事情就很简单(临时变量thread_mask等于0),该irqaction的thread_mask成员总是使用第一个bit来标识该irqaction。但是,如果支持共享的话,事情变得有点复杂。我们假设这个one shot类型的IRQ上有A,B和C三个irqaction,那么A,B和C三个irqaction的thread_mask成员会有不同的bit来标识自己。例如A的thread_mask成员是0x01,B的是0x02,C的是0x04,如果有更多共享的irqaction(必须是oneshot类型),那么其thread_mask成员会依次设定为0x08,0x10……

(a)在上面“共享中断的检查”这个section中,thread_mask变量保存了所有的属于该interrupt line的thread_mask,这时候,如果thread_mask变量如果是全1,那么说明irqaction list上已经有了太多的irq action(大于32或者64,和具体系统和编译器相关)。如果没有满,那么通过ffz函数找到第一个为0的bit作为该irq action的thread bit mask。

(b)irq_default_primary_handler的代码如下:

static irqreturn_t irq_default_primary_handler(int irq, void *dev_id) 

    return IRQ_WAKE_THREAD; 
}

代码非常的简单,返回IRQ_WAKE_THREAD,让kernel唤醒threaded handler就OK了。使用irq_default_primary_handler虽然简单,但是有一个风险:如果是电平触发的中断,我们需要操作外设的寄存器才可以让那个asserted的电平信号消失,否则它会一直持续。一般,我们都是直接在primary中操作外设寄存器(slow bus类型的interrupt controller不行),尽早的clear interrupt,但是,对于irq_default_primary_handler,它仅仅是wakeup了threaded interrupt handler,并没有clear interrupt,这样,执行完了primary handler,外设中断仍然是asserted,一旦打开CPU中断,立刻触发下一次的中断,然后不断的循环。因此,如果注册中断的时候没有指定primary interrupt handler,并且没有设定IRQF_ONESHOT,那么系统是会报错的。当然,有一种情况可以豁免,当底层的irq chip是one shot safe的(IRQCHIP_ONESHOT_SAFE)。

(6)用户IRQ flag和底层interrupt flag的同步(TODO)


原创文章,转发请注明出处。蜗窝科技。

标签: request_threaded_irq

评论:

passerby 
2015-06-18 16:35
hi linux, 
    有个小问题,我们通过request_irq申请时指定中断触发标志IRQF_TRIGGER_RISING ,但是这个表示我在代码中没有看到怎么写到寄存器里面的。能否指点一下
passerby 
2015-06-18 17:15
@passerby:OK,刚才是老眼昏花了。。。找到了,是__irq_set_trigger里面定义的,然后调用irq chip的irq_set_type进行寄存器设置的。
RobinHsiang 
2015-05-14 17:09
@linuxer 
如果中断控制器可以支持足够多的interrupt source,那么在两个外设间共享一个interrupt request line是不推荐的,毕竟有一些额外的开销(发生中断的时候要逐个询问是不是你的中断,软件上就是遍历action list) 
------------------------------------------------------------------------------------------------------------------------- 
按上面描述,在遍历的时候做一次对比。没完全搞明白这个是如何实现的。 
我的理解是: 
首先,中断到来,那肯定先得到一个硬件寄存器中的信息。 
接着,那另一边就遍历所有这些IRQF_SHARED flags的信息,我不知道这些中断处理程序(标记IRQF_SHARED flags的)是如何被保存的,是抽象保存到一个链表里面?如果是这样,那这个是内核访问的全局地址抽象出来的,还是在request_irq(....,void *dev_id)时抽取的这个设备结构体指针?感觉应该保存这个dev_id,至少我表面上看上去这样会直观一点。不知道内核如何管理这些的?
linuxer 
2015-05-14 23:00
@RobinHsiang:我自己感觉你的问题都可以在中找到答案,你先看看这份文档,如果仍有疑问,那么我再进一步解答
super- 
2015-05-15 11:49
@linuxer:@linuxer: 

    看到您的这一句描述 "为了让系统的performance不受影响,irqaction的callback函数必须在函数的最开始进行判断,是否是自己的硬件设备产生了中断(读取硬件的寄存器),如果不是,尽快的退出。" 

    有一个关于共享中断的疑问, 共享中断是不是根据您上面的描述来区分具体的判断是哪一个设备发生了中断, 然后调用具体的action list中的specific handler ?
super- 
2015-05-15 12:18
@super-:我看了技术手册上的一些设备, 没有找到用于记录设备产生了中断的寄存器?  还是我没有找到?
linuxer 
2015-05-15 18:09
@super-:如果多个设备共享interrupt request line,那么任何一个产生了中断都可以触发中断线的有效状态,从而让interrupt controller向CPU汇报该中断,对于通用中断子系统而言,它无法分辨哪个设备产生了中断,因此只能是遍历action list,让specific handler自己判断。 

一般而言,设备中总是有记录中断状态寄存器的,你再仔细看看datasheet
super- 
2015-05-18 09:53
@linuxer:非常感谢。
阿孟 
2015-03-25 16:42
Hi Linuxer, 
请教一下, 

“新的内核已经不区分slow handler和fast handle,都是fast handler,都是需要关闭CPU中断的,那些需要后续处理的内容推到threaded interrupt handler中去执行。” 

如果有的中断比较频繁,会不会可能丢失,如果刚好有其他中断已经关闭了CPU中断。 如果有这样的情况有什么建议吗?
linuxer 
2015-03-26 09:55
@阿孟:这是和系统(HW and SW)设计有关的,我们假设有硬件A和硬件B,在A的的handler中是关闭中断的,因此,这时候B产生中断只能体现在中断控制器的中断状态寄存器上,如果该寄存器不能反应多次中断,那么在由于A handler而关闭CPU中断的期间,多次B产生的中断只能是合成一次。 

如何解决这个问题?我想可能有下面的方法: 
1、fast handler就是fast handler,不要做额外的事情,尽量快的处理,减少关闭中断的时间 
2、硬件这么快的产生中断是为何呢?是不是硬件的FIFO不够大?
阿孟 
2015-03-26 10:58
@linuxer:是想使用mcu的一个timer中断,定时处理一些事务。 我这个timer中断是不是可能会导致其他中断收不到?把timer中断处理函数线程化会不会好些。 

中断的原理这一块不是很了解,我补补课。
linuxer 
2015-03-26 12:48
@阿孟:不能用softtime timer吗?为何要用硬件timer的中断来处理某些定时的事务?
阿孟 
2015-03-26 13:29
@linuxer:这个也是我们一个客户搞的东西,具体为啥用个硬件timer还没搞清楚。 

另外 
还有点疑惑,现在版本linux都会禁止cpu中断,那么之前版本linux如果不加IRQF_DISABLED这个flag是支持中断嵌套吗?
linuxer 
2015-03-27 11:54
@阿孟:是的,slow handler执行时间太长了,必须开中断,以便在执行该handler的时候有机会让其他类型的中断handler抢占执行。当然,同一种类型的中断不会嵌套
阿孟 
2015-03-27 15:17
@linuxer:明白了,多谢linuxer的解答。
RobinHsiang 
2015-03-10 21:10
Hi Linuxer, 
请教一个问题,ARM外设使用GPIO中断唤醒CPU时,像GPIO配置成输入,和让该GPIO关联系统中断,以及让GPIO作为中断唤醒源等等动作一定要在外设驱动的初始化函数中完成吗? 

可以在device tree中编写,然后留一个类似的接口,让驱动只单纯的调用一个函数可以吗? 
我其实遇到的问题是,外设(中断源)的驱动是客户写的,但是这些系统相关的他们又不想管。
linuxer 
2015-03-11 09:03
@RobinHsiang:你问的问题涉及了三个模块: 
1、GPIO子系统模块 
2、电源管理子系统模块 
3、中断子系统模块 

实际上,一个外设驱动当然需要调用内核各个子系统的接口来完成自己的具体功能。因此: 
GPIO配置、申请中断以及设置wakeup source本身都是驱动功能的一部分。device tree的主要功能是让系统知道硬件的拓扑结构,不可能提供一个一统天下的接口来完成你说的那一系列功能。 

如果外设驱动是客户写的,那么你是否在他撰写该驱动的时候仔细的定义了规格?我觉得电源管理也是规格之一的。不过我猜你们和客户其实没有那么正式的合同来约定该驱动的开发,因此,他们只想关注硬件功能的代码而不想涉及其他,如果这样的话,那么该驱动的责任人应该是你们,客户仅仅是提供示例代码而已。
RobinHsiang 
2015-03-11 10:16
@linuxer:Thanks..!! 
确实是你所说的,这几个驱动没有约定这么详细,当初只是定了大致如何实现功能。 
关键是这个客户专门做这几个模块的研发,而且可能牵涉到一些军工和政府项目,不开源给我们。 
客户的建议是系统平台部分我们做,他们只open /dev/ttyhsl,至于电源和唤醒部分,我们也提供接口。 
最近也讨论了一些方案,比如侦测UART的传输情况来关闭电源和clock啊,或者将系统平台的情况写一些接口出来供他们调用,但是都感觉怪怪的,也怕实现不好。 

最终看来,只能是我们将代码写好,让他们做填空题,我们再测试了。
linuxer 
2014-09-25 10:16
你可以先用arm-linux-objdump的命令看看是否你编译的异常向量表就是这样的。 
指令“EAFFFFFE”应该是被翻译成一个跳转指令,跳转范围是24-bit的立即数,对于“EAFFFFFE”,显然还没有填入这个立即数,一般而言,当你编译出.o文件的时候,异常向量表的反汇编结果就是: 
__vectors_start(): 
arch/arm/kernel/entry-armv.S:1134 
   0:   eaffffff        b       4 <__vectors_start+0x4> 
arch/arm/kernel/entry-armv.S:1135 
   4:   eafffffe        b       1a0 <vector_und> 
arch/arm/kernel/entry-armv.S:1136 
   8:   e59ffff0        ldr     pc, [pc, #4080] ; 1000 <.vectors+0x1000> 
arch/arm/kernel/entry-armv.S:1137 
   c:   eafffffe        b       120 <vector_pabt> 
arch/arm/kernel/entry-armv.S:1138 
  10:   eafffffe        b       a0 <vector_dabt> 
arch/arm/kernel/entry-armv.S:1139 
  14:   ea000086        b       220 <vector_swi> 
arch/arm/kernel/entry-armv.S:1140 
  18:   eafffffe        b       20 <vector_irq> 
arch/arm/kernel/entry-armv.S:1141 
  1c:   ea000087        b       224 <vector_fiq_offset> 
这时候,还没有link,因此地址还是reallocated。当连接完成,“EAFFFFFE”会被修正,其24-bit的立即数的位置会被填入真正跳转的地址,例如: 
__vectors_start(): 
c000e4e4:    ef9f0000     svc    0x009f0000 
c000e4e8:    ea0000dd     b    c000e864 <early_mem+0x8> 
c000e4ec:    e59ff410     ldr    pc, [pc, #1040]    ; c000e904 <early_initrd+0x38> 
c000e4f0:    ea0000bb     b    c000e7e4 <parse_tag_serialnr+0x8> 
c000e4f4:    ea00009a     b    c000e764 <parse_tag_ramdisk+0x20> 
c000e4f8:    ea0000fa     b    c000e8e8 <early_initrd+0x1c> 
c000e4fc:    ea000078     b    c000e6e4 <parse_tag_videotext+0x20> 
c000e500:    ea0000f7     b    c000e8e4 <early_initrd+0x18> 

我建议你一步一步的检查: 
1、先检查你编译出来的kernel image 
2、完成early_trap_init之后,检查看看exception table的copy的对不对 
3、是否运行时,有些代码误操作了exception table
forion 
2014-09-24 20:05
hi linuxer 
我现在遇到一个问题,就是很多时候我看到0xffff000c的地方是一个机器码为EAFFFFFE也就是 while(1)一样的指令,我不知道什么情况下vector table 会变成这样,不知道你有什么思路没有
阅读(631) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~