【读书笔记】Linux内核设计与实现-定时器和时间管理

1.内核中的时间概念

硬件为内核提供了一个系统定时器用以计算流逝的时间,该时钟在内核中可看成是一个电子时间资源,比如数字时钟或处理器频率等。
系统定时器以某种频率自行触发(常被称为击中(hitting)或者射中(popping))时钟中断,该频率可以通过编程预定,称作节拍率(tick rate)。

内核维护两种时间:墙上时间和系统运行时间。

Q:何为墙上时间?
A:墙上时间也就是实际时间,对用户空间的应用程序来说是最重要的。

Q:何为系统运行时间?
A:系统运行时间也就是自系统启动开始所经的时间,对用户空间和内核都很有用。

Q:这两种时间内核如何维护计算?
A:通过预编的节拍率可以知道连续两次时钟中断的间隔时间(街拍(tick))。 这个间隔时间等于节拍率分之一(1/(tick rate))秒。

2.节拍率:HZ

系统定时器频率(节拍率)是通过静态预处理定义的,也就是HZ(赫兹),在系统启动时按照HZ值对硬件进行设置。定义在<asm/param.h>文件中。

ps:编写内核代码时候,不要认为HZ值是一个固定不变的值。因为大多数体系结构的节拍率是可调的。
在这里插入图片描述
ps:当系统定时器频率默认值为100时,即每秒钟时钟中断100次。也就是每10ms产生一次。

2.1 理想的HZ值–100HZ

2.2 高HZ的优势

更高的时钟中断频率和更高的准确度又会带来如下优点:

  1. 内核定时器能够以更高的频度和更高的准确度运行;
  2. 依赖定时值执行的系统调用,比如poll()和select(),能够以更高的精度运行;
  3. 提高进程抢占的准确度。

2.3 高HZ的劣势

节拍率越高,意味着时钟中断频率越高,也就意味着系统负担越重。
因为处理器必须花时间来执行时钟中断处理程序,所以节拍率越高,中断处理程序占用的处理器时间越多。
这样不但减少了处理器处理其他工作的时间,而且还会更频繁的打乱处理器高速缓存并增加耗电。

ps:无节拍的OS(操作系统)? – 省电
Linux内核支持“无节拍操作”这样的选项。
即当编译内核时设置了CONFIG_HZ配置选项,系统就根据这个选项动态调度时钟中断。并不是每隔固定的时间间隔(比如1ms)触发时钟中断,而是按需动态调度和重新设置。
如:如果下一个时钟频率设置为3ms,就每3ms触发一次时钟中断。之后,如果50ms内都无事可做,内核以50ms重新调度时钟中断。

3.jiffies

全局变量jiffies用来记录自系统启动以来产生的节拍的总数。
jiffies定义于文件<linux/jiffies.h>中:

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秒*/

ps:存放jiffies类型数据的时候必须用无符号长整型(unsigned long)

3.1 jiffies的内部表示

jiffies变量总是无符号长整数(unsigned long),因此在32位体系结构上是32位,在64位体系结构上是64位。
除了前面提到的jiffies的定义,第二个变量也定义在<linux/jiffies.h>中:

extern u64 jiffies_64;

在这里插入图片描述
ps:在64位体系结构上,jiffies_64和jiffies指的是同一个变量。
可参考源码:jiffies = jiffies_64

3.2 jiffies的回绕(wrap around)

和任何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参数是需要对比的值。
宏time_after(unknown,known),当时间unknown超过指定的known时,返回真,否则返回假;
宏time_before(unknown,known),当时间unknown没超过指定的known时,返回真,否则返回假。
后面两个宏作用和前面两个宏一样,只有当两个参数相等时,它们才返回真。

3.3 用户空间和HZ

内核时以节拍数/秒的形式给用户空间导出这个值的。
内核使用函数jiffies_64_to_clock_t()将64位的jiffies值的单位从HZ转换为USER_HZ。

ps:jiffies_to_clock_t()定义在kernel/time.c中。

4.硬时钟和定时器(RTC和系统定时器)

4.1 实时时钟(RTC)

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

当系统启动时,内核通过读取RTC来初始化墙上时间,该时间存放在xtime变量中。

ps:
1.内核通常不会再系统启动后在读取xtime变量。
2.rtc最主要的作用仍然是在启动时初始化xtime变量。

4.2 系统定时器

系统定时器的根本思想-提供一种周期性触发中断机制。

5.时钟中断处理程序

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

体系结构相关的部分作为系统定时器的中断处理程序而注册到内核中,以便在产生时钟中断时,它能够相应的运行。处理程序的具体工作依赖于特定的体系结构,但绝大多数最低限度也要执行如下工作:

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

tick_perodic()执行下面更多工作:

  1. 给jiffies_64变量增加1(这个操作即使是在32位体系结构上也是安全的,因为前面已经获得了xtime_lock锁);
  2. 更新资源消耗的统计值,比如当前进程所消耗的系统时间和用户时间;
  3. 执行已经到期的动态定时器。
  4. 执行sheduler_tick函数;
  5. 更新墙上时间,该时间存放在xtime变量中;
  6. 计算平均负载值。

6.实际时间

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

struct timespec xtime;

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

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

xtime.tv_sec 以秒为单位,存放着自1970年1月1日(UTC)以来经过的时间,1970年1月1日被称为纪元。
xtime.v_nsec记录自上一秒开始经过的ns数。

读写xtime变量需要使用xtime_lock锁,该锁不是普通自旋锁而是一个seqlock锁(顺序锁)。

从用户控件取得墙上时间的主要接口是gettimeofday(),在内核中对应系统调用为sys_gettimeofday(),定义于kernel/time.c
在这里插入图片描述
系统调用settimeofday()来设置当前时间,需要具有CAP_SYS_TIME权能。

ps:
除了更新xtime时间以外,内核不会像用户空间程序那样频繁使用xtime。但也有需要注意的特殊情况,那就是在文件系统的实现代码中存放访问时间戳(创建、存取、修改等)时需要使用xtime。

7.定时器–动态定时器/内核定时器

Q:定时器为何产生?
A:定时器是管理内核流逝的时间的基础。内核经常需要推后执行某些代码–不是有下半部机制吗?不幸的是,下半部机制并非是放到以后的某个时间去执行任务,而是仅仅是不在当前时间执行就可以了。
但我们所需要的是一种能够使工作在指定时间点上执行–不长不短,正好在希望的时间点上的工具–内核定时器。

ps:
定时器并不周期运行,超时后就自行撤销–称为动态定时器的一个原因。

7.1 使用定时器

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

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

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

函数描述
init_timer(struct timer_list *);初始化定时器数据结构的内部值
void (*function)(unsinged long);定时器处理回调函数
add_timer(struct timer_list *);激活定时器
mod_timer(struct timer_list *, unsinged long)更改已经激活的定时器超时时间,没激活的会激活
del_timer(struct timer_list *)在定时器超时前停止定时器
del_timer_sync(struct timer)list *)同del_timer(),但不可用于中断上下文

eg:

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);
mod_timer(&my_timer,jiffies+new_delay);

del_timer(&my_timer);

7.2 定时器竞争条件

因为定时器与当前代码是异步的,因此就可能存在潜在的竞争条件。

一般情况下应该使用del_timer_sync()函数取代del_timer()函数,因为无法确定在删除定时器时,它是否正在其他处理器上运行。

为了防止这种情况发生,应该调用del_timer_sync()函数,而不是del_timer()函数。否则,对定时器执行删除操作后,代码会继续执行,但它有可能会去操作在其他处理器上运行的定时器正在使用的资源,因而造成并发访问。

ps:内核异步执行中断处理程序,需要保护定时器中断处理程序中的共享数据。

7.3 实现定时器

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

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

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

run_timer_softirq()函数处理软中断TIMER_SOFTIRQ,从而在当前处理器上运行所有的(如果有的话)超时定时器。

8.延迟执行

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

8.1 忙等待–(节拍整数倍+不精确)

最简单的延时方法(通常也是最不理想的办法)是忙等待(或者说忙循环)。

Q:如何实现忙等待(忙循环)?
A:在循环中不断旋转直到希望的时钟节拍数耗尽。

unsigned long timeout = jiffies+10;	/* 10 个节拍 */
while(time_before(jiffies, timeout));

ps:
要注意该方法仅仅在想要延时的时间是节拍的整数倍,或者精确率要求不高的时才可以使用。

当代码等待时,处理器只能在原地旋转等待–它不会去处理其他任何任务。

Q:如何改进?
A:在代码等待时,允许内核重新调度执行其他任务。

unsigned long delay = jiffies + 5*HZ;

while(time_before(jiffies,delay))
	cond_resched();

cond_resched()函数将调度一个新程序投入运行,但它只有在设置完need_resched标志后才能生效。也就是该方法有效的条件是系统中存在更重要的任务需要运行

注意:因为该方法需要调用调度程序,所以它不能再中断上下文中使用。

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

Q:怎么能保证前面的循环已经执行了?
A:C编译器通常只将变量装载一次。一般情况下不能保证循环中的jiffies变量在每次循环中读取时都重新被载入。但是要求jiffies在每次循环时都必须重新装载,因为在后台jiffies值会随时钟中断的发生而不断增加。为了解决这个问题,<linux/jiffies.h>中jiffies变量被标记未关键字volatile。

关键字volatile指示编译器在每次访问变量时都重新从主内存中获得,而不是通过寄存器中的变量别名来访问,从而确保前面的循环能按预期的方式进行。

8.2 短延迟–delay类函数(短+精确)

有时候内核代码不但需要很短暂的延时(比时钟节拍还短加粗样式),而且还要求延时的事件很精确

内核提供了三个可以处理us、ns和ms级别的延时函数,定义在文件<linux/delay.h>和<asm/delay.h>中,可以看到它们并不使用jiffies:

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

udelay()函数依靠执行数次循环达到延迟效果;
mdelay()函数通过udelay()函数实现。

why?
因为内核知道处理器在1秒内能执行多少次循环–BogoMIPS值,所以udelay()函数仅仅需要根据指定的延时时间在1秒中占的比例,就能决定需要进行多少次循环即可达到要求的推迟时间。

Q:何为BogoMIPS?
A:
BogoMIPS主要被udelay()函数和mdelay()函数使用。
BogoMIPS值记录处理器在给定时间内忙循环执行的次数。即BogoMIPS记录处理器在空闲时速度有多快,该值存放在变量loops_per_jiffy中,可以从文件/proc/cpuinfo中读到它。
延时循环函数使用loops_per_jiffy值来计算为提供精确延迟而需要进行多少次循环。
内核启动时利用calibrate_delay()计算loops_per_jiffy值,该函数在文件init/main.c中。
在这里插入图片描述

8.3 schedule_timeout()–更理想的延时执行方法

schedule_timeout()函数会让需要延时执行的任务睡眠到指定的延时时间耗尽后再重新运行。当指定的时间到期后,内核唤醒被延时的任务并将其重新放回运行队列。

ps:不能保证睡眠时间正好等于指定的延时时间,只能尽量使睡眠时间接近指定的延时时间。

Q:如何使用?
A:

/* 将人物设置为可中断睡眠状态 */
set_current_state(TASK_INTERRUPTIBLE);

/* 小睡一会儿, s 秒后唤醒 */
schedule_timeout(s*HZ);

schedule_timeout()函数单位为jiffies,即节拍数。

这里的睡眠状态分为可中断和不可中断睡眠。也就是在调用schedule_timeout()函数前必须首先将任务设置成这两种状态之一,否则任务不会睡眠。

注意:由于schedule_timeout()函数需要调用调度程序,所以调用它的代码必须保证能够睡眠。即调用代码必须处于进程上下文,并且不能持有锁。

ps:若在进程上下文中的代码为了等待特定事件发生,可以将自己放入等待队列,然后调用调度程序去执行新任务,一旦事件发生后,内核调用wake_up()函数唤醒在睡眠队列上的任务,使其重新投入运行。

Q:为啥有时候代码需要检查被唤醒的原因?
A:有时等待队列上的某个任务可能既在等待一个特定事件到来,又在等待一个特定事件到期–就看谁来得更快。这种情况下,代码可以简单地使用schedule_timeout()函数代替schedule()函数,这样一来,当希望的指定时间到期,任务都会被唤醒。所以,代码需要检查被唤醒的原因(有可能是被事件唤醒,有可能是因为延时的时间到期,有可能是接收了信号等),然后执行相应操作。

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

wang 恒

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值