Linux内核设计与实现 总结笔记(第十一章)定时器和时间管理

时间管理在内核中占用非常重要的地位,内核中有大量的函数都需要基于时间驱动的,内核对相对时间和绝对时间都非常需要。

一、内核中的时间概念

内核必须在硬件的帮助下才能计算和管理时间,系统定时器以某种频率自行触发(击中hitting或者射中popping)时钟中断,该频率可以通过编程预定,称作节拍率。

因为预编的节拍率对内核来说是可知的,所以内核知道连续两次时钟中断的间隔时间,这个间隔时间称为节拍(tick),它等于节拍率分之一。

下面是利用时间中断周期执行的工作:

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

二、节拍率:HZ

系统定时器频率是通过静态预处理定义的,也就是HZ,在系统启动时按照HZ值对硬件进行设置。

内核在<asm/param.h>文件中定义了这个值。

编写内核代码时,不要认为HZ值是一个固定不变的值。

2.1 理想的HZ值

提高节拍率意味着时钟中断产生得更加频繁,所以中断处理程序也会更频繁地执行。

  • 更高的时钟中断解析度,可提高时间驱动事件的解析度。
  • 提高了时间驱动事件的准确度

2.2 高HZ的优势

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

2.3 高HZ的劣势

节拍率越高,意味着时钟中断频率越高,意味着系统负担越重。中断处理程序占用处理器的时间越多。增加了耗电和打乱了处理器的高速缓存。

三、jiffies

全局变量jiffies用来记录自系统启动以来产生的节拍的总数。启动时内核初始化为0

因为一秒内时钟中断的次数等于HZ,所以jiffies一秒内增加的值也就为HZ。系统运行时间以秒为单位计算,就等于jiffies/HZ。

jiffies定义于文件<linux/jiffies.h>中:

extern unsigned long volatile jiffies;

如下的使用例子:

jiffies = seconds * HZ
jiffies/HZ = seconds
/* jiffies和seconds相互转换 */ unsigned long time_stamp = jiffies; /* 现在 */
unsigned long next_tick = jiffies+; /* 从现在开始1个节拍 */
unsigned long later = jiffies+*HZ; /* 从现在开始5秒 */
unsigned long fraction = jiffies + HZ / ; /* 从现在开始1/10秒 */

jiffies用法

3.1 jiffies的内部表示

jiffies变量总是无符号长整数,在32位上是32位,在64位上是64位。因为jiffies会溢出,

jiffies_64定义在<linux/jiffies.h>中:

extern u64 jiffies_64;

jiffies = jiffies_64;

jiffies

3.2 jiffies的回绕

如果jiffies超过最大存放范围后就会发生溢出,它的值会回绕到0。

unsgined long timeout = jiffies + HZ/;    /* 0.5秒后超过 */
/* 执行一些任务 ... */ /* 然后查看是否花的时间过长 */
if(timeout>jiffies) {
/* 没有超时,很好 ... */
} else {
/* 超时了,发生错误 ... */
}

回绕例子

内核提供给了四个宏来帮助比较节拍计数,他们能正确地处理节拍计数的回绕情况。这些宏在文件<linux/jiffies.h>中:

#define time_after(unknown, known)    ((long)(known) - (long)(unknown)<0)
#define time_before(unknown, known) ((long)(known) - (long)(unknown)<0)
#define time_after_eq(unknown, known) ((long)(known) - (long)(unknown)>=0)
#define time_before_eq(unknown, known) ((long)(known) - (long)(unknown)>=0)

简化版宏

宏time_after(unknown, known);当unkown超过指定known时,返回真,否则返回假。

宏time_before(unknown, known);当时间unknow 没超过指定的know时,返回真,否则返回假

后面两个宏,当两个参数相等时,才返回真。

3.3 用户空间和HZ

如果改变了内核中HZ的值,用户空间中某些程序造成异常结果。内核是以节拍数/秒的形式给用户空间导出这个值的。

因此内核定义了USER_HZ来代表用户空间看到的HZ值。内核可以用函数jiffies_to_clock_t()(在kernel/time.c中)将一个HZ表示的节拍计数转换成一个由USER_HZ表示的节拍计数。

return x /  ( HZ / USER_HZ);

jiffies

在需要把以节拍数/秒为单位的值导出到用户空间时,需要使用上面这几个函数,比如:

unsigned long start;
unsgined long total_time; start = jiffies;
/* 执行一些任务 ... */
total_time = jiffies - start;
printk("That took %lu ticks\n", jiffies_to_clock_t(total_time));

节拍数/秒导出到用户空间

四、硬时钟和定时器

体系结构提供了两种设备进行计时,一种系统定时器,一种实时定时器。他们有着相同的作用和设计思路。

4.1 实时时钟

实时时钟(RTC)是用来持久存放系统时间的设备,即便系统关闭后,它也可以靠主板上的微型电池提供的电力保持系统的计时。

4.2 系统定时器

系统定时器是内核定时机制中最为重要的角色。

有些体系结构体是通过电子晶振进行分频来实现系统定时器,还有些体系结构提供一个衰减测量器。

衰减测量器设置一个初始值,该值以固定频率递减,当减到零时,触发一个中断。

五、时钟中断处理程序

时钟中断处理程序可以划分为两个部分:体系结构相关部分和体系结构无关部分。

绝大多数处理程序最低限度也都要执行如下工作:

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

tick_periodic()执行下面更多的工作:

  • 给jiffies_64变量增加1
  • 更新资源消耗的统计值,比如当前进程所消耗的系统时间和用户时间。
  • 执行已经到期的动态定时器
  • 执行第4章曾讨论的sheduler_tick()函数
  • 更新墙上时间,该时间存放在xtime变量中。
  • 计算平均负载值。

上述工作分别都由单独的函数负责完成,所以tick_periodic()例程代码看起来非常简单。

static void tick_periodic(int cpu)
{
if(tick_do_timer_cpu == cpu) {
write_seqlock(&xtime_lock); /* 记录下一节拍事件 */
tick_next_period = ktime_add(tick_next_period, tick_period); do_timer();
write_sequnlock(&xtime_lock);
} update_process_times(user_mode(get_irq_regs()));
profile_tick(CPU_PROFILING);
}

tick_periodic()函数

很多重要的操作都在do_timer()和update_process_times()函数中进行。前者承担着对jiffies_64的实际增加操作:

函数update_wall_timer根据所流逝的时间更新墙上的时钟,calc_global_load()更新系统的平均负载统计值。

void do_timer(unsigned long ticks)
{
jiffies_64 += ticks;
update_wall_time();
calc_global_load();
}

do_timer()函数

do_timer返回时,调用update_process_times()更新所耗费的各种节拍数,通过user_tick区别话费在用户空间还是内核空间:

void update_process_times(int user_tick)
{
struct task_struct *p = current;
int cpu = smp_processor_id();
/* 注意:也必须对这个时钟irg的上下文说明一下原因 */
account_process_tick(p, user_tick);
run_local_timers();
rcu_check_callbacks(cpu, user_tick);
printk_tick();
scheduler_tick();
run_posix_cpu_timers(p);
}

update_process_times

account_process_tick()函数对进程的时间进行实质性更新:

void account_process_tick(struct task_struct *p, int user_tick)
{
cputime_t one_jiffy_scaled = cputime_to_scaled(cputime_one_jiffy);
struct rq *rq = this_rq(); if(user_tick)
account_user_time(p, cputime_one_jiffy, one_jiffy_scaled);
else if((p != rq->idle) || (irq_count() != HARDIRQ_OFFSET))
account_system_time(p, HARDIRQ_OFFSET, cputime_one_jiffy, one_jiffy_scaled);
else
account_idle_time(cputime_one_jiffy);
}

account_process_tick

内核对进程进行时间计数时,是根据中断发生时处理器所处的模式进行分类统计的,它把上一个节拍全部算给了进程。

但事实上进程在上一个节拍期间,可能多次进入和退出内核模式,而且在上一个节拍器件,该进程也不一定是唯一一个运行进程。

run_lock_timers()函数标记了一个软中断去处理所有到期的定时器。

scheduler_tick()函数负责减少当前运行进程的时间片计数值并且在需要时设置need_resched标志。

tick_periodic()函数执行完毕后返回与体系结构相关的中断处理程序,继续执行后面的工作,释放xtime_lock锁,然后退出。

以上全部工作每1/HZ秒都要发生一次,就是说x86上时钟中断处理程序每秒执行100次或者1000次。

六、实际时间

当前实际时间(墙上时间)定义在文件kernel/time/timekeeping.c中:

struct timespec xtime;

timespec数据结构定义在文件<linux/time.h>中,形式如下:

struct timespec {
_kernel_time_t tv_sec; /* 秒 */
long tv_nsec; /* ns */
};

timespec数据结构

xtime.tv_sec以秒为单位,存放着自1970年1月1日(UTC)以来经过的时间,xtime变量需要使用xtime_lock锁,它是一个seqlock锁,不是普通的自旋锁。

更新xtime首先要申请一个seqlock锁:

write_seqlock(&xtime_lock);
/* 更新xtime ... */
write_sequnlock(&xtime_lock);

更新xtime

读取xtime时也要使用read_seqbegin()和read_seqretry()函数:

unsigned long seq;
do {
unsigned long lost;
seq = read_seqbegin(&xtime_lock); usec = timer->get_offset();
lost = jiffies - wall_jiffies;
if(lost)
usec += lost * ( / HZ);
sec = xtime.tv_sec;
usec += (xtime.tv_nsec / );
} while(read_seqretry(&xtime_lock, seq));

read_seqretry()

该循环不断重复,直到读者确认读取数据没有写操作介入。如果循环期间更新了xitme,read_seqretry()函数就返回无效序列号,继续循环等待。

用户空间取得的墙上时间接口是gettimeofday(),在内核中对应系统调用为sys_gettimeofday(),定义于kernel/time.c:

asmlinkage long sys_gettimeofday(struct timeval *tv, struct timezone *tz)
{
if(likely(tv)) {
struct timeval ktv;
do_gettimeofday(&ktv);
if(copy_to_user(tv, &ktv, sizeof(ktv)))
return -EFAULT;
}
if(unlikely(tz)) {
if(copy_to_user(tz, &sys_tz, sizeof(sys_tz)))
return -EFAULT;
}
return ;
}

sys_gettimeofday

如果用户提供的tv参数非空,do_gettimeofday()函数将被调用。它循环读取xtime操作。如果tz参数为空,将把系统时区返回用户。如果在给用户空间拷贝墙上时间或时区时发生错误,返回-EFAULT;成功返回0

gettimeodfay()函数几乎完全取代了time()系统调用。系统调用settimeofday()来设置当前时间,需要具有CAP_SYS_TIME权能。

除了更新xtime时间以外,内核不会像用户空间程序那样频繁使用xtime。 在文件系统的实现代码中存放访问时间戳时需要使用xtime。

七、定时器

7.1 使用定时器

定时器是管理内核流逝的时间的基础。我们需要一种工具,能够使工作在指定时间点上执行。

7.2 实现定时器

定时器由结构timer_list表示,定义在文件<linux/timer.h>中。

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

struct timer_list

内核提供了一组与定时器相关的结构用来简化管理定时器的操作。所有接口都声明在<linux/timer.h>,大多数接口在文件kernel/timer.c中实现。

定时器的例子:

/* 创建需要先定义它 */
struct timer_list my_timer;
/* 然后需要初始化它 */
init_timer(&my_timer);
/* 然后填充数据结构 */
my_timer.expires = jiffies + delay; /* 定时器超时时的节拍数 */
my_timer.data = ; /* 给定时器处理函数传入0值 */
my_timer.function = my_function; /* 定时器超时时调用的函数 */
/* 最后激活定时器 */
add_timer(&my_timer);

timer_list使用例子

在填充结构体中,expires表示超时时间,data是长整型的参数,function的函数原型必须符合:

void my_timer_function(unsigned long data);

my_timer_function

内核可能延误定时器的执行,所以不能用定时器实现任何硬实时任务。

改变指定的定时器超时时间:

mod_timer(&my_timer, jiffies+new_delay);    /* 新的定时值 */

mod_timer

如果需要在定时器超时前停止定时器,可以使用del_timer函数:

del_timer(&my_timer);

del_timer

删除定时器时需要等待可能在其他处理器上运行的定时器处理程序都退出。

del_timer_sync(&my_timer);

del_timer_sync

7.2 定时器竞争条件

定时器与当前执行代码是异步的,因此可能存在潜在竞争条件,下面的代码是不安全的。

del_timer(my_timer);
my_timer->expires = jiffies + new_delay;
add_timer(my_timer);

不安全的代码

一般情况下del_timer_sync()函数取代del_timer()函数。

7.3 实现定时器

内核在时钟中断发生后执行定时器,定时器作为软中断在下半部上下文中执行。

时钟中断处理程序会执行update_process_times()函数,该函数随即调用run_local_timers()函数:

void run_local_timers(void)
{
hrtimer_run_queues();
raise_softirq(TIMER_SOFTIRQ); /* 执行定时器软中断 */
softlockup_tick();
}

run_local_timers

内核将定时器按它们的超时时间划分为五组。当定时器超时时间接近时,定时器将随组一起下移。减少搜索超时定时器所带来的负担。

八、延迟执行

除了使用定时器或下半部机制以外,还需要其他方法来推迟执行任务。这种推迟通常发生在等待硬件完成某些工作时,而且等待的时间往往非常短。

比如,重新设置网卡的以太模式需要花费2ms,所以在设定网卡速度后,驱动程序必须至少等待2ms才能继续运行。

8.1 忙等待

最简单的延迟方法是忙等待,但是仅仅在想要延迟的时间是节拍的整数倍,或者精确率要求不高时才可以使用。

unsigned long time_out = jiffies + ;        /* 10个节拍 */
while(time_before(jiffies, time_out))
; unsigned long delay = jiffies + *HZ; /* 2秒 */
while(time_before(jiffies, delay))
; unsgined long delay = jiffies + *HZ;
while(time_before(jiffies, delay))
condresched();

忙循环例子

cond_resched()函数将调度一个新程序投入运行,但它只有在设置完need_resched标志后才能生效。

另外,延迟执行不管在那种情况下,都不应该在持有锁时或禁止中断时发生。

jiffies变量在<linux/jiffies.h>中被标记为关键字volatile,这样每次都会被从内存中读入。

8.2 短延迟

有时内核代码不但需要很短暂的延迟,而且还要求延时的时间很精确。

内核提供了三个可以处理ms、ns和us级别的延时函数,定义在文件<linux/delay.h>和<asm/delay.h>中

void udelay(unsgined long usecs)
void ndelay(unsigned long nsecs)
void mdelay(unsigned long msecs)

udelay,ndelay,mdelay

通产超过1ms的范围不适用udelay()进行延迟,对于较长的延迟,mdelay()工作良好。

8.3 schedule_timeout()

更理想的方法是使用schedule_timeout()函数,该方法会让需要延迟执行的任务睡眠到指定的延时时间耗尽后在重新运行。

但是睡眠时间不能保证指定的延时事件按,只能尽量接近指定时间。用法如下:

/* 将任务设置为可中断睡眠状态 */
set_current_state(TASK_INTERRUPTIBLE);
/* 小睡一会,"s"秒后唤醒 */
schedule_timeout(s*HZ);

schedule_timeout例子

在调用shcedule_timeout时,任务必须设置状态为TASK_INTERRUPTIBLE或者TASK_UNINTERRUPTIBLE。

signed long schedule_timeout(signed long timeout)
{
time_t timer;
unsigned long expire; switch(timeout)
{
case MAX_SCHEDULE_TIMEOUT:
schedule();
goto out;
default:
if(timeout < )
{
printk(KERN_ERR "schedule_timeout: wrong timeout "
"value %lx from %p\n", timeout,
__builtin_return_address());
current->state = TASK_RUNNING;
goto out;
}
} expire = timeout + jiffies; init_timer(&timer);
timer.expires = expires;
timer.data = (unsigned long)current;
timer.function = process_timeout; add_timer(&timer);
schedule();
del_timer_sync(&timer); timeout = expire - jiffies; out:
return timeout < ? : timeout;
}

schedule_timeout()实现

该函数用原始的名字timer创建了一个定时器timer,然后设置它的超时时间timeout,设置超时执行函数process_timeout();接着激活定时器而且调用schedule()。

当定时器超时时,process_timeout()函数会被调用:

void process_timeout(unsigned long data)
{
wake_up_process((taks_t *)data);
}

process_timeout

该函数将任务设置为TASK_RUNNING状态,然后将其放入运行队列。

上一篇:Linux内核设计与实现 总结笔记(第六章)内核数据结构


下一篇:linux设备驱动----利用mdev(udev)自动创建设备文件节点