1 中断概念

  • 一句话,中断上半部要求快进快出,耗时操作则可放在中断下半部执行
  • 下半部实现方式:



  • 软中断
  • tasklet
  • 工作队列
  • 内核定时器,也可以将工作退后一段时间(精准的)执行


2 tasklet

tasklet是中断处理下半部最常见的一种方式,驱动程序一般先申请中断,在中断处理函数内完成中断上半部分的工作之后再调用tasklet,tasklet有如下优点:


  • 一个tasklet只可以在一个CPU上同步执行,不同的tasklet可以在不同的CPU上同步执行
  • tasklet的实现是建立在两个软件中断的基础之上的,即​​HI_SOFTIRQ​​​和​​ACKLET_SOFTIRQ​​​,本质上没什么区别,只不过​​HI_SOFTIRQ​​的优先级更高一些
  • 由于tasklet是在软中断上实现的,所以像软中断一样不能睡眠、不能阻塞、处理函数内不能含有导致睡眠的动作,如:减小信号量、从用户空间拷贝数据或者手动分配内存等
  • 一个tasklet能够被禁止并且之后被重新使能,它不会执行直到它被使能的次数与被禁止的次数相同
  • tasklet的串行化使tasklet函数不必是可重入的(不需要考虑并发),因此简化了设备驱动程序开发者的工作
  • 每个cpu拥有一个tasklet_vec链表,具体是哪个cpu的tasklet_vec链表,是根据当前线程是运行在哪个cpu来决定的

2.1 tasklet API

/* tasklet结构体 */
struct tasklet_struct
{
struct tasklet_struct *next; //链表节点
unsigned long state; //属性
atomic_t count; //计数,用于禁止使能,每禁止一次计数加1,每使能一次计数减1,只有禁止次数和使能次数一样(count=0)的时候tasklet才会执行调用函数
void (*func)(unsigned long); //中断下半部实现方法,不能阻塞,不能睡眠
unsigned long data; //中断服务函数的参数
};

/* tasklet结构体变量是tasklet_vec链表下的一个节点,next是链表下一个节点,status使用了2个位,如下:*/
enum
{
TASKLET_STATE_SCHED, //1表示已经被调度,0表示还没调度
TASKLET_STATE_RUN //1表示tasklet正在执行,0表示尚未执行,只针对SMP有效,单处理器无意义
};

/* tasklet初始化 */
/* 1.定义时初始化:定义变量名为name的tasklet_struct结构体变量,并初始化调用函数func,参数为data,使能tasklet */
DECLARE_TASKLET(name, func, data);
#define DECLARE_TASKLET(name, func, data) struct tasklet_struct name = {NULL, 0, ATOMIC_INIT(0), func, data}
/* 2.定义时初始化:定义变量名为name的tasklet_struct结构体变量,并初始化调用函数func,参数为data,禁止tasklet */
DECLARE_TASKLET_DISABLED(name, func, data);
#define DECLARE_TASKLET_DISABLED(name, func, data) struct tasklet_struct name = {NULL, 0, ATOMIC_INIT(1), func, data}
/* 3.运行中初始化,先定义struct tasklet_struct name,后初始化 */
void tasklet_init(struct tasklet_struct * t, void(* func)(unsigned long), unsigned long data)
{
t->next = NULL;
t->state = 0; //设置为未调度,未执行
atomic_set(&t->count, 0); //默认使能
t->func = func;
t->data = data;
}

/* 启动中断下半部 */
static inline void tasklet_schedule(struct tasklet_struct * t);
static inline int tasklet_trylock(struct tasklet_struct * t);
static inline void tasklet_unlock(struct tasklet_struct * t);
static inline void tasklet_unlock_wait(struct tasklet_struct * t);

/* 使能之前一个被disable的tasklet;若这个tasklet已经被调度,他会很快运行。tasklet_enable和task_disable必须匹配使用,因为内核跟踪每个tasklet的“禁止次数” */
static inline void tasklet_enable(struct tasklet_struct * t);

/* 与tasklet_schedule类似,只是在更高优先级执行。当软中断处理运行时,它处理高优先级tasklet,在其他软中断之前,只有具有低响应周期要求的驱动(实时性不强)才应使用这个函数,可避免其他软件中断处理引入的附加周期 */
void tasklet_hi_schedule(struct tasklet_struct * t);

/* 确保了tasklet不会被再次调度运行,即执行此函数之后若再次调用tasklet_schedule函数,中断下半部也不会执行。通常当一个设备正在被关闭或者模块卸载时被调用。如果tasklet正在运行,那么这个函数需等待直到tasklet执行完毕。若tasklet重新调度它自己,则必须阻止在调用tasklet_kill之前它重新调度自己,如同使用del_timer_sync。注意:该函数不是真的去杀掉被调度的tasklet,而是保证tasklet不再调用 */
void tasklet_kill(struct tasklet_struct *t);

2.2 tasklet使用示例


  • 驱动代码,由于目前没有开发板,故而缺少具体的硬件中断触发源,所以将会无法触发中断下半部份的执行。在本示例中直接将中断下半部份触发放在了模块初始化函数里面,也就是说当模块一旦被安装,那么就会触发中断下半部分的执行

hq@hq-virtual-machine:~/桌面/linux-5.8/drivers/study_driver_hq/tasklet$ cat tasklet.c 
#include <linux/init.h>
#include <linux/module.h>
#include <linux/slab.h>
#include <linux/interrupt.h>

struct message {
int id;
int val;
};

struct tasklet_struct mytask;

void mytask_func(unsigned long data)
{
struct message *msg = (struct message *)data;
printk("id = %d, val = %d\n", msg->id, msg->val);
kfree(msg);
msg = NULL;
}
static int __init task_init(void)
{
struct message *msg;
printk(KERN_INFO "%s start\n", __func__);
msg = (struct message *)kzalloc(sizeof(struct message), GFP_KERNEL);
if (msg == NULL) {
printk(KERN_ERR "failed to kmalloc\n");
return -ENOMEM;
}
msg->id = 7;
msg->val = 77;
tasklet_init(&mytask, mytask_func, (unsigned long)msg);
tasklet_schedule(&mytask);
printk(KERN_INFO "%s end\n", __func__);
return 0;
}

static void __exit task_exit(void)
{
printk(KERN_INFO "%s start\n", __func__);
tasklet_kill(&mytask);
printk(KERN_INFO "%s end\n", __func__);
}

module_init(task_init);
module_exit(task_exit);
MODULE_LICENSE("GPL");

  • 调试过程,可以看到,当模块安装之后就会触发中断下半部分,进而去执行了中断回调函数

hq@hq-virtual-machine:~/桌面/linux-5.8/drivers/study_driver_hq/tasklet$ sudo insmod tasklet.ko 
hq@hq-virtual-machine:~/桌面/linux-5.8/drivers/study_driver_hq/tasklet$ sudo rmmod tasklet.ko
hq@hq-virtual-machine:~/桌面/linux-5.8/drivers/study_driver_hq/tasklet$ dmesg
[ 583.990890] task_init start
[ 583.990893] task_init end
[ 583.990897] id = 7, val = 77
[ 593.107581] task_exit start
[ 593.107582] task_exit end
3 工作队列

工作队列是一种将工作退后执行的形式,交由一个内核线程去执行进在程上下文执行,其不能访问用户空间。最重要的特点是工作队列允许重新调度甚至是睡眠。工作队列子系统提供了一个默认的工作线程来处理这些工作。默认的工作者线程叫做​​/events/n​​,这里的n是处理器编号,每个处理器对应一个线程,也可以自己创建工作者线程

3.1 工作队列 API


  • 系统默认的工作队列

/* 工作队列work_struct结构体 */
struct work_struct {
/* atomic_long_t data; */
unsigned long data;
struct list_head entry;
work_func_t func;
#ifdef CONFIG_LOCKDEP
struct lockdep_map lockdep_map;
#endif
};
typedef void (*work_func_t)(struct work_struct *work);

/* 初始化工作队列 */
INIT_WORK(struct work_struct *work, work_func_t func);
DECLARE_WORK(n, f);

/* 开启中断下半部,即将给定工作的处理函数提交给缺省的工作队列和工作线程 */
int schedule_work(struct work_struct *work);

/* 确保没有工作队列入口在系统中任何地方运行 */
void flush_scheduled_word(void);

/* 延时执行一个任务 */
int schedule_delayed_work(struct delayed_struct *work, unsigned long delay);

/* 从一个工作队列中去除入口 */
int cancel_dalayed_work(struct delayed_struct *work);

  • 使用自定义工作队列:创建工作队列使用3个宏,成功后返回​​workqueue_struct *​​​指针,并创建了工作线程。三个宏主要区别在于后面两个参数​​singlethread​​​和​​freezable​​​,​​singlethread​​​为0时会为每个CPU上创建一个工作者线程,为1时只在当前运行的cpu上创建一个工作者线程。​​freezable​​​会影响内核线程结构体​​thread_info​​​的​​PF_NOFREEZE​​标记

/* 多处理器时会为每个cpu创建一个工作线程 */
#define create_workqueue(name) __create_workqueue((name), 0, 0)

/* 只创建一个工作线程,系统挂起时线程也挂起 */
#define create_freezeable_workqueue(name) __create_workqueue((name), 1, 1)

/* 只创建一个工作线程,系统挂起时线程不会挂起 */
#define create_singlethread_workqueue(name) __create_workqueue((name), 1, 0)

/* 释放创建的工作队列资源 */
void destroy_workqueue(struct workqueue_struct *wq);

/* 延时调用指定工作队列的工作 */
queue_delayed_work(struct workqueue_struct *wq, struct delay_struct *work, unsigned long delay);

/* 取消指定工作队列的延时工作 */
cancel_delayed_work(struct delay_struct *work);

/* 将工作加入到工作队列中进行调度 */
queue_work(struct workqueue_struct *wq, struct work_struct *work);

/* 等待队列中的任务全部执行完毕 */
void flush_workqueue(struct workqueue_struct *wq);

3.2 工作队列使用示例


  • 使用系统的工作队列

#include <linux/init.h>
#include <linux/module.h>
#include <linux/workqueue.h>

void my_func(struct work_struct *ws);
DECLARE_WORK(my_work, my_func);

void my_func(struct work_struct *ws)
{
printk(KERN_INFO "doing work\n");
}

static int __init work_queue_init(void)
{
schedule_work(&my_work);
return 0;
}

static void __exit work_queue_exit(void)
{

}

module_init(work_queue_init);
module_exit(work_queue_exit);
MODULE_LICENSE("GPL");

  • 调试

hq@hq-virtual-machine:~/桌面/linux-5.8/drivers/study_driver_hq/workqueue$ sudo insmod work_queue.ko 
hq@hq-virtual-machine:~/桌面/linux-5.8/drivers/study_driver_hq/workqueue$ dmesg
[ 4097.029387] doing work

  • 使用自己的工作队列

#include <linux/init.h>
#include <linux/module.h>
#include <linux/workqueue.h>

struct work_struct my_work;
struct workqueue_struct *my_wq;

void my_func(struct work_struct *ws)
{
printk(KERN_INFO "doing work\n");
}

static int __init work_queue_init(void)
{
/* create my work queue */
my_wq = create_workqueue("my_wq");
if (!my_wq) {
printk(KERN_ERR "failed to create workqueue\n");
return -1;
}
/* init my work */
INIT_WORK(&my_work, my_func);
/* schedule my work to my work queue */
queue_work(my_wq, &my_work);
return 0;
}

static void __exit work_queue_exit(void)
{
/* destroy my work queue */
destroy_workqueue(my_wq);
my_wq = NULL;
}

module_init(work_queue_init);
module_exit(work_queue_exit);
MODULE_LICENSE("GPL");

  • 调试

hq@hq-virtual-machine:~/桌面/linux-5.8/drivers/study_driver_hq/workqueue$ sudo insmod work_queue.ko 
hq@hq-virtual-machine:~/桌面/linux-5.8/drivers/study_driver_hq/workqueue$ dmesg \
[ 5472.402008] doing work

  • 查找命令

find -name "*.c" | xargs grep "create_workqueue"
4 内核定时器

  • 软件意义上的定时器最终依赖硬件定时器来实现,内核在时钟中断发生后检测检测一个定时器释放到期,到期后的定时器处理函数将作为软中断底半部执行。驱动编程中,可以利用一组函数和数据结构来完成定时器触发工作或某些周期性任务
  • 定时器在linux内核中主要是采用一个数据结构来实现的,但是需要注意的是定时器是一个只运行一次的对象,即当一个定时器结束以后,还需要重新添加定时器。但是可以采用​​mod_timer()​​函数动态的改变定时器到达时间

4.1 原理


  • 这个驱动主要实现内核定时器的基本操作,内核定时器主要是通过下面结构体​​struct timer_list​​​实现,需要的头文件包括​​#include <linux/timer.h>​​​,但是实际开发过程中不需要包含该头文件,因为在​​sched.h​​中包含了该头文件

struct timer_list {
struct list_head entry;
unsigned long expires;

void (*function)(unsigned long);
unsigned long data;
}

  • 定时器的实现主要是该数据结构的填充和部分函数的配合即可完成。其中上述部分元素意义如下:



  • ​expires​​​:定义定时器到期时间,通常采用​​jiffies​​​和​​HZ​​​这个两个全局变量来配合设置该参数的值,如​​expires = jiffies + n * HZ​​​,其中​​jiffies​​​是自启动以来的滴答数,​​HZ​​​是一秒钟的滴答数。假设系统1秒的滴答数为1,我们希望定时10秒钟,而且我们在启动定时器的时候​​jiffies​​​数值为5,那么10秒后滴答总数将为​​5 + 10 * 1 = 15​
  • ​function​​:即一个函数指针,就是定时器处理函数,当定时时间到达之后就会触发该函数的执行
  • ​data​​​:通常实现参数的传递,从​​function​​​的参数类型值我们可以看到,​​data​​可以作为定时器处理函数的参数,其他的元素可通过内核的函数来初始化


4.2 定时器API

/* 定时器初始化 */
init_timer(struct timer_list *timer);
/* 定时器初始化 */
#define DEFINE_TIMER(_name, _function, _expires, _data) struct timer_list_name = TIMER_INITIALIZER(_function, _expires, _data)

/* 添加定时器到内核 */
void add_timer(struct timer_list *timer)
{
BUG_ON(timer_pending(timer));
mod_timer(timer, timer->expires;
}

/* 删除定时器,如果定时器的定时时间还没有到达,那么才可以删除定时器 */
int del_timer(struct timer_list *timer);

/* 修改定时器到达时间,该函数特点是:不管定时器是否到达定时时间,都会重新添加一个定时器到内核,所以可以在定时器出路函数中调用该函数去修改新的定时时间 */
int mode_timer(struct timer_list *timer, unsigned long expires);

4.3 定时器示例(老版本内核)

#include <linux/module.h>
#include <linux/init.h>
#include <linux/timer.h>

struct timer_list g_timer;

void timer_func(unsigned long data)
{
printk(KERN_INFO "%s->current jiffies = %ld\n", __func__, jiffies);
mod_timer(&g_timer, jiffies + HZ);
}

static int __init timer_init(void)
{
/* init timer old kernel can use init_timer(), but new kernel can not use this function */
init_timer(&g_timer);
// DEFINE_TIMER(g_timer, timer_func);
/* set timer parameters */
printk(KERN_INFO "%s->current jiffies = %ld\n", __func__, jiffies);
g_timer.expires = jiffies + 2 * HZ;
g_timer.function = timer_func;
/* add timer */
add_timer(&g_timer);
return 0;
}

static void __exit timer_exit(void)
{
del_timer(&g_timer);
}

module_init(timer_init);
module_exit(timer_exit);
MODULE_LICENSE("GPL");