天天看点

Linux中断子系统(四)之中断申请注册Linux中断子系统(四)之中断申请注册

Linux中断子系统(四)之中断申请注册

备注:

  1. Kernel版本:5.4

  2. 使用工具:Source Insight 4.0

  3. 参考博客:

Linux中断子系统(一)中断控制器及驱动分析

Linux中断子系统(二)-通用框架处理

吐血整理 | 肝翻Linux中断所有知识点

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

前言

  中断的处理主要有以下几个功能模块:硬件中断号到Linux irq中断号的映射,并创建好irq_desc中断描述符;中断注册时,先获取设备的中断号,根据中断号找到对应的irq_desc,并将设备的中断处理函数添加到irq_desc中;设备触发中断信号时,根据硬件中断号得到Linux irq中断号,找到对应的irq_desc,最终调用到设备的中断处理函数;上述的描述比较简单,更详细的过程,往下看吧。

中断号的获取流程

  设备硬件中断号到Linux irq中断号的映射

Linux中断子系统(四)之中断申请注册Linux中断子系统(四)之中断申请注册
  • 硬件设备的中断信息都在设备树device tree中进行了描述,在系统启动过程中,这些信息都已经加载到内存中并得到了解析;

      

  • 驱动中通常会使用platform_get_irq或irq_of_parse_and_map接口,去根据设备树的信息去创建映射关系(硬件中断号到linux irq中断号映射);

      

  • 在irq_create_fwspec_mapping接口中,会先去找到匹配的irq domain,再去回调该irq domain中的函数集,通常irq domain都是在中断控制器驱动中初始化的,以ARM GICv2为例,最终回调到gic_irq_domain_hierarchy_ops中的函数;

      

  • 如果已经创建好了映射,那么可以直接进行返回linux irq中断号了,否则的话需要irq_domain_alloc_irqs来创建映射关系;

irq_domain_alloc_irqs完成两个工作:

  • 针对linux irq中断号创建一个irq_desc中断描述符;

      

  • 调用domain->ops->alloc函数来完成映射,在ARM GICv2驱动中对应gic_irq_domain_alloc函数,这个函数很关键,所以下文介绍一下;

  

gic_irq_domain_alloc函数如下:

Linux中断子系统(四)之中断申请注册Linux中断子系统(四)之中断申请注册
  • gic_irq_domain_translate:负责解析出设备树中描述的中断号和中断触发类型(边缘触发、电平触发等);

      

  • gic_irq_domain_map:将硬件中断号和linux中断号绑定到一个结构中,也就完成了映射,此外还绑定了irq_desc结构中的其他字段,最重要的是设置了irq_desc->handle_irq的函数指针,这个最终是中断响应时往上执行的入口,这个是关键,下文讲述中断处理过程时还会提到;

      

  • 根据硬件中断号的范围设置irq_desc->handle_irq的指针,共享中断入口为handle_fasteoi_irq,私有中断入口为handle_percpu_devid_irq;

上述函数执行完成后,完成了两大工作:

  1. 硬件中断号与Linux中断号完成映射,并为Linux中断号创建了irq_desc中断描述符;
  2. 数据结构的绑定及初始化,关键的地方是设置了中断处理往上执行的入口;

中断标志位的含义

#define IRQF_SHARED                    0x00000080 //多个设备共享一个中断号,需要外设硬件支持 
#define IRQF_PROBE_SHARED       0x00000100 //中断处理程序允许sharing mismatch发生 
#define __IRQF_TIMER                    0x00000200 //时钟中断 
#define IRQF_PERCPU                     0x00000400 //属于特定CPU的中断 
#define IRQF_NOBALANCING         0x00000800 //禁止在CPU之间进行中断均衡处理 
#define IRQF_IRQPOLL                    0x00001000 //中断被用作轮训 
#define IRQF_ONESHOT                 0x00002000 //一次性触发的中断,不能嵌套,
                                                                             //1)在硬件中断处理完成后才能打开中断;
                                                                            //2)在中断线程化中保持关闭状态,直到该中断源上的所有thread_fn函数都执行完 
#define IRQF_NO_SUSPEND             0x00004000 //系统休眠唤醒操作中,不关闭该中断 
#define IRQF_FORCE_RESUME          0x00008000 //系统唤醒过程中必须强制打开该中断 
#define IRQF_NO_THREAD               0x00010000 //禁止中断线程化 
#define IRQF_EARLY_RESUME           0x00020000 //系统唤醒过程中在syscore阶段resume,而不用等到设备resume阶段 
#define IRQF_COND_SUSPEND        0x00040000 //与NO_SUSPEND的用户共享中断时,执行本设备的中断处理函数

#define IRQF_TIMER		(__IRQF_TIMER | IRQF_NO_SUSPEND | IRQF_NO_THREAD)
           
定义 描述
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_PERCPU 对于这种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_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的外设的中断响应延迟。

中断的注册流程

  设备驱动中,获取到了irq中断号后,通常就会采用request_irq/request_threaded_irq来注册中断,其中request_irq用于注册普通处理的中断,request_threaded_irq用于注册线程化处理的中断;

Linux中断子系统(四)之中断申请注册Linux中断子系统(四)之中断申请注册
  • request_irq也是调用request_threaded_irq,只是在传参的时候,线程处理函数thread_fn函数设置成NULL;

      

  • 由于在硬件中断号和Linux中断号完成映射后,irq_desc已经创建好,可以通过irq_to_desc接口去获取对应的irq_desc;

      

  • 创建irqaction,并初始化该结构体中的各个字段,其中包括传入的中断处理函数赋值给对应的字段;

      

  • __setup_irq用于完成中断的相关设置,包括中断线程化的处理:

  1. 中断线程化用于减少系统关中断的时间,增强系统的实时性;

  2. ARM64默认开启了CONFIG_IRQ_FORCED_THREADING,引导参数传入threadirqs时,则除了IRQF_NO_THREAD外的中断,其他的都将强制线程化处理;

  3. 中断线程化会为每个中断都创建一个内核线程,如果中断进行共享,对应irqaction将连接成链表,每个irqaction都有thread_mask位图字段,当所有共享中断都处理完成后才能unmask中断,解除中断屏蔽;

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)
{
	struct irqaction *action;
	struct irq_desc *desc;
	int retval;

	if (irq == IRQ_NOTCONNECTED)
		return -ENOTCONN;

	/*
	 * Sanity-check: shared interrupts must pass in a real dev-ID,
	 * otherwise we'll have trouble later trying to figure out
	 * which interrupt is which (messes up the interrupt freeing
	 * logic etc).
	 *
	 * Also IRQF_COND_SUSPEND only makes sense for shared interrupts and
	 * it cannot be set along with IRQF_NO_SUSPEND.
	 */
	if (((irqflags & IRQF_SHARED) && !dev_id) ||//对于那些需要共享的中断,在request irq的时候需要给出dev id,否则会出错退出。
	    (!(irqflags & IRQF_SHARED) && (irqflags & IRQF_COND_SUSPEND)) ||
	    ((irqflags & IRQF_NO_SUSPEND) && (irqflags & IRQF_COND_SUSPEND)))
		return -EINVAL;

	desc = irq_to_desc(irq);//通过IRQ number获取对应的中断描述符
	if (!desc)
		return -EINVAL;

	//并非系统中所有的IRQ number都可以request,
	//有些中断描述符被标记为IRQ_NOREQUEST,
	//标识该IRQ number不能被其他的驱动request。
	if (!irq_settings_can_request(desc) ||
	    WARN_ON(irq_settings_is_per_cpu_devid(desc)))//判断一个中断描述符是否需要传递per cpu的device ID
		return -EINVAL;

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

	//初始化 irqaction
	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;

	retval = irq_chip_pm_get(&desc->irq_data);
	if (retval < 0) {
		kfree(action);
		return retval;
	}

	retval = __setup_irq(irq, desc, action);

	if (retval) {
		irq_chip_pm_put(&desc->irq_data);
		kfree(action->secondary);
		kfree(action);
	}

#ifdef CONFIG_DEBUG_SHIRQ_FIXME
	if (!retval && (irqflags & IRQF_SHARED)) {
		/*
		 * It's a shared IRQ -- the driver ought to be prepared for it
		 * to happen immediately, so let's make sure....
		 * We disable the irq to make sure that a 'real' IRQ doesn't
		 * run in parallel with our fake.
		 */
		unsigned long flags;

		disable_irq(irq);
		local_irq_save(flags);

		handler(irq, dev_id);

		local_irq_restore(flags);
		enable_irq(irq);
	}
#endif
	return retval;
}
EXPORT_SYMBOL(request_threaded_irq);
           

__setup_irq函数分析:

Linux中断子系统(四)之中断申请注册Linux中断子系统(四)之中断申请注册

继续阅读