跳到主要内容位置

Linux内核-中断机制

1.中断子系统架构#

这里的用户层不是 APP 层,还是在驱动内核中

1.1 中断控制器#

GIC(Generic Interrupt Controller),控制中断的打开、关闭、优先级等等。

1.2 中断号#

IRQ number:CPU 需要为每一个外设中断编号,我们称之 IRQ Number。这个 IRQ number 是一个虚拟的 interrupt ID,和硬件无关,仅仅是被 CPU 用来标识一个外设中断。

HW interrupt ID:对于 GIC 中断控制器而言,它收集了多个外设的 interrupt request line 并向上传递,因此,GIC 中断控制器需要对外设中断进行编码。GIC 中断控制器用 HW interrupt ID 来标识外设的中断。

linux kernel 中的中断子系统需要提供一个将 HW interrupt ID 映射到 IRQ number 上来的机制,也就是 irq domain

1.3 中断类型#

RK3566 与 RK3568 使用的是 GIC-V3

GIC-V3 支持四种类型的中断,分别是 SGI、PPI、SPI 和 LPI,每个中断类型的介绍如下:

  • SGI(Software Generated Interrupt,软件生成中断):SGI 是通过向 GIC 中的 SGI 寄存器写入来生成的中断。它通常用于处理器之间的通信,允许一个 PE 发送中断给一个或多个指定的 PE,中断号 ID0 - ID15 用于 SGI。
  • PPI(Private Peripheral Interrupt,私有外设中断):针对特定 PE 的外设中断。不与其他 PE 共享,中断号 ID16 - ID31 用于 PPI,比如每个核上有个 tick 中断,用于进程调度使用。
  • SPI(Shared Peripheral Interrupt,共享外设中断):全局外设中断,可以路由到指定的处理器核心(PE)或一组 PE,它允许多个 PE 接收同一个中断,此类中断时由外设触发的中断信号,比如按键、串口。中断号 ID32 - ID1019 用于 SPI。
  • LPI(Locality-specific Peripheral Interrupt,特定局部外设中断):LPI 是 GICv3 中引入的一种中断类型,与其他类型的中断有几个不同之处。LPI 总是基于消息的中断,其配置存储在内存表中,而不是寄存器中。

2.内核中断的申请过程(中断上半部)#

2.1 request_irq 函数#

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

请求一个中断号(IRQ number)并将一个中断处理程序(irq_handler_t)与该中断关联起来;flags 是中断标志,例如上升沿触发;name 是名字;dev 是传递给中断处理函数的参数。

request_irq()函数实际上是调用了 request_threaded_irq()函数来完成中断申请的过程:

request_threaded_irq()函数的主要作用是在系统中注册中断处理函数。

其中维护了两个结构体:

struct irqaction action; // 中断动作结构体指针
struct irq_desc desc; // 中断描述符指针

struct irqaction#

struct irqaction {
irq_handler_t handler;
void *dev_id;
void __percpu *percpu_dev_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;
} ____cacheline_internodealigned_in_smp;

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

struct irq_desc#

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;

在 Linux 内核中,每个外设的外部中断都是用的struct irq_desc结构体进行描述的。

每个硬件中断都有一个对应的 irq_desc 实例,它用于记录与该中断相关的各种信息和状态。该结构体的主要功能是管理中断处理函数、中断行为以及与中断处理相关的其他数据。

struct irq_domain#

xlate 解析设备树,map 函数创建或者更新硬件中断号(hw 参数)和虚拟中断号(virq 参数)的映射关系,保存到 platform_device 中

/* include/linux/irqdomain.h */
struct irq_domain {
/* 系统中的irq_domain都会挂入全局链表irq_domain_list,link就是挂入该链表的节点 */
struct list_head link;
//IRQ domain的name
const char *name;
//irq_domain操作集
const struct irq_domain_ops *ops;
void *host_data;
unsigned int flags;
......
#ifdef CONFIG_IRQ_DOMAIN_HIERARCHY
struct irq_domain *parent; /* //支持级联的话,指向父设备 */
#endif
/* 该domain中最大的硬件中断号 */
irq_hw_number_t hwirq_max;
//直接映射的最大hwirq,对于线性、Radix Tree map映射该域为0
unsigned int revmap_direct_max_irq;
/* 线性映射的size,对于Radix Tree map和no map,该值等于0 */
unsigned int revmap_size;
/* 对于Radix Tree map,revmap_tree指向Radix tree的root node */
struct radix_tree_root revmap_tree;
/* 线性映射使用的lookup table */
unsigned int linear_revmap[];
};
struct irq_domain_ops {
/* match是判断一个指定的device_node(node参数)是否和一个irq domain匹配(d参数),
如果匹配的话,返回1 */
int (*match)(struct irq_domain *d, struct device_node *node,
enum irq_domain_bus_token bus_token);
//和match的功能是一样的,系统会优先使用select函数
int (*select)(struct irq_domain *d, struct irq_fwspec *fwspec,
enum irq_domain_bus_token bus_token);
/* 创建或者更新硬件中断号(hw参数)和软件中断号(virq参数)的映射关系 */
int (*map)(struct irq_domain *d, unsigned int virq, irq_hw_number_t hw);
//map和unmap是操作相反的函数
void (*unmap)(struct irq_domain *d, unsigned int virq);
/* 在DTS文件中,各个使用中断的device node会通过一些属性(interrupts、interrupt-parent属性)
来提供中断信息,以便kernel可以正确的进行driver的初始化动作。
而xlate函数就是将指定的设备(node参数)上若干个(intsize参数)中断属性(intspec参数)翻译
成硬件中断号(保存在out_hwirq参数)和中断trigger类型(保存在out_type)
*/
int (*xlate)(struct irq_domain *d, struct device_node *node,
const u32 *intspec, unsigned int intsize,
unsigned long *out_hwirq, unsigned int *out_type);
#ifdef CONFIG_IRQ_DOMAIN_HIERARCHY //支持中断控制器级联
//分配IRQ描述符和中断控制器相关资源
int (*alloc)(struct irq_domain *d, unsigned int virq,
unsigned int nr_irqs, void *arg);
//释放IRQ描述符和中断控制器相关资源
void (*free)(struct irq_domain *d, unsigned int virq,
unsigned int nr_irqs);
......
//和xlate的功能是一样的,系统会优先使用translate
int (*translate)(struct irq_domain *d, struct irq_fwspec *fwspec,
unsigned long *out_hwirq, unsigned int *out_type);
#endif
};

2.2 (重点)内核的中断组织形式与处理流程#

示例:

硬件连接如下图:

处理流程如下图,

1)发生了 GPIO 中断;

2)首先通过 GIC 的中断处理 handle_irq 函数,读取 GPIO 的寄存器,发现是 GPIO 模块的中断,就调用下面的 GPIO 中断 B;

2.1)读取 GPIO 寄存器可以得到 hwirq,根据 hwirq 得到之前映射的 irq;

2.2)再根据映射的 irq 就可以从中断号“数组”里找到对应的 irq_desc;

3)然后在中断中断处理函数 handle_irq,会调用下面挂载的每一个 irqaction 链表的处理函数(这个是对应具体设备的处理函数);

3)在中断 B 的 irqaction 中会判断中断源,进行相应的中断处理(调用 handler,以及内核线程 thread_fn 等)。(irqaction 是我们注册一个中断时,内核创建的)。

3.中断下半部#

3.1 软件中断#

什么是软件中断?#

软件中断是实现中断下半部的方法之一,但是软中断的资源优先,对应的中断号不多,一般用在网络设备、块设备驱动中。

所有软件中断如下:

enum
{
HI_SOFTIRQ=0, // 高优先级软中断
TIMER_SOFTIRQ, // 定时器软中断
NET_TX_SOFTIRQ, // 网络传输发送软中断
NET_RX_SOFTIRQ, // 网络传输接收软中断
BLOCK_SOFTIRQ, // 块设备软中断
IRQ_POLL_SOFTIRQ, // 中断轮询软中断
TASKLET_SOFTIRQ, // 任务软中断
SCHED_SOFTIRQ, // 调度软中断
HRTIMER_SOFTIRQ, /* Unused, but kept as tools rely on the numbering. Sigh! */
RCU_SOFTIRQ, /* Preferable RCU should always be the last softirq */
NR_SOFTIRQS
};

以上代码定义了一个枚举类型,用于标识软中断的不同类型或优先级,每个枚举常量对应一个特定的软中断类型。

内核开发者不希望我们直接添加软中断,而是使用tasklet

以下 API 用于软中断:

void open_softirq(int nr, void (*action)(struct softirq_action *)); //初始一个软中断
void raise_softirq(unsigned int nr); //打开软中断
void raise_softirq_irqoff(unsigned int nr); //关闭软中断

tasklet 的使用#

  • 一种特殊的软中断机制,被广泛用于处理中断下文相关的任务。

  • 一种常见且有效的方法,在多核处理系统上可以避免并发问题。

  • tasklet 绑定的函数在同一时间只能在一个 CPU 上运行,因此不会出现并发冲突。

struct tasklet_struct {
struct tasklet_struct *next;
unsigned long state;
atomic_t count;
void (*func)(unsigned long);
unsigned long data;
};
typedef struct tasklet_struct tasklet_t;

tasklet 的 API 如下:

// 静态初始化
#define DECLARE_TASKLET(name,func,data) \
struct tasklet_struct name = { NULL,0,ATOMIC_INIT(0),func,data}
// 动态初始化
void tasklet_init(struct tasklet_struct *t, void (*func)(unsigned long), unsigned long data);
void tasklet_disable(struct tasklet_struct *t); //关闭
void tasklet_enable(struct tasklet_struct *t); //开启
void tasklet_schedule(struct tasklet_struct *t); // 调度(触发)一个已经初始化的 tasklet 执行
void tasklet_kill(struct tasklet_struct *t); //销毁

tasklet_disable关闭 tasklet 后,即使调用 tasklet_schedule 函数触发 tasklet,tasklet 的处理函数也不会再被执行。这可以用于临时暂停或停止 tasklet 的执行,直到再次启用(通过调用 tasklet_enable 函数)。

需要注意的是,

  • tasklet_disable关闭 tasklet 并不会销毁 tasklet 结构体,因此可以随时通过调用 tasklet_enable 函数重新启用 tasklet,或者调用 tasklet_kill 函数来销毁 tasklet;
  • tasklet_enable使能 tasklet 并不会自动触发 tasklet 的执行,而是通过调用 tasklet_schedule函数来触发;
  • 调度 tasklet 只是将 tasklet 标记为需要执行,并不会立即执行 tasklet 的处理函数。实际的执行时间取决于内核的调度和处理机制;
  • 在销毁 tasklet 之前,应该确保该 tasklet 已经被停止(通过调用 tasklet_disable 函数)。否则,销毁一个正在执行的 tasklet 可能导致内核崩溃或其他错误。

tasklet 内核源码分析#

1)tasklet 如何初始化?#

软中断处理函数的定义内核源码 kernel/kernel/softirq.c 文件

void __init softirq_init(void)
{
int cpu;
// 初始化每个可能的 CPU 的 tasklet_vec 和 tasklet_hi_vec
// 将 tail 指针设置为对应的 head 指针的初始位置
for_each_possible_cpu(cpu) {
per_cpu(tasklet_vec, cpu).tail = &per_cpu(tasklet_vec, cpu).head;
per_cpu(tasklet_hi_vec, cpu).tail = &per_cpu(tasklet_hi_vec, cpu).head;
}
// 注册 TASKLET_SOFTIRQ 软中断,并指定对应的处理函数为 tasklet_action
open_softirq(TASKLET_SOFTIRQ, tasklet_action);
// 注册 HI_SOFTIRQ 软中断,并指定对应的处理函数为 tasklet_hi_action
open_softirq(HI_SOFTIRQ, tasklet_hi_action);
}

在执行__init softirq_init 函数时,会触发 TASKLET_SOFTIRQ,然后会调用 tasklet_action 函数,一路调用,最终是调用tasklet_action_common函数

static void tasklet_action_common(struct softirq_action *a, struct tasklet_head *tl_head, unsigned int softirq_nr)
{
struct tasklet_struct *list;
// 禁用本地中断
local_irq_disable();
// 获取 tasklet_head 中的任务链表
list = tl_head->head;
// 清空 tasklet_head 中的任务链表
tl_head->head = NULL;
// 将 tail 指针重新指向 head 指针的位置
tl_head->tail = &tl_head->head;
// 启用本地中断
local_irq_enable();
// 遍历任务链表,处理每一个 tasklet
while (list) {
struct tasklet_struct *t = list;
// 获取下一个 tasklet,并更新链表
list = list->next;
if (tasklet_trylock(t)) { // 尝试获取 tasklet 的锁
if (!atomic_read(&t->count)) { // 检查 count 计数器是否为 0
if (!test_and_clear_bit(TASKLET_STATE_SCHED, &t->state))
BUG(); // 如果 state 标志位不正确,则发生错误
t->func(t->data); // 执行 tasklet 的处理函数
tasklet_unlock(t); // 解锁 tasklet
continue;
}
tasklet_unlock(t); // 解锁 tasklet
}
// 禁用本地中断
local_irq_disable();
// 将当前 tasklet 添加到 tasklet_head 的尾部
t->next = NULL; *tl_head->tail = t;
// 更新 tail 指针
tl_head->tail = &t->next;
// 触发软中断
__raise_softirq_irqoff(softirq_nr);
// 启用本地中断
local_irq_enable();
}
}

在上面的代码中,tasklet_action_common()函数对任务链表中的每个 tasklet 进行处理。

首先禁用本地中断,获取任务链表头指针,清空任务链表,并重新设置尾指针。

然后它循环遍历任务链表,对每个 tasklet 进行处理。

如果 tasklet 的锁获取成功,并且计数器为 0,它将执行 tasklet 的处理函数,并清除状态标志位。

如果锁获取失败或计数不为 0,它将 tasklet 添加到任务链表的尾部,并触发指定的软中断。

最后,它启用本地中断,完成任务处理过程。

2)那么 tasklet 在什么时候加到链表里面的呢?#

tasklet 是通__tasklet_schedule_common()函数加入到链表中的。

static void __tasklet_schedule_common(struct tasklet_struct *t, struct tasklet_head __percpu *headp, unsigned int softirq_nr)
{
struct tasklet_head *head;
unsigned long flags;
// 保存当前中断状态,并禁用本地中断
local_irq_save(flags);
// 获取当前 CPU 的 tasklet_head 指针
head = this_cpu_ptr(headp);
// 将当前 tasklet 添加到 tasklet_head 的尾部
t->next = NULL;
*head->tail = t;
// 更新 tasklet_head 的尾指针
head->tail = &(t->next);
// 触发指定的软中断
raise_softirq_irqoff(softirq_nr);
// 恢复中断状态
local_irq_restore(flags);
}
3)tasklet 相比自己添加软中断有哪些优点和缺点呢?#

优点:

  1. 简化的接口和编程模型:Tasklet 提供了一个简单的接口和编程模型,使得在内核中处理延迟工作变得更加容易。相比自己添加软中断,Tasklet 提供了更高级的抽象。

  2. 低延迟:Tasklet 在软中断上下文中执行,避免了内核线程的上下文切换开销,因此具有较低的延迟。这对于需要快速响应的延迟敏感任务非常重要。

  3. 自适应调度:Tasklet 具有自适应调度的特性,当多个 Tasklet 处于等待状态时,内核会合并它们以减少不必要的上下文切换。这种调度机制可以提高系统的效率。

缺点:

  1. 无法处理长时间运行的任务:Tasklet 适用于短时间运行的延迟工作,如果需要处理长时间运行的任务,可能会阻塞其他任务的执行。对于较长的操作,可能需要使用工作队列或内核线程来处理。

  2. 缺乏灵活性:Tasklet 的执行受限于软中断的上下文,不适用于所有类型的延迟工作。某些情况下,可能需要更灵活的调度和执行机制,这时自定义软中断可能更加适合。

  3. 资源限制:Tasklet 的数量是有限的,系统中可用的 Tasklet 数量取决于架构和内核配置。如果需要大量的延迟工作处理,可能会受到 Tasklet 数量的限制。

综上所述,Tasklet 提供了一种简单且低延迟的延迟工作处理机制,适用于短时间运行的任务和对响应时间敏感的场景。

3.2 内核线程#

工作队列 work queue#

1)什么是工作队列#

工作队列是操作系统中管理和调度异步任务执行的一种机制,协调和分配待处理的任务可用的工作线程或工作进程

基本原理是

  • 将需要执行的任务按顺序排列在队列中,并提供一组工作线程或者工作进程来处理队列中的任务。

  • 当有新的任务到达时,它们会被添加到队列的末尾,工作线程或工作进程从队列的头部获取任务,并执行相应的处理操作。

工作队列将工作推后以后,会交给内核线程去执行。Linux 在启动过程中会创建一个工作者内核线程(worker thread),这个线程创建以后处于 sleep 状态。当有工作需要处理的时候,会唤醒这个线程去处理工作。

工作队列包括共享工作队列和自定义工作队列两种:

  • 共享队列是由内核管理的全局工作队列,用于处理内核中一些系统级任务。共享工作队列是内核中一个默认工作队列,可以由多个内核组件和驱动程序共享使用。
  • 自定义工作队列是由内核或驱动程序创建的特定工作队列,用于处理特定的任务。自定义工作队列通常与特定的内核模块或驱动程序相关联,用于执行该模块或驱动程序相关的任务。
2)共享工作队列#

在 Linux 内核中,使用 work_struct 结构体表示一个工作项,这些工作组织成工作队列,工作队列使用 workqueue_struct 结构体表示

struct work_struct {
atomic_long_t data;
struct list_head entry;
work_func_t func; /* 工作队列处理函数 */
};
typedef void (*work_func_t)(struct work_struct *work); //工作函数

工作队列相关接口函数:

// 直接定义一个 work_struct 结构体变量即可,然后使用INIT_WORK 宏来初始化工作
#define INIT_WORK(_work,_func) //初始化,INIT_WORK 宏接受两个参数:_work 和 _func,分别表示要初始化的工作项和工作项的处理函数
#define DECLARE_WORK(n, f) // 一次性完成工作的创建和初始化
// 调度/取消调度工作队列函数
static inline bool schedule_work(struct work_struct *work)
bool cancel_work_sync(struct work_struct *work);

示例:

#include <linux/module.h>
#include <linux/init.h>
#include <linux/interrupt.h>
#include <linux/gpio.h>
#include <linux/delay.h>
#include <linux/workqueue.h>
int irq;
struct work_struct test_workqueue;
// 工作项处理函数
void test_work(struct work_struct *work)
{
msleep(1000);
printk("This is test_work\n");
}
// 中断处理函数
irqreturn_t test_interrupt(int irq, void *args)
{
printk("This is test_interrupt\n");
// 提交工作项到工作队列
schedule_work(&test_workqueue);
return IRQ_RETVAL(IRQ_HANDLED);
}
static int interrupt_irq_init(void)
{
int ret;
irq = gpio_to_irq(101); // 将 GPIO 映射为中断号
printk("irq is %d\n", irq);
// 请求中断
ret = request_irq(irq, test_interrupt, IRQF_TRIGGER_RISING, "test", NULL);
if (ret < 0)
{
printk("request_irq is error\n");
return -1;
}
// 初始化工作项
INIT_WORK(&test_workqueue, test_work);
return 0;
}
static void interrupt_irq_exit(void)
{
free_irq(irq, NULL); // 释放中断
printk("bye bye\n");
}
module_init(interrupt_irq_init);
module_exit(interrupt_irq_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("topeet");
3)自定义工作队列#

共享队列是由内核管理的全局工作队列,自定义工作队列是由内核或驱动程序创建的特定工作队列,用于处理特定的任务。

结构体 struct work_struct 描述的是要延迟执行的工作项,定义在 include/linux/workqueue.h 当中,如下所示

struct work_struct {
atomic_long_t data; // 工作项的数据字段
struct list_head entry; // 工作项在工作队列中的链表节点
work_func_t func; // 工作项的处理函数
#ifdef CONFIG_LOCKDEP
struct lockdep_map lockdep_map; // 锁依赖性映射
#endif
};

这些工作组织成工作队列,内核使用 struct workqueue_struct 结构体描述一个工作队列,定义在 include/linux/workqueue.h 当中:

struct workqueue_struct {
struct list_head pwqs; // 工作队列上的挂起工作项列表
struct list_head delayed_works; // 延迟执行的工作项列表
struct delayed_work_timer dwork_timer; // 延迟工作项的定时器
struct workqueue_attrs *unbound_attrs; // 无绑定工作队列的属性
struct pool_workqueue *dfl_pwq; // 默认的池化工作队列
...
};

工作队列相关接口函数:

struct workqueue_struct *create_workqueue(const char *name);
// 参数 name 是创建的工作队列的名字。使用这个函数可以给每个 CPU 都创建一个 CPU 相关的工作队列。创建成功返回一个 struct workqueue_struct 类型指针,创建失败返回 NULL。
// 如果只给一个 CPU 创建一个 CPU 相关的工作队列,使用以下函数。
#define create_singlethread_workqueue(name) \ alloc_workqueue("%s", WQ_SINGLE_THREAD, 1, name)
// 参数 name 是创建的工作队列的名字。使用这个函数只会给一个 CPU 创建一个 CPU 相关的工作队列。创建成功之后返回一个 struct workqueue_struct 类型指针,创建失败返回 NULL。

当工作队列创建好之后,需要将要延迟执行的工作项放在工作队列上,调度工作队列,使用 queue_work_on 函数,函数原型如下所示:

bool queue_work_on(int cpu, struct workqueue_struct *wq, struct work_struct *work);

该函数有三个参数,第一个参数是一个整数 cpu,第二个参数是一个指向 struct workqueue_struct 的指针 wq,第三个参数是一个指向 struct work_struct 的指针 work。

bool cancel_work_sync(struct work_struct *work); //取消一个已经调度的工作,如果被取消的工作已经正在执行,则会等待他执行完成再返回。
void flush_workqueue(struct workqueue_struct *wq); // 刷新该工作队列中所有已提交但未执行的工作项。
void destroy_workqueue(struct workqueue_struct *wq); // 删除自定义的工作队列
4)延迟工作#

延迟工作是一种将工作的执行延迟到稍后时间点进行处理的技术。任务可以在指定的延迟时间后执行,也可以根据优先级,任务类型或者其他条件进行排序和处理。

延迟工作在许多应用场景中都非常有用,尤其是在需要处理大量任务,提供系统性能和可靠性的情况下。以下是一些常用的应用场景:

1 延迟工作常用于处理那些需要花费较长时间的任务,比如发送电子邮件,处理图像等。通过将这些任务放入队列中并延迟执行,可以避免阻塞应用程序的主线程,提高系统的响应速度。

2 延迟工作可以用来执行定时任务,比如定时备份数据库,通过将任务设置为在未来的某个时间点执行,提高系统的可靠性和效率。

使用 struct delayed_work 来描述延迟工作,定义在 include/linux/workqueue.h 当中,原型定义如下所示:

struct delayed_work{
struct work_struct work;// 延迟工作的基本工作结构
struct timer_list timer;// 定时器,用于延迟执行工作
};

示例:

#include <linux/module.h>
#include <linux/init.h>
#include <linux/interrupt.h>
#include <linux/gpio.h>
#include <linux/delay.h>
#include <linux/workqueue.h>
int irq;
struct workqueue_struct *test_workqueue;
struct delayed_work test_delayed_work;
// 工作项处理函数
void test_work(struct work_struct *work)
{
msleep(1000);
printk("This is test_work\n");
}
// 中断处理函数
irqreturn_t test_interrupt(int irq, void *args)
{
printk("This is test_interrupt\n");
// 提交延迟工作项到自定义工作队列:delay:表示延迟的时间长度,以内核时钟节拍数 jiffies 为单位。
queue_delayed_work(test_workqueue, &test_delayed_work, 3 * HZ);
return IRQ_RETVAL(IRQ_HANDLED);
}
static int interrupt_irq_init(void)
{
int ret;
irq = gpio_to_irq(101); // 将 GPIO 映射为中断号
printk("irq is %d\n", irq);
// 请求中断
ret = request_irq(irq, test_interrupt, IRQF_TRIGGER_RISING, "test", NULL);
if (ret < 0)
{
printk("request_irq is error\n");
return -1;
}
// 创建工作队列
test_workqueue = create_workqueue("test_workqueue");
// 初始化延迟工作项
INIT_DELAYED_WORK(&test_delayed_work, test_work);
return 0;
}
static void interrupt_irq_exit(void)
{
free_irq(irq, NULL); // 释放中断
cancel_delayed_work_sync(&test_workqueue_work); // 取消延迟工作项
flush_workqueue(test_workqueue); // 刷新工作队列
destroy_workqueue(test_workqueue); // 销毁工作队列
printk("bye bye\n");
}
module_init(interrupt_irq_init);
module_exit(interrupt_irq_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("topeet");
5)工作队列传参#

之前软中断-tasklet 可以直接传入参数 data:

// 定义 tasklet 处理函数
void my_tasklet_handler(unsigned long data)
{
// Tasklet 处理逻辑
// ...
}

那么工作队列如何传入参数呢?这是工作队列的处理函数:传入的是结构体 work_struct。

// 工作项处理函数
void test_work(struct work_struct *work)
{
...
}

我们可以使用 Linux 中常用的方法:定义一个私有数据结构体,把 work_struct 放入,然后可以通过 work_struct 获得包含它的数据结构。

struct work_data
{
struct work_struct test_work;
int a;
int b;
};
struct work_data test_workqueue_work;

创建工作队列时传入:

// 创建工作队列
test_workqueue = create_workqueue("test_workqueue");
// 初始化工作项
INIT_WORK(&test_workqueue_work.test_work, test_work);

然后在中断处理函数里面调度:

// 提交工作项到工作队列
queue_work(test_workqueue, &test_workqueue_work.test_work);

最后,在工作队列处理函数中,通过container_of函数获得结构体struct work_data

// 工作项处理函数
void test_work(struct work_struct *work)
{
struct work_data *pdata;
pdata = container_of(work, struct work_data, test_work);
printk("a is %d", pdata->a);
printk("b is %d", pdata->b);
}
7)并发管理工作队列#

在使用工作队列时,首先定义一个work 结构体,然后将 work 添加到workqueue(工作队列)中,最后 worker thread 执行 workqueue。

当执行完结束的时候,worker thread 会睡眠,等到新的中断产生,work 再继续添加到工作队列,然后工作线程执行每个工作,周而复始。

多核系统中

  • 存在多个工作队列,每个工作队列与一个工作线程(Worker Thread)绑定。
  • 当有新的工作项产生时,系统需要决定将其分配给哪个工作队列:一种常见的策略是使用负载均衡算法,根据工作队列的负载情况来平衡分配工作项,以避免某个工作队列过载而导致性能下降。
  • 在多核线程系统中,多个工作线程可以同时执行各自绑定的工作队列中的工作项,这样可以实现并行处理,提高系统的整体性能和响应速度。

普通工作队列的弊端

  • 排队等待:在工作项 w0 工作甚至是睡眠时,工作项 w1 w2 是排队等待的,在繁忙的系统中,工作队列可能会积累大量的待处理工作项,导致任务调度的延迟;
  • 线程相互影响:如果工作项的处理时间差异很大,一些工作线程可能会一直忙于处理长时间的工作项,而其他工作线程则处于空闲状态,导致资源利用不均衡;
  • 排队等待,无优先级:在某些场景下,可能需要根据工作项的重要性或紧急程度进行优先级调度,而工作队列本身无法提供这种级别的优先级控制;
  • 当工作线程从工作队列中获取工作项并执行时,可能需要频繁地进行上下文切换,将处理器的执行上下文从一个线程切换到另一个线程。这种上下文切换开销可能会影响系统的性能和效率。

并发管理工作队列:一种并发编程模式,用于有效地管理和调度待执行的任务或工作项。

通过餐厅来类比:

你是一个餐厅的服务员,有很多顾客同时来到餐厅用餐。为了提高效率,你需要将顾客的点菜请求放到一个队列中,这就是工作队列。然后,你和其他服务员可以从队列中获取顾客的点菜请求,每个服务员独立地为顾客提供服务。通过这种方式,你们可以并发地处理多个顾客的点菜请求,而不需要等待上一个顾客点完菜再去处理下一个顾客的请求。每个服务员可以独立地从队列中获取任务,并根据需要执行相应的服务。这种独立获取任务的过程就是从工作队列中取出任务并执行的过程。

struct workqueue_struct *alloc_workqueue(const char *fmt, unsigned int flags, int max_active);
fmt:指定工作队列的名称格式。
flags:指定工作队列的标志,可以控制工作队列的行为和属性,如 WQ_UNBOUND 表示无绑定的工作队列,WQ_HIGHPRI 表示高优先级的工作队列等。
max_active:指定工作队列中同时活跃的最大工作项数量。
函数返回一个指向工作队列结构体(struct workqueue_struct)的指针,或者返回 NULL 表示创建失败。

示例:

#include <linux/module.h>
#include <linux/init.h>
#include <linux/interrupt.h>
#include <linux/gpio.h>
#include <linux/delay.h>
#include <linux/workqueue.h>
int irq;
struct workqueue_struct *test_workqueue;
struct work_struct test_workqueue_work;
// 工作项处理函数
void test_work(struct work_struct *work)
{
msleep(1000);
printk("This is test_work\n");
}
// 中断处理函数
irqreturn_t test_interrupt(int irq, void *args)
{
printk("This is test_interrupt\n");
queue_work(test_workqueue, &test_workqueue_work); // 提交工作项到工作队列
return IRQ_RETVAL(IRQ_HANDLED);
}
static int interrupt_irq_init(void)
{
int ret;
irq = gpio_to_irq(101); // 将 GPIO 映射为中断号
printk("irq is %d\n", irq);
// 请求中断
ret = request_irq(irq, test_interrupt, IRQF_TRIGGER_RISING, "test", NULL);
if (ret < 0)
{
printk("request_irq is error\n");
return -1;
}
// 用于创建和分配一个工作队列
test_workqueue = alloc_workqueue("test_workqueue", WQ_UNBOUND, 0);
// 初始化工作项
INIT_WORK(&test_workqueue_work, test_work);
return 0;
}
static void interrupt_irq_exit(void)
{
free_irq(irq, NULL); // 释放中断
cancel_work_sync(&test_workqueue_work); // 取消工作项
flush_workqueue(test_workqueue); // 刷新工作队列
destroy_workqueue(test_workqueue); // 销毁工作队列
printk("bye bye\n");
}
module_init(interrupt_irq_init);
module_exit(interrupt_irq_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("topeet");

中断线程化#

中断线程化是实时 Linux 项目开发的一个新特性,目的是降低中断处理对系统实时延迟的影响。

将中断处理和主线程的工作分开,让它们可以并行执行。中断线程负责处理中断事件,而主线程负责执行主要的工作任务。这样一来,不仅可以减少切换的开销,还可以提高整个程序的响应速度和性能。

中断线程化的处理仍然可以看作是将原来的中断上半部分和中断下半部分。上半部分还是用来处理紧急的事情,下半部分也是出路比较耗时的操作,但是下半部分会交给一个专门的内核线程来处理。

这个内核线程只用于这个中断。当发生中断的时候,会唤醒这个内核线程,然后由这个内核线程来执行中断下半部分的函数。

接口:

request_threaded_irq 是 Linux 内核中用于请求并注册一个线程化的中断处理函数的函数。
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);
irq:中断号,表示要请求的中断线路。
handler:是在发生中断时首先要执行的处理程序,非常类似于顶半部,该函数最后会返回 IRQ_WAKE_THREAD 来唤醒中断,一般 handler 设为 NULL,用系统提供的默认处理。
thread_fn:线程化的中断处理函数,非常类似于底半部。如果此处设置为 NULL 则表示没有使用中断线程化。
irqflags:中断标志,用于指定中断的属性和行为。
devname:中断的名称,用于标识中断请求的设备。
dev_id:设备标识符,用于传递给中断处理函数的参数。
返回值:函数返回一个整数值,表示中断请求的结果。如果中断请求成功,返回值为 0,否则返回一个负数错误代码。

4.设备树#

4.1 设备树里中断节点的语法#

设备树中,GIC 下面的节点需要三个 cells 表示。分别表示类、哪个、触发类型。

4.2 设备树里中断节点的示例#

gpio0: gpio@fdd60000 {
compatible = "rockchip,gpio-bank";
reg = <0x0 0xfdd60000 0x0 0x100>;
interrupts = <GIC_SPI 33 IRQ_TYPE_LEVEL_HIGH>; // 表示共享中断的33号中断源,即GPIO0
clocks = <&pmucru PCLK_GPI00>, <&pmucru DBCLK_GPI00>;
gpio-controller;
#gpio-cells = <2>;
gpio-ranges = <&pinctrl 0 0 32>;
interrupt-controller; // 中断控制器
#interrupt-cells = <2>; // 子节点用2个寄存器表示
};
ft5x06: ft5x06@38 {
status = "disabled";
compatible = "edt,edt-ft5306";
reg = <0x38>;
touch-gpio = <&gpio0 RK_PB5 IRQ_TYPE_EDGE_RISING>;
interrupt-parent = <&gpio0>;
interrupts = <RK_PB5 IRQ_TYPE_LEVEL_LOW>;
reset-gpios = <&gpio0 RK_PB6 GPIO_ACTIVE_LOW>;
touchscreen-size-x = <800>;
touchscreen-size-y = <1280>;
touch_type = <1>;
};

4.3 在代码中获得中断#

5.内核驱动 gpio-keys.c 分析(GPIO/Input/中断/Timer)#

5.1 程序框架#

  • 通过平台设备模型,在 probe 函数中注册 input_dev;
  • 使用 GPIO 获得中断,注册中断,在中断里启动定时器,实现消抖;
  • 在定时器回调函数里上报 input event.(EV_KEY 事件,根据设备树的linux,code参数上报 KEYBIT)

5.2 gpio_keys_probe 函数#

先解析设备树的信息

if (!pdata) {
pdata = gpio_keys_get_devtree_pdata(dev);
if (IS_ERR(pdata))
return PTR_ERR(pdata);
}
gpio_keys_get_devtree_pdata:
pdata->rep = device_property_read_bool(dev, "autorepeat"); // 按下不松开,自动上报多个输入
...
device_for_each_child_node(dev, child) { // 遍历每个gpiokey子节点
if (is_of_node(child))
button->irq =
irq_of_parse_and_map(to_of_node(child), 0);
if (fwnode_property_read_u32(child, "linux,code",
&button->code)) {
dev_err(dev, "Button without keycode\n");
fwnode_handle_put(child);
return ERR_PTR(-EINVAL);
}
...

分配、构造 input_dev

input = devm_input_allocate_device(dev);
if (!input) {
dev_err(dev, "failed to allocate input device\n");
return -ENOMEM;
}
ddata->pdata = pdata;
ddata->input = input;
mutex_init(&ddata->disable_lock);
platform_set_drvdata(pdev, ddata);
input_set_drvdata(input, ddata);
input->name = pdata->name ? : pdev->name;
input->phys = "gpio-keys/input0";
input->dev.parent = dev;
input->open = gpio_keys_open;
input->close = gpio_keys_close;
input->id.bustype = BUS_HOST;
input->id.vendor = 0x0001;
input->id.product = 0x0001;
input->id.version = 0x0100;
input->keycode = ddata->keymap;
input->keycodesize = sizeof(ddata->keymap[0]);
input->keycodemax = pdata->nbuttons;
...

接着,遍历所有的按键:注册中断(注意使用 GPIO 或使用中断,分别对应gpio_keys_gpio_isrgpio_keys_irq_isr

for (i = 0; i < pdata->nbuttons; i++) {
const struct gpio_keys_button *button = &pdata->buttons[i];
if (!dev_get_platdata(dev)) {
child = device_get_next_child_node(dev, child);
if (!child) {
dev_err(dev,
"missing child device node for entry %d\n",
i);
return -EINVAL;
}
}
error = gpio_keys_setup_key(pdev, input, ddata,
button, i, child);
...
}
static int gpio_keys_setup_key(struct platform_device *pdev,
struct input_dev *input,
struct gpio_keys_drvdata *ddata,
const struct gpio_keys_button *button,
int idx,
struct fwnode_handle *child)
{
...
if (bdata->gpiod) {
bool active_low = gpiod_is_active_low(bdata->gpiod);
if (button->debounce_interval) {
error = gpiod_set_debounce(bdata->gpiod,
button->debounce_interval * 1000);
/* use timer if gpiolib doesn't provide debounce */
if (error < 0)
bdata->software_debounce =
button->debounce_interval;
}
if (button->irq) {
bdata->irq = button->irq;
} else {
irq = gpiod_to_irq(bdata->gpiod);
if (irq < 0) {
error = irq;
dev_err(dev,
"Unable to get irq number for GPIO %d, error %d\n",
button->gpio, error);
return error;
}
bdata->irq = irq;
}
INIT_DELAYED_WORK(&bdata->work, gpio_keys_gpio_work_func);
isr = gpio_keys_gpio_isr;
irqflags = IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING;
switch (button->wakeup_event_action) {
case EV_ACT_ASSERTED:
bdata->wakeup_trigger_type = active_low ?
IRQ_TYPE_EDGE_FALLING : IRQ_TYPE_EDGE_RISING;
break;
case EV_ACT_DEASSERTED:
bdata->wakeup_trigger_type = active_low ?
IRQ_TYPE_EDGE_RISING : IRQ_TYPE_EDGE_FALLING;
break;
case EV_ACT_ANY:
/* fall through */
default:
/*
* For other cases, we are OK letting suspend/resume
* not reconfigure the trigger type.
*/
break;
}
} else {
if (!button->irq) {
dev_err(dev, "Found button without gpio or irq\n");
return -EINVAL;
}
bdata->irq = button->irq;
if (button->type && button->type != EV_KEY) {
dev_err(dev, "Only EV_KEY allowed for IRQ buttons.\n");
return -EINVAL;
}
bdata->release_delay = button->debounce_interval;
timer_setup(&bdata->release_timer, gpio_keys_irq_timer, 0);
isr = gpio_keys_irq_isr;
irqflags = 0;
/*
* For IRQ buttons, there is no interrupt for release.
* So we don't need to reconfigure the trigger type for wakeup.
*/
}
...
error = devm_request_any_context_irq(dev, bdata->irq, isr, irqflags,
desc, bdata); //注册中断
}

最后,注册中断处理函数

error = input_register_device(input);

5.3 gpio_keys_gpio_isr 中断处理函数#

10_工作队列_gpio_keys_gpio_work_func

为了消抖,可以使用定时器或者推迟的工作队列。

5.4 gpio_keys_irq_isr 中断处理函数#

09_gpio_keys_irq_timer

可以看出使用中断描述按键的方法,不能精确上报按键松开。所以需要使用 GPIO 方法。