7.4内核定时器

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/shenwanjiang111/article/details/81701370

7.4 内核定时器


内核的调度肯定需要定时器。 比如, 硬件无法提供中断响应时, 就可以使用内核定时器周期性的轮询设备的状态。 再比如, 关闭软盘电机或某一个冗长的 close 操作。 在这样的情况下, 延迟返回直到调用 close, 会造成资源的浪费。 最后, 内核本身也有几种需要内核定时器的情况, 包括 schedule_timeout 的实现。

内核定时器是内核用来控制在未来某个时间点(基于 jiffies )调度执行某个函数的一种机制, 其定义和实现位于文件 <linux/timer.h>kernel/timer.c 中。

到目前为止, 我们所有示例驱动程序都是在所属的进程的上下文中执行。 但是, 使用内核定时器后, 被调度的函数和注册它们的进程是异步执行的, 即, 当定时器运行时, 调度它的进程可能处于休眠, 在另一个CPU上执行, 或很可能已经退出了。

这种异步执行类似于硬件中断。 事实上, 内核定时器是一个 “软件中断”。 当在这种上下文中运行时, 必须满足一些约束条件。 定时器函数必须是原子操作。 当处于进程上下文(比如, 中断上下文)之外时, 必须遵守下面的规则:

  • 不允许访问用户空间。 因为没有进程上下文, 相关代码和中断的进程没有任何联系。

  • current 指针在原子模式下没有意义且不能被使用,因为相关代码和被中断的进程没有联系。

  • 不能休眠和执行调度。原子代码不可以调用 schedule 或 wait_event 形式, 因为它们可能调用其它可能休眠的函数。 例如, 调用 kmalloc(…, GFP_KERNEL) 就是违反规则。 信号量也不可以使用, 因为它们也可以引起休眠。

内核代码可以通过函数 in_interrupt() 来判断是否正在中断上下文中运行。 如果正在中断上下文中运行, 则返回非零, 表示处于硬件中断或软件中断。

内核可以通过调用 in_atomic() 判断当前是否允许调度。 不允许调度的情况包括: 处于软硬中断上下文以及拥有自旋锁的上下文。 在拥有自旋锁的上下文中, current 指针是合法的, 但是仍然禁止访问用户空间, 因为它可以造成调度的发生。 当你使用 in_interrupt()
的时候, 应该考虑 in_atomic() 是否是你想要的意思。 这些函数都声明在 <asm/hardirq.h> 文件中。

内核定时器的另一个重要特性是任务可以重新注册自己以便以后再次运行。 这是可能的, 因为每个timer_list结构在运行之前都与激活计时器列表取消链接, 因此可以立即重新链接到其他位置。 尽管反复重新安排相同的任务可能看起来是毫无意义的操作,但它有时是有用的。 例如,它可用于实现设备的轮询。

值得一提的是, 定时器函数总是由注册它的CPU执行, 以期尽可能地获得更好的cache局域性。 因此, 定时器自身重新注册的定时器, 总是运行在同一个CPU上。

定时器还有一个重要特性就是, 因为是异步执行的, 它是竞争条件的来源, 即使是单处理器系统。 所以, 定时器所要访问的数据结构必须注意并发访问, 即使是原子类型。


7.4.1 定时器API


内核为驱动程序提供了许多函数来使用内核定时器。 如下所示:

#include <linux/timer.h>
struct timer_list {
    struct list_head entry;
    unsigned long expires;
    struct tvec_base *base;

    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);

这里并没有列出 timer_list 的所有元素, 仅列出了 3 个比较重要的。

  • entry:

    双向链表元素, 用来将多个定时器连接成一条双向循环队列

  • expires:

    以jiffies为单位的超时时间,这个时间被表示成自系统启动以来的时钟滴答计数(也即时钟节拍数)。当一个定时器的expires值小于或等于jiffies变量时,我们就说这个定时器已经超时或到期了。在初始化一个定时器后,通常把它的expires域设置成当前expires变量的当前值加上某个时间间隔值(以时钟滴答次数计)。

  • base:

    当前timer所属的base。 由于考虑SMP的情况, 每个CPU都含有一个base

  • function:

    定时器处理函数

  • data:

    定时器处理函数的参数

该结构在使用之前必须被初始化,初始化可以保证所有字段被正确的设置,包括那些对用户不可见的字段。对于定时器数据结构要用 init_timer 初始化,或者对于静态的数据结构可以通过将 TIMER_INITIALIZER 赋值给它来完成初始化。

jit 模块包括一个示例文件 /proc/jitimer (用于“just in timer”), 它返回一个标题行和六个数据行。 数据行表示代码运行的当前环境; 第一个由读取文件操作生成, 其他由计时器生成。 编译内核时记录以下输出:

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 返回的值, bool 型
  • pid: 进程ID
  • command: 进程命令
  • cpu: 所使用的CPU ( 单CPU处理系统, 总是0)

如果在卸载系统时, 读取 /proc/jitimer , 会发现定时器运行在进程 0 , 这个 idle 任务里, 因为历史的原因, 称之为 "swapper"

默认情况下, 每10个 jiffies 时间周期, 定时器产生一次 /proc/jitimer 数据。 可以在加载模块的时候, 通过设置 tdelay 参数来更改该值。

以下代码片段显示了 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; /* parameter */
add_timer(&data->timer);
/* wait for the buffer to fill */
wait_event_interruptible(data->wait, !data->loops);

实际的定时器函数, 参考如下:

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, 下面是内核提供的函数列表:

初始化后, 您可以在调用 add_timer 之前更改三个公共字段。 要在注册的计时器到期之前禁用它, 请调用 del_timer

  • int add_timer(struct timer_list *timer);

    使定时器生效, 链接到内核专门的链表中。

  • int mod_timer(struct timer_list *timer, unsigned long expires);

    修改定时器的到时时间, 也可以在非激活的定时器上被调用。

  • int del_timer_sync(struct timer_list *timer);

    其中 del_timer_sync 是用在 SMP 系统上的(在非SMP系统上,它等于del_timer), 当要被注销的定时器函数正在另一个 cpu 上运行时,del_timer_sync() 会等待其运行完,所以这个函数会休眠。 另外, 还应避免它和被调度的函数争用同一个锁。 对于一个已经被运行过且没有重新注册自己的定时器而言, 注销函数其实也没什么事可做。

  • int timer_pending(const struct timer_list * timer);

    判断一个定时器是否被添加到了内核链表中以等待被调度运行。 注意, 当一个定时器函数即将要被运行前, 内核会把相应的定时器从内核链表中删除(相当于注销)。

  • int del_timer(struct timer_list *timer);

    注销一个定时器


7.4.2 内核定时器的实现


Linux内核2.4版中去掉了老版本内核中的静态定时器机制, 而只留下动态定时器。 动态定时器与静态定时器这二个概念是相对于Linux内核定时器机制的可扩展功能而言的,动态定时器是指内核的定时器队列是可以动态变化的,然而就定时器本身而言,二者并无本质的区别。考虑到静态定时器机制的能力有限,因此Linux内核2.4版中完全去掉了以前的静态定时器机制。2.6内核为了支持SMP及CPU热插拔,对定时器相关结构又做了改动。本文所有代码基于3.3.7内核。


动态内核定时器的组织结构


各定时器向量数据结构定义在kernel/timer.c文件中,如下述代码段所示:

/*
 *
 */
 #define TVN_BITS (CONFIG_BASE_SMALL) ? 4 : 6)
 #define TVR_BITS (CONFIG_BASE_SMALL) ? 6 : 8)
 #define TVN_SIZE (1 << TVN_BITS)
 #define TVR_SIZE (1 << TVR_BITS)
 #define TVN_MASK (TVN_SIZE - 1)
 #define TVR_MASK (TVR_SIZE - 1)

 struct tvec {
    struct list_head vec[TVN_SIZE];
 }

 struct tvec_root {
    struct list_head vec[TVR_SIZE];
 }

 struct tvec_base {
    spinloct_t lock;
    struct timer_list *running_timer;
    unsigned long timer_jiffies;
    unsigned long next_timer;
    struct tvec_root tv1;
    struct tvec tv2;
    struct tvec tv3;
    struct tvec tv4;
    struct tvec tv5;
 } ____cacheline_aligned;

 struct tvec_base boot_tvec_bases;

代码说明:

  • lock:
    由于内核动态定时器链表是一种系统全局共享资源,为了实现对它的互斥访问,Linux定义了专门的自旋锁lock成员来保护。任何想要访问动态定时器链表的代码段都首先必须先持有该自旋锁,并且在访问结束后释放该自旋锁。

  • running_timer:
    用于SMP

*timer_jiffies:
定时器是在软中断中执行的,从触发到真正执行这段时间内可能会有几次时钟中断发生。因此内核必须记住上一次运行定时器机制是什么时候,也即内核必须保存上一次运行定时器机制时的jiffies值。

  • tv1:
    0-255,第一级定时器队列

  • tv2:
    。。。。。


定时器的组织原则


具体的组织方案可以分为两大部分:

(1) 对于内核最关心的、interval值在[0,255]之间的前256个定时器向量,内核是这样组织它们的:这256个定时器向量被组织在一起组成一个定时器向量数组,并作为数据结构timer_vec_root的一部分。基于数据结构timer_vec_root,Linux定义了一个成员tv1,以表示内核所关心的前256个定时器向量。这样内核在处理是否有到期定时器时,它就只从定时器向量数组tv1.vec[256]中的某个定时器向量内进行扫描。而利用timer_jiffies对TVR_SIZE求余后即可自动获得每个tv1当前处理的向量,也即tv1.vec[]数组的索引index,其初值为0,最大值为255(以256为模)。每个时钟节拍时timer_jiffies字段都会加1。显然,index字段所指定的定时器向量tv1.vec[index]中包含了当前时钟节拍内已经到期的所有动态定时器。而定时器向量tv1.vec[index+k]则包含了接下来第k个时钟节拍时刻将到期的所有动态定时器。当timer_jiffies求余后又重新变为0时,就意味着内核已经扫描了tv1变量中的所有256个定时器向量。在这种情况下就必须将那些以松散定时器向量语义来组织的定时器向量补充到tv1中来。

(2) 而对于内核不关心的、interval值在[0xff,0xffffffff]之间的定时器,它们的到期紧迫程度也随其interval值的不同而不同。显然interval值越小,定时器紧迫程度也越高。因此在将它们以松散定时器向量进行组织时也应该区别对待。通常,定时器的interval值越小,它所处的定时器向量的松散度也就越低(也即向量中的各定时器的expires值相差越小);而interval值越大,它所处的定时器向量的松散度也就越大(也即向量中的各定时器的expires值相差越大)。


定时器的实现


虽说,编写驱动程序无需知道内核定时器的设计细节,但是熟知内部实现对熟练使用内核定时器肯定有好处。内核定时器的设计实现,满足下面的要求和假设:

  • 定时器管理必须尽可能轻量级
  • 随着激活定时器的增加, 设计应该能够很好地扩展
  • 大多数定时器都只有几秒或几分钟, 长时间的定时器非常少
  • 定时器运行在注册它的CPU之上

内核开发者设计的解决方案基于每个CPU的数据结构。 结构 timer_list 在其 base 字段中, 包含了指向该数据的指针。 如果 basenull, 则定时器不被调度执行; 否则, 该指针告知哪个CPU运行它。

每当内核代码注册一个定时器时 (通过 add_timer 或 mod_timer), 最终会调用 internal_add_timer (位于kernel/timer.c), 该函数将新的定时器添加到当前CPU相关的级联表中定时器双向链表中。

这个级联表工作原理是:对于即将在0-255个系统嘀嗒后到期的定时器,被添加到expires字段低位表示索引的256个列表之一中。对于在256-16384个嘀嗒计数到时的定时器,被添加到expires字段中的9-14位表示索引的64个列表之一中。时间更久的定时器,以此类推,分别被添加到expires字段中的15-20,21-26,27-31位分别表示索引的列表中。对于更长周期的定时器(仅在64位平台上发生),添加到使用0xffffffff作为索引的列表中。对于已经到期的定时器,在下一个嘀嗒到时被调度运行。(在高负载的情况下, 定时器可能早就有到时的了, 尤其是抢占式内核)

当函数__run_timer被执行时,它会执行当前定时器嘀嗒挂起的所有定时器。 如果jiffies是256的倍数,那么该函数还会重新将下一级列表的定时器散列到256个短期列表中,根据jiffies的位表示可能会级联一个或多个其它层级的列表。

乍看上去,这种方法极其复杂,但是不管是数量较少的定时器还是有大量的定时器都运转的很好。管理每个活动的定时器所需的时间与已注册的定时器数量是无关的,仅仅与它在expires字段中的二进制位表示的逻辑操作有关。这种实现仅是占用了512个列表头的内存-4KB-而已(256个短期列表和4组64个长时间列表)。

正如/proc/jitimer所示,函数__run_timers运行在原子上下文中。除了我们已经描述的限制外,这还带来了一个有趣的特点:即便运行的不是抢占式内核,且CPU在内核空间很繁忙,定时器也能在正确的时间到达。可以在后台读取/proc/jitbusy,在前台读取/proc/jitimer时看看发生了什么。虽然系统好像被繁忙的等待中的系统调用锁死,但是内核定时器仍然能够很好地工作。

但请记住,内核定时器远非完美,因为它受到硬件中断以及其它定时器和异步任务等造成的抖动和缺陷的影响。尽管与简单的数字I/O相关的定时器足以完成诸如运行步进电机或其他业余电子设备之类的简单任务,但它通常不适用于工业环境中的生产系统。 对于此类任务,您很可能需要求助于实时内核扩展。


7.4.3 总结-定时器的用法


  • 先定义一个timer_list变量timer、先初始化timer

    init_timer(&timer);

  • 对timer的相关参数赋值

    timer.function = fun;

    timer.expires = jiffies + TIMER_DELAY;

    add_timer(&timer);

  • 在定时器时间到的时候,会执行fun; 如果继续定时,可以通过在fun中执行

    mod_timer(&timer, jiffies + TIMER_DELAY);

  • 注销定时器

    del_timer(&timer);

猜你喜欢

转载自blog.csdn.net/shenwanjiang111/article/details/81701370