Linux 驱动程序笔记4——内核定时器struct timer_list

时间:2021-04-14 23:28:14

内核中最终的计时资源是定时器。定时器用于定时器超时处理程序在未来某个特定时间点执行,或者周期性的轮询硬件的状态。Linux提供了内核定时器完成这类工作。
    定时器的只需要执行一些初始化的操作,如:设置一个超时时间,指定超时要调用的函数,然后激活定时器就可以了。它的处理和工作队列还是有点类似的。和任务队列一样,内核定时器并不是周期运行,它在超时后自动销毁。因此,如果要实现周期轮询,就需要在定时器执行函数返回前再次激活定时器。

    一般来说,定时器都在超时后马上就会执行超时处理函数,但是也有可能被推迟到下一个时钟节拍时才能执行,所以不能用定时器来实现任何硬实时任务。

    内核定时器用于控制某个函数(定时器处理函数)在未来的某个特定的时间执行。内核定时器注册的处理函数只执行一次,不循环执行
    内核定时器被组织成双向链表,并使用struct timer_list结构描述
struct timer_list{
 struct list_head entry; //内核使用
 unsigned long exoires; //设定的超时的值
 void(*function)(unsigned long); //超时处理函数
 unsigned long data; //超时处理函数参数
 struct tvec_base *base; //内核使用
}


下面看看一个实现轮询操作的小例子:


struct timer_list polling_timer;

init_timer(&polling_timer);
polling_timer.data = (unsigned long)something;
polling_timer.function = polling_handler;
polling_timer.expires = jiffies + 2 * HZ;
add_timer(&polling_timer);

void polling_handler(unsigned long data) {
...
polling_timer.expires = jiffies + 2 * HZ;
add_timer(&polling_timer);

}

    jiffies是Linux内核中的一个全局变量,用来记录自系统启动以来产生的节拍的总数。启动时,内核将该变量初始化为0,此后,每次时钟中断处理程序都会增加该变量的值。因为一秒内时钟中断的次数等于HZ,所以jiffies一秒内增加的值也就为HZ。系统运行时间以秒为单位计算,就等于jiffies/HZ。


    节拍率HZ是通过静态预处理定义的,在系统启动时按照HZ值对硬件进行设置。体系结构不同,HZ值就不同,在i386体系结构在include/asm-i386/param.h中定义如下:
#define HZ 1000

每秒钟时钟中断1000次,也就是说,在1秒里jiffies会被增加1000。因此jiffies + 2 * HZ表示推后2秒钟。

 

void init_timer(struct timer_list *timer);

该函数用来初始化定时器结构,其实它只将prev和next指针清零;

 

void add_timer(struct timer_list *timer);

该函数将定时器插入活动定时器的全局队列中,即激活定时器;

 

void mod_timer(&polling_timer, jiffies + new_delay);

需要更改已经激活的定时器的超时时间,则调用上面函数;

del_timer(&polling_timer);

如果需要在定时器超时前将定时器从链表中删除,则调用上面函数。注意:定时器超时后,系统会自动删除定时器,则不用调用上面函数。


多处理器的情况下使用:del_timer_sync(&polling_timer);和del_timer函数类似,不同的是该函数确保在它返回时,定时器函数不在任何CPU上运行,大多数情况下使用该函数。再次注意:不需要为已经超时的定时器调用该函数,因为系统会自动删除。在已经从链表删除的定时器上调用del_timer()或del_timer_sync()没什么害处,所以说从定时器函数中删除定时器是一种很好的习惯。


    内核定时器是在时钟中断发生后,作为软中断在下半部的上下文中执行的。所有的定时器结构都以链表的形式存储。时钟中断发生后,内核按链表顺序依次执行。因为内核定时器发生在软中断中,因此,定时器执行函数不能够睡眠,也不能够持有信号量。如果对硬件的访问需要使用信号量同步,或者可能睡眠(比如需要调用kmalloc内存分配,但是由于某种原因不能使用GFP_ATOMIC标志),就不能直接通过定时器来实现了。一个变通的做法是在内核定时器执行函数里调用工作队列,在工作队列处理函数中实现对硬件的访问。

 

 

----------------------------------------------------------------------------------------------

无论何时你需要调度一个动作以后发生, 而不阻塞当前进程直到到时, 内核定时器是给你的工具.这些定时器用来调度一个函数在将来一个特定的时间执行, 基于时钟嘀哒, 并且可用作各类任务;例如, 当硬件无法发出中断时, 查询一个设备通过在定期的间隔内检查它的状态. 其他的内核定时器的典型应用是关闭软驱马达或者结束另一个长期终止的操作. 在这种情况下, 延后来自 close 的返回将强加一个不必要(并且吓人的)开销在应用程序上. 最后, 内核自身使用定时器在几个情况下, 包括实现 schedule_timeout.
    一个内核定时器是一个数据结构, 它指导内核执行一个用户定义的函数使用一个用户定义的参数在一个用户定义的时间. 这个实现位于 <linux/timer.h> 和 kernel/timer.c 并且在"内核定时器"一节中详细介绍.
被调度运行的函数几乎确定不会在注册它们的进程在运行时运行. 它们是, 相反, 异步运行. 直到现在, 我们在我们的例子驱动中已经做的任何事情已经在执行系统调用的进程上下文中运行. 当一个定时器运行时, 但是, 这个调度进程可能睡眠, 可能在不同的一个处理器上运行, 或者很可能已经一起退出.
    这个异步执行类似当发生一个硬件中断时所发生的( 这在第 10 章详细讨论 ). 实际上, 内核定时器被作为一个"软件中断"的结果而实现. 当在这种原子上下文运行时, 你的代码易受到多个限制. 定时器函数必须是原子的以所有的我们在第 1 章"自旋锁和原子上下文"一节中曾讨论过的方式, 但是有几个附加的问题由于缺少一个进程上下文而引起的. 我们将介绍这些限制; 在后续章节的几个地方将再次看到它们. 循环被调用因为原子上下文的规则必须认真遵守, 否则系统会发现自己陷入大麻烦中.
为能够被执行, 多个动作需要进程上下文. 当你在进程上下文之外(即, 在中断上下文), 你必须遵守下列规则:
● 没有允许存取用户空间. 因为没有进程上下文, 没有和任何特定进程相关联的到用户空间的途径.
● 这个 current 指针在原子态没有意义, 并且不能使用因为相关的代码没有和已被中断的进程的联系.
● 不能进行睡眠或者调度. 原子代码不能调用 schedule 或者某种 wait_event, 也不能调用任何其他可能睡眠的函数. 例如, 调用 kmalloc(..., GFP_KERNEL) 是违犯规则的. 旗标也必须不能使用因为它们可能睡眠.内核代码能够告知是否它在中断上下文中运行, 通过调用函数 in_interrupt(), 它不要参数并且如果处理器当前在中断上下文运行就返回非零, 要么硬件中断要么软件中断.一个和 in_interrupt() 相关的函数是 in_atomic(). 它的返回值是非零无论何时调度被禁止; 这包含硬件和软件中断上下文以及任何持有自旋锁的时候. 在后一种情况, current 可能是有效的, 但是存取用户空间被禁止, 因为它能导致调度发生. 无论何时你使用 in_interrupt(), 你应当真正考虑是否in_atomic 是你实际想要的. 2 个函数都在 <asm/hardirq.h> 中声明.
内核定时器的另一个重要特性是一个任务可以注册它本身在后面时间重新运行. 这是可能的, 因为每个 timer_list 结构在运行前从激活的定时器链表中去连接, 并且因此能够马上在其他地方被重新连接. 尽管反复重新调度相同的任务可能表现为一个无意义的操作, 有时它是有用的. 例如, 它可用作实现对设备的查询.
也值得了解在一个 SMP 系统, 定时器函数被注册时相同的 CPU 来执行, 为在任何可能的时候获得更好的缓存局部特性. 因此, 一个重新注册它自己的定时器一直运行在同一个 CPU.
    不应当被忘记的定时器的一个重要特性是, 它们是一个潜在的竞争条件的源, 即便在一个单处理器系统. 这是它们与其他代码异步运行的一个直接结果. 因此, 任何被定时器函数存取的数据结构应当保护避免并发存取, 要么通过原子类型( 在第 1 章的"原子变量"一节) 要么使用自旋锁( 在第 9 章讨论 ).
7.4.1. 定时器 API
内核提供给驱动许多函数来声明, 注册, 以及去除内核定时器. 下列的引用展示了基本的代码块:
#include <linux/timer.h>
struct timer_list
{
    unsigned long expires;
    void (*function)(unsigned long);
    unsigned long data;
};
void init_timer(struct timer_list *timer);
struct timer_list TIMER_INITIALIZER(_function, _expires, _data);
void add_timer(struct timer_list * timer);
int del_timer(struct timer_list * timer);
这个数据结构包含比曾展示过的更多的字段, 但是这 3 个是打算从定时器代码自身以外被存取的.
这个 expires 字段表示定时器期望运行的 jiffies 值; 在那个时间, 这个 function 函数被调用使用 data 作为一个参数. 如果你需要在参数中传递多项, 你可以捆绑它们作为一个单个数据结构并且传递一个转换为 unsiged long 的指针, 在所有支持的体系上的一个安全做法并且在内存管理中相当普遍( 如同 15 章中讨论的 ). expires 值不是一个 jiffies_64 项因为定时器不被期望在将来很久到时, 并且 64-位操作在 32-位平台上慢.
    expire这个结构必须在使用前初始化. 这个步骤保证所有的成员被正确建立, 包括那些对调用者不透明的. 初始化可以通过调用 init_timer 或者 安排 TIMER_INITIALIZER 给一个静态结构, 根据你的需要. 在初始化后, 你可以改变 3 个公共成员在调用 add_timer 前. 为在到时前禁止一个已注册的定时器, 调用 del_timer.
jit 模块包括一个例子文件, /proc/jitimer ( 为 "just in timer"), 它返回一个头文件行以及 6 个数据行. 这些数据行表示当前代码运行的环境; 第一个由读文件操作产生并且其他的由定时器. 下列的输出在编译内核时被记录:
phon% cat /proc/jitimer
time delta inirq pid cpu command
33565837 0 0 1269 0 cat
33565847 10 1 1271 0 sh
33565857 10 1 1273 0 cpp0
33565867 10 1 1273 0 cpp0
33565877 10 1 1274 0 cc1
33565887 10 1 1274 0 cc1
    在这个输出, time 字段是代码运行时的 jiffies 值, delta 是自前一行的 jiffies 改变值, inirq 是由
in_interrupt 返回的布尔值, pid 和 command 指的是当前进程, 以及 cpu 是在使用的 CPU 的数目( 在单处理器系统上一直为 0).
    如果你读 /proc/jitimer 当系统无负载时, 你会发现定时器的上下文是进程 0, 空闲任务, 它被称为"对换进程"只要由于历史原因.
    用来产生 /proc/jitimer 数据的定时器是缺省每 10 jiffies 运行一次, 但是你可以在加载模块时改变这个值通过设置 tdelay ( timer delay ) 参数.
    下面的代码引用展示了 jit 关于 jitimer 定时器的部分. 当一个进程试图读取我们的文件, 我们建立这个定时器如下:
unsigned long j = jiffies;
data->prevjiffies = j;
data->buf = buf2;
data->loops = JIT_ASYNC_LOOPS;
data->timer.data = (unsigned long)data;
data->timer.function = jit_timer_fn;
data->timer.expires = j + tdelay;
add_timer(&data->timer);

wait_event_interruptible(data->wait, !data->loops);
The actual timer function looks like this:
void jit_timer_fn(unsigned long arg)
{
    struct jit_data *data = (struct jit_data *)arg;
    unsigned long j = jiffies;
    data->buf += sprintf(data->buf, "%9li %3li %i %6i %i %s\n",j, j - data->prevjiffies, in_interrupt() ? 1 : 0,current->pid, smp_processor_id(), current->comm);
    if (--data->loops) {
        data->timer.expires += tdelay;
        data->prevjiffies = j;
        add_timer(&data->timer);
    } else {
        wake_up_interruptible(&data->wait);
    }
}
定时器 API 包括几个比上面介绍的那些更多的功能. 下面的集合是完整的核提供的函数列表:
int mod_timer(struct timer_list *timer, unsigned long expires);
更新一个定时器的超时时间, 使用一个超时定时器的一个普通的任务(再一次, 关马达软驱定时器是一个典型例子). mod_timer 也可被调用于非激活定时器, 那里你正常地使用 add_timer.
int del_timer_sync(struct timer_list *timer);
如同 del_timer 一样工作, 但是还保证当它返回时, 定时器函数不在任何 CPU 上运行.
del_timer_sync 用来避免竞争情况在 SMP 系统上, 并且在 UP 内核中和 del_timer 相同. 这个函数应当在大部分情况下比 del_timer 更首先使用. 这个函数可能睡眠如果它被从非原子上下文调用, 但是在其他情况下会忙等待. 要十分小心调用 del_timer_sync 当持有锁时; 如果这个定时器函数试图获得同一个锁, 系统会死锁. 如果定时器函数重新注册自己, 调用者必须首先确保这个重新注册不会发生; 这常常同设置一个" 关闭 "标志来实现, 这个标志被定时器函数检查.
int timer_pending(const struct timer_list * timer);

返回真或假来指示是否定时器当前被调度来运行, 通过调用结构的其中一个不透明的成员.
7.4.2. 内核定时器的实现
为了使用它们, 尽管你不会需要知道内核定时器如何实现, 这个实现是有趣的, 并且值得看一下它们的内部.
定时器的实现被设计来符合下列要求和假设:
● 定时器管理必须尽可能简化.
● 设计应当随着激活的定时器数目上升而很好地适应.
● 大部分定时器在几秒或最多几分钟内到时, 而带有长延时的定时器是相当少见.
● 一个定时器应当在注册它的同一个 CPU 上运行.
由内核开发者想出的解决方法是基于一个每-CPU 数据结构. 这个 timer_list 结构包括一个指针指向这个的数据结构在它的 base 成员. 如果 base 是 NULL, 这个定时器没有被调用运行; 否则, 这个指针告知哪个数据结构(并且, 因此, 哪个 CPU )运行它. 每-CPU 数据项在第 8 章的"每-CPU变量"一节中描述.
无论何时内核代码注册一个定时器( 通过 add_timer 或者 mod_timer), 操作最终由internal_add_timer 进行( 在kernel/timer.c), 它依次添加新定时器到一个双向定时器链表在一个关联到当前 CPU 的"层叠表" 中.
这个层叠表象这样工作: 如果定时器在下一个 0 到 255 jiffies 内到时, 它被添加到专供短时定时器256 列表中的一个上, 使用 expires 成员的最低有效位. 如果它在将来更久时间到时( 但是在 16,384jiffies 之前 ), 它被添加到基于 expires 成员的 9 - 14 位的 64 个列表中一个. 对于更长的定时器, 同样的技巧用在 15 - 20 位, 21 - 26 位, 和 27 - 31 位. 带有一个指向将来还长时间的 expires 成员的定时器( 一些只可能发生在 64-位平台上的事情 ) 被使用一个延时值 0xffffffff 进行哈希处理, 并且带有在过去到时的定时器被调度来在下一个时钟嘀哒运行. (一个已经到时的定时器模拟有时在高负载情况下被注册, 特别的是如果你运行一个可抢占内核).
当触发 __run_timers, 它为当前定时器嘀哒执行所有挂起的定时器. 如果 jiffies 当前是 256 的倍数, 这个函数还重新哈希处理一个下一级别的定时器列表到 256 短期列表, 可能地层叠一个或多个别的级别, 根据jiffies 的位表示.
这个方法, 虽然第一眼看去相当复杂, 在几个和大量定时器的时候都工作得很好. 用来管理每个激活定时器的时间独立于已经注册的定时器数目并且限制在几个对于它的 expires 成员的二进制表示的逻辑操作上. 关联到这个实现的唯一的开销是给 512 链表头的内存( 256 短期链表和 4 组 64 更长时间的列表) -- 即 4 KB 的容量.
函数 __run_timers, 如同 /proc/jitimer 所示, 在原子上下文运行. 除了我们已经描述过的限制, 这个带来一个有趣的特性: 定时器刚好在合适的时间到时, 甚至你没有运行一个可抢占内核, 并且 CPU 在内核空间忙. 你可以见到发生了什么当你在后台读 /proc/jitbusy 时以及在前台 /proc/jitimer. 尽管系统看来牢固地被锁住被这个忙等待系统调用, 内核定时器照样工作地不错.
但是, 记住, 一个内核定时器还远未完善, 因为它受累于 jitter 和 其他由硬件中断引起怪物, 还有其他定时器和其他异步任务. 虽然一个关联到简单数字 I/O 的定时器对于一个如同运行一个步进马达或者其他业余电子设备等简单任务是足够的, 它常常是不合适在工业环境中的生产系统. 对于这样的任务, 你将最可能需要依赖一个实时内核扩展.