让我门看看内核代码是如何对时间为题进行处理的,并按由简到难的顺序逐步讨论,包括:①如何衡量时间差,②如何获得当前时间,③如何将操作延迟一段时间,④如何调度异步函数到指定的时间之后执行。
7.1 测量时间差
内核通过定时中断来度量时间流动,中断在第10章详述。
硬件定时器 → 中断 → HZ → jiffies → 驱动用 jiffies 做延时/超时
硬件定时器:启动时内核根据HZ编程确定。
每发生一次时钟中断,内核内部计数器加一(该值在系统引导时初始化为0,因此该值就是自上次操作系统引导以来的时钟滴答数,被称为 jiffies_64, jiffies 可能是其底32位)(jiffiy机制是内核管理的低分辨率计时机制,某些平台还具备软件可读高分辨率计数器)
7.1.1. jiffies 计数器 (ms级精度、全局、可移植)
所有架构都有 jiffies,可移植性高。作为Linux程序员,你需要了解如何使用jiffies进行计时(比如计算1s后的jiffies的值,然后缓存起来),这包括:
-
原理:
jiffies的本质是个全局变量。记录系统启动以来经过了多少个"时钟滴答"(Tick)。- HZ 是内核配置的时钟中断频率 ,单位是 Hz(每秒中断次数) 。定义在
./arch/arm64/include/uapi/asm/param.h或其他文件。 jiffies与HZ的关系:HZ 是"每秒多少 jiffies"。HZ=100 → 1 jiffy = 10 毫秒;HZ=1000 → 1 jiffy = 1 毫秒(可以在编译kernel时在系统配置界面设置比如CONFIG_HZ=250。这个值越大,则频率越快,则CPU需要处理的中断数越多,但现在的CPU性能强了不在乎这点儿)
-
精度:较低。最小分辨率通常是 1ms ~ 10ms。无法测量微秒(us)甚至纳秒(ns)级别的代码执行时间。
-
用途:用于超时控制、延时较长的等待、统计运行时间等。
-
头文件 :
<linux/sched.h>
jiffies_64和jiffies其实是同一个东西,jiffies_64用于64位系统,而jiffies用于32位系统。为了兼容不同的硬件,jiffies其实就是jiffies_64的低32位。不管是32位还是64位的jiffies,都有溢出的风险,溢出以后会重新从0开始计数,相当于绕回来了,因此有些资料也将这个现象也叫做绕回 。假如HZ为最大值1000的时候,32位的jiffies只需要49.7天就发生了绕回,对于64位的jiffies_64来说大概需要5.8亿年才能绕回,因此jiffies_64的绕回忽略不计。处理32位jiffies的绕回显得尤为重要,Linux内核提供了如表30.1.1.1所示的几个API函数来处理绕回,防止手动对比时因绕回导致的错误:
c
# 位于include/linux/jiffies.h 提供了如下宏定义
# unkown 通常为 jiffies,known 通常是需要对比的值。
int time_after(unsigned log unkown, unsigned long known); # unkown 超过 known的话返回真
int time_before(unsigned log unkown, unsigned long known); # unkown 没超过 known的话返回真
int time_after_eq(unsigned log unkown, unsigned long known);# unkown 超过或等于 known的话返回真
int time_before_eq(unsigned log unkown, unsigned long known);# unkown 没超过或等于 known的话返回真
使用jiffies判断超时的小例子:
c
unsigned long timeout;
timeout = jiffies + (2 * HZ); /* 超时的时间点 */
/*************************************
要被度量时间的代码逻辑
************************************/
/* 判断有没有超时 */
if(time_before(jiffies, timeout)) {
/* 超时未发生 */
} else {
/* 超时发生 */
}
为何在32位处理器上直接读取 jiffies_64 的值不可靠?
因为在32位机器上,对 jiffies_64 的读取分两次进行,这期间可能发生更新从而获取错误的值。应该使用内核提供的 u64 get_jiffies_64(void)辅助函数,该函数完成了适当的锁定。
-
为了方便开发,Linux内核提供了几个**
jiffies和 ms、us、ns 之间的转换函数**:c#include <linux/jiffies.h> //头文件 // 将毫秒、微秒、纳秒转换为 jiffies 类型。 unsigned long msecs_to_jiffies(const unsigned int m) unsigned long usecs_to_jiffies(const unsigned int u) unsigned long nsecs_to_jiffies(u64 n) // 将 jiffies 类型的参数 j 分别转换为对应的毫秒、微秒、纳秒。 int jiffies_to_msecs(const unsigned long j) int jiffies_to_usecs(const unsigned long j) u64 jiffies_to_nsecs(const unsigned long j) -
内核提供了哪些函数用于完成
jiffies与用户空间时间表述法(struct timespec)之间的转换?c#include <linux/jiffies.h> // 用户时间 → jiffies: unsigned long timespec64_to_jiffies(const struct timespec64 *value) // jiffies → 用户时间: jiffies_to_timespec64(const unsigned long jiffies, struct timespec64 *value);
7.1.2. 处理器特定的寄存器(ns/us级精度)
如果需要度量非常短的时间,或是需要极高的时间精度,就可以使用高精度时钟了(由于 CPU 内部的不确定性(缓存等)导致执行时间在ns/us级剧烈波动,而 jiffies 的粒度是ms级,它完全无法捕捉到这些波动。*)。
现在处理器中由于缓存,指令调度,分支预测等技术的应用,在大部分的cpu设计中,指令时序本质上是不可预测的,这样依赖于指令周期的经验型性能描述方法就不再使用,为了解决这一问题,CPU制造商引入了一种通过计算时钟周期来度量时间差的简便而可靠的方法,绝大多数现在处理器都包含一个随时钟周期不断递增的计数寄存器。这个时钟计数器是完成高分辨率计时任务的唯一可靠路径。不同平台对该寄存器的实现不同,ARMv8 架构有一个标准的 通用计时器 (Generic Timer / Architectural Timer) 。(x86 叫 TSC, ARM 叫 Generic Timer, 代码不通用)。自 ARMv7 晚期和所有 ARMv8 (Aarch64, 包括 Cortex-A55) 起,这是强制要求实现的硬件特性。(在IMX93RM手册25.2.6和LX2160A手册Chapter 7以及ARM官方文档的'Generic Timer.pdf'文件中都有介绍)
原理:这个时钟是平台无关的,他的来源是外部晶振。(但是可能可以通过中间的PLL分频器调整,但跟CPU工作频率无关)
举例:阅读imx93RM - 25.2.6 Timestamp Generation 一节,即可发现一个 System Counter 的计数器为所有的核心提供时钟。64.1.3 Global System Counter (SCTR) 节指出该时钟通过总线连接Cortex-A55 MPCore generic timers。多时钟域同步。(这个imx93的 System Counter 外接24M物理时钟,正常模式可以认为频率与CPU频率无关)。看lx0146RM也会发现系统时钟来源于一个外部100M时钟。
不要直接写汇编去读寄存器,用内核封装好的宏/函数(虽然平台不同,但内核还是贴心的统一了接口)来获取单调时间:
-
get_cycles()-- 头文件 :
<linux/timex.h> - 功能 : 返回当前的时钟周期数 (
cycles_t)。 - 对应关系 :
- x86: 编译为
rdtsc指令。 - ARM: 编译为读取
CNTVCT_EL0寄存器。
- x86: 编译为
- 注意: 返回的是"周期数",不是"纳秒"。你需要知道频率才能换算。某些古老架构如果不支持可能返回 0。
- 头文件 :
-
ktime_get()(首选) (待确认,可能是错误的)-
头文件 :
<linux/ktime.h> -
功能 : 返回
ktime_t类型,内部已经是纳秒 (ns) 单位。 -
优势: 自动处理了周期到时间的换算,且自动选择高精度时钟源(hrtimer)。
-
用法:
cktime_t start = ktime_get(); // ... code ... s64 ns = ktime_us_delta(ktime_get(), start); // 直接得到微秒差
-
用户态 代码有没有使用到这个计数器的接口?有!而且非常常用。
Linux 提供了多种机制让用户态安全地读取这些寄存器,而无需陷入内核(System Call),在用户态需要高性能计时时用(比如游戏引擎,高频交易,工业协议同步如EtherCAT),用户态调用方法:
clock_gettime() (标准 POSIX 接口------最通用)
c
#include <time.h>
struct timespec ts;
// CLOCK_MONOTONIC_RAW 通常直接映射到硬件计数器,不做 NTP 修正,速度最快
clock_gettime(CLOCK_MONOTONIC_RAW, &ts);
| 时钟 ID | 特性 |
|---|---|
CLOCK_REALTIME |
系统真实时间(可被 NTP/settimeofday 修改),类似 gettimeofday |
CLOCK_MONOTONIC |
单调递增时间 (不受系统时间调整影响),适合测量间隔 (测时间间隔一律用这个) |
CLOCK_MONOTONIC_RAW |
更原始的单调时间(跳过 NTP 频率调整) |
CLOCK_BOOTTIME |
包含系统 suspend 时间的单调时间(从开机算起) |
7.2 获取当前时间(内核很少用)
本节介绍内核中获取时间的不同方式及其适用场景。驱动开发中,90% 场景应使用"单调时间"。只有极少数需要与外部世界同步的场景才用"真实时间"。
-
jiffies:测量时间间隔的首选,看上文。
-
避免在驱动中处理"墙上时间"(Wall-clock Time) ,墙上时间指的是年月日时分格式的时间。
mktime已经废弃。如果非要获取:(未验证)#include <linux/timekeeping.h> // 方式1:填充 struct timespec64(推荐) void ktime_get_real_ts64(struct timespec64 *ts); // 方式2:直接返回 ktime_t(真实时间) ktime_t ktime_get_real(void); // 方式3:直接返回纳秒数(u64) u64 ktime_get_real_ns(void); -
❌ 不鼓励直接读取
xtime变量,因为很难原子地访问timeval变量的两个成员。 -
已经废弃:
do_gettimeofday,current_kernel_time
7.3 延迟执行
7.3.1. 长延时
长于一个滴答时钟的延时。(先这样粗略的区分)
7.3.1.1. 忙等待(过时)
"LDD3 中的 /proc/jitbusy 是一个教学用的反面示例,用于展示忙等待的危害。该接口并非内核标准功能,需手动编译示例模块才存在。在现代驱动开发中,应绝对避免此类实现,而使用 msleep() 或高精度定时器等可调度、低功耗的延时机制。
7.3.1.2. 让出处理器
因为忙等待使得CPU负担大,如何优雅地处理延时(Delay)以避免浪费CPU资源呢。先引出不推荐的做法:
- 可以通过在循环中直接调用
schedule()进行延时 ,该函数在<linux/sched.h>中声明。在这种方法时,如果系统中没有其他进程需要运行,调度器会立即再次选中该进程,导致它实际上仍在空转(自旋),无法让真正的"空闲任务"(Idle Task,进程号0)运行。无法让CPU进入低功耗模式,这是种伪休眠。现代内核开发中,绝对禁止 在驱动中使用while(time_before(...)) { schedule(); }这种模式来做延时。如果调度器在忙其他的,这个进程还不会按照期望的时间得到调度。
现代内核中正确的延时和等待机制:
-
短时间延时 (原子上下文/中断上下文中使用)
如果需要在极短时间内(微秒级)等待,且不能睡眠:
udelay(unsigned long usecs):忙等待微秒。适用于非常短的延时(通常<1ms),此时忙等待的开销是可以接受的。ndelay(unsigned long nsecs):纳秒级忙等待。
-
较长时间延时 (可睡眠上下文中使用)
如果需要毫秒级或更长的延时,且当前上下文允许睡眠(即不在中断处理程序或持有自旋锁时):
-
msleep(unsigned int msecs):最推荐 。让当前进程进入TASK_UNINTERRUPTIBLE状态睡眠指定毫秒数。这是替代文中schedule()循环的正确做法。 -
msleep_interruptible(unsigned int msecs):类似msleep,但可以被信号唤醒(TASK_INTERRUPTIBLE)。适用于用户空间触发的操作,允许用户在等待时通过 Ctrl+C 终止。 -
usleep_range(unsigned long min, unsigned long max):高精度推荐 。用于微秒级的睡眠(通常10us - 20ms)。它允许内核根据系统负载和定时器精度,在最小和最大值之间选择一个最优的唤醒时间,比固定的udelay更节能,比msleep更精准。
-
-
等待特定事件 (替代轮询)
如果是在等待硬件状态变化(而不是单纯的时间流逝):等待队列 (Wait Queues):使用
wait_event(),wait_event_interruptible(),wait_event_timeout()等宏。进程主动进入睡眠,直到硬件中断或其他进程调用wake_up()唤醒它。参见【6.2.2 简单休眠 - 等待队列】小节。wait_event_timeout:基于等待队列的超时。适用于驱动既在等待某个硬件事件(通过wake_up唤醒),又需要设置一个保底时间的场景。如果超时时间到,函数返回0;如果被提前唤醒,返回剩余的jiffies时间。schedule_timeout:基于调度器的超时。适用于单纯的"延时"场景,不需要等待特定的队列事件。原理是将当前进程状态改为TASK_INTERRUPTIBLE或TASK_UNINTERRUPTIBLE,然后调用schedule_timeout进入休眠,直到时间流逝。
7.3.1.3. 超时
这段文字的核心在于教导驱动开发者:不要使用"忙等待"(空循环)来实现延时,而应该让出CPU,使用内核提供的调度机制来实现"休眠"。
7.3.2. 短延时
短时间延时 (原子上下文/中断上下文中使用)
如果需要在极短时间内(微秒级)等待,且不能睡眠:#include <linux/delay.h>(具体跟体系架构有关)
mdelay(unsigned long nsecs):纳秒级忙等待。udelay(unsigned long usecs):忙等待微秒。适用于非常短的延时(通常<1ms),此时忙等待的开销是可以接受的。ndelay(unsigned long nsecs):纳秒级忙等待。
注意这些等待操作都是盲等。且函数的输入值有限制,要根据你要延时的长度选择合适的函数。
7.4 内核定时器
内核定时器用于调度未来特定时间执行函数,无需阻塞当前进程,适用于硬件轮询资源回收等场景,其核心特征是异步执行,定时器函数运行于中断上下文 ,而非原进程上下文,因此原进程可能已经睡眠、切换CPU或退出。
由于缺乏进程上下文 ,定时器函数必须遵守原子操作规则:严禁访问用户空间,不能用current指针(指向当前正在CPU上运行的进程,方便获取进程信息)、严禁睡眠或调度(如不可调用schedule或kmalloc(...,GFP_KERNEL)分配内存),开发者可通过in_interrupt()和in_atomic()判断当前上下文状态。
定时器与其他代码异步运行,容易引发竞态条件,因此访问共享数据时需要使用原子变量或自旋锁严格保护,确保系统稳定。
!IMPORTANT
这个内核定时器与前几节的
jiffies计数器有什么关系?jiffies相关的函数足够完成定时一段时间的任务了为什么还要设计这个定时器链表?答:
明确两者关系:
jiffies是时间的"标尺",内核定时器是基于这把标尺构建的"闹钟"机制。
jiffies:是一个全局变量,随硬件时钟中断(Tick)每秒增加HZ次。它只负责记录"现在是什么时间"。- 内核定时器 :是一个数据结构(
timer list),它记录了"未来某个jiffies值时需要执行什么函数"。- 两者协作方式 :每次发生时钟中断后,内核不仅更新
jiffies,还会去检查内核定时器链表,看是否有定时器到期(如果有就执行回调函数)。- 既然
jiffies可用,为何还需内核定时器链表? :
- 避免忙等,异步执行,释放CPU资源。
- 定时器链表的设计解决了多任务并发时检查进程是否到时的复杂度。
7.4.1. 定时器 API
旧的内核教程常使用 init_timer + function/data 字段的方式,这在现代内核中已被标记为过时(Deprecated)。
推荐的标准流程(现代内核):
-
第一步:定义定时器结构
在你的驱动结构体中嵌入
struct timer_list:cinclude <linux/timer.h> struct my_device { struct timer_list my_timer; int data_value; // ... 其他成员 }; -
第二步:编写回调函数
注意函数原型的变化。现代内核不再通过
data参数传递数据,而是直接传入timer_list指针,你需要用from_timer宏反推容器结构体。c// 回调函数原型固定为:void func(struct timer_list *t) void my_timer_callback(struct timer_list *t) { // 关键:从 timer_list 指针反推包含它的结构体指针 struct my_device *dev = from_timer(dev, t, my_timer); // 执行业务逻辑 (例如:读取硬件状态) pr_info("Timer expired! Data value is: %d\n", dev->data_value); // 【可选】若需要周期执行回调,需在此重新添加定时器 // mod_timer(&dev->my_timer, jiffies + msecs_to_jiffies(1000)); } -
第三步:初始化与启动
在驱动初始化或打开设备时设置定时器:
cstruct my_device *dev = kmalloc(sizeof(*dev), GFP_KERNEL); // 1. 初始化定时器 (关联回调函数) // 参数:1定时器指针 ,2回调函数名 ,3标志位 (通常为 0) timer_setup(&dev->my_timer, my_timer_callback, 0); // 2. 启动定时器 - 把你的 struct timer_list 节点插入到全局的定时器链表(定时器轮,Timer Wheels)中 // 参数:定时器指针,到期时间 (基于 jiffies) // 例如:1 秒后执行 (假设 HZ=1000) mod_timer(&dev->my_timer, jiffies + msecs_to_jiffies(1000)); -
第四步:删除定时器
在驱动卸载或设备关闭时,必须清理定时器,防止回调函数在模块卸载后仍然被调用(导致内核崩溃)。
del_timer_sync函数是del_timer函数的同步版,会等待其他处理器使用完定时器再删除,del_timer_sync不能使用在中断上下文中。// 同步删除:确保如果回调正在运行,会等待其执行完毕再返回 del_timer_sync(&dev->my_timer);
最小化可运行代码示例:
c
#include <linux/module.h>
#include <linux/init.h>
#include <linux/timer.h>
#include <linux/jiffies.h>
static struct timer_list example_timer;
static int count = 0;
// 回调函数
static void timer_func(struct timer_list *t)
{
count++;
pr_info("Timer fired! Count: %d\n", count);
if (count < 5) {
// 重新调度:1 秒后再次触发
mod_timer(t, jiffies + msecs_to_jiffies(1000));
} else {
pr_info("Timer stopped.\n");
}
}
static int __timer_init(void)
{
pr_info("Initializing timer...\n");
// 1. 设置定时器
timer_setup(&example_timer, timer_func, 0);
// 2. 启动定时器 (立即开始,1 秒后第一次触发)
mod_timer(&example_timer, jiffies + msecs_to_jiffies(1000));
return 0;
}
static void __timer_exit(void)
{
pr_info("Cleaning up timer...\n");
// 3. 安全删除
del_timer_sync(&example_timer);
}
module_init(__timer_init);
module_exit(__timer_exit);
MODULE_LICENSE("GPL");
!WARNING
由于定时器是异步运行的,它可能在你的主代码正在修改共享数据时突然触发。
- 问题:如果主代码和定时器同时修改同一个变量,会导致数据错乱。
- 解决 :必须使用**自旋锁(Spinlock)或 原子变量(Atomic Variables)**来保护共享数据。
- 切记 :在定时器回调(中断上下文)中,只能使用
spin_lock_irqsave/spin_unlock_irqrestore,绝对不能使用互斥锁(Mutex),因为 Mutex 可能会睡眠。
7.4.2. 内核定时器的实现
(看了,没总结)
7.5 tasklet 机制
参考学习链接:【中断】tasklet机制解析
中断管理(第十章会讲)大量使用该机制。在现代 Linux 内核(大约从 5.9 版本开始)中,Tasklet 已经被标记为废弃(Deprecated) 。不推荐在新代码中使用 :如果你现在开始写一个新的设备驱动,不应该再使用 Tasklet。替代方案 :官方推荐的替代方案是工作队列 (Workqueue) 或线程化中断 (Threaded IRQ)。
根本原因在于 Tasklet 运行在中断上下文,导致不能睡眠,调试困难。(工作队列运行在进程上下文,可以睡眠,功能更优)
但是,我们还是在这里引入中断处理的"两阶段"思想 (Top Half / Bottom Half):
- 上半部 (Top Half):硬件中断处理函数,唯一任务是快速响应硬件,比如读一个状态寄存器,清除中断标志,将书籍从硬件缓冲区拷贝到内存等。要求要快,因为在此期间,同类型的硬件中断通常会被屏蔽,处理慢了会影响系统实时性。
- 下半部**(Bottom Half):即 Tasklet 这类机制。它负责处理那些可以延迟、但必须由中断触发**的"繁重"工作。比如对接收到的网络数据包进行协议解析、唤醒等待数据的进程等。
类比:你正在洗衣服(CPU主任务),此时门铃响了(硬件中断),你需要立即去门口发现是快递包裹(上半部快速响应),你把包裹放在一边想着衣服洗完了再拆吧(下半部),然后继续洗衣服去了。
Tasklet 就是实现"下半部"的一种轻量级机制。Tasklet 的核心特性有:基于软中断 (Softirq)。原子性执行。
一个 tasklet 的完整生命周期:
创建与初始化:
DECLARE_TASKLET(name, func, data): 静态声明并初始化一个 tasklet。tasklet_init(): 动态初始化一个 tasklet。- 上面这两个都是用于建立一个
tasklet_struct结构体,并告诉内核,当这个tasklet被执行时,请调用funx函数,并把data传给它。
调度 (Schedule):
tasklet_schedule(&my_tasklet): 这是触发下半部工作的"开关"。通常在上半部中断处理函数的末尾调用它。作用是告诉内核:"嘿,有个延迟任务要处理,请在合适的时候执行它。"tasklet_hi_schedule(): 高优先级版本,会比普通 tasklet 更早被执行。
禁用与使能 (Disable / Enable)
tasklet_disable(&my_tasklet): 临时禁止一个 tasklet 执行。即使被调度了,它也不会运行。这通常用于保护临界区或模块卸载时的同步。tasklet_enable(&my_tasklet): 解除禁止。如果之前被调度过,它会很快被执行。
销毁 (Kill)
tasklet_kill(&my_tasklet): 在模块卸载或设备关闭时调用。它会等待这个 tasklet 如果正在运行则运行完毕,并确保它不会再被调度。这是资源清理的关键一步。
7.6 工作队列
一篇很牛逼的博文供参考:workqueue(linux kernel 工作队列)知乎作者:大雨
#include <linux/workqueue.h> struct demo_type { char *name; struct work_struct wk; //一份工作 }; static void demo_work(struct work_struct *work) { struct demo_type *dm = container_of(work, struct demo_type, wk); printk(KERN_INFO "demo work begin\n"); //用于调试验证 msleep(1000); printk(KERN_INFO "demo's name: %s\n", dm->name); //用于调试验证 } void demo_init(void) { struct demo_type *dm = NULL; //申请demo结构体内存(为简洁省略判空) dm = kzalloc(sizeof(*struct demo_type), GFP_KERNEL); dm->name = "Demo"; //初始化一个工作(把工作函数demo_work 绑定到工作变量wk上) INIT_WORK(&dm->wk, demo_work); //在需要的地方激活一次工作(demo_work会被调用一次) schedule_work(&dm->wk); //使用系统共用的工作队列 printk(KERN_INFO "demo work wake\n"); //用于调试验证 }
工作队列类似于tasklet,允许我们把工作推迟执行,或者是把高耗时任务放到另一个线程执行。用于中断下半部:及在中断函数中不能运行太耗时的工作,在中断中只做一个"触发函数运行的动作",然后退出中断。
tasklet在软中断上下文运行,因此所有tasklet代码都必须是原子的。而工作队列在特殊内核进程上下文运行,因此灵活性更好。工作队列可以休眠。- 多核处理器(SMP)环境下Tasklet 对 CPU 的"忠诚"是强制的、底层的;而工作队列对 CPU 的"忠诚"是可选的、应用层的。
- 内核代码可以请求工作队列函数的执行延迟给定的时间间隔。
结论:工作队列的诞生,就是为了解决 Tasklet 不能睡觉、不能处理复杂任务的痛点。
!CAUTION
工作队列与等待队列什么关系?
工作队列:用来干活的。当中断发生时,如何处理中断下半部的问题。把中断下半部交给内核线程执行,是一种异步执行机制。
等待队列:用来睡觉的。进程同步机制,让自己挂起,直到等到某个条件满足被唤醒。
怎么使用工作队列?
在书本中的关于创建工作队列,填充,初始化,将工作提交到工作队列,取消工作队列等等操作。但是随着Linux kernel的发展,不需要一个线程对应一个工作队列了。而现在Linux 引入了 并发管理工作队列 (CMWQ全称"Concurrency Managed Workqueue"),内核维护了一个全局的工作队列,你只需要用系统默认的 system_wq,直接调用 schedule_work(&my_work) 即可,一般不需要自己创建队列(但是要直到包括①使用系统队列,②使用私有队列,③使用延时运行队列等API)。(工作队列缺点:多个工作挤在某个内核线程中依次序执行,前面的函数如果执行得很慢,就会影响到后面的函数。)
使用系统队列最简单(包含linux/workqueue.h文件):
c
#include <linux/workqueue.h>
struct demo_type {
char *name;
struct work_struct wk; //一份工作
};
static void demo_work(struct work_struct *work)
{
struct demo_type *dm = container_of(work, struct demo_type, wk);
printk(KERN_INFO "demo work begin\n"); //用于调试验证
msleep(1000);
printk(KERN_INFO "demo's name: %s\n", dm->name); //用于调试验证
}
void demo_init(void)
{
struct demo_type *dm = NULL;
//申请demo结构体内存(为简洁省略判空)
dm = kzalloc(sizeof(*struct demo_type), GFP_KERNEL);
dm->name = "Demo";
//初始化一个工作(把工作函数demo_work 绑定到工作变量wk上)
INIT_WORK(&dm->wk, demo_work);
//在需要的地方激活一次工作(demo_work会被调用一次)
schedule_work(&dm->wk); //使用共享工作队列(我们在和其他人共享该工作队列)
printk(KERN_INFO "demo work wake\n"); //用于调试验证
}
下面几个函数用到时再详细了解(参见linux/workqueue.h文件):
| 全局workqueue,sysem_wq | 指定cpu | 指定workqueue | 指定workqueue、cpu | |
|---|---|---|---|---|
| 无延迟 | schedule_work | schedule_work_on | queue_work | queue_work_on |
| 延迟 | schedule_delayed_work | schedule_delayedwork_on | queue_delayed_work | queue_delayed_work_on |
7.6.1 共享队列
就是共享工作队列,上面一小节讲完了。