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

一、内核中的时间概念

内核必须在硬件的帮助下才能计算和管理时间,系统定时器以某种频率自行触发(击中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内核设计与实现 总结笔记(第十一章)定时器和时间管理的更多相关文章

  1. 《Linux内核设计与实现》读书笔记(十一)- 定时器和时间管理【转】

    转自:http://www.cnblogs.com/wang_yb/archive/2013/05/10/3070373.html 系统中有很多与时间相关的程序(比如定期执行的任务,某一时间执行的任务 ...

  2. Linux内核设计与实现 总结笔记(第二章)

    一.Linux内核中的一些基本概念 内核空间:内核可独立于普通应用程序,它一般处于系统态,拥有受保护的内存空间和访问硬件设备的所有权限.这种系统态和被保护起来的内存空间,称为内核空间. 进程上下文:当 ...

  3. Linux内核设计与实现 总结笔记(第九章)内核同步介绍

    在使用共享内存的应用程序中,程序员必须特别留意保护共享资源,防止共享资源并发访问. 一.临界区和竞争条件 1.1 临界区和竞争条件 所谓临界区就是访问和操作共享数据代码段.多个执行线程并发访问同一个资 ...

  4. 《Linux内核设计与实现》课本第四章自学笔记——20135203齐岳

    <Linux内核设计与实现>课本第四章自学笔记 进程调度 By20135203齐岳 4.1 多任务 多任务操作系统就是能同时并发的交互执行多个进程的操作系统.多任务操作系统使多个进程处于堵 ...

  5. 《Linux内核设计与实现》课本第三章自学笔记——20135203齐岳

    <Linux内核设计与实现>课本第三章自学笔记 进程管理 By20135203齐岳 进程 进程:处于执行期的程序.包括代码段和打开的文件.挂起的信号.内核内部数据.处理器状态一个或多个具有 ...

  6. 《Linux内核设计与实现》课本第五章学习笔记——20135203齐岳

    <Linux内核设计与实现>课本第五章学习笔记 By20135203齐岳 与内核通信 用户空间进程和硬件设备之间通过系统调用来交互,其主要作用有三个. 为用户空间提供了硬件的抽象接口. 保 ...

  7. Linux内核设计与实现 读书笔记 转

    Linux内核设计与实现  读书笔记: http://www.cnblogs.com/wang_yb/tag/linux-kernel/ <深入理解LINUX内存管理> http://bl ...

  8. 《Linux内核设计与实现》第一、二章学习笔记

    <Linux内核设计与实现>第一.二章学习笔记 姓名:王玮怡  学号:20135116 第一章 Linux内核简介 一.关于Unix ——一个支持抢占式多任务.多线程.虚拟内存.换页.动态 ...

  9. 初探内核之《Linux内核设计与实现》笔记上

    内核简介  本篇简单介绍内核相关的基本概念. 主要内容: 单内核和微内核 内核版本号 1. 单内核和微内核   原理 优势 劣势 单内核 整个内核都在一个大内核地址空间上运行. 1. 简单.2. 高效 ...

  10. 《LINUX内核设计与实现》第一、二章学习总结

    第一章 Linux内核简介 (一)Unix是一个强大.健壮和稳定的操作系统,特点是: Unix很简洁,仅仅提供几个几百个系统调用并且有一个非常明确的设计目的 在Unix中,所有的东西都被当作文件对待, ...

随机推荐

  1. sql语句小记录

    测试过程中,需要去数据库中查询一些结果,比如验证码 常用的是查询 更新比较少用 删除一般不用 sql查询语句的嵌套用法,比较实用 比如in的用法:第一种:查询多个值时 SELECT "栏位名 ...

  2. 安卓和IOS抓包工具

    安卓手机抓包软件:Packet Capture,抓包精灵…… 1.Packet Capture又名无root抓包(一款依托安卓系统自身VPN来达到免Root抓取数据包的应用程序) 功能特点: 捕获网络 ...

  3. SPA应用性能优化(懒加载)

    前提: 如今开发方式都是采用前后台分离的方式,前台采用的方式则是单页面应用开发简称SPA,这种开发模式最大的一个特点就是将有所代码打包成了一个文件, 这会导致了一个问题就是如果这个应用过大,打出来的这 ...

  4. ubutnu同时安装OpenCV2和OpenCV3及contrib

    1.OpenCV2源码安装 安装依赖项 sudo apt-get install build-essential //build-essential是c语言的开发包,包含了gcc make gdb和l ...

  5. 【Linux开发】如何更改linux文件的拥有者及用户组(chown和chgrp)

    本文整理自: http://blog.163.com/yanenshun@126/blog/static/128388169201203011157308/ http://ydlmlh.iteye.c ...

  6. [转帖]解决K8S 安装只有 一直提示:kernel:unregister_netdevice: waiting for eth0 to become free. Usage count = 1 的方法

    Centos7 终端报Message from syslogd :kernel:unregister_netdevice https://www.jianshu.com/p/96d7e2cd9e99 ...

  7. 认识 JVM

    1 什么是JVM?  JVM是Java Virtual Machine(Java虚拟机)的缩写,JVM是一种用于计算设备的规范.比如 对Class文件类型,运行时数据,帧栈 ,指令集等的规范 ,Hot ...

  8. BSP中uboot初体验

    一. uboot源码获取 1.1. 从板级厂家获取开发板BSP级uboot(就是由开发板厂家提供的) 1.2. 从SOC厂家获取相同SOC的BSP级uboot 1.3. 从uboot官方下载 1.4. ...

  9. Pycharm2019.1.3破解

    搬运: T3ACKYHDVF-eyJsaWNlbnNlSWQiOiJUM0FDS1lIRFZGIiwibGljZW5zZWVOYW1lIjoi5bCP6bifIOeoi+W6j+WRmCIsImFzc ...

  10. redis 教程(一)-基础知识

    redis 简介 redis 是高性能的 key-value 数据库,读的速度是110000次/s,写的速度是81000次/s ,它以内存作为主存储 具有以下优点: 1. 支持数据的持久化,将内存中的 ...