文章目录
内核时钟的概念
硬件 为 内核提供系统定时器 用以计算流逝的时间,该时钟在内核可看成是一个电子时钟资源,比如数字时钟或处理器频率。
系统定时器以某种频率自行触发时钟中断,频率称为节拍率(tick rate),两次中断的间隔时间称为节拍(tick)。
内核就是靠这种已知时钟中断间隔来计算墙上时间(实际时间)和系统运行时间。
利用时钟中断周期执行的工作,每次时钟中断都要被时钟中断处理的任务:
- 更新系统运行时间
- 更新实际时间
- 在SMP上,均衡调度程序尽量使运行队列负载均衡
- 检查当前进程是否用尽了自己的时间片
- 运行超时的动态定时器
- 更新资源消耗和处理器时间统计值
节拍率(Hz)
系统定时器频率(节拍率)通过静态预处理定义。在asm/param.h文件中定义了这个值。与体系结构有关。
Hz值的选择
提高节拍率意味着时钟中断产生更加频繁,所以中断处理程序也会更频繁执行。
好处:
- 某些依赖定时值的系统调用以更高精度运行,如poll(), select()
- 提高进程抢占进度,加快调度响应时间
- 提供更高准确度的内核定时器
缺点:
- 系统负担变重,中断处理程序占用处理器时间越多,处理其他任务时间减少
- 频繁打乱处理器高速缓存,增加耗电
无节拍的操作系统
Linux内核支持“无节拍操作”,选项。编译内核时设置了CONFIG_HZ配置选项,系统根据这个选项动态调度时钟中断,不是固定间隔。例如果50ms内无事可做,内核以50ms重新调度时钟中断。
优点:开销小、省电。
全局变量 jiffies (unsigned long)
用来记录系统自启动以来产生节拍的总数,一秒内增加的值就是系统节拍率。
定义在linux/jiffies.h中,
extern unsigned long volatile jiffies;
jiffies与秒的转换,常用是把秒转化成jiffies,
// 将jiffies化成秒,用于内核和用户空间交互
jiffies/HZ
// 将秒化成jiffies,
seconds * HZ
// 常用把秒转化成jiffies举例
unsigned long time_stamp = jiffies; /* 现在*/
unsigned long next_tick = jiffies + 1; /* 从现在开始一个节拍 */
unsigned long later = jiffies + 5*HZ; /* 从现在开始5秒 */
unsigned long fraction = jiffies + HZ / 10; /* 从现在开始1/10秒 */
在32体系结构中jiffies是32位的,在时钟频率为100HZ时,497天溢出,1000HZ时49.7天溢出。
在64位上完全忽略溢出。
jiffies回绕
jiffies由于溢出后回绕为0,在比较大小时容易出问题,内核提供宏来正确处理节拍计数。
// 简化,便于理解的版本,实际复杂很多
#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)
用户空间和HZ
在2.6以前的版本中,改变内核的HZ值会影响用户空间的某些程序。
用USER_HZ代表用户空间看到的HZ,用 jiffies_to_clock_t()
将HZ转换成USER_HZ表示的节拍数。
硬时钟和定时器
实时时钟
实时时钟(RTC real-time clock)用来持久存放系统时间的硬件设备。主要作用是在系统启动时初始化墙上时钟。
在PC体系结构中,RTC和CMOS集成在一起,而且RTC运行和BIOS设置保存都是通过同一个电池供电。
系统定时器
系统定时器的根本思想是 提供一种周期性触发中断机制。
有对晶振分频实现定时器的,也有衰减值定时的。
x86中主要采用 可编程中断时钟(PIT),还有其他的时钟资源如本地APIC时钟和时间戳计数(TSC)等。
时钟中断处理程序
时钟中断处理程序可以划分为两个部分:体系相关部分 和 体系无关部分。
定时器
也称为动态定时器或内核定时器,是管理内核流逝时间的基础。
内核经常需要推后执行一些代码,而推后的本意是不在当前时间执行就行,实现需要借助定时器。
定时器使用简单,初始化操作、设置超时时间、指定超时后执行的函数、激活定时器就ok了。定时器不周期运行,超时后自行撤销,所以也叫动态定时器。
使用定时器
定时器结构由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); /* 不能在中断上下文中使用,但优先使用*/
定时器竞争条件
定时器与当前执行代码是异步的,可能存在潜在的竞争条件。
还要着重保护定时器中断处理程序中的共享数据。
实现定时器
内核在时钟中断发生后执行定时器,定时器作为软中断在下半部上下文中执行。
时钟中断处理程序会执行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组,超时时间相近的定时器为一组。确保内核在执行软中断的多数情况下减少搜索超时定时器的负担。
延迟执行
内核代码(尤其是驱动程序)除了使用定时器或下半部机制以外,还有其他方法推迟执行任务。
这种延迟常用于 等待硬件完成某些工作,而且等待时间非常短。 如重新设置以太网卡模式需要等待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记录处理器在给定时间内忙循环执行的次数。处理器在空闲时速度有多快。该值存放在变量loops_per_jiffy
中,可以从文件/proc/cpuinfo
中读到。
更理想的延迟方法schedule_timeout()
该方法会使需要延迟执行的任务睡眠到指定延迟时间耗尽后再重新运行。也不保证睡眠时间刚好等于,时间到期后唤醒延迟任务,重新放回运行队列。schedule_timeout需要调用调度程序,调用它的代码必须保证能够睡眠,也就是说 必须处于进程上下文并且不能持有锁。
/* 调用schedule_timeout之前必须设置任务为某个睡眠状态。将任务设置为可中断睡眠状态 */
set_current_state(TASK_INTERRUPTIBLE);
/* 小睡一会儿, s秒后唤醒 */
schedule_timeout(s * HZ);