1.内核中时间的概念
(1)内核中有大量事件是基于时间驱动的,相对时间和绝对时间这两个概念对内核时间管理来说都至关重要。
(2)系统定时器和时钟中断处理程序是Linux系统内核管理机制中的中枢。内核必须在硬件的帮助下才能计算和管理时间,定时器以某种频率自行触发时钟中断。当中断发生时,内核就通过一种特殊的中断处理程序对其进行处理。
(3)内核可以动态创建和撤销动态定时器--- 一种用来推迟执行程序的工具。
墙上时间:就是实际时间,对用户空间的应用程序很重要,可以用来维护实际日期和时间。
系统运行时间:自系统启动以来经历的时间,对内核和用户空间都有用。
2.HZ
系统定时器频率(节拍率),是通过静态预处理定义的。在 定义,对用户空间,内核HZ几乎完全隐藏,确切的 HZ 值只能通过 /proc/interrupts 获得:=(/proc/interrupts) / (/proc/uptime) ,自开机以来系统的滴答数除以运行时间.
更高的节拍率意味着时钟产生更加频繁,中断处理程序也会执行更频繁。更高的时钟中断解析度可提高时间事件驱动时间的解析度和准确度(时钟中断执行准确度是+/-1/(2HZ))。
高HZ优势:
①内核定时器能够以更高的频率和更高的准确度运行;
②依赖定时值执行的系统调用,比如poll()和select(),能够以更高的精度运行;
③对诸如资源消耗和系统运行时间等的测量会有更精细的解析度;
④提高进程抢占的准确度;
高HZ劣势
节拍率越高,时钟中断频率越高,中断程序占用处理器时间就越多。这样不但减少了处理器工作时间,还更频繁的打乱了处理器高速缓存,增加功耗。
无节拍OS
当内核配置CONFIG_HZ时,系统根据这个选项动态调度时钟中断,这样实质性受益是省电。
3.jiffies
(1)用来记录自系统启动以来产生的节拍总数,转化为时间(jiffies/HZ)
Jiffies总是全局的unsigned long变量,在32位体系结构上,它就是32位,在HZ=100时,497天溢出。在64位体系上是64位,我们看不到溢出。通过连接,jiffies被jiffies_64覆盖(在64位机两者相同,在32位机jiffies取jiffies_64低32位)。通过函数get_jiffies_64()可以读取完整64位jiffies值。
(2)为防止溢出问题,内核提供几个宏来比较节拍计数
此处)折叠或打开
- time_after(unknown,known);//当jiffies超过指定known时,返回真
- time_before(unknown,known)
- time_after_eq(unknown,known)
- time_before_eq(unknown,known)
- unknown通常是jiffies,known = jiffies+HZ/2
(3)用户空间和HZ
内核定义USER_HZ给应用层,当USER_HZ小于等于HZ,且两者互为整数倍时返回给应用的时间是 return x/(HZ/USER_HZ)
内核使用函数jiffies_64_to_clock_t()将64位jiffies从HZ转换到USER_HZ
此处)折叠或打开
- unsigned long start;
- unsigned long total_time;
- start = jiffies;
- /* do some work ... */
- total_time = jiffies - start;
- printk(“That took %lu ticksn”, jiffies_to_clock_t(total_time));
- printk(“That took %lu secondsn”, total_time / HZ);
4.硬时钟和定时器
(1)实时时钟(RTC)用来持久存放系统时间,系统启动时,内核读取RTC来初始化墙上时间,该时间存放在xtime变量中。
(2)系统定时器,在X86中,采用可编程中断时钟(PIT)作为时钟中断源。
5.时钟中断处理函数
分两部分:体系结构相关和体系结构无关。
(1)体系结构相关例程,作为系统定时器的中断处理函数注册到内核,主要完成以下工作:
获取xtime_lock锁,以便对访问jiffies_64和墙上时间xtime进行保护;
需要时应答或重新设置系统时钟;
周期性地使用墙上时间更新实时时钟;
调用体系无关时钟例程:tick_periodic().
(2)中断主要通过体系无关例程tick_periodic()完成更多工作
给jiffies_64变量加1;
更新资源消耗的统计值,比如当前进程所消耗的系统时间和用户时间;
执行已经到期的动态定时器;
执行sheduler_tick()函数;
更新墙上时间,该事件存放在xtime中
此处)折叠或打开
- /*
- * Periodic tick
- */
- static void tick_periodic(int cpu)
- {
- if (tick_do_timer_cpu == cpu) {
- write_seqlock(&xtime_lock);
- /* Keep track of the next tick event */
- tick_next_period = ktime_add(tick_next_period, tick_period);
- do_timer(1); //操作jiffies_64,更新墙上时间,更新系统平均负载
- write_sequnlock(&xtime_lock);
- }
- update_process_times(user_mode(get_irq_regs()));//更新所消耗的各种节拍数
- profile_tick(CPU_PROFILING);
- }
对进程进行时间计数,是根据中断发生时处理器所处模式统计的,它把上个节拍全部算给了进程。这种粒度的进程统计方式是传统unix具有的,现在还没有更加精密的统计算法。
run_lock_timer(),标记一个软中断去处理所有到期定时器。
以上全部工作每1/HZ秒发生一次。
6.实际时间
当前实际时间(墙上时间)定义在文件kernel/time/timekeeping.c中
此处)折叠或打开
- struct timespec xtime;
- timespec 定义在<linux/time.h>
- struct timespec {
- time_t tv_sec; //秒,存放自1970年1月1日(utc)以来经过的时间
- long tv_nsec; //自从上一秒开始结果的ns数
- };
- 读写xtime变量需要使用xtime_lock锁,一个seqlock锁。
- write_seqlock(&xtime_lock);
- /* update xtime ... */
- write_sequnlock(&xtime_lock);
- 读取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 * (1000000 / HZ);
- sec = xtime.tv_sec;usec += (xtime.tv_nsec / 1000);
- } while (read_seqretry(&xtime_lock, seq));
在用户空间取得墙上时间用gettimeofday(),其在内核中调用sys_systimeofday()实现,可以用settimtofday()来设置当前时间,它需要有CAP_SYS_TIME权能。
7.定时器
指定函数将在定时器到期时自动异步执行,执行后自动撤销。
定义在文件 中
此处)折叠或打开
- struct timer_list {
- struct list_head entry; //定时器链表的入口
- unsigned long expires; //以jiffes为单位的计时值
- void (*function)(unsigned long); //定时器处理函数
- unsigned long data; //传给处理函数的参数
- struct tvec_base *base; //定时器内部值,用户不使用
- };
用户一般不操作这些结构体,而是通过内核定义好的一系列宏来处理
此处)折叠或打开
- 定义定时器:
- struct timer_list my_timer;
- 初始化定时器:
- init_timer(&my_timer);
- 填充定时器结构体:
- my_timer.expires = jiffes+HZ;//延时1s
- my_timer.data = NULL; //可以利用同一个处理器函数注册多个定时器
- my_timer.function = my_function;//定时器处理函数
- 内核可以保证不会在超时时间到期前运行处理器函数,但是有可能会延误执行,误差是+/-半个节拍,所以定时器不能用来做硬实时任务。
- 激活定时器
- add_timer(&my_timer);
- mod_timer(&my_timer);//激活或修改定时器,若调用时已激活,返回0,否则返回1
- del_timer(&my_timer);//删除定时器,定时器未被激活返回0,否则返回1
- 为了防止删除定时器时,定时器在多处理器上可能正在运行,即出现竞争条件,使用
- del_timer_sync(&my_timer);//不能在中断上下文使用
在中断处理函数中,要保护好共享数据。
定时器作为软中断在下半部上下文中执行,在run_local_timers()中调用所有到时定时器。
内核采用分组定时器方法来管理定时器,可以减少搜索超时定时器所带来的负担。
8.延迟执行
(1)忙等待
该方法用于延迟时间是节拍的整数倍,或对精确度不高时使用
unsigned long timeout = jiffies+10;//延迟10个节拍
while(time_befor(jiffes,timeout)) ;
这是最简单的延迟方法,但是延迟独占CPU,很少用。改进版是:
unsigned long delay = jiffies + 5*HZ;
while (time_befor(jiffies,delay))
cond_resched();//调度一个新程序运行,该方法有效条件是系统中存在更重要的任务要运行
因为调用了调度程序,所以不能在中断上下文使用,只能在进程上下文使用。所有延迟方法在进程上下文都使用的很好,中断处理都应该尽快的执行,最好不用延迟。
延迟不管在哪种情况下,都不应该在持有锁时或禁止中断时使用。
(2)短延迟,比节拍短,短暂又精确的延迟
此处)折叠或打开
- 定义在<linux/delay.h>和<asm/delay.h>
- void udelay(unsigned long usecs);
- void ndelay(unsigned long usecs);
- void mdelay(unsigned long usecs);
udelay是由忙循环实现的,mdelay调用udelay实现,但是会睡眠,1ms以上的延迟就不要用udelay(),防止溢出。
BogoMIPS值记录处理器在给定时间内忙循环执行的次数,存放在loops_per_jiffy中
(3)schedule_timeout()
更理想的方案是schedule_timeout()函数,让需要延迟睡眠执行的任务睡眠到指定延迟时间耗尽再重新执行,同样,只保证延迟时间尽量接近睡眠时间,时间到期后,内核唤醒被延迟任务并将其重新放回运行队列。
set_current_state(TASK_INTERRUPTIBLE);//将任务设置为可中断睡眠状态
schedule_timeout(s*HZ); //睡眠s秒
①schedule_timeout()的实现
此处)折叠或打开
- signed long __sched schedule_timeout(signed long timeout)
- {
- struct timer_list timer;
- unsigned long expire;
- switch (timeout)
- {
- case MAX_SCHEDULE_TIMEOUT: //用来检查是否无限期睡眠
- /*
- * These two special cases are useful to be comfortable
- * in the caller. Nothing more. We could take
- * MAX_SCHEDULE_TIMEOUT from one of the negative value
- * but I' d like to return a valid offset (>=0) to allow
- * the caller to do everything it want with the retval.
- */
- schedule();
- goto out;
- default:
- /*
- * Another bit of PARANOID. Note that the retval will be
- * 0 since no piece of kernel is supposed to do a check
- * for a negative retval of schedule_timeout() (since it
- * should never happens anyway). You just have the printk()
- * that will tell you if something is gone wrong and where.
- */
- if (timeout < 0) {
- printk(KERN_ERR "schedule_timeout: wrong timeout "
- "value %lxn", timeout);
- dump_stack();
- current->state = TASK_RUNNING;
- goto out;
- }
- }
- expire = timeout + jiffies; //设置超时时间
- setup_timer_on_stack(&timer, process_timeout, (unsigned long)current);//设置超时函数
- __mod_timer(&timer, expire);
- schedule();//
- del_singleshot_timer_sync(&timer);
- /* Remove the timer from the object tracker */
- destroy_timer_on_stack(&timer);
- timeout = expire - jiffies;
- out:
- return timeout < 0 ? 0 : timeout;
- }
若任务提前唤醒(比如收到信号),那么定时器被撤销,返回剩余时间。
②设置超时时间,在等待队列上睡眠
有时,等待队列上的某个任务可能既在等待一个特定事件到来,又在等待一个特定时间到期(看谁先到),这样可以使用schedule_timeout()代替schedule().当然,需要检查被唤醒的原因(可能是信号,也可能是时间到),然后执行相应操作。