Linux驱动---中断上下半部
一、中断上下半部
在上一篇文章按键驱动中,代码做了一个这样的设计。大家可以思考一下,为什么不在中断函数gpio_key_irq
中完成消抖及上报事件呢?为什么要通过定时器回调函数debounce_timer_func
呢?
/* Timer callback function for debounce */
static void debounce_timer_func(struct timer_list *t)
{
struct keys_desc *key = from_timer(key, t, timer);
int value = gpio_get_value(key->gpio);
if (value != key->last_value) {
key->last_value = value;
if (value == 0) {
input_report_key(input_device, key->key_code, 1); /* Key press event */
} else {
input_report_key(input_device, key->key_code, 0); /* Key release event */
}
input_sync(input_device);
}
}
/* GPIO IRQ handler */
static irqreturn_t gpio_key_irq(int irq, void *arg)
{
struct keys_desc *key = arg;
/* start debounce timer(20ms) to delay event processing */
mod_timer(&key->timer, jiffies + msecs_to_jiffies(20));
return IRQ_HANDLED;
}
我们知道中断的目的是快速响应外部事件,因此中断处理程序(ISR)应尽可能简洁高效,以避免长时间占用CPU资源。但事实上,当中断到来时,要完成的工作往往并不会是短小的,它可能要进行较大量的耗时处理。所以Linux内核设计了中断上下半部的机制。上半部完成尽可能少的比较紧急的功能,它往往只是简单地读取寄存器中的中断状态并清除中断标志后就进行“登记中断”的工作;下半部来完成中断事件的绝大多数任务。
在前面使用定时器的过程中,我们发现定时器的响应过程跟中断类似,只是它是由软件定时器来触发的,因此Linux下定时器也叫做软件中断。因此在内核中CPU就有可能处于三种上下文,它使用了一个per-pcu
的变量preempt_count
来区分这些上下文,同时管理中断嵌套、抢占行为。
- 硬中断上下文:CPU在执行中断上半部时处于硬中断上下文,在较新的内核中断设计中,硬中断上下文会禁用中断,因此不会产生中断嵌套,只有等待当前中断上半部分执行结束后才能响应下一个中断。
- 软中断上下文:CPU在执行中断下半部时处于软中断上下文,在软中断上下文中可以被中断抢占,但是在CPU上软中断不允许嵌套,如果在执行中断下半部时发生中断,在处理完新的中断上半部之后不会在进入新的软中断上下文。
- 进程上下文:CPU在执行进程代码时处于进程上下文。
二、下半部实现机制
在Linux内核中,中断下半部是指在中断处理程序(ISR)执行完毕后,将较长时间的处理或不紧急的任务推迟到其他地方执行的机制。内核通过不同的机制来实现中断上下半部的任务处理,这些机制包括定时器、软中断、任务队列、工作队列、内核线程等。
2.1、定时器
在Linux内核中,定时器(Timer)是一种机制,用于在指定的时间后或周期性地触发回调函数。定时器的回调函数通常是一些轻量级的任务,用于处理超时、周期性任务或延迟执行的操作。定时器的精度由系统时钟决定,通常是由jiffies
(内核计时单位)来调度定时器。
创建和使用内核定时器的典型步骤如下:
1.初始化定时器:通常是通过timer_setup()
来初始化,并指定回调函数。
2.设置定时器:通过mod_timer()
或add_timer()
启动定时器,并设置定时器的超时值(到期时间)。
3.删除定时器:当不需要定时器时,使用del_timer()
或del_timer_sync()
来删除定时器。
Timer常见函数
(1)用于初始化定时器并设置定时器的回调函数
void timer_setup(struct timer_list *timer, void (*callback)(struct timer_list *), unsigned long flags);
//timer:定时器结构体对象。
//callback:定时器触发时调用的回调函数。
//flags:标志,用于配置定时器的行为。
(2)用于初始化定时器并指定回调函数,通常用于创建普通定时器
void setup_timer(struct timer_list *timer, void (*function)(unsigned long), unsigned long data);
//timer:定时器结构体对象。
//function:定时器触发时调用的回调函数。
//data:传递给回调函数的数据。
(3)add_timer() 将其添加到内核定时器管理队列中,从而启动定时器
int add_timer(struct timer_list *timer);
//timer:要启动的定时器结构体对象。
(4)用于修改定时器的触发时间,可以改变定时器的到期时间
int mod_timer(struct timer_list *timer, unsigned long expires);
//timer:要修改的定时器结构体对象。
//expires:定时器到期的时间戳,通常使用 jiffies 来设置。
(5) 删除定时器,取消定时器的计时,并停止触发回调函数
int del_timer(struct timer_list *timer);
//timer:要修改的定时器结构体对象。
(6)同步删除定时器,确保定时器回调函数执行完毕后再删除定时器
int del_timer_sync(struct timer_list *timer);
//timer:要修改的定时器结构体对象。
参考代码可如下:
点击查看代码
#include <linux/timer.h>
#include <linux/jiffies.h>
static struct timer_list my_timer;
void my_timer_callback(struct timer_list *t)
{
pr_info("Timer expired\n");
}
static int __init my_init(void)
{
timer_setup(&my_timer, my_timer_callback, 0);
mod_timer(&my_timer, jiffies + msecs_to_jiffies(1000));
return 0;
}
module_init(my_init);
MODULE_LICENSE("GPL");
2.2、软中断
软中断(Softirq)是Linux内核中用于处理延迟敏感任务的一种机制。它介于硬件中断和内核线程之间,允许内核在适当的时机处理一些需要快速响应但不适合在硬件中断上下文中直接处理的任务。通常用于网络数据处理(如接收和发送网络数据包)、块设备操作(磁盘I/O操作)、定时器处理(如内核定时器到期后的处理)等。
(1)用来初始化并注册一个软中断类型
通常在内核模块或初始化过程中调用。它允许你定义一个软中断处理函数,并将其与软中断的编号绑定。
void open_softirq(int nr, void (*action)(struct softirq_action *));
//nr:软中断的编号。
//action:处理软中断的函数,该函数会在软中断触发时执行.
(2)用来手动触发某种软中断类型的函数
它会将指定类型的软中断标记为待处理,等到中断上下文完成后,内核会在合适的时机执行对应的软中断任务。
void raise_softirq(int nr);
//nr:软中断类型的编号。每种软中断类型对应一个数字,如网络接收(NET_RX)、网络发送(NET_TX)等。
(3)用来控制软中断
这在某些需要禁止软中断执行的情况下非常有用。
void local_bh_disable(void);
void local_bh_enable(void);
//local_bh_disable():禁用当前 CPU 上的所有软中断,防止软中断的执行。
//local_bh_enable():恢复软中断的处理。
参考代码如下:
点击查看代码
#include <linux/interrupt.h>
static void my_softirq_handler(struct softirq_action *action)
{
pr_info("SoftIRQ handler processing\n");
}
static irqreturn_t my_interrupt_handler(int irq, void *dev_id)
{
pr_info("Interrupt occurred\n");
raise_softirq(MY_SOFTIRQ);
return IRQ_HANDLED;
}
static int __init my_init(void)
{
open_softirq(MY_SOFTIRQ, my_softirq_handler);
return 0;
}
module_init(my_init);
MODULE_LICENSE("GPL");
2.3、任务队列
Tasklet 是 Linux 内核中的一种延迟执行机制,属于软中断(SoftIRQ)的一个实现,使得某些操作能够在中断上下文中异步执行。与 SoftIRQ 不同,Tasklet 提供了较低级别的接口,并且可以由内核中的中断上下文调度。
Tasklet 的工作流程:
1.Tasklet 初始化:Tasklet 必须先调用 tasklet_init()
初始化,通过注册一个函数来处理 Tasklet 需要执行的任务。
2.Tasklet 调度:调用 tasklet_schedule()
函数将 Tasklet 添加到软中断队列中。
3.Tasklet 执行:一旦软中断被处理,Tasklet 中注册的函数会被执行。该函数通常用于处理不需要立即执行的任务,但必须尽快完成的操作(例如:网络包的接收后处理、设备驱动的后处理等)。
4.Tasklet 结束:任务执行完成后,Tasklet 会被标记为已处理,内核会在下一个合适的时机重新调度该 Tasklet。
Tasklet常见函数
(1)用来初始化一个 Tasklet 结构
通过此函数,我们可以指定 Tasklet 在触发时执行的回调函数和传递给该回调函数的参数。
void tasklet_init(struct tasklet_struct *t, void (*func)(unsigned long), unsigned long data);
//t:要初始化的 Tasklet 结构体。
//func:Tasklet 执行时调用的回调函数。
//data:传递给回调函数的参数。
(2)用来将一个初始化过的 Tasklet 调度到软中断队列中,等待执行
void tasklet_schedule(struct tasklet_struct *t);
//t:要调度的 Tasklet 结构体。
(3)用来停止并销毁一个已调度的 Tasklet
它会阻止 Tasklet 的进一步调度,并等待其当前执行完成。
void tasklet_kill(struct tasklet_struct *t);
\\t:要停止的 Tasklet 结构体。
参考代码如下:
点击查看代码
#include <linux/interrupt.h>
static struct tasklet_struct my_tasklet;
static void my_tasklet_handler(unsigned long data)
{
pr_info("Tasklet processing\n");
}
static irqreturn_t my_interrupt_handler(int irq, void *dev_id)
{
pr_info("Interrupt occurred\n");
tasklet_schedule(&my_tasklet);
return IRQ_HANDLED;
}
static int __init my_init(void)
{
tasklet_init(&my_tasklet, my_tasklet_handler, 0);
return 0;
}
module_init(my_init);
MODULE_LICENSE("GPL");
2.4、工作队列
在 Linux 内核中,Workqueue 是一个用于处理延迟任务的机制。它允许任务在稍后的时间以进程上下文执行,而不是在中断上下文或软中断上下文中立即执行。与 Tasklet 或 SoftIRQ 相比,Workqueue 提供了更灵活的任务调度方式,能够让任务在正常的内核线程上下文中执行,因此它允许在任务执行时进行较长时间的操作,且不会阻塞中断或软中断的处理。
Workqueue 的工作流程:
1.创建 Workqueue:通过 create_workqueue()
或 alloc_workqueue()
函数创建一个 Workqueue,可以指定 Workqueue 处理任务的内核线程的属性。
2.初始化 Work_struct:每个任务都需要一个 work_struct 结构体,它包含了任务执行时需要的信息和回调函数。
3.调度任务:通过 queue_work()
或 queue_delayed_work()
函数将任务添加到 Workqueue 中。任务会在合适的时机(由 Workqueue 线程调度)执行。
4.执行任务:内核会在 Workqueue 的工作线程中执行这些任务。如果有多个任务,Workqueue 会按顺序执行。
5.销毁 Workqueue:当任务完成后,如果不再需要 Workqueue,可以使用 destroy_workqueue()
销毁它。
Workqueue的相关函数
(1)用于创建一个 Workqueue
create_workqueue()
用于创建一个新的 Workqueue,而 alloc_workqueue()
允许传入一些额外的属性,如线程的优先级等。
struct workqueue_struct *create_workqueue(const char *name);
struct workqueue_struct *alloc_workqueue(const char *fmt, unsigned int flags, int max_active);
//name:Workqueue 的名字,用于标识该 Workqueue。
//flags:Workqueue 的属性,可以是 WQ_UNBOUND(任务调度到任何 CPU)或者 WQ_HIGHPRI(优先级高的任务)。
//max_active:最大并行执行任务数。
(2)用于初始化任务结构体 work_struct
INIT_WORK()
用于普通任务,而 INIT_DELAYED_WORK()
用于延迟任务。
void INIT_WORK(struct work_struct *work, void (*func)(struct work_struct *));
void INIT_DELAYED_WORK(struct delayed_work *dwork, void (*func)(struct work_struct *));
//work:待初始化的 work_struct 结构体。
//func:任务执行时调用的回调函数。
(3)用于将任务调度到 Workqueue 中执行
int queue_work(struct workqueue_struct *wq, struct work_struct *work);
int queue_delayed_work(struct workqueue_struct *wq, struct delayed_work *dwork, unsigned long delay);
//wq:目标 Workqueue。
//work:要调度的任务结构体。
//delay:延迟执行的时间,单位为 jiffies(内核时间单位)。
(4)等待指定的 Workqueue 上所有任务完成并退出
void flush_workqueue(struct workqueue_struct *wq);
//wq:目标 Workqueue。
(5)销毁一个 Workqueue
void destroy_workqueue(struct workqueue_struct *wq);
//wq:目标 Workqueue。
参考代码如下:
点击查看代码
#include <linux/workqueue.h>
static struct workqueue_struct *my_wq;
static struct work_struct my_work;
void my_work_handler(struct work_struct *work)
{
pr_info("Workqueue processing\n");
}
static irqreturn_t my_interrupt_handler(int irq, void *dev_id)
{
pr_info("Interrupt occurred\n");
schedule_work(&my_work);
return IRQ_HANDLED;
}
static int __init my_init(void)
{
my_wq = create_workqueue("my_workqueue");
if (!my_wq)
return -ENOMEM;
INIT_WORK(&my_work, my_work_handler);
return 0;
}
module_init(my_init);
MODULE_LICENSE("GPL");
2.5、内核线程
内核线程(Kernel Threads)是运行在操作系统内核空间中的线程,它们通常不与用户交互,而是执行系统层面的任务,这与用户空间的线程有显著的不同。内核线程是由操作系统内核创建和调度的,通常用于处理那些需要在内核空间中执行的任务,如设备驱动、文件系统管理、网络处理等内核操作。
内核线程的工作流程:
1.创建:内核线程通过 kthread_create()
创建,并通过 wake_up_process()
启动。
2.执行:内核线程会在内核上下文中执行其任务。它们可以执行阻塞操作,如等待 I/O 或睡眠。
3.停止:内核线程可以通过 kthread_stop()
或在任务完成后自行退出。通常通过 kthread_should_stop()
来检测是否需要停止。
内核线程的相关函数
(1)用于创建一个新的内核线程
它返回一个 task_struct
指针,表示这个线程。如果创建失败,返回一个错误指针。
struct task_struct *kthread_create(int (*threadfn)(void *data), void *data, const char *namefmt, ...);
//threadfn:内核线程的回调函数。
//data:传递给线程回调函数的数据,可以是指向任何数据的指针。
//namefmt:线程的名字(格式化字符串)。
(2)用于启动一个已经创建的内核线程
内核线程在调用 kthread_create()
后不会立即开始执行,需要显式调用此函数来启动。
void wake_up_process(struct task_struct *p);
//p:指向 task_struct 的指针,该指针表示内核线程。
(3)用于创建并启动内核线程
它实际上是 kthread_create()
和 wake_up_process()
的组合,简化了内核线程的创建和启动过程。当你调用 kthread_run()
时,它不仅创建一个内核线程,还会立即启动该线程。
struct task_struct *kthread_run(int (*threadfn)(void *data), void *data, const char *namefmt, ...);
//threadfn:这是线程的回调函数(线程的执行逻辑)。该函数的参数是一个指向 void 的指针,可以传递任何类型的数据。
//data:传递给线程函数 threadfn 的数据,可以是任何类型的数据,通常是指向结构体或其他数据的指针。
//namefmt:线程名称的格式化字符串,内核线程会使用这个名称,方便在调试或查看进程信息时识别。
(4)用于检查内核线程是否应停止
内核线程在循环中检查这个函数的返回值,以确定是否应该终止线程的执行。
bool kthread_should_stop(void);
//返回值:如果线程应该停止,则返回 true,否则返回 false。
(5)用于标记内核线程需要停止
void kthread_stop(struct task_struct *p);
//p:指向 task_struct 的指针,该指针表示内核线程。
参考代码如下:
点击查看代码
#include <linux/kthread.h>
#include <linux/delay.h>
static struct task_struct *my_thread;
int my_kernel_thread(void *data)
{
while (!kthread_should_stop()) {
pr_info("Kernel thread running\n");
msleep(1000);
}
return 0;
}
static int __init my_init(void)
{
my_thread = kthread_run(my_kernel_thread, NULL, "my_kernel_thread");
if (IS_ERR(my_thread)) {
pr_err("Failed to create kernel thread\n");
return PTR_ERR(my_thread);
}
return 0;
}
static void __exit my_exit(void)
{
kthread_stop(my_thread);
}
module_init(my_init);
module_exit(my_exit);
MODULE_LICENSE("GPL");
本文作者:小信嵌梦
本文链接:https://www.cnblogs.com/Xin-Code9/p/18714200
版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步