【嵌入式环境下linux内核及驱动学习笔记-(13-中断管理)】

目录

  • 1、中断基本概念
  • 2、ARM体系中断系统
    • 2.1 ARM具有的七种异常模式与中断的关系
    • 2.2 ARM多核环境下的中断
    • 2.3 exynos4412(contex A9)的中断
  • 3、中断处理程序架构
  • 4、 中断接口编程
    • 4.1 中断接口函数
      • 4.1.1 request_irq
      • 4.1.2 free_irq
      • 4.1.3 irqreturn_t
      • 4.1.4 irq_handler_t 中断处理程序原型
      • 4.1.5 devm_request_irq
      • 4.1.6 disable_irq / disable_irq_nosync / enable_irq
      • 4.1.7 local_irq_save(flags) / local_irq_restore(flags)
      • 4.1.8 local_irq_enable / local_irq_disable
    • 4.2 底半部机制相关的中断接口函数
      • 4.2.1 tasklet
        • 4.2.1.1 struct tasklet_struct
        • 4.2.1.2 底半部处理函数原型
        • 4.2.1.3 tasklet_init
        • 4.2.1.4 DECLARE_TASKLET
        • 4.2.1.5 tasklet_schedule
        • 4.2.1.6 tasklet_action
        • 4.2.1.7 tasklet使用模板
      • 4.2.2 工作队列 workqueue
        • 4.2.2.1 struct work_struct
        • 4.2.2.2 work_func_t 队列处理函数定义
        • 4.2.2.3 INIT_WORK 宏
        • 4.2.2.4schedule_work
        • 4.2.2.5 flush_workqueue函数详解
        • 4.2.2.6 cancel_work_sync
        • 4.2.2.7 工作队列使用模板
      • 4.2.3 软中断
        • 4.2.3.1 struct softirq_action 与 软中断处理函数
        • 4.2.3.2 open_softirq 函数详解
        • 4.2.3.3 raise_softirq函数详解
        • 4.2.3.4 local_bh_disable / local_bh_enable 函数详解
        • 4.2.3.5 softirq_vec数组
        • 4.2.3.6 软中断模板
      • 4.2.4 线程化irq
        • 4.2.4.1 request_threaded_irq
        • 4.2.4.2 devm_request_threaded_irq(数
        • 4.2.4.3 线程化irq机制模板
    • 4.3 设备树操作相关函数
      • 4.3.1 irq_of_parse_and_map
  • 5、中断实例
    • 5.1 实例描述:
    • 5.2 环境:
    • 5.3 硬件说明
    • 5.4 设备树定义的说明:
      • 5.4.1 顶层 , cpu中断控制器节点
      • 5.4.2 上层,gpio中断控制器节点
      • 5.4.3 下层, 中断生成节点(按键节点)
    • 5.5 程序代码
      • 5.5.1 按键结构体相关头文件
      • 5.5.2 驱动程序
      • 5.5.3 测试

1、中断基本概念

中断:是指CPU在执行程序的过程中,出现了某些突发事件急待处理,CPU必须暂停当前程序的执行,转去处理突发事件,处理完毕后又返回源程序被中断的位置继续执行。

按中断来源分类:

  • 内部中断 :CPU执行程序的过程中,发生的一些出错(溢出、除法错误等)、软件中断指令SWI等,不可被屏蔽。
  • 外部中断 :外设发生某种情况,通过一个引脚的高、低电平变化来通知CPU (如外设产生了数据、某种处理完毕等等)

按是否可以屏蔽分类:

  • 可屏蔽中断 : 可以通过设置中断控制器寄存器等方法被屏蔽,屏蔽后,该中断不再得到响应。
  • 不可屏蔽中断(NMI):如上,内部中断,必须响应,不可被屏蔽。

根据中断入口跳转方法分类:

  • 向量中断:cpu通常为不同的中断分配不同的中断号,当检测到某中断号的中断来到后,就自动跳转到与该中断号对应的地址执行。不同中断号的中断有不同的入口地址。ARM系统中的异常用的是向量中断模式。
  • 非向量中断:多个中断共享一个入口地址,进入该入口地址后,再通过软件判断 断标志来识别具体的哪个中断。ARM系统中的中断用的是非向量中断模式。

2、ARM体系中断系统

2.1 ARM具有的七种异常模式与中断的关系

在ARM系统中,中断与异常模式有密切的关系:
1、 中断是外部事件引起的异常,由外部设备触发。异常是由CPU内部异常条件触发的。
2、 当中断或异常发生时,CPU会从当前的执行模式(如用户模式或系统模式)切换到一个异常模式。ARM支持7种异常模式:

  • Reset:上电复位后进入的模式
  • Undefined Instruction:执行未定义指令时进入
  • Supervisor Call:执行SVC指令时进入,用于系统调用
  • Prefetch Abort:预取指令时发生异常时进入
  • Data Abort:读/写数据时发生异常时进入
  • IRQ:接收到IRQ中断请求时进入
  • FIQ:接收到FIQ中断请求时进入

3、 在异常模式下,CPU会保存当前模式的程序状态,然后跳转到对应的异常入口地址执行异常服务程序。
4、 执行完成后,异常服务程序使用指令SUBS PC, LR, #imm返回到中断前的模式和程序计数器。

所以中断是通过异常模式来处理的,IRQ和FIQ是两种用于处理中断的异常模式。其他异常模式用于处理其它异常条件。 中断的实际处理入口是通过IRQ或FIQ这两种异常模式的入口来实现的。

2.2 ARM多核环境下的中断

\qquad 在ARM多核处理器里最常用的中断控制器是GIC(Generic Interrupt Controller),如下图:

ARM把中断分成以下三种类型:

  • SGI:(sofware generated interrupt):(软件生成中断)是由软件指令触发的中断,会激活对应的异常模式(如Supervisor Call模式),然后跳转到该模式的异常入口。 可以用于多核间的核间通信,一个CPU可以爱过写GIC寄存器给另外一个CPU产生中断。
  • PPI:(private peripheral interrupt):(外设专用中断)是由外设触发的中断,会激活IRQ异常模式或FIQ异常模式,然后跳转到对应的异常入口。是某个CPU私有的外设中断,这类外设的中断只能发给绑定的那个CPU。
  • SPI:(shared peripheral interrupt):(共享外设中断)是由外设触发的中断,可以配置为激活IRQ异常模式或FIQ异常模式,然后跳转到对应的异常入口。这类外设的中断可以路由到任何一个CPU。

\qquad 中断控制器(如GIC)负责检测SPI和PPI外设中断,并激活中断的目标异常模式。而SGI直接通过软件指令激活异常模式。

中断的处理流程

中断源(SGI/PPI/SPI) -> 异常模式(FIQ/IRQ/SVC/…) -> 异常入口 -> 中断服务程序 -> 正常模式

中断源通过触发相应的异常模式,异常模式通过异常入口调用中断服务程序进行中断处理,完成后返回正常模式。

2.3 exynos4412(contex A9)的中断

exynos4412(contex A9) ,总共支持160个中断,包括软件生成的中断(SGI[15:0],ID[15:0])、专用外围设备中断(PPI[15:0]、ID[31:16])和共享外围设备中断。对于SPI,最多可以提供32* 4=128个中断请求。
具体的中断表如下:(在驱动时涉及到的中断必须查询下表)
【嵌入式环境下linux内核及驱动学习笔记-(13-中断管理)】_第1张图片
【嵌入式环境下linux内核及驱动学习笔记-(13-中断管理)】_第2张图片
【嵌入式环境下linux内核及驱动学习笔记-(13-中断管理)】_第3张图片
【嵌入式环境下linux内核及驱动学习笔记-(13-中断管理)】_第4张图片
【嵌入式环境下linux内核及驱动学习笔记-(13-中断管理)】_第5张图片
【嵌入式环境下linux内核及驱动学习笔记-(13-中断管理)】_第6张图片
【嵌入式环境下linux内核及驱动学习笔记-(13-中断管理)】_第7张图片
【嵌入式环境下linux内核及驱动学习笔记-(13-中断管理)】_第8张图片

3、中断处理程序架构

\qquad 设备的中断会打断内核进程中的正常调度和运行,系统必然要求中断服务程序能短小精悍,这也是为什么我们之前的内容里有中断上下文中不允许有阻塞出现。但在大多数真实的系统当中,中断要处理完成的工作往往并不会短小,它可能要进行较大量的耗时处理。

\qquad linux内核的中断处理机制中,为了在中断执行时间尽量短 与 中断处理需 完成的工作尽量大之间找到平衡点,linux将中断处理程序分解为两个半部:顶半部(Top Half)和底半部 (Bottom Half)。

【嵌入式环境下linux内核及驱动学习笔记-(13-中断管理)】_第9张图片

\qquad 顶半部用于完成尽量少的比较紧急的功能,并将底半部处理程序挂到该设备的底半部执行队列中去。这样,顶半部执行的速度就会很快,及时返回。
\qquad 底半部则以“普通任务”形态,参与到系统的任务调度中去执行,它需要完成中断事件的长耗时工作,并可以被新的中断打断,这也是底半部和顶半部最大的不同。
\qquad 当然,如果中断处理程序本来要完成的工作就耗时很短,则无需一定要分成两个半部。

底半部的实现机制主要有以下四种:

  • tasklet
  • 工作队列
  • 软中断
  • 线程化irq

以上四种机制,分别是利用了内核的不同框架,在实现上有各自不同的函数接口。此处先了解,在后面的章节进行具体的介绍。

4、 中断接口编程

中断接口编程的一般流程顺序如下:

  1. request_irq向内核申请使用一个中断号,并连接中断处理程序。dev是中断处理程序的设备结构。
  2. 中断可以配置为电平触发或上升/下降沿触发。
  3. 使用enable_irq使能中断,否则中断将不会被激活。disable_irq可以屏蔽指定中断。
  4. 中断处理程序是真正处理中断的函数。它包含设备特有的中断处理逻辑,通常会唤醒等待队列或标志位。
  5. 使用free_irq释放申请的中断号,以便其他设备使用。释放后,中断号与处理程序的关联将被移除。
    以上就是linux内核中中断接口编程的一般流程顺序。中间可能还涉及到中断同步、嵌套等问题,但流程基本如上。

4.1 中断接口函数

include/linux/interrupt.h这个头文件定义了绝大部分与Linux内核中断管理相关的接口,程序员可以通过这些接口对中断进行申请、配置、处理、释放等操作。

4.1.1 request_irq

request_irq函数声明在linux内核的include/linux/interrupt.h头文件中

int request_irq(unsigned int irq, 
				irq_handler_t handler, 
                unsigned long flags, 
                const char *name, 
                void *dev)

参数:

  • irq:要申请的中断号。
  • handler:中断处理程序函数指针。当中断触发时,内核会调用此函数。
  • flags:中断标志位,包含中断触发方式、共享模式等设置。
  • name:中断名,用于调试目的。
  • dev:传递给处理程序的设备指针,一般是设备结构体指针。

flags参数包含以下常用设置:

【触发方式】相关的标志:

  • IRQF_TRIGGER_NONE //无触发
  • IRQF_TRIGGER_RISING //上升沿触发
  • IRQF_TRIGGER_FALLING //下降沿触发
  • IRQF_TRIGGER_HIGH //高电平触发
  • IRQF_TRIGGER_LOW //低电平触发
    【处理方式】相关标志:
  • IRQF_DISABLED //用于在快速中断处理中,屏蔽所有其它中断
  • IRQF_SHARED //当有多设备置共享同一中断时,使用该标志

返回值:

  • 申请成功则返回0,否则返回错误号。常见的错误有:
    • -EBUSY: 中断号正在被其他设备使用
    • -EINVAL: 无效的中断号或参数
    • -ENODEV: 中断号不存在

request_irq函数返回值宏 参考:
#define EPERM 1 /* Operation not permitted /
#define ENOENT 2 /
No such file or directory /
#define ESRCH 3 /
No such process /
#define EINTR 4 /
Interrupted system call /
#define EIO 5 /
I/O error /
#define ENXIO 6 /
No such device or address /
#define E2BIG 7 /
Argument list too long /
#define ENOEXEC 8 /
Exec format error /
#define EBADF 9 /
Bad file number /
#define ECHILD 10 /
No child processes /
#define EAGAIN 11 /
Try again /
#define ENOMEM 12 /
Out of memory /
#define EACCES 13 /
Permission denied /
#define EFAULT 14 /
Bad address /
#define ENOTBLK 15 /
Block device required /
#define EBUSY 16 /
Device or resource busy /
#define EEXIST 17 /
File exists /
#define EXDEV 18 /
Cross-device link /
#define ENODEV 19 /
No such device /
#define ENOTDIR 20 /
Not a directory /
#define EISDIR 21 /
Is a directory /
#define EINVAL 22 /
Invalid argument /
#define ENFILE 23 /
File table overflow /
#define EMFILE 24 /
Too many open files /
#define ENOTTY 25 /
Not a typewriter /
#define ETXTBSY 26 /
Text file busy /
#define EFBIG 27 /
File too large /
#define ENOSPC 28 /
No space left on device /
#define ESPIPE 29 /
Illegal seek /
#define EROFS 30 /
Read-only file system /
#define EMLINK 31 /
Too many links /
#define EPIPE 32 /
Broken pipe */

使用实例:

ret = request_irq(IRQ_NUM, irq_handler, IRQF_TRIGGER_FALLING, "MY_IRQ", dev);
if (ret) 
    printk(KERN_ERR "my_driver: Can't get assigned irq %d\n", IRQ_NUM);

4.1.2 free_irq

free_irq函数用于释放request_irq函数申请的中断。它的原型为:

void free_irq(unsigned int irq, void *dev);

参数说明:

  • irq: 要释放的中断号。
  • dev: request_irq时的dev参数,通常是设备结构体指针。

用法:
当我们不再需要使用某个中断时,必须调用free_irq函数释放它,否则该中断号将不会被重用,可能导致中断号资源短缺。

一个典型的示例如下:

ret = request_irq(IRQ_NUM, irq_handler, IRQF_TRIGGER_FALLING, 
                  "MY_IRQ", dev);
// ......
// 不再需要该中断
free_irq(IRQ_NUM, dev);

需要注意的是,dev参数一定要和request_irq时传入的dev参数相同,否则free_irq会失败。
free_irq函数执行的主要操作是:

  1. 将中断处理程序与中断号的关联移除。
  2. 如果该中断被屏蔽(disable_irq),则会重新使能它。
  3. 如果该中断配置为共享中断(IRQF_SHARED),则不执行任何操作。共享中断被最后一个请求者释放。
  4. Wake up 等待该中断号的任何进程。
    所以总之,free_irq会将中断恢复到未请求之前的默认状态,以便其他设备可以申请使用该中断。

4.1.3 irqreturn_t

源码定义:
/include/linux/irqreturn.h
返回IRQ_HANDLED表示处理完了,返回IRQ_NONE在共享中断表示不处理

/**
 * enum irqreturn
 * @IRQ_NONE		interrupt was not from this device 
 * @IRQ_HANDLED		interrupt was handled by this device 
 * @IRQ_WAKE_THREAD	handler requests to wake the handler thread  
 */
enum irqreturn {
	IRQ_NONE		= (0 << 0),
	IRQ_HANDLED		= (1 << 0),
	IRQ_WAKE_THREAD		= (1 << 1),
};

typedef enum irqreturn irqreturn_t;

具体的3种返回状态含义如下:

  • IRQ_NONE: 中断没有被本处理程序处理。这通知内核需要调用链中的下一个处理程序。
  • IRQ_HANDLED: 中断被成功处理。内核会结束中断处理程序的调用链。
  • IRQ_WAKE_THREAD: 中断处理程序触发了任务,并需要唤醒一个中断线程来处理。内核会调用中断线程进行实际处理工作。
    一个简单的中断处理程序示例:
irqreturn_t irq_handler(int irq, void *dev_id)
{
    // ......处理中断
    return IRQ_HANDLED;
}

这个处理程序成功处理了中断,所以返回IRQ_HANDLED。

如果我们的中断处理程序没有完全处理中断,而是触发了一个需要在线程环境下处理的工作,可以:

irqreturn_t irq_handler(int irq, void *dev_id) 
{
    // 触发处理工作
    schedule_work(&some_work);  
    
    // 唤醒中断线程
    return IRQ_WAKE_THREAD;
}

所以,irqreturn枚举类型给中断处理程序返回值定义了丰富的语义,我们可以根据实际情况进行灵活设置,实现更好的中断处理机制。

4.1.4 irq_handler_t 中断处理程序原型

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

4.1.5 devm_request_irq

函数用于申请中断,它定义在include/linux/interrupt.h头文件中。
原型为:

int devm_request_irq(struct device *dev, 
					unsigned int irq, 
                     irq_handler_t handler, 
                     unsigned long irqflags, 
                     const char *devname, 
                     void *dev_id);

参数说明:

  • dev: 申请中断的设备。中断会绑定至该设备。
  • irq: 要申请的中断号。
  • handler: 中断处理程序。
  • irqflags: 中断标志,如触发方式、共享等设置。
  • devname: 中断名称,用于调试。
  • dev_id: 传递给handler的参数,通常是设备结构体指针。

返回值:

  • 成功返回0,失败返回错误码。

与request_irq的区别:
devm_request_irq将申请的中断"绑定"到dev指定的设备上。当该设备被驱动释放时,devm_request_irq也会自动释放中断。
而request_irq则需要我们手动调用free_irq来释放中断。
使用示例:

struct device *dev = ...;  // 设备

ret = devm_request_irq(dev, irq, irq_handler, IRQF_TRIGGER_RISING,  
                       "my_irq", dev);
if (ret)
    printk(KERN_ERR "mydriver: Can't get assigned irq %d\n", irq);

// ......

退出驱动时,devm_request_irq自动释放中断

devm_request_irq简化了资源管理的工作,防止因为 programmer error 导致资源泄露的问题。所以在能使用devm_xxx系列函数的场景下,优先选择这些"managed"函数可以简化驱动开发并提高可靠性。

4.1.6 disable_irq / disable_irq_nosync / enable_irq

disable_irq, disable_irq_nosync和enable_irq都是定义在include/linux/interrupt.h头文件中的中断管理接口。
它们的原型分别为:

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

disable_irq和disable_irq_nosync的作用都是禁用指定的中断号irq。
区别在于:

  • disable_irq_nosync()执行后立即返回,而disable_irq()会等待目前的中断处理完成。
    所以,如果我们在中断上下文中调用disable_irq,那么由于禁用中断前进行了同步,这可能导致死锁问题。这时应使用disable_irq_nosync。
    在非中断上下文中,两个函数没有区别,建议使用disable_irq。
    enable_irq用来重新启用先前通过disable_irq或disable_irq_nosync禁用的中断。
    使用示例:
// 禁用irq
disable_irq(irq);  

// ......

// 重新使能
enable_irq(irq);

这三个函数对中断管理很有帮助,我们可以通过临时禁用和使能中断来:

  1. 防止并发问题,如临界区代码。
  2. 实现不同中断优先级,通过选择性地临时禁用中断。
  3. 在非原子操作代码段临时禁用中断等。

4.1.7 local_irq_save(flags) / local_irq_restore(flags)

/include/linux/irqflags.h

在linux 3.14及更高版本中,local_irq_save和local_irq_restore的定义为:

#define local_irq_save(flags)           \
        do {                            \
                raw_local_irq_save(flags);       \
                trace_hardirqs_off();           \
        } while (0)

#define local_irq_restore(flags)            \
        do {                                \
                if (raw_irqs_disabled_flags(flags)) {        \
                        raw_local_irq_restore(flags);        \
                        trace_hardirqs_off();                \
                } else {                    \
                        trace_hardirqs_on();                \
                        raw_local_irq_restore(flags);        \
                }                            \
        } while (0)   

它们的作用是:

  • local_irq_save:
    • 调用raw_local_irq_save关闭本地CPU中断,并获取关闭前的中断状态flags。
    • 调用trace_hardirqs_off()进行hardirq跟踪,记录此时hardirq被关闭的事件。
  • local_irq_restore:
    • 根据flags判断本地CPU中断是否被关闭。
      • 如果是,调用raw_local_irq_restore恢复中断至先前状态,并调用trace_hardirqs_off()记录hardirq被关闭的事件。
      • 否则,调用trace_hardirqs_on()记录hardirq被打开的事件,然后调用raw_local_irq_restore恢复中断(实际上不进行任何操作)。
    • 所以,该宏根据传递的flags智能地判断是否需要打开本地中断,并在恢复中断的同时进行hardirq跟踪。
      总之,这两个宏基于raw_local_irq_save和raw_local_irq_restore实现临时关闭和恢复本地CPU中断的机制。但同时,它们利用trace系统来跟踪hardirq的状态变化,用于kernel分析和调试。
      示例:
unsigned long flags;

// 关闭中断,保存状态并跟踪        
flags = local_irq_save();

// 临界区代码    
do_some_noneatomic_work();  

// 恢复中断状态并跟踪
local_irq_restore(flags);

作用范围是本CPU内。

4.1.8 local_irq_enable / local_irq_disable

这两个函数的定义实际上在include/linux/irqflags.h头文件中。
代码如您所示:

#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)

它们是两个宏定义,通过调用raw_local_irq_enable()和raw_local_irq_disable()这两个底层寄存器级函数来实现本地中断的启用和禁止。
在此过程中,它们还分别调用了trace_hardirqs_on()和trace_hardirqs_off()来进行debug跟踪。

所以,这两个函数是实现临界区保护与中断禁止的重要机制,它们通过操作CPU的状态来达到关闭与打开本地中断的效果,为需要原子操作的代码提供一个无中断的运行环境。

作用范围是本CPU内。

4.2 底半部机制相关的中断接口函数

前面说了,底半部的实现机制有四种,分别是 tasklet 、 工作队列 、 软中断 、 线程化irq。接下来分别介绍:

4.2.1 tasklet

tasklet的操作流程:

  • 初始化:调用tasklet_init()来初始化一个tasklet,传入待执行的底半部handler函数
  • 触发:当需要执行底半部handler函数时,调用tasklet_schedule()来触发tasklet
  • 执行:在退出中断上下文后,内核检查是否有等待执行的tasklet。如果有,则调用赋予其的handler函数来执行底半部逻辑,此时已开中断,可被中断抢占
  • 同步等待结束:若需要确保tasklet执行结束再继续,可调用tasklet_kill()来杀死该tasklet,然后调用tasklet_kill_sync()来同步等待其执行结束

tasklet机制特点:

  • 不可重入:一个tasklet可以被多次触发,内核会仅执行其底半部逻辑一次
  • 非提交性:tasklet不会被再次触发,当其执行结束,不会自动重新触发
  • 非抢占:tasklet一旦开始执行handler函数,不会被其他tasklet抢占CPU,但会被其他中断抢占
  • 轻量级:tasklet是一种轻量级的底半部mechanism,相比工作队列更加轻量

tasklet内核原理:

  • tasklet通过tasklet_vec数组来管理所有初始化的tasklet,每个CPU一个此数组
  • 当触发一个tasklet时,其位于每个CPU变量tasklet_vec中的tasklet_struct结构体会被标记为待执行
  • 在退出中断上下文后,内核会检查每个CPU的tasklet_vec并执行其中标记了待执行的tasklet
  • 每个tasklet都有一个handler函数,内核通过调用此函数来执行tasklet的底半部逻辑
  • 当kill一个tasklet时,内核等待其完全执行结束再返回,以保证其所有逻辑都已执行

以上详细解释了tasklet机制的流程,特点,内核实现原理等方面内容。tasklet是一种轻量级的底半部机制,用于在中断处理的底半部执行可被抢占的较长时间工作。

tasklet是基于软中断实现的,运行于软中断上下文,仍然属于原子上下文的一种,在此不允许睡眠。

4.2.1.1 struct tasklet_struct

tasklet_struct是tasklet机制的核心数据结构,它的定义在include/linux/interrupt.h头文件中。

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

该结构体包含了一个tasklet所需要的所有信息,具体的每个字段解释如下:
struct tasklet_struct *next;

next指针用于将所有tasklet连接成一个链表,便于内核管理与执行。

unsigned long state;

state字段表示tasklet的当前状态,可以是以下几种:
TASKLET_STATE_SCHED: 已触发,待执行
TASKLET_STATE_RUN: 当前正在执行handler函数
TASKLET_STATE_KILLED: 已杀死,handler函数不会再被调用

atomic_t count;

count是一个原子计数器,表示该tasklet当前的触发次数。内核会在handler函数执行前将其置0,以判断是否需要再次触发该tasklet。

void (*func)(unsigned long);

func指针指向该tasklet所需要执行的handler函数。当tasklet被调度执行时,内核会调用此函数来执行底半部逻辑。

unsigned long data;

data字段可用于传递参数给handler函数,它的值是在初始化tasklet时通过tasklet_init()传入的。

所以,tasklet_struct结构体中包含了tasklet所需要的一切信息:

  • 用于串联管理的next指针
  • 表示状态的state字段
  • 用于判断是否需要再次触发的count计数器
  • 指向实际handler函数的func指针
  • 可传参给handler函数的数据data
    通过这个结构体,内核完整地定义并管理着所有初始化的tasklet。当需要执行tasklet底半部逻辑时,直接操作此结构体即可。
4.2.1.2 底半部处理函数原型

在tasklet机制中,我们需要指定一个底半部处理函数(handler function)来完成实际的工作。
该函数的原型如下:

void handler_func(unsigned long data);
  • handler_func:该函数名称可以自定义,它代表tasklet的底半部处理逻辑
  • unsigned long data:该参数可选,在初始化tasklet时可以通过tasklet_init()函数传递参数到这个handler函数
    当一个tasklet被触发并执行时,内核会调用我们指定的这个handler函数来执行底半部逻辑,并会传入我们初始化时传递的数据(如果有的话)。
    在handler函数内,我们可以完成一些较长时间的工作,这些工作会在中断被打开的环境下执行,所以有可能被其他中断抢占。但该函数是非可重入的, mean 同一个tasklet不会被再次触发执行直到当前一次执行完毕。
4.2.1.3 tasklet_init

函数用于初始化一个tasklet。其定义在include/linux/interrupt.h头文件中。
原型为:

void tasklet_init(struct tasklet_struct *t, 
                  void (*func)(unsigned long), 
                  unsigned long data);

参数解释:

  • t:指向要初始化的tasklet_struct结构体的指针
  • func:tasklet的handler函数指针,当tasklet被执行时会调用此函数
  • data:传递给handler函数func的参数

举例:

#include 

void tasklet_handler(unsigned long data) 
{
    // tasklet handler logic here
}

void init_tasklet(void)
{
    struct tasklet_struct t;
    tasklet_init(&t, tasklet_handler, 0); 
}

这个例子初始化了一个tasklet t,并指定其handler函数为tasklet_handler。当这个tasklet被执行时,内核会调用tasklet_handler函数来完成底半部逻辑,并向其传递初始化时指定的数据0。
tasklet_init()函数在初始化tasklet时,会做以下工作:

  • 将tasklet的state置为TASKLET_STATE_SCHED,表示其已初始化但未执行
  • 将tasklet的count计数器清0
  • 保存func函数指针,以便日后执行
  • 保存data的数据,以传递给func函数
  • 将该tasklet连接到per-CPU的tasklet链表上,以备日后执行
    一旦初始化结束,该tasklet可以通过调用tasklet_schedule()来触发并执行,内核会调用我们指定的func handler函数来完成底半部工作。
4.2.1.4 DECLARE_TASKLET

DECLARE_TASKLET宏:

  • 是定义在include/linux/interrupt.h头文件中的宏
  • 仅用于静态初始化tasklet,即在编译时完成初始化
  • 语法为: DECLARE_TASKLET(name, func, data)
#define DECLARE_TASKLET(name, func, data) \
struct tasklet_struct name = { NULL, 0, ATOMIC_INIT(0), func, data }

DECLARE_TASKLET与 tasklet_init函数,两种方法的主要区别在于初始化的时机:

  • DECLARE_TASKLET:在编译时静态初始化,用于在源码中定义并初始化tasklet
  • tasklet_init:在运行时动态初始化,用于在函数中定义tasklet结构体,并通过调用该函数来动态完成初始化
    除此之外,无论使用哪种方法,都需要指定同样的3个参数来提供handler函数,传递的数据以及 tasklet_struct结构体。
    两种方法都可以完成tasklet的初始化工作,区别仅在于时机的不同。我们可以根据需要选择编译时静态初始化还是运行时动态初始化来定义一个tasklet。
4.2.1.5 tasklet_schedule

函数用于触发一个已经初始化的tasklet来执行。其定义在include/linux/interrupt.h头文件中。
原型为:

void tasklet_schedule(struct tasklet_struct *t);

参数t为指向要触发的tasklet_struct结构体的指针。
功能:该函数会标记传入的tasklet t为待执行状态, so在稍后恢复中断的合适时机,内核会调用我们初始化时为该tasklet指定的handler函数来执行底半部逻辑。
举例:

struct tasklet_struct t;
void tasklet_handler(unsigned long data)
{
    printk(KERN_INFO "Tasklet running...\n"); 
}

void init_tasklet(void)
{
    tasklet_init(&t, tasklet_handler, 0);
}

void trigger_tasklet(void)
{
    tasklet_schedule(&t);
}

这里首先初始化了一个tasklet t,指定其handler函数为tasklet_handler。
然后在trigger_tasklet函数中,通过调用tasklet_schedule(&t)来触发这个tasklet。
此时,内核会标记这个tasklet为待执行状态。在稍后退出中断上下文的时候,内核会调用我们指定的tasklet_handler函数来执行底半部逻辑并打印信息。
所以,tasklet_schedule()函数的主要作用就是触发一个已经初始化好的tasklet,让内核在适当的时候执行其底半部handler函数。这是完成tasklet机制的关键一步。

4.2.1.6 tasklet_action

函数用于手动执行一个tasklet的handler函数。其定义在kernel/softirq.c文件中。
原型为:

static void tasklet_action(struct softirq_action *a) 

参数a为指向要执行的tasklet的softirq_action结构体的指针。该结构体包含了tasklet的所有信息,其中包含了我们初始化时指定的handler函数指针。
举例:

struct tasklet_struct t;
void tasklet_handler(unsigned long data)
{
    printk(KERN_INFO "Tasklet running...\n"); 
}

void init_tasklet(void)
{
    tasklet_init(&t, tasklet_handler, 0);
}

void trigger_tasklet(void) 
{
    tasklet_action((struct softirq_action *)&t); 
}

这里我们首先初始化一个tasklet t,指定其handler函数为tasklet_handler。
然后在trigger_tasklet函数中,通过调用tasklet_action(&t)来手动执行这个tasklet的handler函数。
此时,内核会直接调用我们指定的tasklet_handler函数来执行底半部逻辑并打印信息。
作用:tasklet_action()函数的作用是手动触发一个tasklet的执行,让内核直接调用其handler函数完成工作,而不需要mark它为待执行状态并等待 later 执行。
这可用于一些特殊情况,我们需要手动立即执行一个tasklet的底半部逻辑,而不是触发其等待稍后执行。
所以,总结来说,tasklet_action()函数的作用是:

  • 手动执行一个tasklet的handler函数
  • 使得内核立即调用我们为tasklet指定的handler函数来完成工作
  • 不会mark该tasklet为待执行状态,仅完成一次immediate执行
    这是一种更加主动的执行tasklet的方式,可根据实际需要选择是触发等待执行还是手动立即执行。
4.2.1.7 tasklet使用模板

【嵌入式环境下linux内核及驱动学习笔记-(13-中断管理)】_第10张图片

4.2.2 工作队列 workqueue

\qquad 工作队列的使用方法和tasket非常相似,但是工作队列的执行上下文是内核线程,因此可以调度和睡眠。

工作队列是运行在进程上下文,所以可以调度和睡眠
workqueue机制的详细说明如下:

  1. 流程:
  • 初始化:调用alloc_workqueue()来初始化一个工作队列,此时未关联任何工作
  • 初始化工作(work):调用INIT_WORK()宏或alloc_work()函数初始化工作,指定工作的handler函数
  • 向队列添加工作:调用queue_work()或schedule_work()将工作添加到工作队列中
  • 内核检查并执行:在软中断或时钟中断中,内核会检查每个CPU上的工作队列,并执行其中的工作,调用工作的handler函数
  • 同步等待:如果需要等待某个工作完成才继续,可以调用flush_workqueue()来等待工作队列中的所有工作完成
  1. 特点:
  • 可重入:一个工作可以被多次添加到队列,并多次执行handler函数
  • 提交性:已完成工作会自动重新入队列,等待再次被执行
  • 可抢占:工作的handler函数执行过程中可被其他中断抢占CPU
  • 线程安全:工作队列可被多个线程或中断安全地访问,工作也可在中断或线程环境中执行
  1. 内核原理:
  • 每个CPU有一个工作队列数组,每个工作队列一个此数组中的结构体代表
  • workqueue通过struct workqueue_struct表示,每个CPU都有一个此结构体的数组
  • work通过struct work_struct表示,用以定义工作及其handler函数
  • 当一个work被添加到队列,它会被添加到该工作队列的pending链表等待执行
  • 在软中断或时钟中断中,内核会检查每个工作队列的pending链表,并执行上面挂载的work,调用其handler函数
  • handler函数会在中断开启的环境下执行,在func函数内可以完成较长时间的工作,但也可被抢占
  • 如果一个工作完成但工作队列被设置为提交性,该工作会被重新添加到pending链表等待再次执行
    以上详细解释了workqueue机制的流程,特点,内核实现原理等方面内容。workqueue是一种强大的用以在中断环境下执行较长工作的机制,在内核各处广泛应用。
4.2.2.1 struct work_struct

work_struct是workqueue机制的核心数据结构,它定义在include/linux/workqueue.h头文件中。
原型如下:

struct work_struct {
	atomic_long_t data;
	struct list_head entry;
	work_func_t func;
	struct lockdep_map lockdep_map;
};

该结构体包含了一个work所需要的所有信息,具体的每个字段解释如下:
atomic_long_t data;

data是一个原子整型,用于传递参数给工作的handler函数。当我们初始化一个work时,可以指定此值,然后在handler函数中可以获取到该值。

struct list_head entry;

entry是一个链表头,内核会使用此链表将多个work连接起来,以形成工作队列的pending链表。通过此链表,内核可以高效地管理所有的已添加但未执行的work。

work_func_t func;

func是一个函数指针,指向该work的handler函数。当内核执行该work时,会调用此handler函数来完成实际的工作。

struct lockdep_map lockdep_map;

这个部分与锁有关,用于锁检测机制。我们这里不需要过多关注。

所以,总结来说,work_struct结构体中包含了work所需要的一切信息:

  • 可传参给handler函数的数据data
  • 用于将多个work串联为链表的entry
  • 指向实际handler函数的func指针
  • 与锁检测相关的部分
    通过这个结构体,内核可以完整地定义一段工作,并在需要时调用我们为其指定的handler函数来执行这段工作。
    work结构体是workqueue机制的基石,内核通过管理这些结构体,来完成在中断环境下异步执行较长工作的功能。
4.2.2.2 work_func_t 队列处理函数定义

typedef void (*work_func_t)(struct work_struct *work);

4.2.2.3 INIT_WORK 宏

INIT_WORK 宏用于初始化一个work结构体,为其指定handler函数。其定义在include/linux/workqueue.h头文件中。
语法为:

INIT_WORK(work, func);
  • work:指向要初始化的work_struct结构体的指针
  • func:work的handler函数指针
    举例:
struct work_struct my_work;

void work_handler(struct work_struct *work)
{
    // handler function logic here 
}

void init_work(void) 
{
    INIT_WORK(&my_work, work_handler);
}

这里我们定义了一个work_struct结构体my_work,并使用INIT_WORK宏来初始化它,指定其handler函数为work_handler。
INIT_WORK宏在初始化work时,会做以下工作:

  • 将work的entry链表头初始化,以便用于连接到工作队列的pending链表
  • 将work的data原子整型清0
  • 保存func函数指针,以便内核执行work时调用
  • 初始化work的锁检测相关部分(如果启用的话)
    一旦初始化结束,该work就可以通过调用queue_work()或schedule_work()添加到工作队列中了。内核会在适当的时候调用我们指定的work_handler handler函数来执行实际的工作。
4.2.2.4schedule_work

函数用于将一个work添加到system_wq工作队列中。system_wq是内核的默认工作队列,用于大多数异步工作。
该函数的定义在kernel/workqueue.c文件中。

原码:

static inline bool schedule_work(struct work_struct *work)
{
	return queue_work(system_wq, work);
}

参数work为指向要添加的work_struct结构体的指针。
该函数会将传入的work添加到system_wq工作队列的pending链表上,等待内核在适当的时候执行。
返回值:该函数会返回true if work was pending, false if it was not pending.
举例:
c
void work_handler(struct work_struct *work)
{
printk(“Work handler function called\n”);
}

struct work_struct my_work;
INIT_WORK(&my_work, work_handler);

void queue_work_example(void)
{
schedule_work(&my_work);
}
这里我们首先定义并初始化了一个work,指定其handler函数为work_handler。
然后在queue_work_example函数中,通过调用schedule_work(&my_work)来将这个work添加到system_wq工作队列中。
内核会在稍后选择一个时机,调用我们指定的work_handler函数来执行这个work,并打印相关信息。
所以,schedule_work()函数的主要作用就是将一个work提交到system_wq工作队列中,等待内核在适当的时候执行该work的handler函数。
这是使用workqueue机制的关键步骤之一,我们通过调用该函数来异步调度工作,并由内核运行其处理逻辑。
和其他正在执行的work比,通过该函数添加的work可能立即执行,也可能稍后执行,这取决于内核的调度。

4.2.2.5 flush_workqueue函数详解

flush_workqueue()函数用于等待一个工作队列中的所有工作完成执行。其定义在kernel/workqueue.c文件中。
原型为:

void flush_workqueue(struct workqueue_struct *wq);

参数wq为要等待的工作队列。如果传入NULL,则会等待system_wq默认工作队列。
该函数会阻塞当前执行环境,直到指定工作队列wq中的所有work完成执行为止。
举例:

void work_handler(struct work_struct *work) 
{
    printk("Work handler function called\n");
}

struct work_struct work1, work2;
INIT_WORK(&work1, work_handler);
INIT_WORK(&work2, work_handler);

void test_flush_workqueue(void) 
{ 
    schedule_work(&work1);
    schedule_work(&work2);
    
    flush_workqueue(NULL);
    
    printk("All works completed!\n");
}

这里我们定义并初始化了两个work,都指定work_handler为handler函数。
然后分别通过schedule_work()将这两个work添加到system_wq工作队列。
紧接着,我们调用flush_workqueue(NULL)来等待system_wq中的所有work完成。
该函数会阻塞,直到work1和work2都完成执行,然后继续往下执行,打印相关信息。
所以,flush_workqueue()函数的主要作用是:

  • 使调用者等待指定工作队列中的所有work完成执行
  • 可用于同步等待workqueue中work完成,从而确保相关工作在继续执行之前完成
  • 常用于系统退出或者关闭前等待workqueue中所有work完成等场景
  • 如果传入NULL,则会等待system_wq默认工作队列中的work完成
    该函数通过阻塞调用者来实现等待工作队列中work完成的功能。这是workqueue机制的一个非常有用的同步手段。
4.2.2.6 cancel_work_sync

函数用于取消一个work,并等待其处理逻辑完成。其定义在kernel/workqueue.c文件中。
原型为:

bool cancel_work_sync(struct work_struct *work);

参数work为要取消的work。
该函数会将传入的work从其所在的工作队列的 pending 链表中删除,以取消该work;
并会等待该work目前正在执行的任务完成,才会返回。
返回值:如果成功取消work并等待其完成,则返回true;如果work已经完成执行则返回false。
举例:

void work_handler(struct work_struct *work) 
{
    printk("Work handler function called\n");
}

struct work_struct my_work;
INIT_WORK(&my_work, work_handler);

void test_cancel_work_sync(void) 
{
    schedule_work(&my_work);
    
    if (cancel_work_sync(&my_work))
        printk("Work cancelled!\n");
    else
        printk("Work already finished!\n");
} 

这里我们定义并初始化一个work,指定其handler函数为work_handler。
然后通过schedule_work()将该work添加到默认工作队列执行。
紧接着,我们调用cancel_work_sync(&my_work)试图取消该work。
如果work尚未开始执行,则会从工作队列中删除并取消该work,并打印相关信息;
如果work已经完成执行,则函数会直接返回false,并打印相应信息。
所以,cancel_work_sync()函数的主要作用是:

  • 尝试取消一个已添加到工作队列的work
  • 如果work还未执行,会从工作队列中删除该work并取消
  • 如果work已经在执行,会等待其完成后再返回
  • 无论work是否已完成,该函数都会同步等待,然后返回执行结果
  • 用于取消一个work,并确保其不会继续执行或正在执行的任务完成后再继续
    该函数比较 cancel_work() 的同步版本,可以用于在继续执行之前确保work完成或被取消。
    这也是workqueue机制中一个很有用的同步手段。
4.2.2.7 工作队列使用模板

【嵌入式环境下linux内核及驱动学习笔记-(13-中断管理)】_第11张图片
【嵌入式环境下linux内核及驱动学习笔记-(13-中断管理)】_第12张图片

4.2.3 软中断

\qquad 软中断(Softirq)也是一种传统的底半部处理机制,它的执行时机通常是顶半部返回的时候。
tasklet和软中断都是基于软中断实现的,运行于软中断上下文,仍然属于原子上下文的一种,在此不允许睡眠。

softirq机制的详细说明如下:

  1. 流程:
  • 开启软中断:调用open_softirq()来为指定的软中断号注册一个handler函数
  • 触发软中断:调用raise_softirq()来触发指定号的软中断,标记其为待处理
  • 检查并执行:在时钟中断或其他中断的尾部,内核会检查每个CPU上的软中断,并执行编号为最高优先级的待处理软中断,调用其handler函数。
  • 关闭软中断:调用disable_softirq()及enable_softirq()来禁止和重新开启软中断的执行
  1. 特点:
  • 运行在中断环境下:软中断的handler函数会在中断被屏蔽的环境下运行,可被抢占
  • 编号优先级:软中断按编号的优先级执行,编号越低优先级越高
  • 不可重入:同一软中断不可重入,须等待上一次执行完成才能再次触发
  • 提交性:可配置某个软中断为提交性,使得其handler函数完成后自动重新触发该软中断
  1. 内核原理:
  • softirq通过enum表示不同的软中断号,其实质是用于在handler函数数组中索引
  • 每个CPU都有一个软中断action数组,每个元素代表一个软中断号,包含handler等信息
  • 每个CPU还有多个待处理软中断位图,用于标记待处理的软中断号
  • 调用raise_softirq()时,内核会将对应软中断号在位图中置1,标记为待处理
  • 在时钟中断尾部,内核会检查每个CPU的位图,获得最高优先级的待处理软中断号,并执行其handler函数
  • handler函数运行在中断被屏蔽状态,可完成较长时间工作,但也可被抢占
    以上详细解释了softirq机制的流程,特点,内核实现原理等方面内容。softirq可用于在中断环境下完成一些较长时间的底半部工作,是内核的基础机制之一。
4.2.3.1 struct softirq_action 与 软中断处理函数

/include/linux/interrupt.h

struct softirq_action
{
	void	(*action)(struct softirq_action *);
};

softirq_action结构体用于定义一个软中断的handler函数及相关信息。其定义在include/linux/interrupt.h头文件中。

action是一个函数指针,指向该软中断的handler函数。当该软中断被触发并执行时,内核会调用此handler函数来完成实际的工作。
handler函数的原型为:

void handler_func(struct softirq_action *a)

参数a为指向该软中断对应的softirq_action结构体的指针。在handler函数中可以使用该指针访问软中断的信息。
举例:

void softirq_handler(struct softirq_action *a)
{
    printk("Softirq handler function called\n"); 
}

void test_softirq(void)
{
    struct softirq_action a; 
    a.action = softirq_handler;
    
    open_softirq(3, &a);
}

这里我们定义了一个softirq_action结构体a,并指定其handler函数为softirq_handler。
然后通过open_softirq()来开启软中断号3,注册该handler函数。
当软中断3被触发时,内核会调用我们指定的softirq_handler函数来执行工作,并打印信息。
所以,softirq_action结构体为每个软中断保存了其handler函数等重要信息。内核通过管理这些结构体,来实现软中断机制并在适当的时候调用相应的handler函数。
这个结构体是软中断机制的基础,它抽象出了软中断的handler实体,用于在内核的不同地方注册和触发软中断。

4.2.3.2 open_softirq 函数详解

open_softirq()函数用于为指定的软中断号注册一个handler函数。声明在/include/linux/interrupt.h头文件里,
其定义在kernel/softirq.c文件中。
原型为:

void open_softirq(int nr,   void (*action)(struct softirq_action *))
  • nr:要注册handler函数的软中断号
  • action:软中断的handler函数指针
    该函数会为指定的软中断号nr创建或更新一个softirq_action结构体,并保存传入的handler函数指针。
    当该软中断被触发时,内核会调用我们这里指定的handler函数来执行相关工作。
    举例:
void softirq_handler(struct softirq_action *a) 
{
    printk("Softirq handler function called\n");
}

void test_open_softirq(void)
{
    open_softirq(3, softirq_handler);
}

这里我们调用open_softirq()来开启软中断号3,并注册处理函数softirq_handler。
当软中断3被触发时,内核会调用我们指定的softirq_handler函数,并打印相关信息。
所以,open_softirq()函数的主要作用是:

  • 为指定的软中断注册一个handler函数
  • 创建或更新该软中断对应的softirq_action结构体,保存传入的handler函数指针
  • 允许该软中断在 буд后被raise_softirq()触发并由内核调用我们注册的handler函数处理
  • 是使用软中断机制的第一步,我们需要通过该函数指定一个软中断的handler入口
    与关闭软中断的close_softirq()函数相对应,该函数用于开启并给指定软中断号注册处理逻辑。
    内核会使用softirq_action结构体来管理所有的软中断handler函数, open_softirq()函数就是这个管理结构体的创建入口之一。
4.2.3.3 raise_softirq函数详解

raise_softirq()函数用于触发一个软中断。其定义在kernel/softirq.c文件中。
原型为:

void raise_softirq(unsigned int nr)

参数nr为要触发的软中断号。
该函数会标记指定软中断号nr的softirq待处理位图,表示该软中断现在有一个待处理事件。
在适当的时候,内核会检查每个CPU的软中断位图,并执行该软中断对应的handler函数。
举例:

void softirq_handler(struct softirq_action *a)
{
    printk("Softirq handler function called\n");
}

void test_raise_softirq(void)
{
    open_softirq(3, softirq_handler);
    
    raise_softirq(3);
}

这里我们首先通过open_softirq()为软中断号3注册handler函数softirq_handler。
然后调用raise_softirq(3)来触发软中断号3。
内核会在稍后适当的时候执行该软中断,调用我们指定的softirq_handler函数,并打印相关信息。
所以,raise_softirq()函数的主要作用是:

  • 触发一个软中断,并标记其为待处理
  • 内核会在时钟中断或其他中断的尾部执行该软中断
  • 我们需要首先通过open_softirq()为某软中断注册handler函数,才能通过该函数触发
  • 用于异步通知内核执行某软中断的处理逻辑
    该函数是使用软中断机制的关键步骤之一,我们通过调用该函数来通知内核执行指定软中断的handler函数。
    与其相对应的,disable_softirq()函数可用于禁止软中断执行, enable_softirq()可重新开启软中断执行。
    raise_softirq()函数简单但非常关键,它是我们唯一的软中断触发手段,用于驱动内核执行我们注册的handler函数。
4.2.3.4 local_bh_disable / local_bh_enable 函数详解

local_bh_disable()和local_bh_enable()函数用于禁止和重新开启本CPU上的软中断及tasklet的底半部处理。
这两个函数的定义在include/linux/hardirq.h头文件中。
原型为:

void local_bh_disable(void);
void local_bh_enable(void); 

local_bh_disable()函数会禁止本CPU上的软中断处理、时钟事件处理和BSD会和处理。
local_bh_enable()函数重新开启上述被禁止的底半部处理。
这两个函数用于在本地CPU上临时禁止底半部载入,以避免并发执行导致的问题。在两者之间的代码段,只有硬中断会被执行。
举例:

void bh_test(void) 
{
    local_bh_disable();
    
    // Critical section, only hard IRQ handlers will run
    ... 
    
    local_bh_enable(); 
}

这里我们通过local_bh_disable()禁止本CPU的底半部处理,在中间的代码区域执行一些关键工作。
然后调用local_bh_enable()再重新开启底半部处理。
这确保了中间代码区域只会被硬中断抢占,避免软中断或其他底半部事件导致的并发问题。
所以,这两个函数的主要作用是:

  • 临时禁止或开启本CPU上的软中断、时钟事件和底半部处理
  • 用于临界区保护,避免关键代码区域被底半部抢占
  • 只有硬中断会被执行,软中断和其他底半部事件会被屏蔽
  • 实现简单的本地中断保护,不改变中断屏蔽状态
    这两个函数一般成对出现,用于临时保护某一代码区域,在该区域内只执行硬中断,避免软中断或其他底半部事件导致的并发问题。
    它们为内核提供了简单轻量级的临界区保护手段,在需要的情况下可以替代更重量级的spinlock等机制。
4.2.3.5 softirq_vec数组

这个数组定义在kernel/softirq.c文件中,包含每个软中断号对应的softirq_action结构体。
softirq_vec数组的定义如下:

static struct softirq_action softirq_vec[NR_SOFTIRQS] __cacheline_aligned_in_smp;

每个元素softirq_action包含了一个软中断号的handler函数和其他信息。
我们可以通过直接打印这个数组中的信息来查看内核已注册的软中断详情。
举例:

void test_print_softirqs(void)
{
    int i;
    printk("Registered softirqs: \n");
    
    for (i = 0; i < NR_SOFTIRQS; i++) {
        if (softirq_vec[i].action) 
            printk("softirq %d, handler function %ps\n", i, softirq_vec[i].action);
    }
}

这里我们循环遍历softirq_vec数组中的每个元素,并打印非空元素的软中断号和handler函数地址。
这会输出内核注册表中的所有软中断信息,包括号码和处理函数。
在我的环境中,执行该函数会打印出类似如下信息:
Registered softirqs:
softirq 0, handler function events_handle_irq
softirq 1, handler function work_handle_irq

所以,如果您想了解内核当前已注册的软中断信息,可以直接打印softirq_vec数组中的内容,它包含了每个软中断的处理函数等详细信息。

4.2.3.6 软中断模板

使用软中断机制的一般步骤是:
定义软中断号 -> 注册处理函数 -> 触发软中断 -> 软中断处理函数完成工作 -> (可选)重新触发软中断循环处理
这是一个简单但非常有用的机制,用于异步下半部处理和循环工作。

软中断机制的一般模板如下:

  1. 定义软中断号:
    在include/linux/interrupt.h中定义一个软中断号标识,如:
    #define MY_SOFTIRQ 10
  2. 注册软中断处理函数:
    在初始化时通过open_softirq()注册该软中断的处理函数,如:
void my_softirq_handler(struct softirq_action *a)
{
    ...
}

static int __init example_init(void) 
{
    open_softirq(MY_SOFTIRQ, my_softirq_handler);
}
  1. 触发软中断:
    调用raise_softirq(MY_SOFTIRQ)来触发该软中断,如:
void trigger_my_softirq(void)
{
    raise_softirq(MY_SOFTIRQ);
}
  1. 软中断处理函数:
    软中断处理函数会在时钟中断尾部执行,我们在这里完成相关底半部工作,如:
void my_softirq_handler(struct softirq_action *a)
{
    printk("Running my softirq handler \n");
    ...
    // Handle bottom half work
} 
  1. 重新触发软中断(可选):
    在软中断处理函数中可以调用raise_softirq()再次触发同一个软中断,以实现循环处理的效果:
void my_softirq_handler(struct softirq_action *a)
{
    ... 
    // Handle some work
    
    raise_softirq(MY_SOFTIRQ);  // Re-trigger 
}

4.2.4 线程化irq

中断底半部线程化(IRQ threading)是Linux内核中一个重要的机制。它用于将硬件中断的底半部处理工作转移到内核线程上执行,从而实现IRQ的线程化。

该机制的主要工作流程如下:

  1. 硬件中断到来,执行中断顶半部,标记底半部工作待处理
  2. 内核维护一个pending位图,记录哪些IRQ的底半部工作待处理
  3. 中断下半部检查该位图,发现有工作待处理则唤醒对应的内核线程
  4. 内核线程启动并执行IRQ底半部工作,完成后标记为Idle等待下次处理
  5. 内核线程被调度运行,再次检查位图查找待处理IRQ并完成底半部工作

IRQ线程化的主要特点是:

  1. 将硬件中断的底半部工作从软中断环境转移到内核线程执行
  2. 可以提高IRQ处理的并发度,不同CPU上的线程可以并发执行底半部工作
  3. 更容易实现IRQ的负载均衡,内核可以根据CPU使用状况调度IRQ线程
  4. 可以提供更好的实时性,通过实时调度策略运行IRQ线程
  5. 更容易对IRQ处理进行监控和限制

要实现IRQ的线程化,主要涉及以下方面:

  1. 为IRQ定义对应的内核线程,如ethX_irq_thread等
  2. 在硬件中断顶半部标记对应的IRQ底半部工作待处理
  3. 中断下半部检查该标记,启动对应的IRQ线程来执行底半部工作
  4. IRQ线程启动后执行底半部处理逻辑,完成后进入Idle状态
  5. 内核通过调度策略重新运行IRQ线程,以完成其他待处理IRQ的底半部工作
  6. IRQ线程配合pending位图等机制实现IRQ工作的轮询与处理
    所以,IRQ线程化是通过引入内核线程,从而将硬件中断的底半部环境转移到线程环境实现的,这为IRQ处理带来更高的并发度、实时性与灵活性。
4.2.4.1 request_threaded_irq

函数用于请求一个带有线程处理的中断。它定义在头文件中。
该函数的作用是:注册一个中断的线程处理函数和中断处理函数,并将中断的底半部处理工作转移到线程上执行,实现中断的线程化。
其函数原型为:

int __must_check 
request_threaded_irq(unsigned int irq, 
					irq_handler_t handler,  
             		irq_handler_t thread_fn,
             		unsigned long flags, 
             		const char *name, 
             		void *dev)

各个参数的含义如下:

  • irq: 要请求的中断号
  • handler: 中断处理函数,用于处理中断顶半部工作
  • thread_fn: 中断线程函数,用于处理中断底半部工作
  • flags: 中断属性标志,如IRQF_SHARED等
  • name: 中断名称,用于调试和管理
  • dev: 用于传入设备的私有数据

该函数的主要工作是:

  1. 检查并获取指定的中断号
  2. 注册handler作为中断处理函数,用于顶半部处理
  3. 创建一个内核线程并注册thread_fn作为线程函数
  4. 配置中断属性,如是否可以共享等
  5. 将中断底半部工作转移到内核线程上执行
  6. 返回中断号用于释放,或错误码
    通过该函数,我们可以轻松实现一个中断的线程化,从而提高其处理效率和灵活性。

举例:

irq_handler_t thread_fn(void *dev) 
{ 
    ...     // 中断线程函数,完成底半部工作
}

irq_handler_t irq_handler(int irq, void *dev)  
{
    ...      // 中断处理函数,完成顶半部工作   
}   

int err;
err = request_threaded_irq(IRQ_NUM, irq_handler, thread_fn, 
                           IRQF_SHARED, "example", dev); 
if (err)
    panic("IRQ registration failed");     

这里我们请求IRQ_NUM这个中断号,注册irq_handler为顶半部处理函数,thread_fn为底半部线程函数。
该中断被配置为可共享,并给出了一个示例名称用于调试。
request_threaded_irq()函数是实现中断线程化的关键接口,理解其实现机制与参数含义,对学习Linux内核中断处理机制大有裨益。

4.2.4.2 devm_request_threaded_irq(数

是针对资源管理的版本,用于请求一个线程化的中断。函数是针对资源管理的版本,用于请求一个线程化的中断。
它的函数原型为:

int devm_request_threaded_irq(struct device *dev, 
							unsigned int irq,
             				irq_handler_t handler, 
             				irq_handler_t thread_fn,
             				unsigned long irqflags, 
             				const char *devname,
             				void *dev_id);

【嵌入式环境下linux内核及驱动学习笔记-(13-中断管理)】_第13张图片

与request_threaded_irq()函数相比,devm_request_threaded_irq()有以下不同:

  1. 它接受一个struct device *dev参数
  2. 它会自动在设备移除时释放中断,无需调用free_irq()
  3. 它使用devres内核基础设施来管理中断资源

除此之外,其余参数与功能与request_threaded_irq()基本相同,用于请求一个线程化的中断处理。

devm_request_threaded_irq()函数的工作流程如下:

  1. 检查并获取指定中断号irq
  2. 调用devm_lect_irq()为中断分配一个devres资源项
  3. 注册handler为中断顶半部处理函数,thread_fn为底半部线程函数
  4. 配置中断属性irqflags,如IRQF_SHARED等
  5. 将中断底半部工作迁移到内核线程执行
  6. 返回0表示成功,否则返回错误码
  7. 当设备dev移除时,corresponding devres项会自动释放中断资源

所以,与request_threaded_irq()相比,devm_request_threaded_irq()带来的主要好处是:

  1. 简化了资源管理,自动在设备移除时释放中断
  2. 使用devres机制维护中断资源,更易于调试与管理
  3. 无需手动调用free_irq()释放中断,避免资源泄漏的风险
    举例:
int err; 
err = devm_request_threaded_irq(&pdev->dev, irq, irq_handler, 
                                thread_fn, IRQF_SHARED, 
                                "example", dev);
if (err) 
    panic("devm irq request failed");

这里我们为pdev设备请求一个IRQ中断,并注册irq_handler和thread_fn为其处理函数,配置其为可共享中断。
一旦pdev设备被移除,对应中断资源会被自动释放。
所以,对有设备资源管理需求的驱动来说,devm_request_threaded_irq()通常是一个更好的选择。理解其实现机制,对编写规范的驱动程序很有帮助。

4.2.4.3 线程化irq机制模板

中断底半部线程化irq机制的一般模板如下:

1、定义中断号:
在中断编号头文件(如arch/x86/include/asm/irq_vectors.h)中定义一个中断号,如:
#define IRQ_NUM 16

2、为中断定义线程函数:
定义一个内核线程,作为中断底半部处理函数,如:

irq_handler_t irq_thread_fn(void *data) 
{
    ...
}

3、 注册中断处理函数与线程函数:
在驱动初始化时调用request_threaded_irq()注册中断处理函数与线程函数,如:

int err;  
err = request_threaded_irq(IRQ_NUM, irq_handler, irq_thread_fn, 
                           IRQF_SHARED, "example", dev);

4、 中断处理函数:
中断处理函数仅完成必要的顶半部工作,并标记底半部工作待处理,如:

irq_handler_t irq_handler(int irq, void *data)
{
    ...      // 顶半部处理
    
    mark_irq_threaded();   // 标记底半部待处理
}

5、 中断线程函数:
中断线程函数在其它线程环境下运行,并完成底半部工作,如:

irq_handler_t irq_thread_fn(void *data) 
{
    ...      // 底半部处理逻辑  
}

6、 重新检查待处理中断(可选):
中断线程函数在处理完当前中断后,可以通过irq_bit在butmp map中重新检查是否有其他待处理中断,如:

irq_handler_t irq_thread_fn(void *data)  
{ 
    ...      // 处理当前中断底半部工作  
    
    irq_thread_check_affinity(); // 检查其他待处理中断  
}

7、 释放中断(可选):
如果在驱动卸载时需要释放中断,则调用free_irq()完成释放,如:
free_irq(IRQ_NUM, dev);

所以, thread irq机制的使用模板是:
定义中断号 -> 注册处理函数与线程函数 -> 中断处理函数标记底半部待处理
-> 中断线程函数完成底半部工作
-> (可选)重新检查其他待处理中断
-> (可选)在卸载时调用free_irq()释放中断
理解此模板,是深入学习Linux内核中断底半部分离与线程化机制的基础。

4.3 设备树操作相关函数

在这个设备树相关操作的头文件/include/linux/of_irq.h

4.3.1 irq_of_parse_and_map

irq_of_parse_and_map函数用于从设备树中获取中断号。它定义在include/linux/of_irq.h头文件中。

int irq_of_parse_and_map(struct device_node *node, int index);

参数说明:

  • node: 设备节点指针,指向要获取中断的设备节点。
  • index: 要获取的中断在节点中断列表中的索引。

返回值:

  • 成功返回获取的中断号。
  • 失败返回0。
    用法:
    \qquad 当我们的驱动需要从设备树中获取与设备相关联的中断号时,可以调用这个函数。我们只需要传入设备节点pointer和想要获取的中断在该节点interrupt列表中的索引,函数会解析节点获取中断号,并将其映射为内核可使用的中断号,并返回。
    实例:
struct device_node *node = dev->of_node;   // 获取设备节点
int irq = irq_of_parse_and_map(node, 0);   // 获取第一个中断

if (irq > 0) 
    ret = request_irq(irq, xxx_interrupt, IRQF_TRIGGER_RISING, 
                     "xxx_irq", xxx_dev);   // 请求该中断     
else
    printk("xxx irq_of_parse_and_map failed!");

这个例子从设备节点中获取第一个中断,即索引为0的中断,并向内核请求该中断,连接中断处理程序xxx_interrupt。
irq_of_parse_and_map函数简化了从设备树中获取中断号的过程。

5、中断实例

5.1 实例描述:

\qquad 将开发板上的一个按键,设定成下降延触发的方式。每按一次后,按键的数据存主内核的缓冲内。应用层有个程序阻塞式读取缓冲,并打印出相应的数据。

5.2 环境:

华清fs4412(exynos4412)开发板,linux3.14

5.3 硬件说明

按键电路
【嵌入式环境下linux内核及驱动学习笔记-(13-中断管理)】_第14张图片

\qquad 板上有三个按键,分别为K2、K3、K4。本实验只用到K2,K2按键连接的是GPX1_1这个GPIO接口,其对应的外部中断名称为XEINT9。GPX1_1是通过R13电阻上拉为高电平,当按下K2后,接地导通,则GPX1_1变为低电平。

查询中断表:
该表在exynos4412的用户手册中:

【嵌入式环境下linux内核及驱动学习笔记-(13-中断管理)】_第15张图片

5.4 设备树定义的说明:

5.4.1 顶层 , cpu中断控制器节点

该节点定义在设备树文件在arch/arm/boot/dts/exynos4.dtsi中,系统已帮我们写好,需要知道其含义,以及该含义的出处

【嵌入式环境下linux内核及驱动学习笔记-(13-中断管理)】_第16张图片

5.4.2 上层,gpio中断控制器节点

该节点定义在设备树 文件在arch/arm/boot/dts/exynos4x12-pinctrl.dtsi ,系统已帮我们写好,需要知道其含义及出处

【嵌入式环境下linux内核及驱动学习笔记-(13-中断管理)】_第17张图片
interrupts说明符的含义及格式的说明文档在/Documentation/devicetree/bindings/arm/gic.txt

#interrupt-cells : Specifies the number of cells needed to encode an interrupt source.
The type shall be a < u32 > and the value shall be 3.

  • The 1st cell is the interrupt type; 0 for SPI interrupts, 1 for PPI
    interrupts.
  • The 2nd cell contains the interrupt number for the interrupt type.
    SPI interrupts are in the range [0-987]. PPI interrupts are in the range [0-15].
  • The 3rd cell is the flags, encoded as follows:
    • bits[3:0] trigger type and level flags.
      1 = low-to-high edge triggered
      2 = high-to-low edge triggered
      4 = active high level-sensitive
      8 = active low level-sensitive
    • bits[15:8] PPI interrupt cpu mask. Each bit corresponds to each of the 8 possible cpus attached to the GIC. A bit set to ‘1’ indicated the interrupt is wired to that CPU. Only valid for PPI interrupts.

5.4.3 下层, 中断生成节点(按键节点)

该节点的树文件为arch/arm/boot/dts/exynos4412-fs4412.dts

【嵌入式环境下linux内核及驱动学习笔记-(13-中断管理)】_第18张图片

中断产生者的interrupts说明符格式含义:/Documentation/devicetree/bindings/pinctrl/samsung-pinctrl.txt

外部GPIO中断:为了支持外部GPIO中断,应在引脚控制器设备节点中指定以下属性。

  • interrupt-parent:将外部GPIO中断转发到的中断父级的标题。
  • interrupts控制器的中断说明符。中断说明符的格式和值取决于控制器的中断父级。

此外,支持GPIO中断的每个引脚组的节点中必须存在以下属性:

  • interrupt-controller:将控制器节点标识为中断父节点。
  • #interrupt-cells:此属性的值应为2。
  • First Cell: represents the external gpio interrupt number local to the external gpio interrupt space of the controller.
    表示控制器本地gpio中断编号(列表的顺序号,从0开始计算)。
  • Second Cell:标识中断类型的标志
    • 1=上升沿触发
    • 2=触发下降沿
    • 3=上升沿和下降沿触发
    • 4=高电平触发
    • 8=低电平触发

以上内容,从上到下,把设备树的层次结构,以及cell的含义的规则出处都讲清楚了。因为在开发驱动时,从硬件到设备树到程序里的每一个配置对应关系必须清楚

5.5 程序代码

5.5.1 按键结构体相关头文件

/*************************************************************************
> File Name: publuc.h
************************************************************************/

#ifndef _PUBLUC_H
#define _PUBLUC_H

enum KEYNUM {
KEY2 =2,
KEY3,
KEY4,
};

enum KEYSTATUE{
KEY_DOWN = 0,
KEY_UP = 1,
};
struct key_data_t {
int key_num;
int statue;
int new;
};
#endif

5.5.2 驱动程序

/*************************************************************************
> File Name:key-mem.c
************************************************************************/

#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include

#include “public.h”

/1、定义重要的变量及结构体/

#define DEV_NUM 1

struct x_dev_t{
struct cdev my_dev; //cdev设备描述结构体变量
wait_queue_head_t rq; //读等待队列
spinlock_t lock;
unsigned int gpio_num;
struct key_data_t key_data;
int IRQ;
};

struct x_dev_t *x_dev;

/所有驱动函数声明/
ssize_t read (struct file *, char __user *, size_t, loff_t *);
int open (struct inode *, struct file *);
int release (struct inode *, struct file *);

//驱动操作函数结构体,成员函数为需要实现的设备操作函数指针
//简单版的模版里,只写了open与release两个操作函数。
struct file_operations fops={
.open = open,
.release = release,
.read = read,
};
//中断处理函数
irqreturn_t irq_handler(int irq,void *p){

struct x_dev_t *xdev =(struct x_dev_t *)p;

spin_lock(&xdev->lock);

xdev-> key_data.key_num = irq;

xdev-> key_data.statue = gpio_get_value(xdev->gpio_num);

xdev->key_data.new = 1;
printk("driver: Interrupt is happend , IRQ = %d\n" , xdev->key_data.key_num);
printk("driver: key status is %d\n",xdev->key_data.statue);
spin_unlock(&xdev->lock);
return IRQ_HANDLED;

}

/3、初始化 cdev结构体,并将cdev结构体与file_operations结构体关联起来/
/这样在内核中就有了设备描述的结构体cdev,以及设备操作函数的调用集合file_operations结构体/
static int cdev_setup(struct x_dev_t *p_dev , dev_t devno ){
int unsucc =0;
int ret = 0;
struct device_node *IRQ_NODE=NULL,*P=NULL;

cdev_init(&p_dev->my_dev , &fops);
p_dev->my_dev.owner = THIS_MODULE;
/*4、注册cdev结构体到内核链表中*/
unsucc = cdev_add(&p_dev->my_dev , devno , 1);
if (unsucc){
    printk("driver : cdev add faild \n");
    return -1;
}
spin_lock_init( &p_dev->lock); //初始化自旋锁,为1
init_waitqueue_head(&p_dev->rq);//22初始化读等待队列
//读取设备树key节点 和gpio属性
        IRQ_NODE =  of_find_node_by_path("/key2_node");
        p_dev->gpio_num = of_get_named_gpio(IRQ_NODE , "key2_gpio",0);

if (IRQ_NODE){
     p_dev->IRQ = irq_of_parse_and_map(IRQ_NODE ,0);
     if (p_dev->IRQ == 0){
         printk("driver:IRQ is not found , MINOR is %d\n ",MINOR(devno));
        return -1;
    }
 
    ret = request_irq(p_dev->IRQ , irq_handler,IRQF_TRIGGER_FALLING , "fs4412_key2-4",p_dev);
    if (ret){
        return -1;
    }
}else{
    printk("driver : IRQ_NODE is not found,MINOR is %d\n", MINOR(devno));
    return -1;
}

return 0;

}

static int __init my_init(void){
int major , minor;
dev_t devno;
int unsucc =0;
int i=0;

x_dev = kzalloc(sizeof(struct x_dev_t)*DEV_NUM , GFP_KERNEL);
if (!x_dev){
    printk(" driver : allocating memory is  failed");
    return  -1;
}

/*2、创建 devno */
unsucc = alloc_chrdev_region(&devno , 0 , DEV_NUM , "key_irq");
if (unsucc){
    printk(" driver : creating devno  is failed\n");
    return -1;
}else{

    major = MAJOR(devno);
    minor = MINOR(devno);
    printk("diver : major = %d  ; minor = %d\n",major,minor);
}
/*3、 初始化cdev结构体,并联cdev结构体与file_operations.*/
/*4、注册cdev结构体到内核链表中*/
for (i=0;i

}

static void __exit my_exit(void)
{
int i=0;
dev_t devno;
devno = x_dev->my_dev.dev;
for (i=0 ; i

    cdev_del(&(x_dev+i)->my_dev);
    free_irq((x_dev+i)->IRQ , (x_dev+i));
}
unregister_chrdev_region(devno , DEV_NUM);
kfree(x_dev);

printk("***************the driver operate_memory exit************\n");

}

/5、驱动函数的实现/
/file_operations结构全成员函数.open的具体实现/

int open(struct inode *pnode , struct file *pf){
int minor = MINOR(pnode->i_rdev);
int major = MAJOR(pnode->i_rdev);
struct x_dev_t *p = container_of(pnode->i_cdev , struct x_dev_t , my_dev);
pf->private_data = p; //把全局变量指针放入到struct file结构体里

if (pf->f_flags & O_NONBLOCK){    //非阻塞
    printk("driver : block_memory[%d , %d] is opened by nonblock mode\n",major , minor);
}else{
    printk("driver : block_memory[%d , %d] is opened by block mode\n",major,minor);
}
return 0;

}
/file_operations结构全成员函数.release的具体实现/
int release(struct inode *pnode , struct file *pf){
printk(“block_memory is closed \n”);
return 0;
}

/file_operations结构全成员函数.read的具体实现/
ssize_t read (struct file * pf, char __user * buf, size_t size , loff_t * ppos){
int res;
struct x_dev_t *pdev = pf->private_data;
if (pf->f_flags & O_NONBLOCK ){ // nonblock
if (pdev->key_data.new == 0){
printk(“driver:no new interrupt\n”);
return 0;
}
goto copy;
}

wait_event_interruptible(pdev->rq,(pdev->key_data.new >0));

copy:
spin_lock(&pdev->lock);
res = copy_to_user(buf , &pdev->key_data, sizeof(struct key_data_t));
spin_unlock(&pdev->lock);

if (res == 0)
    return sizeof(struct key_data_t);
else 
    return 0;

}

module_init(my_init);
module_exit(my_exit);
MODULE_LICENSE(“GPL”);
MODULE_AUTHOR(“”);

5.5.3 测试

insmod key-irq.ko
mknod /dev/key-irq c 251 0
chmod 777 /dev/key-irq

然后,通过按板上的key2 或 key3按键,就会在相应的信息在屏上出现。

你可能感兴趣的:(Linux内核与驱动,linux,内核与驱动,嵌入式,字符设备,驱动中断机制)