文章目录
- 1、内核中的时间概念
- 2、 节拍率:HZ
- 3、jiffies
- 4、硬时钟和定时器
- 5、时钟中断处理程序
- 6、实际时间
- 7、定时器
- 8、延迟执行
1、内核中的时间概念
- 硬件为内核提供了一个系统定时器用以计算流逝的时间,该时钟在内核中可看成是一个电子时间资源,比如数字时钟或处理器频率等。
- 系统定时器以某种频率自行触发(常被称为击中(hitting)或者射中(popping))时钟中断,该频率可以通过编程预定,称作节拍率(tick rate)。
- 内核维护两种时间:墙上时间和系统运行时间。
- 什么是墙上时间?
墙上时间也就是实际时间,对用户空间的应用程序来说是最重要的。 - 什么是系统运行时间?
系统运行时间也就是自系统启动开始所经的时间,对用户空间和内核都很有用。 - 这两种时间内核如何维护计算?
通过预编的节拍率可以知道连续两次时钟中断的间隔时间(节拍(tick))。 这个间隔时间等于节拍率分之一(1/(tick rate))秒。 - 利用时钟中断周期执行的任务有哪些?
注意:有些工作随时钟频率反复执行,有些是n个时钟中断执行一次。- 更新系统运行时间
- 更新实际时间
- 在SMP上,均衡调度程序尽量使运行队列负载均衡
- 检查当前进程是否用尽了自己的时间片
- 运行超时的动态定时器
- 更新资源消耗和处理器时间统计值
- 言下之意,还有一些工作不是周期执行的,但依旧需要使用时间管理资源。
2、 节拍率:HZ
- 系统定时器频率(节拍率)通过静态预处理定义,也就是HZ(赫兹),在
asm/param.h
文件中定义了这个值。与体系结构有关。 - 例如:在x86体系结构中,系统定时器频率默认为100。因此,x86上时钟中断的频率就是100HZ,也就是说每秒时钟中断100次,即每10ms中断一次。
- HZ值不是一个固定不变的值,大多数体系结构的节拍率是可调的。
- 下面是各种体系结构与之对应的时钟中断频率
- 高HZ值的优势是什么?
- 内核定时器能够以更高的频度和更高的准确度运行;
- 依赖定时值执行的系统调用,比如
poll()
和select()
,能够以更高的精度运行; - 提高进程抢占的准确度。
- 高HZ值的劣势是什么?
- 系统负担变重,中断处理程序占用处理器时间越多,处理其他任务时间减少
- 频繁打乱处理器高速缓存,增加耗电
- 无节拍的OS
- Linux内核支持“无节拍操作”选项。编译内核时设置了
CONFIG_HZ
配置选项,系统根据这个选项动态调度时钟中断,不是固定间隔。 - 例如:如果50ms内无事可做,内核以50ms重新调度时钟中断。
- Linux内核支持“无节拍操作”选项。编译内核时设置了
3、jiffies
- 全局变量
jiffies
用来记录自系统启动以来产生的节拍的总数。 jiffies
定义于文件<linux/jiffies.h>
中,存放jiffies
类型数据的时候必须用无符号长整型(unsigned long
):extern unsigned long volatile jiffies;
- 将以秒为单位的时间转化为
jiffies
:(seconds * HZ)
- 将
jiffies
转化为以秒为单位的时间:(jiffies/HZ)
- 设置将来的时间:
unsigned long time_stamp = jiffies; /*现在*/ unsigned long next_tick = jiffies+1; /*从现在开始1个节拍*/ unsigned long later = jiffies+5*HZ; /*从现在开始5秒*/ unsigned long fraction = jiffies + HZ / 10; /*从现在开始1/10秒*/
jiffies
的内部表示jiffies
变量总是unsigned long
类型,在32位上是32位,在64位上是64位。因为32位jiffies
会溢出,所以需要引入64位的jiffies_64
jiffies_64
定义在<linux/jiffies.h>
中:
这样一来,在32位机器中,访问extern u64 jiffies_64; jiffies = jiffies_64; //非常巧妙
jiffies
等价于访问jiffies_64
的低32
位;而在64
位机器中,访问jiffies
则等价于访问jiffies_64
。- 下面是
jiffies
和jiffies_64
的划分,对照前一句话进行理解。
jiffies
的回绕- 什么是回绕?
和任何C整型一样,当jiffies
变量的值超过它的最大存放范围后就会发生溢出。即:对于32位无符号长整型,最大取值为2^32-1。所以在溢出前,定时器节拍计数最大为4294967295。当节拍计数达到了最大值后还要继续增加,那jiffies
的值会回绕到0。 - 内核提供如下宏来帮助比较节拍计数,以便能够正确处理节拍计数回绕情况,定义在文件
<linux/jiffies.h>
中。简化版如下:
其中#define time_after(unknown,known) ((long)(known) - (long)(unknown) < 0) #define time_before(unknown,known) ((long)(unknown) - (long)(known) < 0) #define time_after_eq(unknown,known) ((long)(unknown) - (long)(known) >= 0) #define time_before_eq(unknown,known) ((long)(known) - (long)(unknown) >= 0)
unknown
参数通常是jiffies
,known
参数是需要对比的值。 - 一个回绕的例子
unsigned long timeout = jiffies + HZ/2; /*0.5秒后超时*/ //if(timeout > jiffies){//没有使用正确处理回绕的宏,那么就会发生错误 if(time_before(jiffies,timeout)){ //使用了正确处理回绕的宏 /*没有超时,很好*/ }else{ /*超时了,发生错误*/ }
- 什么是回绕?
- 用户空间和HZ
- 在Linux 2.6以前的版本中,改变内核的
HZ
值会影响用户空间的某些程序。 - 用
USER_HZ
代表用户空间看到的HZ
,用jiffies_to_clock_t()
将HZ
转换成USER_HZ
表示的节拍数。
- 在Linux 2.6以前的版本中,改变内核的
4、硬时钟和定时器
- RTC
- 什么是实时时钟(RTC)?
实时时钟是用来持久存放系统时间的设备,即便系统关闭后,它也可以靠主板上的微型电池提供的电力保持系统的计时。 - RTC对内核的作用是什么?
当系统启动时,内核通过读取RTC
来初始化墙上时间,该时间存放在xtime
变量中。
- 什么是实时时钟(RTC)?
- 系统定时器
- 系统定时器的根本思想是提供一种周期性触发中断机制。
- 有对晶振分频实现定时器的,也有衰减值定时的。
x86
中主要采用可编程中断时钟(PIT),还有其他的时钟资源如本地APIC
时钟和时间戳计数(TSC)等。
5、时钟中断处理程序
- 时钟中断处理程序可以划分哪两个部分?
体系结构相关和体系结构无关部分。 - 与体系结构相关的部分做哪些工作?
与体系结构相关的部分作为系统定时器的中断处理程序而注册到内核中,以便在产生时钟中断时,它能够相应的运行。处理程序的具体工作依赖于特定的体系结构,最少需要执行以下操作:- 获得
xtime_lock
锁,以便对访问jiffies_64
和墙上时间xtime
进行保护; - 需要时应答或重新设置系统时钟;
- 周期性地使用墙上时间更新实时时钟;
- 调用体系结构无关的时钟部分:
tick_perodic()
。
- 获得
tick_perodic()
函数做什么工作?- 给
jiffies_64
变量增加1(这个操作即使是在32位体系结构上也是安全的,因为前面已经获得了xtime_lock
锁); - 更新资源消耗的统计值,比如当前进程所消耗的系统时间和用户时间;
- 执行已经到期的动态定时器。
- 执行
sheduler_tick()
函数(负责减少当前运行进程的时间片计数值并且设置need_resched
标志); - 更新墙上时间,该时间存放在
xtime
变量中; - 计算平均负载值。
注意:以上全部工作每1/HZ秒
都要发生一次,可以联想STM32
的中断服务程序
- 给
6、实际时间
- 实际时间(墙上时间)的定义
- 定义在文件
kernel/time/timekeeping.c
中:struct timespec xtime;
timespec
数据结构定义在文件<linux/time.h>
中,形式如下:struct timespec{ _kernel_time_t tv_sec; /* 以s为单位,存放着自1970年1月1日(UTC)以来经过的时间,1970年1月1日被称为纪元。*/ long tv_nsec; /* 记录自上一秒开始经过的ns数 */ };
- 定义在文件
- 读写
xtime
变量需要有哪些操作?- 写
xtime
首先需要申请一个seqlock
锁:write_seqlock(&xtime_lock); /*更新xtime*/ write_unseqlock(&xtime_lock);
- 读
xtime
也要使用read_seqbegin()
和read_seqretry()
函数
该循环不断重复,如果发现循环期间有时间中断处理程序更新/* 顺序锁中读锁来循环获取 xtime,直至读取过程中 xtime 没有被改变过 */ do { seq = read_seqbegin(&xtime_lock); *ts = xtime; nsecs = timekeeping_get_ns(); /* If arch requires, add in gettimeoffset() */ nsecs += arch_gettimeoffset(); } while (read_seqretry(&xtime_lock, seq)); /* 省略 。。。。 */ }
xtime
,那么read_seqretry()
函数就返回无效序列号,继续循环等待。 - 关注一下使用顺序锁而非普通锁的原因。
- 写时间的进程少,读时间的进程多。
- 希望写进程优先于读进程(显而易见,写时间延迟的话会造成很大问题),而且不允许读者让写着饥饿。
- 写
- 如何从用户空间得到墙上时间?
从用户空间取得墙上时间的主要接口是gettimeofday()
,在内核中对应的系统调用是sys_gettimeofday()
,定义于kernel/time.c
中。它实现的逻辑是:- 如果用户提供的
tv
参数非空,那么与体系结构相关的do_gettimeofday()
函数将被调用; - 如果
tz
参数为空,该函数就把系统时区返回用户。
- 如果用户提供的
- 用户如何设置当前时间?
- 使用系统调用
settimeofday()
- 使用系统调用
7、定时器
- 什么是定时器?
定时器也称作为动态定时器或内核定时器,它是管理内核流逝时间的基础 - 定时器有什么作用?
例如:内核经常需要推后执行某些代码,而使用定时器就可以执行推后执行的工作。 - 如何使用定时器?
- 分为四步:1. 执行初始化工作;2. 设置超时时间;3. 指定超时发生执行的函数;4.激活定时器。
- 注意:指定的函数会在定时器到期自动执行,定时器不周期运行,在超时后就自行撤销。
-
使用定时器
- 定时器的定义
定时器结构由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; /* 定时器内部值,用户不要使用 */ };
- 定时器相关的接口
与定时器相关的接口,声明在linux/timer.h
,实现在kernel/timer.c
, 需要注意的是,内核可能延误一个节拍才执行处理函数,所有任何硬实时任务都不能用它。- 定义定时器结构
struct timer_list my_timer;
- 初始化定时器数据结构的内部值
init_timer(&my_timer);
- 填充数据
my_timer.expires = jiffies + delay; /* 定时器超时时间节拍数 */ my_timer.data = 0; /* 该参数可使用户利用同一个处理函数注册多个定时器,不需要则传递0或其他任何值 */ my_timer.function = my_function; /* 定时器超时处理函数 */
- 激活定时器
add_timer(&my_timer);
- 更改定时器超时时间
// 改变超时时间,如果定时器没被激活,则自动激活。之前未激活返回0 ,激活返回1 mod_timer(&my_timer, jiffies + new_delay); /* 新的定时值 */
- 在定时器超时之前删除定时器
// 在还未超时时可以删除定时器, // 激活未激活的都行(未激活返回0,否则返回1),但是超时就不用了因为已经自动删除了 del_timer(&my_timer); /* 能在中断上下文使用 */ // 删除定时器存在潜在竞争条件,删除定时器时可能要等待其他处理器上运行的定时器处理程序都退出 // 该函数不能在中断上下文中使用,因为它会造成睡眠。 del_timer_sync(&my_timer); /* 不能在中断上下文中使用,但优先使用*/
- 定义定时器结构
- 定时器的定义
-
定时器竞争条件
- 定时器与当前执行代码是异步的,可能存在潜在的竞争条件。以删除定时器为例,最好使用
del_timer_sync()
而非del_timer()
- 还要着重保护定时器中断处理程序中的共享数据。
- 定时器与当前执行代码是异步的,可能存在潜在的竞争条件。以删除定时器为例,最好使用
-
实现定时器
- 什么时候执行定时器?
内核在时钟中断发生后执行定时器,定时器作为软中断在下半部上下文中执行。 - 执行定时器的步骤是什么?
时钟中断处理程序会执行update_process_times()
,然后调用run_local_timers()
,run_local_timers
函数处理软中断TIMER_SOFTIRQ
,从而在当前处理器上运行所有的超时定时器。void run_local_timers(void) { hrtimer_run_queues(); raise_softirq(TIMER_SOFTIRQ); /* 执行定时器软中断 */ softlockup_tick(); }
- 搜索超时定时器的策略
- 内核按照定时器的超时时间将定时器分为5组,超时时间相近的定时器为一组
- 当定时器超时时间接近时,定时器将随组一起下移
- 为什么需要上述策略?
所有定时器以链表形式存放,寻找超时定时器而遍历链表是不明智的
- 什么时候执行定时器?
8、延迟执行
- 内核代码(尤其是驱动程序)除了使用定时器或下半部机制以外,还有其他方法推迟执行任务。这种延迟常用于等待硬件完成某些工作,而且等待时间非常短。 如重新设置以太网卡模式需要等待
2ms
。 - 内核提供多种延迟方法处理各种延迟要求,有些在延迟任务时挂起处理器,防止处理器执行任何实际工作;有些不会挂起处理器,所以也不能确保延迟代码能够在指定的延迟时间运行。
- 忙等待
- 最简单不理想的方法
延迟节拍的整数倍或者精确度要求不高的延时。例如:等待10
个节拍,处理器原地旋转。unsigned long timeout = jiffies + 10; /* ten ticks */ while (time_before(jiffies, timeout)) ;
- 更好的方法
在代码等待时,允许内核重新调度其他任务。由于需要调度程序,不能在中断上下文中使用,只能在进程上下文中使用。例如:unsigned long delay = jiffies + 5*HZ; while (time_before(jiffies, delay)) cond_resched(); // cond_resched将调度一个新程序投入运行,但需要设置完need_resched标志后才可生效 // 换句话说,系统中存在更重要的任务需要运行。
- 延迟执行都不应该在持有锁时或禁止中断时发生。
- 最简单不理想的方法
- 短延迟
- 什么情况下使用短延迟?
有时内核代码(通常时驱动程序)需要很短延迟而却要精确,多发生在与硬件同步时。大多小于1ms
,因此不能用jiffies
。内核提供三个可以处理ms
、us
、ns
级别的延迟函数,定义在linux/delay.h
和asm/delay.h
中。void udelay(unsigned long usecs) void ndelay(unsigned long nsecs) void mdelay(unsigned long msecs)
- 什么是
BogoMIPS
?
BogoMIPS
这一名字取自bogus
(伪的)和MIPS
(Million Instructions Per Second
)。取该名字的原因是:它记录的并不是机器性能而是在给定时间内忙循环执行的次数,处理器在空闲时速度有多快。该值存放在变量loops_per_jiffy
中,可以从文件/proc/cpuinfo
中读到。该变量可以提供精确延迟需要进行的循环数。
- 什么情况下使用短延迟?
schedule_timeout()
-
更理想的方法是使用
schedule_timeout()
函数,该方法会让需要延迟执行的任务睡眠到指定的延时时间耗尽后再重新运行。 -
睡眠时间不能保证等于指定的延时时间,只能尽量接近指定时间
-
在调用
shcedule_timeout
时,任务必须设置状态为TASK_INTERRUPTIBLE
或者TASK_UNINTERRUPTIBLE
。例如:/* 将任务设置为可中断睡眠状态 */ set_current_state(TASK_INTERRUPTIBLE); /* 小睡一会,"s"秒后唤醒 */ schedule_timeout(s*HZ);
schedule_timeout()
的实现signed long schedule_timeout(signed long timeout) { time_t timer;//创建了一个定时器timer unsigned long expire; switch(timeout) { case MAX_SCHEDULE_TIMEOUT://任务无限期休眠,不允许! schedule(); goto out; default: if(timeout < 0)//超时时间设置小于0,不允许! { printk(KERN_ERR "schedule_timeout: wrong timeout " "value %lx from %p\n", timeout, __builtin_return_address(0)); current->state = TASK_RUNNING; goto out; } } expire = timeout + jiffies; init_timer(&timer); timer.expires = expires; timer.data = (unsigned long)current; timer.function = process_timeout;//设置超时执行函数process_timeout() add_timer(&timer);//激活定时器 schedule();//调用schedule() del_timer_sync(&timer);//任务提前被唤醒,定时器被撤销 timeout = expire - jiffies;//剩余的时间 out: return timeout < 0 ? 0 : timeout; }
- 设置超时时间,在等待队列上睡眠。
等待队列中的任务可能既在等待一个特定事件到来,又在等待一个特定时间到期(取决于两者的速度)。在这种情况下,代码可以简单地使用schedule_timeout()
函数代替schedule()
函数。