Linux中断(interrupt)子系统之四:驱动程序接口层 & 中断通用逻辑层


在本系列文章的第一篇:Linux中断(interrupt)子系统之一:中断系统基本原理,我把通用中断子系统分为了4个层次,其中的驱动程序接口层和中断通用逻辑层的界限实际上不是很明确,因为中断通用逻辑层的很多接口,既可以被驱动程序使用,也可以被硬件封装层使用,所以我把这两部分的内容放在一起进行讨论。

本章我将会讨论这两层对外提供的标准接口和内部实现机制,几乎所有的接口都是围绕着irq_desc和irq_chip这两个结构体进行的,对这两个结构体不熟悉的读者可以现读一下前面几篇文章。

/*****************************************************************************************************/
声明:本博内容均由http://blog.csdn.net/droidphone原创,转载请注明出处,谢谢!
/*****************************************************************************************************/

1.  irq的打开和关闭

中断子系统为我们提供了一系列用于irq的打开和关闭的函数接口,其中最基本的一对是:

  • disable_irq(unsigned int irq);
  • enable_irq(unsigned int irq);
这两个API应该配对使用,disable_irq可以被多次嵌套调用,要想重新打开irq,enable_irq必须也要被调用同样的次数,为此,irq_desc结构中的depth字段专门用于这两个API嵌套深度的管理。当某个irq首次被驱动程序申请时,默认情况下,设置depth的初始值是0,对应的irq处于打开状态。我们看看disable_irq的调用过程:
Linux中断(interrupt)子系统之四:驱动程序接口层 & 中断通用逻辑层_第1张图片

                                                                             图1.1  disable_irq的调用过程

函数的开始使用异步方式的内部函数__disable_irq_nosync(),所谓异步方式就是不理会当前该irq是否正在被处理(有handler在运行或者有中断线程尚未结束)。有些中断控制器可能挂在某个慢速的总线上,所以在进一步处理前,先通过irq_get_desc_buslock获得总线锁(最终会调用chip->irq_bus_lock),然后进入内部函数__disable_irq:

void __disable_irq(struct irq_desc *desc, unsigned int irq, bool suspend)
{
	if (suspend) {
		if (!desc->action || (desc->action->flags & IRQF_NO_SUSPEND))
			return;
		desc->istate |= IRQS_SUSPENDED;
	}

	if (!desc->depth++)
		irq_disable(desc);
}
前面几句是对suspend的处理,最后两句,只有之前的depth为0,才会通过irq_disable函数,调用中断控制器的回调chip->irq_mask,否则只是简单地把depth的值加1。irq_disable函数还会通过irq_state_set_disabled和irq_state_set_masked,设置irq_data.flag的IRQD_IRQ_DISABLED和IRQD_IRQ_MASK标志。

disable_irq的最后,调用了synchronize_irq,该函数通过IRQ_INPROGRESS标志,确保action链表中所有的handler都已经处理完毕,然后还要通过wait_event等待该irq所有的中断线程退出。正因为这样,在中断上下文中,不应该使用该API来关闭irq,同时要确保调用该API的函数不能拥有该irq处理函数或线程的资源,否则就会发生死锁!!如果一定要在这两种情况下关闭irq,中断子系统为我们提供了另外一个API,它不会做出任何等待动作:

  • disable_irq_nosync();
中断子系统打开irq的的API是:

  • enable_irq();
打开irq无需提供同步的版本,因为irq打开前,没有handler和线程在运行,我们关注一下他对depth的处理,他在内部函数__enable_irq中处理:

void __enable_irq(struct irq_desc *desc, unsigned int irq, bool resume)
{
	if (resume) {
            ......
	}

	switch (desc->depth) {
	case 0:
 err_out:
		WARN(1, KERN_WARNING "Unbalanced enable for IRQ %d\n", irq);
		break;
	case 1: {
                ......
		irq_enable(desc);
                ......
	}
	default:
		desc->depth--;
	}
}
当depth的值为1时,才真正地调用irq_enable(),它最终通过chip->unmask或chip->enable回调开启中断控制器中相应的中断线,如果depth不是1,只是简单地减去1。如果已经是0,驱动还要调用enable_irq,说明驱动程序处理不当,造成enable与disable不平衡,内核会打印一句警告信息:Unbalanced enable for IRQ xxx。

2.  中断子系统内部数据结构访问接口

我们知道,中断子系统内部定义了几个重要的数据结构,例如:irq_desc,irq_chip,irq_data等等,这些数据结构的各个字段控制或影响着中断子系统和各个irq的行为和实现方式。通常,驱动程序不应该直接访问这些数据结构,直接访问会破会中断子系统的封装性,为此,中断子系统为我们提供了一系列的访问接口函数,用于访问这些数据结构。

存取irq_data结构相关字段的API:

        irq_set_chip(irq, *chip) / irq_get_chip(irq)  通过irq编号,设置、获取irq_cip结构指针;

        irq_set_handler_data(irq, *data) / irq_get_handler_data(irq)  通过irq编号,设置、获取irq_desc.irq_data.handler_data字段,该字段是每个irq的私有数据,通常用于硬件封装层,例如中断控制器级联时,父irq用该字段保存子irq的起始编号。

        irq_set_chip_data(irq, *data) / irq_get_chip_data(irq)  通过irq编号,设置、获取irq_desc.irq_data.chip_data字段,该字段是每个中断控制器的私有数据,通常用于硬件封装层。

        irq_set_irq_type(irq, type)  用于设置中断的电气类型,可选的类型有:

  • IRQ_TYPE_EDGE_RISING
  • IRQ_TYPE_EDGE_FALLING
  • IRQ_TYPE_EDGE_BOTH
  • IRQ_TYPE_LEVEL_HIGH
  • IRQ_TYPE_LEVEL_LOW

        irq_get_irq_data(irq)  通过irq编号,获取irq_data结构指针;

        irq_data_get_irq_chip(irq_data *d)  通过irq_data指针,获取irq_chip字段;

        irq_data_get_irq_chip_data(irq_data *d)  通过irq_data指针,获取chip_data字段;

        irq_data_get_irq_handler_data(irq_data *d)  通过irq_data指针,获取handler_data字段;

设置中断流控处理回调API:

        irq_set_handler(irq, handle)  设置中断流控回调字段:irq_desc.handle_irq,参数handle的类型是irq_flow_handler_t。

        irq_set_chip_and_handler(irq, *chip, handle)  同时设置中断流控回调字段和irq_chip指针:irq_desc.handle_irq和irq_desc.irq_data.chip。

        irq_set_chip_and_handler_name(irq, *chip, handle, *name)  同时设置中断流控回调字段和irq_chip指针以及irq名字:irq_desc.handle_irq、irq_desc.irq_data.chip、irq_desc.name。

        irq_set_chained_handler(irq, *chip, handle)  设置中断流控回调字段:irq_desc.handle_irq,同时设置标志:IRQ_NOREQUEST、IRQ_NOPROBE、IRQ_NOTHREAD,该api通常用于中断控制器的级联,父控制器通过该api设置流控回调后,同时设置上述三个标志位,使得父控制器的中断线不允许被驱动程序申请。

3.  在驱动程序中申请中断

系统启动阶段,中断子系统完成了必要的初始化工作,为驱动程序申请中断服务做好了准备,通常,我们用一下API申请中断服务:

request_threaded_irq(unsigned int irq, irq_handler_t handler,
		     irq_handler_t thread_fn,
		     unsigned long flags, const char *name, void *dev);
         irq  需要申请的irq编号,对于ARM体系,irq编号通常在平台级的代码中事先定义好,有时候也可以动态申请。

        handler  中断服务回调函数,该回调运行在中断上下文中,并且cpu的本地中断处于关闭状态,所以该回调函数应该只是执行需要快速响应的操作,执行时间应该尽可能短小,耗时的工作最好留给下面的thread_fn回调处理。

        thread_fn  如果该参数不为NULL,内核会为该irq创建一个内核线程,当中断发生时,如果handler回调返回值是IRQ_WAKE_THREAD,内核将会激活中断线程,在中断线程中,该回调函数将被调用,所以,该回调函数运行在进程上下文中,允许进行阻塞操作。

        flags  控制中断行为的位标志,IRQF_XXXX,例如:IRQF_TRIGGER_RISING,IRQF_TRIGGER_LOW,IRQF_SHARED等,在include/linux/interrupt.h中定义。

        name  申请本中断服务的设备名称,同时也作为中断线程的名称,该名称可以在/proc/interrupts文件中显示。

        dev  当多个设备的中断线共享同一个irq时,它会作为handler的参数,用于区分不同的设备。

下面我们分析一下request_threaded_irq的工作流程。函数先是根据irq编号取出对应的irq_desc实例的指针,然后分配了一个irqaction结构,用参数handler,thread_fn,irqflags,devname,dev_id初始化irqaction结构的各字段,同时做了一些必要的条件判断:该irq是否禁止申请?handler和thread_fn不允许同时为NULL,最后把大部分工作委托给__setup_irq函数:

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

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

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

	action = kzalloc(sizeof(struct irqaction), GFP_KERNEL);
	if (!action)
		return -ENOMEM;

	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);
	chip_bus_sync_unlock(desc);
进入__setup_irq函数,如果参数flag中设置了IRQF_SAMPLE_RANDOM标志,它会调用rand_initialize_irq,以便对随机数的生成产生影响。如果申请的不是一个线程嵌套中断(关于线程嵌套中断,请参阅 Linux中断(interrupt)子系统之三:中断流控处理层中的handle_nested_irq一节),而且提供了thread_fn参数,它将创建一个内核线程:
	if (new->thread_fn && !nested) {
		struct task_struct *t;

		t = kthread_create(irq_thread, new, "irq/%d-%s", irq,
				   new->name);
		if (IS_ERR(t)) {
			ret = PTR_ERR(t);
			goto out_mput;
		}
		/*
		 * We keep the reference to the task struct even if
		 * the thread dies to avoid that the interrupt code
		 * references an already freed task_struct.
		 */
		get_task_struct(t);
		new->thread = t;
	}
如果irq_desc结构中断action链表不为空,说明这个irq已经被其它设备申请过,也就是说,这是一个共享中断,所以接下来会判断这个新申请的中断与已经申请的旧中断的以下几个标志是否一致:

  • 一定要设置了IRQF_SHARED标志
  • 电气触发方式要完全一样(IRQF_TRIGGER_XXXX)
  • IRQF_PERCPU要一致
  • IRQF_ONESHOT要一致
检查这些条件都是因为多个设备试图共享一根中断线,试想一下,如果一个设备要求上升沿中断,一个设备要求电平中断,当中断到达时,内核将不知如何选择合适的流控操作。完成检查后,函数找出action链表中最后一个irqaction实例的指针。

		/* add new interrupt at end of irq queue */
		do {
			thread_mask |= old->thread_mask;
			old_ptr = &old->next;
			old = *old_ptr;
		} while (old);
		shared = 1;
如果这不是一个共享中断,或者是共享中断的第一次申请,函数将初始化irq_desc结构中断线程等待结构:wait_for_threads,disable_irq函数会使用该字段等待所有irq线程的结束。接下来设置中断控制器的电气触发类型,然后处理一些必要的IRQF_XXXX标志位。如果没有设置IRQF_NOAUTOEN标志,则调用irq_startup()打开该irq,在irq_startup()函数中irq_desc中的enable_irq/disable_irq嵌套深度字段depth设置为0,代表该irq已经打开,如果在没有任何disable_irq被调用的情况下,enable_irq将会打印一个警告信息。

		if (irq_settings_can_autoenable(desc))
			irq_startup(desc);
		else
			/* Undo nested disables: */
			desc->depth = 1;
接着,设置cpu和irq的亲缘关系:

		/* Set default affinity mask once everything is setup */
		setup_affinity(irq, desc, mask);
然后,把新的irqaction实例链接到action链表的最后:

	new->irq = irq;
	*old_ptr = new;
最后,唤醒中断线程,注册相关的/proc文件节点:

	if (new->thread)
		wake_up_process(new->thread);

	register_irq_proc(irq, desc);
	new->dir = NULL;
	register_handler_proc(irq, new);
至此,irq的申请宣告完毕,当中断发生时,处理的路径将会沿着:irq_desc.handle_irq,irqaction.handler,irqaction.thread_fn(irqaction.handler的返回值是IRQ_WAKE_THREAD)这个过程进行处理。下图表明了某个irq被申请后,各个数据结构之间的关系:

Linux中断(interrupt)子系统之四:驱动程序接口层 & 中断通用逻辑层_第2张图片

                                                     图3.1  irq各个数据结构之间的关系

4.  动态扩展irq编号

在ARM体系的移动设备中,irq的编号通常在平台级或板级代码中事先根据硬件的连接定义好,最大的irq数目也用NR_IRQS常量指定。几种情况下,我们希望能够动态地增加系统中irq的数量:
  • 配置了CONFIG_SPARSE_IRQ内核配置项,使用基数树动态管理irq_desc结构。
  • 针对多功能复合设备,内部具备多个中断源,但中断触发引脚只有一个,为了实现驱动程序的跨平台,不希望这些中断源的irq被硬编码在板级代码中。
中断子系统为我们提供了以下几个api,用于动态申请/扩展irq编号:
         irq_alloc_desc(node)  申请一个irq,node是对应内存节点的编号;
         irq_alloc_desc_at(at, node)  在指定位置申请一个irq,如果指定位置已经被占用,则申请失败;
         irq_alloc_desc_from(from, node)  从指定位置开始搜索,申请一个irq;
         irq_alloc_descs(irq, from, cnt, node)  申请多个连续的irq编号,从from位置开始搜索;
         irq_free_descs(irq, cnt)  释放irq资源;
以上这些申请函数(宏),会为我们申请相应的irq_desc结构并初始化为默认状态,要想这些irq能够正常工作,我们还要使用第二节提到的api,对必要的字段进行设置,例如:
  • irq_set_chip_and_handler_name
  • irq_set_handler_data
  • irq_set_chip_data
对于没有配置CONFIG_SPARSE_IRQ内核配置项的内核,irq_desc是一个数组,根本不可能做到动态扩展,但是很多驱动又确实使用到了上述api,尤其是mfd驱动,这些驱动并没有我们一定要配置CONFIG_SPARSE_IRQ选项,要想不对这些驱动做出修改,你只能妥协一下,在你的板级代码中把NR_IRQS定义得大一些,留出足够的保留数量

5.  多功能复合设备的中断处理

在移动设备系统中,存在着大量的多功能复合设备,最常见的是一个芯片中,内部集成了多个功能部件,或者是一个模块单元内部集成了功能部件,这些内部功能部件可以各自产生中断请求,但是芯片或者硬件模块对外只有一个中断请求引脚,我们可以使用多种方式处理这些设备的中断请求,以下我们逐一讨论这些方法。

5.1  单一中断模式 

       对于这种复合设备,通常设备中会提供某种方式,以便让CPU获取真正的中断来源, 方式可以是一个内部寄存器,gpio的状态等等。单一中断模式是指驱动程序只申请一个irq,然后在中断处理程序中通过读取设备的内部寄存器,获取中断源,然后根据不同的中断源做出不同的处理,以下是一个简化后的代码:

static int xxx_probe(device *dev)
{
	......
	irq = get_irq_from_dev(dev);

	ret = request_threaded_irq(irq, NULL, xxx_irq_thread,
				   IRQF_TRIGGER_RISING,
				   "xxx_dev", NULL);
	......
	return 0;
}

static irqreturn_t xxx_irq_thread(int irq, void *data)
{
	......
	irq_src = read_device_irq();
	switch (irq_src) {
	case IRQ_SUB_DEV0:
		ret = handle_sub_dev0_irq();
		break;
	case IRQ_SUB_DEV1:
		ret = handle_sub_dev1_irq();
		break;
		......
	default:
		ret = IRQ_NONE;
		break;
	}
	......
	return ret;
}


5.2  共享中断模式

共享中断模式充分利用了通用中断子系统的特性,经过前面的讨论,我们知道,irq对应的irq_desc结构中的action字段,本质上是一个链表,这给我们实现中断共享提供了必要的基础,只要我们以相同的irq编号多次申请中断服务,那么,action链表上就会有多个irqaction实例,当中断发生时,中断子系统会遍历action链表,逐个执行irqaction实例中的handler回调,根据handler回调的返回值不同,决定是否唤醒中断线程。需要注意到是,申请多个中断时,irq编号要保持一致,flag参数最好也能保持一致,并且都要设上IRQF_SHARED标志。在使用共享中断时,最好handler和thread_fn都要提供,在各自的中断处理回调handler中,做出以下处理:

  • 判断中断是否来自本设备;
  • 如果不是来自本设备:
    • 直接返回IRQ_NONE;
  • 如果是来自本设备:
    • 关闭irq;
    • 返回IRQ_WAKE_THREAD,唤醒中断线程,thread_fn将会被执行;

5.3  中断控制器级联模式

多数多功能复合设备内部提供了基本的中断控制器功能,例如可以单独地控制某个子中断的打开和关闭,并且可以方便地获得子中断源,对于这种设备,我们可以把设备内的中断控制器实现为一个子控制器,然后使用中断控制器级联模式。这种模式下,各个子设备拥有各自独立的irq编号,中断服务通过父中断进行分发。
对于父中断,具体的实现步骤如下:
  • 首先,父中断的irq编号可以从板级代码的预定义中获得,或者通过device的platform_data字段获得;
  • 使用父中断的irq编号,利用irq_set_chained_handler函数修改父中断的流控函数;
  • 使用父中断的irq编号,利用irq_set_handler_data设置流控函数的参数,该参数要能够用于判别子控制器的中断来源;
  • 实现父中断的流控函数,其中只需获得并计算子设备的irq编号,然后调用generic_handle_irq即可;
对于子设备,具体的实现步骤如下
  • 为设备内的中断控制器实现一个irq_chip结构,实现其中必要的回调,例如irq_mask,irq_unmask,irq_ack等;
  • 循环每一个子设备,做以下动作:
    • 为每个子设备,使用irq_alloc_descs函数申请irq编号;
    • 使用irq_set_chip_data设置必要的cookie数据;
    • 使用irq_set_chip_and_handler设置子控制器的irq_chip实例和子irq的流控处理程序,通常使用标准的流控函数,例如handle_edge_irq;
  • 子设备的驱动程序使用自身申请到的irq编号,按照正常流程申请中断服务即可。

5.4  中断线程嵌套模式

该模式与中断控制器级联模式大体相似,只不过级联模式时,父中断无需通过request_threaded_irq申请中断服务,而是直接更换了父中断的流控回调,在父中断的流控回调中实现子中断的二次分发。但是这在有些情况下会给我们带来不便,因为流控回调要获取子控制器的中断源,而流控回调运行在中断上下文中,对于那些子控制器需要通过慢速总线访问的设备,在中断上下文中访问显然不太合适,这时我们可以把子中断分发放在父中断的中断线程中进行,这就是我所说的所谓中断线程嵌套模式。下面是大概的实现过程:
对于父中断,具体的实现步骤如下:
  • 首先,父中断的irq编号可以从板级代码的预定义中获得,或者通过device的platform_data字段获得;
  • 使用父中断的irq编号,利用request_threaded_irq函数申请中断服务,需要提供thread_fn参数和dev_id参数;
  • dev_id参数要能够用于判别子控制器的中断来源;
  • 实现父中断的thread_fn函数,其中只需获得并计算子设备的irq编号,然后调用handle_nested_irq即可;
对于子设备,具体的实现步骤如下
  • 为设备内的中断控制器实现一个irq_chip结构,实现其中必要的回调,例如irq_mask,irq_unmask,irq_ack等;
  • 循环每一个子设备,做以下动作:
    • 为每个子设备,使用irq_alloc_descs函数申请irq编号;
    • 使用irq_set_chip_data设置必要的cookie数据;
    • 使用irq_set_chip_and_handler设置子控制器的irq_chip实例和子irq的流控处理程序,通常使用标准的流控函数,例如handle_edge_irq;
    • 使用irq_set_nested_thread函数,把子设备irq的线程嵌套特性打开;
  • 子设备的驱动程序使用自身申请到的irq编号,按照正常流程申请中断服务即可。
应为子设备irq的线程嵌套特性被打开,使用request_threaded_irq申请子设备的中断服务时,即是是提供了handler参数,中断子系统也不会使用它,同时也不会为它创建中断线程,子设备的thread_fn回调是在父中断的中断线程中,通过handle_nested_irq调用的,也就是说,尽管子中断有自己独立的irq编号,但是它们没有独立的中断线程,只是共享了父中断的中断服务线程。

你可能感兴趣的:(Linux内核架构,Linux中断子系统,Linux设备驱动)