内核定时器

内核定时器

如果我们需要在将来的某个时间点调试执行某个动作,同时在该时间点到达之前不会阻塞当前里程,则可以使用内核定时器。内核定时器可以在未来的某个时间点(基于时间滴答)调度执行某个函数。如硬件无法产和中断,则可以周期性的轮询设备状态。

一个内核定时器是一个数据结构struct timer_list,它告诉内核在用户定义的时间点使用用户定义的参数来执行一个用户定义的函数。其实现位于<linux/timer.h>和kernel/timer.c文件。我们将在内核定时器的实现一节中对此进行详细描述。

被调度运行的定时器中的函数几乎肯定不会在注册这些函数的进程正在执行的时候运行。相反这些函数会异步的运行。目前为止,我们提供的示例驱动程序代码都在进程执行系统调用的上下文中运行。但是,当定时器运行时,注册该定时器的进程可能正在休眠或在其他处理器上运行,或者干脆已经退出。

这种异步执行类似于硬件中断发生的情景。实际上,内核定时器常常是作为“软件中断”的结果而运行的。在这种原子性的上下文中运行时,代码会受到许多限制。定时器函数必须以我们在我们在第五章“自旋锁和原子上下文”一节中讨论的方式原子地运行,中间不能休眠,这种非进程上下文还会带来其他一些问题。现在我们就来讨论这些限制。

许多动作需要在进程上下文中才能执行。如果处于进程上下文之外(比如在中断上下文中),则必须遵守如下规则

  • 不允许访问用户空间。因为没有进程上下文,无法将任何特定进程与用户空间关联起来。
  • current指针在原子模式下是没有任何意义的,也是不可用的,因为相关代码和被中断的进程没有任何联系。
  • 不能执行休眠或调度。原子代码不可以调用schedule或者wait_event,也不能调用任何可能引起休眠的函数。例如,调用kmalloc就不本规则。信号量也不能用,因为可能引起休眠。

内核代码可以通过in_interrupt()来判断自己是否正运行于中断上下文,该函数无参数,如果是返回非零,无论是硬件中断还是软件中断。

函数in_atomic()可以判断调度是否被禁止,如果禁止函数返回非零。在硬件和软件中断上下文以及拥有自旋锁的任何时间点都不允许调度。在拥有自旋锁时current是可用的,但禁止访问用户空间,因为这会导致调度的发生。

不管何时使用in_interrupt()都应该考虑是否真正应该使用的是in_atomic()。这两个函数均在<asm/hardirq.h>中声明。

内核定时器的另一个重要特性是,任务(定时任务)可以将自己注册以在稍后的时间重新运行。这种可能性是因为每个timer_list结构都会在运行之前从活动定时器链表中移走,这就可以立即链入其它的链表。尽管多次调度同一任务似乎是一你件没多大意义的操作,但有时还是很有用的。如在轮询设备时可以使用,以及在跑I/O的过程中查询速率。

另一个值得了解的是,在SMP系统中,定时器函数会由注册它的同一CPU执行,这样可以尽可能获得缓存的局域性(locality)。因此,一个注册自己的定时器始终会在同一CPU上运行。

关于定时器,还有一点要谨记的是:即使在单处理器系统上,定时器也会是竞态的潜在来源。这是由其它异步执行(有可能是软件中断还可能被硬件中断打断)的特点直接导致的。因此,任何通过定时器函数访问的数据结构都应该针对并发访问进行保护。可以使用原子类型或自自旋锁

 

定时器API

内核为驱动程序提供了一组用来声明、注册和删除内核定时器的函数。

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在使用前必须初始化,在初始化之后,可在调用add_timer之前修改那三个公共的字段(expires,fun,data),如果要在定时器到期前禁止一个已注册的定时器,则可调用del_timer函数。下面是定时器示例代码:

unsigned long j = jiffies;

/*为定时器函数填充数据*/
data->prevjiffies = j;
data->buf = buf2;
data->loops = JIT_ASYNC_LOOPS;

/* register the timer */
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);

实际的定时器函数如下:

void jit_timer_fn(unsigned long arg)
{
    struct jit_data *data = (struct jit_data *)arg;
    unsigned long j = jiffies;
    data->buf += sprinf(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->timer.prevjifies = j;
        add_timer(&data->timer);
    }else{
        wake_up_interruptible(&data->wait);
    }
}

除了上面的几个函数接口外,内核定时器API还包含其他几个函数,下面给出这些函数的完整描述:

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

更新某个定时器到期时间,经常用于超时定时器。我们也可以在通常使用add_timer的时候在不活动的定时器上调用mod_timer。

int del_timer_sync(struct timer_list *timer)

和del_timer的工作类似,但该函数可确保在返回时没有任何CUP在运行定时器函数。它可用于SMP系统上避免竞态,这和单处理器内核中的del_timer是一样的。在大多数情况下,应该优先调用这个函数而不是del_timer函数。在非原子上下文中调用,该函数可能会引起休眠,但在其他情况下调用会进入忙等待。在拥有锁时,应该格外小心调用del_timer_sync,因为如果定时器函数企图获取相同的锁,系统就会进入死锁。

int timer_pending(const struct timer_list *timer);

该函数通过读取timer_list结构的一个不可见字段来返回定时器是否正在被调度运行。

内核定时器的实现

外部机制:当产生时钟中断后,在时钟中断处例程timer_interrupt()中上半部分会调用update_process_times()函数,该函数更新进程的时间片以及修改修改的进程的动态优先级,如果有必要将会告诉高度器重新调试,它还会调用run_local_timers(),在这个函数中标记TIMER_SOFTIRQ,表明有timer需要运行,以便内核在适当的时候去运行定时器,这样softirq机制在适当的时候就会运行定时器队列

内部机制:

Linux实现定时器的算法比较精巧,我们不妨先来看看几个简单的算法:

一个最直接的方法就是将所有的定时器按照时间从小到大的顺序排列起来,这样每次时钟中断下半部分处理例程只要检查当前节点是不是到期就可以了。如果没有到期,那么在下次时钟中断再判断,如果到期了,就执行规定的操作,然后将当前节点的指针往后移一个。在实现上这种方法很简单,也不需要要多余的空间。但是如果链表很长,第次插入的时候排序就要花比较多的时间。

对于上面的方法,我们可以采用hash表的方式来改进,即采用平均分段的方式来组织链表。比如,我们将到期的jiffies数按照0-99,100-199,200-299分段,每一个定时器到自己所属的时段中进行排序。但是当定时器数量太大的时候,这个方法和上面的那个方法面临同样的问题,那就是在插入的时候花在查找上的时间太大。

如果我们采用不平均的方法来分段,那么情况就大为不同了。如0-3,4-7,8-15,16-31,……。区间长度呈指数上升,这样,就不会有太多的分段。而且当前要处理的定时器在比较短的链表中,排序和搜索速度都可以大大加快。因为我们关心的都是当前时刻要处理的定时器,而对离执行时间还有很长的定时器是不是需要关心的,所以时间距离太远的定时器我们只要将它连到链表中就可以了,用不着排序。Linux内核就是采用了上面平均分段的hash表思想。

下面是定时器最基本的一个数据结构:

struct timer_list{
    struct list_head entry;
    unsigned long expires; //到达该jiffies值时,将调用fun函数,它并不是jiffies_64,因为定时器并不适用于长的未来时间点
    ……
    void (*fun)(unsigned long);
    unsigned long data; //fun函数的参数

    struct timer_base_s *base;//定时器应该在注册它的同一CPU上运行,该字段告诉我们哪个CPU在运行定时器,它指向per-CPU数据结构
}

不管何时内核代码注册了一个定时器(通过add_timer或者mod_timer),其操作最终会由internal_add_timer(kernel/timer.c中定义)执行,该函数又会将新的定时器添加到当前CPU关联的“级联表”中的定时器又向链表中。

关于“级联表”的工作方式简单说一下,内核通过 下面结构管理每个CPU上的定时器:

struct tvec_base {
 spinlock_t lock;
 struct timer_list *running_timer;
 unsigned long timer_jiffies;
 unsigned long next_timer;
 struct tvec_root tv1;//保存在接下来的0-255个jiffies中到期的定时器,它是256个链表,每个链表指向在同一时刻到期的一组定时器
 struct tvec tv2;//保存在256-256*64-1个jiffies中到期的定时器,它是64个链表,每个链表指向一组定时器
 struct tvec tv3;//保存在256*64-256*64*64-1个jiffies中到期的定时器
 struct tvec tv4;//256*64*64-256*64*64*64-1
 struct tvec tv5;//256*64*64*64-256*64*64*64*64
}

通过以上结构就能表达在接下来2^32个jiffies到期的定时器。

__run_timers被激发时,它会执行当前定时器上的所有挂起的定时器。如果当前jiffies是256的倍数,该函数还会将一下级定时器链表重新散列到256个短期链表中,同时还能根据jiffies的位对其他级别的定时器做级联处理。

这种方法看起来有些复杂,但能很好处理定时器不多或有大量定时器的情况。用来管理每个活动定时器所需的必要时间和已注册的定时器数量无关,这个时间被限定于定时器expires字段二进制表达上的几个逻辑操作。这种实现唯一的开销在于512个链表头(256个短期链表和4组64个的长期链表)占用了4KB(512*2*4)的存储空间。

__run_timers运行在原子上下文中,即使我们运行的不是抢占式的内核,定时器也会在正确的时间到期。

需要谨记的是,内核定时器离完美还有很大的距离,因为它受到jitter以及由硬件中断、其他定时器和异步任务所产生的影响。和简单数字I/O关联的定时器对简单任务来说足够了,比如控制步进电机或者业余电子设备,但通常不适合于工业环境下的生产系统。对这类任务,我们需要借助某种实时的内核扩展。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值