ICode9

精准搜索请尝试: 精确搜索
首页 > 系统相关> 文章详细

Linux定时器和时间管理

2022-01-26 13:01:44  阅读:172  来源: 互联网

标签:定时器 内核 管理 timer jiffies long Linux 时钟


目录

这部分讲Linux内核定时器。

基本概念

  • 系统定时器:一种可编程硬件芯片,能以固定频率产生中断。
  • 定时器中断:系统定时器固定时间周期产生的中断,其中断处理程序负责更新系统时间,执行周期性任务。
  • 动态定时器:一种用来推迟执行程序的工具。内核可以动态创建、销毁动态定时器。
  • 节拍率(tick rate):系统定时器频率,系统定时器以某种频率自行触发(又称为击中(hitting)或射中(popping))时钟中断,该频率可以通过编程设定。
  • 节拍(tick):预编的节拍率对内核来说是可知的,因此内核知道连续2次时钟中断间隔时间。这个时间称为节拍。节拍 = 1/ 节拍率。常用来计算墙上时间和系统运行时间。
  • 墙上时间(walk clock time):实际时间,对用户空间的应用程序来说很重要。代表从进程开始运行到结束,系统时钟走过的时间(时钟数),包含了进程阻塞的时间。每秒滴答数(节拍率)可通过sysconf(_SC_CLK_TCK)获取。
  • 系统运行时间:自系统启动开始所经过的时间,对用户空间和内核都很有用。墙上时间 = 阻塞时间 + 就绪时间 + 运行时间,运行时间 = 用户CPU实际 + 系统CPU时间。

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

  • 更新系统时间。
  • 更新实际时间。
  • 在smp系统上,均衡调度程序中各处理器上的运行队列。如果运行队列负载不均衡的话,尽量使它们均衡。
  • 检查当前进程是否用尽了自己的时间片。如果用尽,就重新调度。
  • 运行超时的动态定时器。
  • 更新资源消耗和处理器时间的统计值。

[======]

节拍率:HZ

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

#include <asm/param.h>

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

其他体系结构节拍率:

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

  • 内核定时器能以更高频度和准确度运行。
  • 依赖定时值执行的系统调用,如select,poll,能以更高精度运行。
  • 对诸如资源消耗和系统运行时间等的测量会有精细的解析度。
  • 提高进程抢占的准确度。

缺点:

  • 节拍率越高,系统时钟中断频率越高,意味着系统负担越重,即中断处理处理程序占用的处理器时间越多,减少了处理其他工作的时间。

[======]

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)的中断处理程序而注册到内核,以便产生时钟中断时能运行。
处理程序主要执行以下工作:

  • 获得xtime_lock锁,对访问jiffies_64和墙上时间进行保护。
  • 需要时应答或重新设置系统时钟。
  • 周期性地使用墙上时间更新实时时钟。
  • 调用体系结构无关的时钟例程:do_timer()。

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

  • 给jiffies_64 + 1。
  • 更新资源消耗的统计值,如当前进程所消耗的系统时间和用户时间。
  • 执行已经到期的动态定时器。
  • 更新墙上时间,该时间存放在xtime变量中。
  • 计算平均负载值。

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还保留着。

  • time() 返回从1970年1月1日午夜开始所走过的秒数。
  • ftime() 返回一个类型为timeb的数据结构,该结构包含从1970年1月1日午夜开始所走过的秒数;在最后1秒内所走过的毫秒数;时区以及夏令时当前的状态。
  • gettimeofday() 返回的值存放在2个数据结构timeval和timezone,其中包含的信息与ftime相同。

[======]

定时器

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

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

定时器由结构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

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有