Skip to content

LV010-中断申请流程

在 linux 中如何使用中断?基本流程是怎样的?

一、中断相关 API

1. 中断申请函数

1.1 request_irq()

c
// #include <linux/interrupt.h>
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);
}

request_irq() 函数的主要功能是请求一个中断号, 并将一个中断处理程序与该中断号关联起来。 当中断事件发生时, 与该中断号关联的中断处理程序会被调用执行。request_irq() 函数可能会导致睡眠,因此不能在中断上下半部或者其他禁止睡眠的代码段中使用 request_irq() 函数。request_irq() 函数会激活(使能)中断,所以不需要我们手动去使能中断。

参数说明

  • irq: 要请求的中断号(IRQ number) 。中断号需要通过 gpio_to_irq() 函数映射 GPIO 引脚来获得。
  • handler: irq_handler_t 类型,指向中断处理程序的函数指针。中断处理程序是在中断事件发生时调用的函数, 用于处理中断事件 。
  • flags: 标志位, 用于指定中断处理程序的行为和属性, 如中断触发方式、 中断共享等。可以看这里:interrupt.h - include/linux/interrupt.h
txt
IRQF_TRIGGER_NONE   : 无触发方式, 表示中断不会被触发。
IRQF_TRIGGER_RISING : 上升沿触发方式, 表示中断在信号上升沿时触发。
IRQF_TRIGGER_FALLING: 下降沿触发方式, 表示中断在信号下降沿时触发。
IRQF_TRIGGER_HIGH   : 高电平触发方式, 表示中断在信号为高电平时触发。
IRQF_TRIGGER_LOW    : 低电平触发方式, 表示中断在信号为低电平时触发。
IRQF_SHARED         : 中断共享方式, 表示中断可以被多个设备共享使用。

比如 I.MX6U-ALPHA 开发板上的 KEY0 使用 GPIO1_IO18,按下 KEY0 以后为低电平,因此可以设置为下降沿触发,也就是将 flags 设置为 IRQF_TRIGGER_FALLING。

  • name: 中断的名称, 用于标识该中断,设置以后可以在/proc/interrupts 文件中看到对应的中断名字。
  • dev:如果将 flags 设置为 IRQF_SHARED 的话, dev 用来区分不同的中断,一般情况下将 dev 设置为设备结构体, dev 会传递给中断处理函数 irq_handler_t 的第二个参数。

返回值】 成功: 0 或正数, 表示中断请求成功。失败: 负数, 表示中断请求失败, 返回的负数值表示错误代码,如果返回-EBUSY 的话表示中断已经被申请了。

1.2 free_irq()

free_irq() 函数用于释放之前通过 request_irq() 函数注册的中断处理程序。 它的作用是取消对中断的注册并释放相关的系统资源,包括中断号、 中断处理程序和设备标识等。

c
//#include <linux/interrupt.h>
const void *free_irq(unsigned int irq, void *dev_id)
{
	struct irq_desc *desc = irq_to_desc(irq);
	struct irqaction *action;
	const char *devname;
	//......
	action = __free_irq(desc, dev_id);
	//......
	kfree(action);
	return devname;
}
EXPORT_SYMBOL(free_irq);

参数说明

  • irq: 要释放的中断号。
  • dev_id: 设备标识, 用于区分不同的中断请求。 它通常是在 request_irq 函数中传递的设备特定数据指针。(如果中断设置为共享(IRQF_SHARED)的话,此参数用来区分具体的中断。共享中断只有在释放最后中断处理函数的时候才会被禁止掉。)

返回值】 无

1.3 最后一个参数产生的崩溃

后面测试的过程中出现崩溃:

image-20250323100809900

这里分析一下原因,中断申请函数 request_irq()与中断释放函数 free_irq()的最后一个参数(void *dev 设备结构体)要保持一致,必须是同一个指针。我们可以看一下释放函数 free_irq() 的源码:

c
const void *free_irq(unsigned int irq, void *dev_id)
{
	struct irq_desc *desc = irq_to_desc(irq);
	struct irqaction *action;
	const char *devname;

	if (!desc || WARN_ON(irq_settings_is_per_cpu_devid(desc)))
		return NULL;
	//......
	action = __free_irq(desc, dev_id);
	if (!action)
		return NULL;

	devname = action->name;
	kfree(action);
	return devname;
}

再来看这个 __free_irq()

c
static struct irqaction *__free_irq(struct irq_desc *desc, void *dev_id)
{
	//......
	/*
	 * There can be multiple actions per IRQ descriptor, find the right
	 * one based on the dev_id:
	 */
	action_ptr = &desc->action;
	for (;;) {
		action = *action_ptr;

		if (!action) {
			WARN(1, "Trying to free already-free IRQ %d\n", irq);
			raw_spin_unlock_irqrestore(&desc->lock, flags);
			chip_bus_sync_unlock(desc);
			mutex_unlock(&desc->request_mutex);
			return NULL;
		}

		if (action->dev_id == dev_id)
			break;
		action_ptr = &action->next;
	}
	//......
}

可以看到,这里会判断 request_irq 填入的 dev_idfree_irqdev_id(也就是第二个参数),如果一致,才会退出循环。

2. 中断号获取

2.1 gpio_to_irq()

2.1.1 函数说明

gpio_to_irq() 是一个宏,使用的时候需要包含 <linux/gpio.h>,会定义成__gpio_to_irq()

c
static inline int __gpio_to_irq(unsigned gpio)
{
	return gpiod_to_irq(gpio_to_desc(gpio));
}

该函数是一个用于将 GPIO 引脚映射到对应中断号的函数。 它的作用是根据给定的 GPIO 引脚号, 获取与之关联的中断号。

参数说明

  • gpio: 要映射的 GPIO 引脚号。

返回值】 成功,返回值为该 GPIO 引脚所对应的中断号。失败,返回值为负数, 表示映射失败或无效的 GPIO 引脚号。

2.1.2 怎么知道映射到哪个中断?

我们看一下 gpiod_to_irq(),这个函数定义如下:

c
int gpiod_to_irq(const struct gpio_desc *desc)
{
	struct gpio_chip *chip;
	int offset;

	/*
	 * Cannot VALIDATE_DESC() here as gpiod_to_irq() consumer semantics
	 * requires this function to not return zero on an invalid descriptor
	 * but rather a negative error number.
	 */
	if (!desc || IS_ERR(desc) || !desc->gdev || !desc->gdev->chip)
		return -EINVAL;

	chip = desc->gdev->chip;
	offset = gpio_chip_hwgpio(desc);
	if (chip->to_irq) {
		int retirq = chip->to_irq(chip, offset);

		/* Zero means NO_IRQ */
		if (!retirq)
			return -ENXIO;

		return retirq;
	}
	return -ENXIO;
}
EXPORT_SYMBOL_GPL(gpiod_to_irq);

感觉有点复杂,目前的学习重点不在这里,以后有需要再补充。

2.2 irq_of_parse_and_map()

c
/**
 * irq_of_parse_and_map - Parse and map an interrupt into linux virq space
 * @dev: Device node of the device whose interrupt is to be mapped
 * @index: Index of the interrupt to map
 *
 * This function is a wrapper that chains of_irq_parse_one() and
 * irq_create_of_mapping() to make things easier to callers
 */
unsigned int irq_of_parse_and_map(struct device_node *dev, int index)
{
	struct of_phandle_args oirq;

	if (of_irq_parse_one(dev, index, &oirq))
		return 0;

	return irq_create_of_mapping(&oirq);
}
EXPORT_SYMBOL_GPL(irq_of_parse_and_map);

当中断信息写在设备树中的时候,可以通过此函数从 interupts 属性中提取到对应的设备号。

参数说明

  • dev: 设备节点。
  • index:索引号, interrupts 属性可能包含多条中断信息,通过 index 指定要获取的信息。返回值:中断号。

返回值】 成功,返回值为该 GPIO 引脚所对应的中断号。失败,返回值为负数, 表示映射失败或无效的 GPIO 引脚号。

2.3 of_irq_get()

c
int of_irq_get(struct device_node *dev, int index)
{
	int rc;
	struct of_phandle_args oirq;
	struct irq_domain *domain;

	rc = of_irq_parse_one(dev, index, &oirq);
	if (rc)
		return rc;

	domain = irq_find_host(oirq.np);
	if (!domain)
		return -EPROBE_DEFER;

	return irq_create_of_mapping(&oirq);
}

3. 中断服务函数

中断处理程序是在中断事件发生时自动调用的函数。 它负责处理与中断相关的操作, 例如读取数据、 清除中断标志、 更新状态等。中断服务函数的函数类型如下:

c
typedef irqreturn_t (*irq_handler_t)(int, void *);

我们可以定义一个中断服务函数如下:

c
irqreturn_t handler(int irq, void *dev_id);

handler 函数是一个中断服务函数, 用于处理特定中断事件。 它在中断事件发生时被操作系统或硬件调用, 执行必要的操作来响应和处理中断请求。

参数说明

  • irq: 表示中断号或中断源的标识符。 它指示引发中断的硬件设备或中断控制器。

  • dev_id: 是一个 void 类型的指针, 用于传递设备特定的数据或标识符。需要与 request_irq() 函数的 dev 参数保持一致。用于区分共享中断的不同设备, dev 也可以指向设备数据结构。

返回值irqreturn_t 类型,是一个特定类型的枚举值, 用于表示中断服务函数的返回状态。它可以有以下几种取值:

c
/**
 * enum irqreturn
 * @IRQ_NONE		interrupt was not from this device or was not handled
 * @IRQ_HANDLED		interrupt was handled by this device
 * @IRQ_WAKE_THREAD	handler requests to wake the handler thread
 */
enum irqreturn {
    // IRQ_NONE: 表示中断服务函数未处理该中断, 中断控制器可以继续处理其他中断请求。
	IRQ_NONE		= (0 << 0),
    // IRQ_HANDLED: 表示中断服务函数已成功处理该中断, 中断控制器无需进一步处理。
	IRQ_HANDLED		= (1 << 0),
    // IRQ_WAKE_THREAD: 表示中断服务函数已处理该中断, 并且请求唤醒一个内核线程来继续执行进一步的处理。 这在一些需要长时间处理的中断情况下使用。
	IRQ_WAKE_THREAD		= (1 << 1),
};

一般中断服务函数返回值使用如下形式:

c
return IRQ_RETVAL(IRQ_HANDLED)

在处理程序中, 通常需要注意以下几个方面:

(1) 处理程序应该尽可能地快速执行, 以避免中断丢失或过多占用 CPU 时间。

(2) 如果中断源是共享的, 处理程序需要处理多个设备共享同一个中断的情况。

(3) 处理程序可能需要与其他部分的代码进行同步, 例如访问共享数据结构或使用同步机制来保护临界区域。

(4) 处理程序可能需要与其他线程或进程进行通信, 例如唤醒等待的线程或发送信号给其他进程。

4. 中断使能与禁止函数

常用的中断使用和禁止函数如下所示:

c
void enable_irq(unsigned int irq);
void disable_irq(unsigned int irq);

enable_irq()disable_irq() 用于使能和禁止指定的中断, irq 就是要使能/禁止的中断号。 disable_irq() 函数要等到当前正在执行的中断处理函数执行完才返回,因此使用者需要保证不会产生新的中断,并且确保所有已经开始执行的中断处理程序已经全部退出。在这种情况下,可以使用另外一个中断禁止函数 disable_irq_nosync()

c
void disable_irq_nosync(unsigned int irq)

disable_irq_nosync() 函数调用以后立即返回,不会等待当前中断处理程序执行完毕。上面三个函数都是使能或者禁止某一个中断,有时候我们需要关闭当前处理器的整个中断系统,也就是常说的关闭全局中断,这个时候可以使用如下两个函数:

c
local_irq_enable()
local_irq_disable()
    
#ifdef CONFIG_TRACE_IRQFLAGS
    
#define local_irq_enable() \
	do { trace_hardirqs_on(); raw_local_irq_enable(); } while (0)
#define local_irq_disable() \
	do { raw_local_irq_disable(); trace_hardirqs_off(); } while (0)
// ......
#else /* ! CONFIG_TRACE_IRQFLAGS */

#define local_irq_enable()	do { raw_local_irq_enable(); } while (0)
#define local_irq_disable()	do { raw_local_irq_disable(); } while (0)
// ......
#endif /* CONFIG_TRACE_IRQFLAGS */

这两个函数定义在 irqflags.h - include/linux/irqflags.h 中。local_irq_enable 用于使能当前处理器中断系统, local_irq_disable 用于禁止当前处理器中断系统。假如 A 任务调用 local_irq_disable 关闭全局中断 10S,当关闭了 2S 的时候 B 任务开始运行, B 任务也调用 local_irq_disable 关闭全局中断 3S, 3 秒以后 B 任务调用 local_irq_enable 函数将全局中断打开了。此时才过去 2+3 = 5 秒的时间,然后全局中断就被打开了,此时 A 任务要关闭 10S 全局中断的愿望就破灭了,然后 A 任务就“生气了”,结果很严重,可能系统都要被 A 任务整崩溃。为了解决这个问题, B 任务不能直接简单粗暴的通过 local_irq_enable 函数来打开全局中断,而是将中断状态恢复到以前的状态,要考虑到别的任务的感受,此时就要用到下面两个函数(定义在 irqflags.h - include/linux/irqflags.h 中):

c
local_irq_save(flags);
local_irq_restore(flags);

这两个函数是一对, local_irq_save 函数用于禁止中断,并且将中断状态保存在 flags 中。local_irq_restore 用于恢复中断,将中断到 flags 状态。

二、中断申请流程分析

接下来我们看一下中断申请函数,来了解一下中断的申请的大概流程。

1. request_irq()

中断申请使用的是 request_irq() 函数, 它用于请求一个中断号(IRQ number) 并将一个中断处理程序与该中断关联起来:

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

从上面的内容可以得到 request_irq() 函数实际上是调用了 request_threaded_irq() 函数来完成中断申请的过程。 request_threaded_irq() 函数提供了线程化的中断处理方式, 可以在中断上下文中执行中断处理函数。

2. request_threaded_irq()

2.1 函数说明

request_threaded_irq() 函数是 Linux 内核提供的一个功能强大的函数, 用于请求分配一个中断, 并将中断处理程序与该中断关联起来。 该函数的主要作用是在系统中注册中断处理函数,以响应对应中断的发生。函数定义如下:

c
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)
{
   //(1) 声明变量和初始化
   struct irqaction *action; // 中断动作结构体指针
   struct irq_desc *desc;    // 中断描述符指针
   int retval;               // 返回值
   //(2) 参数检查:检查中断号是否为未连接状态
   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.
    */
   // 检查中断标志的有效性, 包括共享标志与设备 ID 的关联性, 条件挂起标志的有效性, 以及无挂起标志与条件挂起标志的关联性。
   if (((irqflags & IRQF_SHARED) && !dev_id) ||
       (!(irqflags & IRQF_SHARED) && (irqflags & IRQF_COND_SUSPEND)) ||
       ((irqflags & IRQF_NO_SUSPEND) && (irqflags & IRQF_COND_SUSPEND)))
   	return -EINVAL;
   //(3) 获取中断描述符:根据中断号获取中断描述符
   desc = irq_to_desc(irq);
   if (!desc)
   	return -EINVAL;
   //(4) 检查中断设置:检查中断设置是否可以进行中断请求, 以及是否为每个 CPU 分配唯一设备 ID
   if (!irq_settings_can_request(desc) ||
       WARN_ON(irq_settings_is_per_cpu_devid(desc)))
   	return -EINVAL;
   //(5) 处理中断处理函数和线程处理函数:如果未指定中断处理函数, 则使用默认的主处理函数
   if (!handler) {
   	if (!thread_fn)
   		return -EINVAL;
   	handler = irq_default_primary_handler;
   }
   //(6) 分配并初始化中断动作数据结构
   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;      // 设备 ID
   //(7)获取中断的电源管理引用计数
   retval = irq_chip_pm_get(&desc->irq_data);
   if (retval < 0) {
   	kfree(action);
   	return retval;
   }
   //(8)设置中断并将中断动作与中断描述符关联
   retval = __setup_irq(irq, desc, action);
   // 处理中断设置失败的情况
   if (retval) {
   	irq_chip_pm_put(&desc->irq_data);
   	kfree(action->secondary);
   	kfree(action);
   }
   //(9)可选的共享中断处理
#ifdef CONFIG_DEBUG_SHIRQ_FIXME
   // 如果设置中断成功且中断标志中包含共享标志(IRQF_SHARED)
   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);

2.2 总结

request_threaded_irq() 函数主要功能和作用如下:

(1) 中断请求: request_threaded_irq 函数用于请求一个中断。 它会向内核注册对应中断号的中断处理函数, 并为该中断分配必要的资源。 中断号是标识特定硬件中断的唯一标识符。

(2) 中断处理函数关联: 通过 handler 参数, 将中断处理函数与中断号关联起来。 中断处理函数是一个预定义的函数, 用于处理中断事件。 当中断发生时, 内核将调用该函数来处理中断事件。

(3) 线程化中断处理: request_threaded_irq 函数还支持使用线程化中断处理函数。 通过指定 thread_fn 参数, 可以在一个内核线程上下文中异步执行较长时间的中断处理或延迟敏感的工作。 这有助于避免在中断上下文中阻塞时间过长。

(4) 中断属性设置: 通过 irqflags 参数, 可以设置中断处理的各种属性和标志。 例如, 可以指定中断触发方式(上升沿、 下降沿、 边沿触发等) 、 中断类型(边沿触发中断、 电平触发中断等) 以及其他特定的中断行为。

(5) 设备标识关联: 通过 dev_id 参数, 可以将中断处理与特定设备关联起来。 这样可以在中断处理函数中访问与设备相关的数据。 设备标识符可以是指向设备结构体或其他与设备相关的数据的指针。

(6) 错误处理: request_threaded_irq 函数会返回一个整数值, 用于指示中断请求的结果。如果中断请求成功, 返回值为 0; 如果中断请求失败, 则返回一个负数错误代码, 表示失败的原因。

3. 中断申请 demo

3.1 demo 源码

demo 源码可以看这里:13_interrupt/04_int_gpio。在这个 demo 源码中,不需要像之前一样自己获取 gpio 的地址,然后映射,也不需要在设备树添加什么东西,就直接通过 GPIO 的编号申请中断:

c
static int gpio_irq_init(_CHAR_DEVICE *p_chrdev)
{
    int irq_num = -1;
    // 将 GPIO 引脚映射到中断号
    irq_num = gpio_to_irq(GPIO_PIN);
    PRT("GPIO %d mapped to IRQ %d\n", GPIO_PIN, irq_num);

    // 请求中断
    if (request_irq(irq_num, gpio_irq_handler, IRQF_TRIGGER_RISING, "irq_test", NULL) != 0)
    {
        PRTE("Failed to request IRQ %d\n", irq_num);
        // 请求中断失败,释放 GPIO 引脚
        gpio_free(GPIO_PIN);
        return -ENODEV;
    }
    return 0;
}
  • 怎么计算 GPIO 编号

i.MX6ULL 的 GPIO 引脚被组织成多个 Bank,每个 Bank 包含 32 个 GPIO 引脚。这个我们可以看数据手册过着参考手册,我这里看的数据手册:

image-20250322222306438

GPIO 引脚的编号通常以 GPIOx_y 的形式表示,其中 x 表示 Bank 编号,y 表示该 Bank 中的引脚编号。例如:

  • GPIO1_0 表示 Bank 1 的第 0 个引脚。
  • GPIO2_15 表示 Bank 2 的第 15 个引脚。

那么,i.MX6ULL 的 GPIO 编号可以通过以下公式计算:

shell
GPIO编号 = (Bank编号 - 1) * 32 + 引脚编号

3.2 开发板验证

我们编译后拷贝到开发板,加载驱动:

shell
insmod sdriver_demo.ko
image-20250322222543265

可以看到这里申请到的中断号是 79。我这里其实有个疑问,就是之前裸机开发的时候,计算过 GPIO1_IO18 的中断号,应该是 67+32 = 99,在 NXP 官方提供的库文件中也是这样的:

image-20250322223249063

但是 linux 申请这里是 79,具体可能是有什么映射关系,这里没有深究了,后面有机会再补充吧。驱动加载成功以后可以通过查看/proc/interrupts 文件来检查一下对应的中断有没有被注册上,输入如下命令:

shell
cat /proc/interrupts
image-20250322222720633

然后我们按下按键,然后抬起,会看到中断服务函数被执行了:

image-20250322222817637

三、重要数据结构

大概会有以下几个相关的数据结构:

image-20250317154008608

最核心的结构体是 struct irq_desc,之前为了易于理解,我们说在 Linux 内核中有一个中断数组,对于每一个硬件中断,都有一个数组项,这个数组就是 irq_desc 数组。注意:如果内核配置了 CONFIG_SPARSE_IRQ,那么它就会用基数树(radix tree)来代替 irq_desc 数组。 SPARSE 的意思是“稀疏”,假设大小为 1000 的数组中只用到 2 个数组项,那不是浪费嘛?所以在中断比较“稀疏”的情况下可以用基数树来代替数组。

1. struct irq_desc

1.1 结构体说明

struct irq_desc 定义如下:

c
struct irq_desc {
	struct irq_common_data	irq_common_data; /* 通用中断数据 */
	struct irq_data		irq_data;            /* 中断数据 */
	unsigned int __percpu	*kstat_irqs;     /* 中断统计信息 */
	irq_flow_handler_t	handle_irq;          /* 中断处理函数 */
#ifdef CONFIG_IRQ_PREFLOW_FASTEOI
	irq_preflow_handler_t	preflow_handler; /* 预处理中断处理函数 */
#endif
	struct irqaction	*action;	/* IRQ action list */
	unsigned int		status_use_accessors;
	unsigned int		core_internal_state__do_not_mess_with_it;/* 内核内部状态标志位, 请勿修改 */
	unsigned int		depth;		/* 嵌套中断禁用计数 */
	unsigned int		wake_depth;	/* 嵌套唤醒使能计数 */
	unsigned int		tot_count;
	unsigned int		irq_count;	/* 用于检测损坏的 IRQ 计数 */
	unsigned long		last_unhandled;	/* 未处理计数的老化计时器 */
	unsigned int		irqs_unhandled; /* 未处理的中断计数 */
	atomic_t		threads_handled;    /* 处理中断的线程计数 */
	int			threads_handled_last;
	raw_spinlock_t		lock;           /* 自旋锁 */
	struct cpumask		*percpu_enabled;/* 指向每个 CPU 的使能掩码 */
	const struct cpumask	*percpu_affinity;/* 指向每个 CPU 亲和性掩码 */
#ifdef CONFIG_SMP
	const struct cpumask	*affinity_hint;     /* CPU 亲和性提示 */
	struct irq_affinity_notify *affinity_notify;/* CPU 亲和性变化通知 */
#ifdef CONFIG_GENERIC_PENDING_IRQ
	cpumask_var_t		pending_mask;           /* 等待处理的中断掩码 */
#endif
#endif
	unsigned long		threads_oneshot;
	atomic_t		threads_active;             /* 活动中的线程计数 */
	wait_queue_head_t       wait_for_threads;   /* 等待线程的等待队列头 */
#ifdef CONFIG_PM_SLEEP
	unsigned int		nr_actions;
	unsigned int		no_suspend_depth;
	unsigned int		cond_suspend_depth;
	unsigned int		force_resume_depth;
#endif
#ifdef CONFIG_PROC_FS
	struct proc_dir_entry	*dir;/* proc 文件系统目录项 */
#endif
#ifdef CONFIG_GENERIC_IRQ_DEBUGFS
	struct dentry		*debugfs_file;/* 调试文件系统文件 */
	const char		*dev_name;   /* 设备名称 */
#endif
#ifdef CONFIG_SPARSE_IRQ
	struct rcu_head		rcu;
	struct kobject		kobj;    /* 内核对象 */
#endif
	struct mutex		request_mutex; /* 请求互斥锁 */
	int			parent_irq;            /* 父中断号 */
	struct module		*owner;        /* 模块拥有者 */
	const char		*name;             /* 中断名称 */
} ____cacheline_internodealigned_in_smp;

每一个 irq_desc 数组项中都有一个函数: irq_desc.handle_irq,还有一个 irq_desc.action 链表。要理解它们,需要先看中断结构图:

image-20250317154800714

外部设备 1、外部设备 n 共享一个 GPIO 中断 B,多个 GPIO 中断汇聚到 GIC(通用中断控制器)的 A 号中断, GIC 再去中断 CPU。那么软件处理时就是反过来,先读取 GIC 获得中断号 A,再细分出 GPIO 中断 B,最后判断是哪一个外部芯片发生了中断。所以,中断的处理函数来源有三:

  • (1)GIC 的处理函数

假设 irq_desc [A].handle_irq 是 XXX_gpio_irq_handler(XXX 指厂家),这个函数需要读取芯片的 GPIO 控制器,细分发生的是哪一个 GPIO 中断(假设是 B),再去调用 irq_desc [B]. handle_irq。

注意 : irq_desc [A].handle_irq 细分出中断后 B , 调用对应的 irq_desc [B].handle_irq。

显然中断 A 是 CPU 感受到的顶层的中断, GIC 中断 CPU 时, CPU 读取 GIC 状态得到中断 A。

  • (2)模块的中断处理函数

比如对于 GPIO 模块向 GIC 发出的中断 B,它的处理函数是 irq_desc [B].handle_irq。

BSP 开发人员会设置对应的处理函数,一般是 handle_level_irq 或 handle_edge_irq,从名字上看是用来处理电平触发的中断、边沿触发的中断。

注意:导致 GPIO 中断 B 发生的原因很多,可能是外部设备 1,可能是外部设备 n,可能只是某一个设备,也可能是多个设备。所以 irq_desc [B].handle_irq 会调用某个链表里的函数,这些函数由外部设备提供。这些函数自行判断该中断是否自己产生,若是则处理。

  • (3)外部设备提供的处理函数

这里说的“外部设备”可能是芯片,也可能总是简单的按键。它们的处理函数由自己驱动程序提供,这是最熟悉这个设备的“人”:它知道如何判断设备是否发生了中断,如何处理中断。

对于共享中断,比如 GPIO 中断 B,它的中断来源可能有多个,每个中断源对应一个中断处理函数。所以 irq_desc [B] 中应该有一个链表,存放着多个中断源的处理函数。

一旦程序确定发生了 GPIO 中断 B,那么就会从链表里把那些函数取出来,一一执行。这个链表就是 action 链表。

对于我们举的这个例子来说, irq_desc 数组如下:

image-20250317172230937

1.2 总结

以下是 struct irq_desc 结构体的主要作用和功能:

(1) 中断处理函数管理: irq_desc 结构体中的 handle_irq 字段保存中断处理函数的指针。当硬件触发中断时, 内核会调用该函数来处理中断事件。

(2) 中断行为管理: irq_desc 结构体中的 action 字段是一个指向中断行为列表的指针。中断行为是一组回调函数, 用于注册、 注销和处理与中断相关的事件。

(3) 中断统计信息: irq_desc 结构体中的 kstat_irqs 字段是一个指向中断统计信息的指针。该信息用于记录中断事件的发生次数和处理情况, 可以帮助分析中断的性能和行为。

(4) 中断数据管理: irq_desc 结构体中的 irq_data 字段保存了与中断相关的数据, 如中断号、 中断类型等。 这些数据用于识别和管理中断。

(5) 通用中断数据管理: irq_desc 结构体中的 irq_common_data 字段保存了与中断处理相关的通用数据, 如中断控制器、 中断屏蔽等。 这些数据用于处理和控制中断的行为。

( 6) 中断状态管理: irq_desc 结构体中的其他字段用于管理中断的状态, 如嵌套中断禁用计数、 唤醒使能计数等。 这些状态信息帮助内核跟踪和管理中断的状态变化。

通过使用 irq_desc 结构体, 内核可以有效地管理和处理系统中的硬件中断。 它提供了一个统一的接口, 用于注册和处理中断处理函数、 管理中断行为, 并提供了必要的信息和数据结构来监视和控制中断的行为和状态。

2. struct irqaction

2.1 结构体说明

struct irqaction 是 Linux 内核中用于描述中断行为的数据结构之一。 它用于定义中断处理过程中的回调函数和相关属性。 irqaction 结构体的主要功能是管理与特定中断相关的行为和处理函数。

c
struct irqaction {
	irq_handler_t		handler; // 中断处理函数
	void			    *dev_id; // 设备 ID
	void __percpu		*percpu_dev_id;// 每个 CPU 的设备 ID
	struct irqaction	*next;   // 下一个中断动作结构体
	irq_handler_t		thread_fn;// 线程处理函数
	struct task_struct	*thread;  // 线程结构体指针
	struct irqaction	*secondary;// 次要中断动作结构体
	unsigned int		irq;       // 中断号
	unsigned int		flags;     // 中断标志
	unsigned long		thread_flags;// 线程标志
	unsigned long		thread_mask; // 线程掩码
	const char		*name;           // 设备名称
	struct proc_dir_entry	*dir;    // proc 文件系统目录项指针
} ____cacheline_internodealigned_in_smp;

当调用 request_irq、 request_threaded_irq 注册中断处理函数时,内核就会构造一个 irqaction 结构体。在里面保存 name、 dev_id 等,最重要的是 handler、 thread_fn、 thread。

  • handler 是中断处理的上半部函数,用来处理紧急的事情。
  • thread_fn 对应一个内核线程 thread,当 handler 执行完毕, Linux 内核会唤醒对应的内核线程。在内核线程里,会调用 thread_fn 函数。

可以提供 handler 而不提供 thread_fn,就退化为一般的 request_irq 函数。可以不提供 handler 只提供 thread_fn,完全由内核线程来处理中断。也可以既提供 handler 也提供 thread_fn,这就是中断上半部、下半部。

在 reqeust_irq 时可以传入 dev_id,为何需要 dev_id?

(1)中断处理函数执行时,可以使用 dev_id 。

(2)卸载中断时要传入 dev_id,这样才能在 action 链表中根据 dev_id 找到对应项。

所以在共享中断中必须提供 dev_id,非共享中断可以不提供。

2.2 总结

以下是 struct irqaction 结构体的主要作用和功能:

(1) 中断处理函数管理: irqaction 结构体中的 handler 字段保存中断处理函数的指针。 该函数在中断发生时被调用, 用于处理中断事件。

(2) 中断处理标志管理: irqaction 结构体中的 flags 字段用于指定中断处理的各种属性和标志。 这些标志控制中断处理的行为, 例如触发方式、 中断类型等。

(3) 设备标识符管理: irqaction 结构体中的 dev_id 字段用于保存与中断处理相关的设备标识符。 它可以是指向设备结构体或其他与设备相关的数据的指针, 用于将中断处理与特定设备关联起来。

(4) 中断行为链表管理: irqaction 结构体中的 next 字段是一个指向下一个 irqaction 结构体的指针, 用于构建中断行为的链表。 这样可以将多个中断处理函数链接在一起, 以便在中断发生时按顺序调用它们。

通过使用 irqaction 结构体, 内核可以灵活地定义和管理与特定中断相关的行为和处理函数。 它提供了一个统一的接口, 用于注册和注销中断处理函数, 并提供了必要的属性和数据结构来控制中断处理的行为和顺序。

3. struct irq_data

struct irq_data 主要内容如下:

image-20250317202410518
c
struct irq_data {
	u32			mask;
	unsigned int		irq;
	unsigned long		hwirq;
	struct irq_common_data	*common;
	struct irq_chip		*chip;
	struct irq_domain	*domain;
#ifdef	CONFIG_IRQ_DOMAIN_HIERARCHY
	struct irq_data		*parent_data;
#endif
	void			*chip_data;
};

它就是个中转站,里面有 irq_chip 指针 irq_domain 指针,都是指向别的结构体。其中的 irq、 hwirq, irq 是软件中断号, hwirq 是硬件中断号。比如上面我们举的例子,在 GPIO 中断 B 是软件中断号,可以找到 irq_desc [B] 这个数组项; GPIO 里的第 x 号中断,这就是 hwirq。

谁来建立 irq、 hwirq 之间的联系呢?由 irq_domain 来建立。 irq_domain 会把本地的 hwirq 映射为全局的 irq,什么意思?比如 GPIO 控制器里有第 1 号中断, UART 模块里也有第 1 号中断,这两个“第 1 号中断”是不一样的,它们属于不同的“域”——irq_domain。

4. struct irq_domain

struct irq_domain 主要内容如下:

image-20250317202625200

当我们后面学习如何在设备树中指定中断,设备树的中断如何被转换为 irq 时, irq_domain 将会起到极大的作为。这里先简单了解一下,在设备树中会看到这样的属性:

c
interrupt-parent = <&gpio1>;
interrupts = <5 IRQ_TYPE_EDGE_RISING>;

它表示要使用 gpio1 里的第 5 号中断, hwirq 就是 5。但是我们在驱动中会使用 request_irq(irq, handler)这样的函数来注册中断, irq 是什么?它是软件中断号,它应该从“ gpio1 的第 5 号中断”转换得来

谁把 hwirq 转换为 irq?由 gpio1 的相关数据结构,就是 gpio1 对应的 irq_domain 结构体。irq_domain 结构体中有一个 irq_domain_ops 结构体,里面有各种操作函数,主要是:

5. struct irq_chip

struct irq_chip 主要内容如下 :

c
struct irq_chip {
	struct device	*parent_device;
	const char	*name;
	unsigned int	(*irq_startup)(struct irq_data *data);
	void		(*irq_shutdown)(struct irq_data *data);
	void		(*irq_enable)(struct irq_data *data);
	void		(*irq_disable)(struct irq_data *data);
	//......
};

这个结构体跟“ chip”即芯片相关,里面各成员的作用在头文件中也列得很清楚:

c
 * @parent_device:	pointer to parent device for irqchip
 * @name:		name for /proc/interrupts
 * @irq_startup:	start up the interrupt (defaults to ->enable if NULL)
 * @irq_shutdown:	shut down the interrupt (defaults to ->disable if NULL)
 * @irq_enable:		enable the interrupt (defaults to chip->unmask if NULL)
 * @irq_disable:	disable the interrupt
 * @irq_ack:		start of a new interrupt
 * @irq_mask:		mask an interrupt source
 * @irq_mask_ack:	ack and mask an interrupt source
 * @irq_unmask:		unmask an interrupt source
 * @irq_eoi:		end of interrupt
 * @irq_set_affinity:	Set the CPU affinity on SMP machines. If the force
 *			argument is true, it tells the driver to
 *			unconditionally apply the affinity setting. Sanity
 *			checks against the supplied affinity mask are not
 *			required. This is used for CPU hotplug where the
 *			target CPU is not yet set in the cpu_online_mask.

我们在 request_irq 后,并不需要手工去使能中断,原因就是系统调用对应的 irq_chip 里的函数帮我们使能了中断。我们提供的中断处理函数中,也不需要执行主芯片相关的清中断操作,也是系统帮我们调用 irq_chip 中的相关函数。

但是对于外部设备相关的清中断操作,还是需要我们自己做的。就像上面图里的“外部设备 1“、“外部设备 n”,外设备千变万化,内核里可没有对应的清除中断操作。

参考资料:

genirq: add threaded interrupt handler support - kernel/git/torvalds/linux.git - Linux kernel source tree

Linux 中断管理 (1)Linux 中断管理机制 - ArnoldLu - 博客园

Linux 卸载驱动时,提示:Trying to free already-free IRQ-CSDN 博客

嵌入式 Linux 驱动笔记(二十七)------中断子系统框架分析_irq: type mismatch-CSDN 博客