系统相关
首页 > 系统相关> > Linux定时器和时间管理

Linux定时器和时间管理

作者:互联网

目录

这部分讲Linux内核定时器。

基本概念

系统定时器中断周期性执行的任务:

[======]

节拍率:HZ

系统定时器(节拍率)通过静态预处理定义,系统启动时按HZ值对硬件进行设置。HZ值取决于体系结构。如i386体系结构,HZ值为1000(Hz),代表每秒钟产生1000次节拍

#include <asm/param.h>

#define HZ 1000 /* 内核时钟频率 */

其他体系结构节拍率:

系统定时器使用高频率优缺点
优点:

缺点:

[======]

jiffies

全局变量jiffies用来记录自系统启动以来产生的节拍总数。启动时,初值0;之后,每次时钟中断处理程序都会让jiffies+1。

jiffies定义:

#include <linux/jiffies.h>

extern unsigned long volatile jiffies;

jiffies内部表示

32bit体系结构上,jiffies是32bit,如果时钟频率100Hz,497天后会溢出;频率1000Hz,49.7天后溢出。
64bit体系结构上,几乎不可能会看到它溢出。

除了前面定义,jiffies还有第二个变量定义:

#include <linux/jiffies.h>

extern u64 jiffies_64;

ld(1) 脚本用于连接主内核映像,然后用jiffies_64初值覆盖jiffies变量:

// x86, arch/i386/kernel/vmlinux.lds.S
jiffies = jiffies_64;

也就是说,jiffies只取jiffies_64低32bit。因为大多数代码只使用jiffies存放流失的时间,二时间管理代码使用整个64bit的jiffies_64,以避免溢出。

在32bit体系结构上,jiffies 读取jiffies_64低32bit值;get_jiffies_64()读取jiffies_64整个64bit值。
周64bit体系结构上,jiffies 等价于get_jiffies_64(),和jiffies_64是同一个变量。

jiffies回绕

jiffies 溢出后,会绕回(wrap around)到0。内核提供4个宏函数,用于比较节拍计数,以避免回绕问题。

#include <linux/jiffies.h>

// unknown是jiffies, known是需要对比的值
#define timer_after(unknown, known) ((long)(known) - (long)(unknown) < 0)
#define timer_before(unknown, known) ((long)(unknown) - (long)(known) < 0)
#define timer_after_eq(unknown, known) ((long)(unknown) - (long)(known) >= 0)
#define timer_after(unknown, known) ((long)(known) - (long)(unknown) >= 0)

用户空间和HZ

Linux内核2.6以前,如果改变内核中HZ值,会给用户空间中某些程序造成异常结果,因为应用程序已经依赖这个特定HZ值。
要避免上面错误,内核需要更改所有导出的jiffies值。因此,内核定义USER_HZ代表用户空间看到的HZ值。

例如,x86体系结构上,HZ值原来一直是100,因此USER_HZ值定义为100。
内核使用宏jiffies_to_clock_t() 将一个由HZ表示的节拍计数转换成一个由USER_HZ表示的节拍数。
当USER_HZ是HZ的整数倍时,

#define jiffies_to_clock_t(x) ((x) / (HZ/USER_HZ))

另外,jiffies_64_to_clock_t()将64位jiffies值单位从HZ转换为USER_HZ。

[======]

硬时钟和定时器

体系结构提供3种硬时钟用于计时:实时时钟,时间戳计数,可编程中断定时器。

实时时钟 RTC
RTC是用来持久存放系统时间的设备,即使PC关掉电源,RTC还能依靠主板电池继续计时。
主要作用:
1)系统启动时,内核通过读取RTC来初始化墙上时间,该时间存放在xtime变量中。
2)Linux只用RTC来获得当前时间和日期。

时间戳计数 TSC
x86包含一个64位的时间戳计数器(寄存器),对每个时钟信号进行计数。例如,如果时钟节拍400MHz,那么TSC每2.5ns计数+1。而时钟信号频率没有在预编译时指定,必须在Linux初始化时确定。通过calibrate_tsc(),在系统初始化阶段完成时钟信号频率计算。

可编程中断定时器 PIT
x86体系结构中,主要采用可编程中断时钟(PIT)作为系统定时器。
Linux中,若PIT以100Hz频率向IRQ0发出定时中断,即每10ms产生1次定时中断。这个10ms间隔,就是一个节拍(tick),以微妙为单位存放在tick变量。

TSC与PIT相比,拥有更高的精度。PIT针对编写软件而言,更加灵活。

[======]

时钟中断处理程序

时钟中断处理程序可划分2个部分:体系结构相关部分,体系结构无关部分。
与体系结构相关的例程作为系统定时器(PIT)的中断处理程序而注册到内核,以便产生时钟中断时能运行。
处理程序主要执行以下工作:

中断服务程序主要通过调用与体系结构无关的do_timer()执行工作:

do_timer()看起来像:

void do_timer(struct pt_regs* regs)
{
    jiffies_64++;
    
    update_process_times(user_mode(regs)); // 对用户或系统进行时间更新
    update_times(); // 更新墙上时钟
}

user_mode()宏查询处理器寄存器regs的状态。如果时钟中断发生在用户空间,它返回1;如果发生在内核,则返回0。update_process_times()函数根据时钟中断产生的位置(用户态 or 内核态),对用户或对系统进行相应的时间更新。

void update_process_times(int user_tick)
{
    struct task_struct *p = current;
    int cpu = smp_processor_id();
    int system = user_tick ^ 1; // user_tick和system只会有一个变量为1,另一个必为0

    update_one_process(p, user_tick, system, cpu); // 更新进程时间
    run_local_timers();  // 标记一个软中断处理所有到期的定时器
    scheduler_tick(user_tick, system); // 负责减少当前运行进程的时间片计数值,并在需要时设置need_resched标志
}

update_one_process() 通过判断分支,将user_tick和system加到进程相应的计数上:

/* 更新恰当的时间计数器,给其加一个jiffy */
p->utime += user;
p->stime += system;

update_times()负责更新墙上时钟:

void update_times(void)
{
    unsigned long ticks; // 记录最近一次更新后新产生的节拍数
    
    ticks = jiffies - wall_jiffies;
    if (ticks) {
        wall_jiffies += ticks;
        update_wall_time(ticks); // 更新存储墙上时间的xtime
    }
    last_time_offset = 0;
    calc_load(ticks); // 更新载入平均值
}

ticks记录最近一次更新后新产生的节拍数。通常,ticks应为1,但时钟中断可能丢失,导致节拍丢失。中断长时间被禁止时,就会出现这种情况(虽然很可能是bug)。

cal_load(0更新载入平均值,到此,update_times()执行完毕。do_timer()亦执行完毕并返回与体系结构相关的中断处理程序,继续执行后面的工作,释放xtime_lock锁,然后退出。

墙上时间(实际时间)

墙上时间定义在kernel/timer.c中

struct timespec xtime;

timespec结构定义:

#include <linux/time.h>
struct timespec {
    time_t tv_sec; /* 秒 */
    long tv_nsec;  /* 纳秒 */
};

xtime.tv_sec 存放着自1970年7月1日(UTC)以来经过的时间。1970年7月1日被称为纪元,Unix墙上时间都是基于该纪元的。
xtime.ntv_sec记录着自上一秒开始经过的纳秒数。

读写xtime变量需要用xtime_lock锁,这是一个seqlock锁。
更新xtime:

write_seqlock(&xtime_lock);

/* 更新xtime... */

write_sequnlock(&time_lock);

读取xtime:

/* 循环更新xtime, 直到确认循环期间没有时钟中断处理程序更新xtime */
do {
    unsigned long lost;
    seq = read_seqbegin(&xtime_lock);
    
    usec = timer->get_offset();
    lost = jiffies->wall_jiffies;
    if (lost) 
        usec += lost * (1000000/HZ);
    sec = xtime.tv_sec;
    usec += (xtime.tv_nsec/1000);
} while(read_seqretry(&xtime_lock, seq));

如果循环期间有时钟中断处理程序更新xtime,read_seqretry()会返回无效序列号,继续循环等待。

从用户空间取得墙上时间的主要接口:gettimeofday(),内核中对应系统调用sys_gettimeofday():

asmlinkage long sys_gettimeofday(struct timeval* tv, struct timezone* tz)
{
    if (likely(tv)) { // <=> if (tv)
        struct timeval ktv;
        do_gettimeofday(&ktv); // 循环读取xtime操作
    }
    if (copy_to_user(tv, &ktv, sizeof(ktv))) // 在给用户空间拷贝墙上时间或时区
        return -EFAULT; // 拷贝时发生错误
    if (unlikely(tz)) { // <=> if (!tz)
        if (copy_to_user(tz, &sys_tz, sizeof(sys_tz))) return -EFAULT;
    }
    return 0;
}

/* 宏likely和unlikey在内核中定义, 便于编译器优化, 以提升性能 */
#define likely(x) __builtin_expect(!!(x), 1)
#define unlikely(x) __builtin_expect(!!(x), 0)

time, ftime, gettimeofday关系

内核也实现了time(), ftime()系统调用,但都被gettimeofday()所取代。为保持向后兼容,Linux还保留着。

[======]

定时器

定时器被称为动态定时器或内核定时器,是管理内核时间的基础。

定时器使用
思路:先进行一些初始化工作,设置一个超时时间,指定超时后执行的函数,然后激活定时器。指定的函数将在定时器到期时自动执行。
定时器不会周期运行,超时后自行销毁。这是这种定时器被称为动态定时器的一个原因。因此,动态定时器是在不断的创建和销毁,而且运行次数不受限制。在内核中应用非常普遍。

定时器由结构timer_list表示,定义于<linux/timer.h>

struct timer_list {
    struct list_head entry;               /* 定时器链表入口 */
    unsigned long expires;                /* 以jiffies为单位的定时值 */
    spinlock_t lock;                      /* 保护定时器的锁 */
    void (*function)(unsigned long);      /* 定时器处理函数 */
    unsigned long data;                   /* 传给处理函数的长整型参数 */
    struct tvec_t_base_s *base;           /* 定时器内部值, 用户不要使用 */
};

使用定时器不用深入了解timer_list结构。内核提供一组接口简化管理定时器的操作。

1)定义定时器

struct timer_list my_timer;

2)初始化定时器

init_timer(&my_timer);

3)填充定时器结构中需要的值

my_timer.expires = jiffies + delay;    /* 定时器超时节拍数 */
my_timer.data = 0;                     /* 给定时器处理函数传入值0 */
my_timer.function = my_function;       /* 定时器超时调用的处理函数 */

超时处理函数必须是这种原型:

void my_timer_function(unsigned long data);

4)激活定时器

add_timer(&my_timer);

定时器工作条件:当前节拍计数jiffies >= my_timer.expires
定时器会在超时后马上执行,但也可能推迟到下一个时钟节拍,因此不能用于硬实时任务。

5)修改定时器
改变超时时间

mod_timer(&my_timer, jiffies + new_delay);  /* new expiration */

mod_timer可用于已经初始化但未激活的定时器;如果定时器未被激活,mod_timer会激活之。
如果调用时,定时器未被激活,函数返回0;否则,返回1.

6)删除定时器
在定时器超时前定制定时器

del_timer(&my_timer);

激活或未被激活的定时器都可以用该函数,如果未被激活,函数返回0;否则,返回1。
已超时的定时器不需要调用该函数,因为会自动被删除。

del_timer只能保证定时器将来不会被激活,不保证当前在其他处理器上已运行时会停止。此时,需要用del_timer_sync,等待其他处理器上运行的超时处理函数退出。

del_timer_sync(&my_timer); /* 如果有并发访问可能性, 推荐优先使用 */
del_timer_sync() 不能在中断上下文中使用,因为会阻塞。

定时器竞争条件

定时器与当前执行(设置定时器的)代码是异步的,因此可能存在潜在竞争条件。因此,不能用如下方式替代mod_timer(),来改变定时器的超时时间,因为在多处理器上是不安全的:

/* 用下面代码替换mod_timer, 修改定时器超时时间是错误的 */
del_timer(&my_timer);
my_timer->expires = jiffies + new_delay;
add_timer(&my_timer);

通常,用过用del_timer_sync() 取代del_timer()删除定时器,避免并发访问的问题,因为无法确定删除定时器的时候,它是否在其他处理器上运行。

实现定时器

定时器作为软中断在下半部上下文中执行。
时钟中断处理程序会执行update_process_timers(),该函数会随即调用run_local_timers()。

void run_local_timers(void)
{
    raise_softirq(TIMER_SOFTIRQ);
}

run_timer_softirq()处理软件中断TIMER_SOFTIRQ,从而在当前处理器上运行所有的超时定时器。

内核定时器是以链表形式存放,但并没有遍历链表以寻找超时定时器,也没有在链表中插入和删除定时器。
而是,将定时器按超时时间分为五组。当定时器超时时间接近时,定时器将随组一起下移。采用分组定时器的方法可以在执行软中断的多数情况下,可以确保内核尽可能减少搜索超时定时器所带来的负担。

[======]

延迟执行

内核代码(尤其驱动程序)除了用定时器或下半部机制外,还需要其他方法来推迟执行任务。
常适用于:短时间等待硬件完成某些工作,比如,重新设计网卡的以太网模式(2ms)。

内核提供多种延迟方法处理各种延迟要求:
1)忙等待
2)短延迟
3)schedule_timeout()
4)设置超时时间,在等待队列上睡眠

忙等待

忙等待(或称忙循环),是最简单的延迟方法,也是最不理想的。
方法仅适用于想要延迟的时间是节拍的整数倍,或者精确度要求不高时使用。

忙循环使用示例:在循环中不断旋转直到希望的时钟节拍数耗尽

unsigned long delay = jiffies + 10; /* 10个节拍 */

while (time_before(jiffies, delay)) /* CPU循环等待 jiffies > delay (自动处理定时器值回绕) */
    ;

上面循环不断旋转,等待10个节拍。HZ值为1000的x86体系结构上,每个节拍1ms,10个节拍总共耗时10ms。

unsigned long delay = jiffies + 2 * HZ; /* 2秒 */

while (time_before(jiffies, delay))
    ;

上面循环自旋时,并不会放弃CPU。下面cond_resched()将调度一个新程序投入运行,不过只有在设置完need_resched标志后,才能生效。因为cond_resched方法会调用调度程序,因此不能在中断上下文中使用,而只能在进程上下文中使用。

unsigned long delay = jiffies + 5 * HZ;

while (time_before(jiffies, delay))
    cond_resched(); /* 调度一个新程序投入运行 */

注意:
1)所有延迟方法都只能在进程上下文使用,不能在中断上下文使用。因为中处理程序应尽快执行。
2)延迟执行 不应在持有锁或者禁止中断的时候发生。

短延迟

有时驱动程序不但需要很短的延迟(比时钟节拍typ.为1ms还短),而且要求延迟的时间很精确。不可能使用精度为1ms的jiffies节拍用于延迟。
此时,可以用内核提供的另外2个函数,用于处理微妙和毫秒级延迟。
头文件:<linux/delay.h>

void udelay(unsigned long usecs);
void mdelay(unsigned long msecs);

mdelay是通过udelay实现的。

如,延迟150微秒,延迟200毫秒

udelay(150);  /* 延迟150us */
mdelay(200);  /* 延迟200ms */

注意:
1)延迟超过1ms时,不要用udelay,应该用mdelay。
2)能不用则不用mdelay,尽量少用。
3)不要在持有锁或者禁止中断时,使用忙等待,因为类似于忙等待,会让系统响应速度和性能大打折扣。

schedule_timeout() 睡眠到指定延迟时间

该方法会让需要延迟执行的任务睡眠到指定的延迟时间耗尽后,再重新运行。不能保证睡眠时间刚好等于指定的延迟时间,只能是尽量接近。当指定时间到期后,内核唤醒被延迟的任务并将其重新放回运行队列。

典型用法:

/* 将任务设置为可中断睡眠状态 */
set_current_state(TASK_INTERRUPTIBLE);

unsigned long S = 10;
/* 小睡一会儿,S秒后唤醒 */
schedule_timeout(s * HZ);

唯一的参数是延迟的相对时间,单位jiffies。

如果睡眠时,想接收信号,可将任务状态设置为TASK_INTERRUPTIBLE;如果不想,可以将任务状态设置为TASK_UNINTERRUPTIBLE。
注意:调用schedule_timeout()前,必须将任务设置为上面两种状态之一,否则任务不会睡眠。

schedule_timeout的简单实现:

signed long schedule_timeout(singed long timeout)
{
    timer_t timer;
    unsigned long expire;
    
    switch(timeout)
    { /* 处理特殊情况 */
    case MAX_SCHEDULE_TIMEOUT: /* 无限期睡眠 */
        schedule(); /* 调度进程: 从就绪队列中选一个优先级最高的进程来替代当前进程运行 */
        goto out;
    default:
        if (timeout < 0) {
            printk(KERN_ERR"schedule_timeout: wrong timeout value %lx from %p\n", timeout, __builtin_return_address(0));
            goto out;
        }
    }
    
    expire = timeout + jiffies;
    init_timer(&timer); /* 初始化动态定时器 */
    timer.expires = expire; 
    timer.data = (unsigned long)current;
    timer.funtion = process_timeout;
    
    add_timer(&timer); /* 激活定时器 */
    schedule();
    del_timer_sync(&timer); /* 同步删除定时器 */

    timeout = expire - jiffies;

out:
    return timeout < 0 ? 0 : timeout;
}

/* 定时器超时处理函数 */
void process_timeout(unsigned long data)
{
    wake_up_progress((task_t *)data); /* 唤醒进程, 将任务设置为TASK_RUNNING */
}

因为任务被标识为TASK_INTERRUPTIBLE或TASK_UNINTERRUPTIBLE(在调用schedule_timeout之前),所以调度程序不会再选择该任务投入运行,而会选择其他新任务运行。

设置超时时间,在等待队列上睡眠

进程上下文中为了等待特定事件发生,会将自己放入等待队列,然后调用调度程序执行新任务。一旦事件发生,内核可调用wake_up()唤醒在睡眠队列上的任务,使其重新投入运行。

schedule_timeout用在什么地方?
当等待队列上的某个任务可能既在等待一个特定事件到来,又在等待一个特定时间到期,看谁先来。此时,可以用schedule_timeout替换schedule(),因为schedule()只是简单的阻塞等待唤醒事件,而schedule_timeout除了可以等待IO事件,还会等待超时。

[======]

小结

1)讲述了时间的基本概念,如墙上时间,时钟中断,时钟节拍,HZ,jiffies等。
2)定时器的实现,应用方法等。
3)开发者用于延迟的方法:忙等待、短延迟、schedule_timeout。

[======]

参考

[1]RobertLove, 洛夫, 陈莉君,等. Linux内核设计与实现[M]. 机械工业出版社, 2006.

标签:定时器,内核,管理,timer,jiffies,long,Linux,时钟
来源: https://www.cnblogs.com/fortunely/p/15846089.html