05、中断下半部:tasklet实验
一、什么是 tasklet
在Linux系统里,Tasklet是一个用来处理中断相关复杂任务的软件机制。它主要有两个特点:
- 安全性:在多核CPU环境下,同一时间每个Tasklet的任务只能在一个CPU核心上运行。这样就能避免多个CPU同时处理同一个任务导致的数据冲突问题。
- 限制:使用时要注意,Tasklet执行的任务不能包含任何会让CPU暂停的函数(比如需要等待I/O或资源的操作)。如果违反这点,会导致系统异常。
简单来说,Tasklet就像一个安全的任务调度员,负责在多核CPU之间协调任务执行,但它的任务必须是快速完成且不会卡住的。
在 Linux 内核中,tasklet 结构体的定义位于 include/linux/interrupt.h 头文件中。其原型如
下:
struct tasklet_struct
{
struct tasklet_struct *next;
unsigned long state;
atomic_t count;
bool use_callback;
union {
void (*func)(unsigned long data);
void (*callback)(struct tasklet_struct *t);
};
unsigned long data;
};
2
3
4
5
6
7
8
9
10
11
12
tasklet_struct结构体包含以下成员:
- next:指向下一个tasklet的指针,用来把多个tasklet连成链表,方便内核统一管理。
- state:表示tasklet的当前状态(比如是否正在运行或等待)。
- count:记录引用次数的计数器,确保在多个地方同时调度或取消tasklet时能正确处理(例如防止同时操作出错)。
- func:指向一个函数的指针,这个函数会在tasklet启动时被自动调用执行。
- data:传递给上述函数的具体参数值。
此外,为了方便使用,还定义了tasklet_t类型作为tasklet_struct的别名。这样可以直接用tasklet_t来声明变量,而不用每次都写struct tasklet_struct的完整名称。
二、tasklet 相关接口函数
2.1、静态初始化函数
在Linux内核中,DECLARE_TASKLET宏可以方便地完成tasklet的静态初始化设置。它让开发者在程序启动时更轻松地配置tasklet。
宏函数的原型如下:
#define DECLARE_TASKLET(name,func,data) \
struct tasklet_struct name = { NULL,0,ATOMIC_INIT(0),func,data}
2
一个tasklet包含三个参数:名称(name)、处理函数(func)和数据(data)。
- 名称用于标识tasklet
- 处理函数是它要执行的任务
- 数据是传给这个函数的参数。
tasklet在初始化时默认处于启用状态。
如果 tasklet 初始化函数为非使能状态,使用以下宏定义:
#define DECLARE_TASKLET_DISABLED(name,func,data) \
struct tasklet_struct name = { NULL,0,ATOMIC_INIT(1),func,data}
2
2.2、动态初始化函数
在 Linux 内核中,可以使用 tasklet_init 函数对 tasklet 进行动态初始化。该函数原型为:
void tasklet_init(struct tasklet_struct *t, void (*func)(unsigned long), unsigned long data);
tasklet_init函数需要三个参数:指向任务结构的指针t、处理函数func,以及要传递给该函数的数据data。这个函数的作用是让我们在程序运行过程中随时创建和设置tasklet任务。
通过这种方式,我们可以根据实际需求灵活控制任务的启动、暂停或调整。当任务不再需要时,可以用tasklet_kill函数停止它并释放占用的资源,避免内存浪费。整个过程就像管理程序中的临时小任务一样方便直接。
2.3、关闭函数
在 Linux 内核中,可以使用 tasklet_disabled 函数来关闭一个已经初始化的 tasklet。该函数
的原型如下:
void tasklet_disable(struct tasklet_struct *t);
当关闭一个tasklet后,即使通过tasklet_schedule函数尝试触发它,它的处理函数也不会再执行。这可以用来临时暂停或完全停止tasklet的运行,直到你再次通过tasklet_enable函数重新开启它。
需要注意的是,关闭tasklet只是让它停止工作,但不会删除它的设置。你可以随时重新打开它继续使用,或者用tasklet_kill函数彻底移除它。简单来说:关闭=暂停运行,但保留配置;销毁=完全删除。
2.4、使能函数
在 Linux 内核中,可以使用 tasklet_enable 函数来使能(启用)一个已经初始化的 tasklet。
该函数的原型如下:
void tasklet_disable(struct tasklet_struct *t);
Tasklet的使用方法如下:
- 首先要有一个指向tasklet结构体的指针t
- 启用tasklet后,需要主动调用tasklet_schedule()函数才能触发执行。这时系统才会运行你预先设置的处理函数。
- tasklet开始执行后会按照预定流程处理任务。
注意:
- 单纯启用tasklet并不会自动运行,必须显式调用tasklet_schedule()才能启动
- 要暂停tasklet时,可以用tasklet_disable()暂时阻止其运行
- 如果要永久停止并释放资源,必须用tasklet_kill()彻底销毁该tasklet
整个过程就像这样:先准备好tasklet,需要运行时手动"开开关",想停的时候按暂停键,完全不用时再彻底关掉。
2.5、调度函数
在 Linux 内核中,可以使用 tasklet_schedule 函数来调度(触发)一个已经初始化的 tasklet
执行。该函数的原型如下:
void tasklet_schedule(struct tasklet_struct *t);
参数 t 是指向一个 tasklet 结构体的指针。
需要注意的是,触发 tasklet 仅仅是给它打上"需要执行"的标记,并不会立刻运行它的处理函数。实际运行时间由系统内核的调度安排决定。
2.6、销毁函数
在 Linux 内核中,可以使用 tasklet_kill 函数来销毁一个已经初始化的 tasklet,释放相关
资源。该函数的原型如下:
void tasklet_kill(struct tasklet_struct *t);
tasklet_kill函数用于结束一个任务块。使用前必须先确保这个任务块已停止运行(通过tasklet_disable函数关闭)。否则强行结束正在运行的任务块会导致系统崩溃或其他错误。
调用tasklet_kill后,系统会:
- 释放该任务块占用的所有资源
- 标记该任务块为不可用状态
一旦任务块被销毁后:
- 再次启动或使用它会导致未知错误
- 如果需要重新使用,必须通过tasklet_init重新初始化
关键步骤总结: 停止 → 销毁 → 重新初始化(如需再次使用)
注意:销毁前务必确认任务块已完全停止运行,否则可能引发严重错误。
三、实验代码
说明:
::: 硬件IO表:https://lceda001.feishu.cn/wiki/UN0pw7Y1KixKirkMh0rcP3Tandc
操作第七个管脚触发中断,对应GPIO1_A4 ::: ::: 引脚编号计算:
公式1:GPIO 小组编号计算公式:number = group * 8 + X
其中:group为小组号(A为0,B为1,C为2,D为3)
公式2:GPIO pin 脚计算公式:pin = bank * 32 + number
其中:bank 为组号,number由公式1计算得来
故:GPIO1_A4
1*32+0*8+4 = 36 :::
驱动代码:
#include <linux/module.h>
#include <linux/init.h>
#include <linux/interrupt.h>
#include <linux/gpio.h>
int irq;
struct tasklet_struct mytasklet;
// 定义tasklet处理函数
void mytasklet_func(unsigned long data)
{
printk("data is %ld\n", data);
}
// 中断处理函数
irqreturn_t test_interrupt(int irq, void *args)
{
printk("This is test_interrupt\n");
tasklet_schedule(&mytasklet); // 调度tasklet执行
return IRQ_RETVAL(IRQ_HANDLED);
}
// 模块初始化函数
static int interrupt_irq_init(void)
{
int ret;
irq = gpio_to_irq(36); // 将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;
}
// 初始化tasklet
tasklet_init(&mytasklet, mytasklet_func, 1);
return 0;
}
// 模块退出函数
static void interrupt_irq_exit(void)
{
free_irq(irq, NULL);
tasklet_enable(&mytasklet); // 使能tasklet(可选)
tasklet_kill(&mytasklet); // 销毁tasklet
printk("bye bye\n");
}
module_init(interrupt_irq_init); // 指定模块的初始化函数
module_exit(interrupt_irq_exit); // 指定模块的退出函数
MODULE_LICENSE("GPL"); // 模块使用的许可证
MODULE_AUTHOR("linux"); // 模块的作者
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
四、实验截图
驱动加载:insmod interrupt.ko
拉高GPIO,触发中断服务程序,打印如下: