《Linux 内核设计与实现》11. 定时器和时间管理

文章目录

    • 内核中时间的概念
    • 节拍率:HZ
      • 理想的 HZ 值
      • 高 HZ 的优势
      • 高 HZ 的劣势
    • jiffies
      • jiffies 的内部表示
      • jiffies 的回绕
      • 用户空间和 HZ
    • 硬时钟和定时器
      • 实时时钟
      • 系统定时器
    • 时钟中断处理程序
    • 实际时间
    • 定时器
      • 使用定时器
      • 定时器竞争条件
      • 实现定时器
    • 延迟执行
      • 忙等待
      • 短延迟
      • schedule_timeout()

内核中时间的概念

系统定时器是一种可编程芯片,它能以固定频率产生中断,这种频率称作节拍率(tick rate)。节拍率是可以自定义的,因此通过连续两次时钟中断可以知道时钟中断间隔的时间,这个间隔时间称作节拍(tick),它等于节拍率分之一,即 1 / ( t i c k   r a t e ) 1/(tick\ rate) 1/(tick rate) 秒。

通过节拍来计数墙上时间和系统运行时间:

  • 墙上时间:真实时间。
  • 系统运行时间:自系统启动以来所运行的时间。

利用时钟中断周期执行的工作:

《Linux 内核设计与实现》11. 定时器和时间管理_第1张图片

节拍率:HZ

系统定时器频率(节拍率)是可自定义的,即 HZ。一个周期为 1/HZ 秒,即产生时钟中断的间隔时间。例如:100HZ,在处理器上每秒时钟中断 100 次(1/100,即每 10ms 产生一次)。

《Linux 内核设计与实现》11. 定时器和时间管理_第2张图片

理想的 HZ 值

提高节拍率意味着时钟中断产生的更加频繁,所以中断处理程序也会频繁执行,好处如下:

  • 更高的时钟中断解析度(resolution)可提高时间驱动事件的解析度。
  • 提高了时间驱动事件的准确度。

高 HZ 的优势

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

《Linux 内核设计与实现》11. 定时器和时间管理_第3张图片

高 HZ 的劣势

节拍率越高,意味着时钟中断频率越高,也就意味着系统负担越重。

因为节拍率越高,处理器就被时钟中断处理程序占用了大量的时间。这样不仅减少了处理器处理其他的工作,还频繁地打乱处理器高速缓存并增加耗电。

现代计算机系统上,始终频率为 1000HZ 不会导致难以接受的负担,并不会对系统性能造成较大的影响。

无节拍的 OS:Linux 提供 CONFIG_HZ 配置选项,系统根据这个选项动态调度时钟中断。并非是固定节拍率,而是按需动态调度和重新设置。如:下一个时钟频率设置为 3ms,之后若 50ms 内都无事可做,内核将以 50ms 重新调度时钟中断。其优点:减少开销、省电。

jiffies

全局变量 jiffies 用来记录自系统启动以来产生的节拍的总数。每次触发时钟中断后,该值都会增加。因为一秒内时钟中断的次数等于 HZ,所以 jiffies 一秒内增加的值也就为 HZ。系统运行时间以秒为单位,就等于 jiffies/HZ。

路径:include\linux\jiffies.h

以秒为单位转 jiffies:(seconds * HZ)
jiffies 转换为以秒为单位的时间:(jiffies / HZ)

jiffies 的内部表示

jiffies 是无符号长整型,在 32 位,时钟频率为 100HZ 的情况下,497 天会溢出,1000HZ 的情况下,49.7 天就会溢出。64 位别指望溢出。

为了使 32 位系统和 64 位系统上的 jiffies 互相兼容,因此在 64 位 jiffies 中只使用低 32 位。

arch\x86\kernel\vmlinux.lds.S

#ifdef CONFIG_X86_32
OUTPUT_ARCH(i386)
ENTRY(phys_startup_32)
jiffies = jiffies_64;
#else
OUTPUT_ARCH(i386:x86-64)
ENTRY(phys_startup_64)
jiffies_64 = jiffies;
#endif

《Linux 内核设计与实现》11. 定时器和时间管理_第4张图片

访问 jiffies 的代码仅会读取 jiffies_64 的低 32 位。

获取整个 64 位的方法:

  • get_jiffies_64()
  • 直接读取 jiffies 变量

在 64 位系统上,jiffies_64 和 jiffies 指向的是同一个变量。

jiffies 的回绕

jiffies 达到最大值后,若持续增加,会绕回到 0。如果设置后 timeout(=jiffies+delay) 后,jiffies 绕回到 0,那么此时 jiffies 必然小于 timeout,而执行条件是 jiffies >= timeout,解决方案如下:

image-20230425085656592

/*
 *	These inlines deal with timer wrapping correctly. You are 
 *	strongly encouraged to use them
 *	1. Because people otherwise forget
 *	2. Because if the timer wrap changes in future you won't have to
 *	   alter your driver code.
 *
 * time_after(a,b) returns true if the time a is after time b.
 *
 * Do this with "<0" and ">=0" to only test the sign of the result. A
 * good compiler would generate better code (and a really good compiler
 * wouldn't care). Gcc is currently neither.
 */
#define time_after(a,b)		\
	(typecheck(unsigned long, a) && \
	 typecheck(unsigned long, b) && \
	 ((long)(b) - (long)(a) < 0))
#define time_before(a,b)	time_after(b,a)

#define time_after_eq(a,b)	\
	(typecheck(unsigned long, a) && \
	 typecheck(unsigned long, b) && \
	 ((long)(a) - (long)(b) >= 0))
#define time_before_eq(a,b)	time_after_eq(b,a)

用户空间和 HZ

内核以节拍数或秒的形式给用户空间导出这个值,应用程序依赖于这个值。若随意改变内核中的 HZ 值,而不及时更新用户空间的 HZ,则会给用户空间中的某些程序造成些异常结果。

USER_HZ 代表用户空间所用到的 HZ 值。jiffies_to_clock_t() 将一个由 HZ 表示的节拍计数转换成一个由 USER_HZ 表示的节拍计数。

HZ 和 USER_HZ 是整数倍:

return x / (HZ / USER_HZ);

jiffies_to_clock_t() 在 kernel/time.c

硬时钟和定时器

实时时钟

实时时钟 RTC 是用来持久存放系统时间的设备,即便系统关闭后,它也能依靠 CMOS 保持系统计时。

当系统启动时,内核通过读取 RTC 来初始化墙上时间,该时间存放在 xtime 变量中。通常内核不会在系统启动后再读取 xtime 变量,但有些体系结构会周期性地将当前时间值存回 RTC 中。

实时时钟最主要的作用是:启动时初始化 xtime 变量。

系统定时器

系统定时器提供了一种周期性触发中断的机制。实现方式:

  • 衰减测量器
  • 可编程中断时钟(PIT):内核启动时对 PIT 初始化,使其能够以 HZ/秒的频率产生时钟中断。

x86 中其它的时钟资源包括:本地 APIC 时钟和时间戳计数(TSC)等。

时钟中断处理程序

时钟中断处理程序划分为两个部分:体系结构相关的、体系结构无关的。

与体系结构相关的:

《Linux 内核设计与实现》11. 定时器和时间管理_第5张图片

与体系结构无关的:

《Linux 内核设计与实现》11. 定时器和时间管理_第6张图片

实际时间

当前实际时间定义在:kernel/time/timekeeping.c

struct timespec xtime;

timespec 数据结构定义在 include/linux/time.h 中

struct timespec {
	__kernel_time_t	tv_sec; /* seconds, 1970/1/1 起 */
	long		tv_nsec;    /* nanoseconds 记录自上一秒开始经过的 ns 数 */
};

更新 xtime 需要一个 seq 锁:

write_seqlock(&xtime_lock);
// change xtime...
write_sequnlock(&xtime_lock);

读取 xtime 时同理需要 read_seqbegin() 和 read_seqretry()。

《Linux 内核设计与实现》11. 定时器和时间管理_第7张图片

该循环不断重复,直到读者确认读取数据时没有写操作介入。若发现循环中有时钟中断处理程序更新了 xtime,那么 read_seqretry() 返回无效序列号,继续循环等待。

从用户空间得到墙上时间:gettimeofday(),对应内核系统调用 sys_gettimeofday()。

《Linux 内核设计与实现》11. 定时器和时间管理_第8张图片

定时器

路径:

  • include/linux/timer.h
  • kernel/timer.c

使用定时器

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

使用定时器需要注意:

  • 不能用定时器来执行任何硬实时任务。

定时器竞争条件

由于定时器和当前代码是异步的,因此存在潜在的竞争条件。因此决不能用下面的程序来代替 mod_timer()。

del_timer(my_timer);
my_timer->expires = jiffies + new_delay;
add_timer(my_timer);

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

由于内核异步执行中断处理程序,所以应该要保护好定时器中断处理程序中的共享数据。

实现定时器

内核正在时钟中断发生后执行定时器,定时器作为软中断在下半部上下文中执行。具体的,时钟中断处理程序会执行 update_process_time() 函数,该函数调用 run_local_timer():

《Linux 内核设计与实现》11. 定时器和时间管理_第9张图片

TIMER_SOFTIRQ 对应 run_timer_softirq() 软中断处理函数。

定时器都以链表形式存放在一起,但为了寻找一个定时器而遍历整个链表是不明智的。将超时时间排序也是不明智的。为了提高搜索效率,内核将定时器按超时时间划分为 5 组。当定时器超时时间接近时,定时器随组一起下降。采用分组定时器的方法可以在执行软中断的多数情况下,确保内核尽可能减少搜索超时定时器所带来的负担。

延迟执行

忙等待

  • 实现方式:循环,直到超过 timeout 为止。

    unsigned long timeout = jiffies + 10;
    while(time_before(jiffies, timeout));
    
  • 上面会导致处理器一直处于循环等待中。我们应该在代码中等待,即异步等待:

    unsigned long delay = jiffies + 5 * HZ;
    while(time_before(jiffies, delay)) cond_resched();
    

    cond_resched() 将调入一个新程序投入运行。注意,该函数需要调度程序,因此不可在中断上下文中被调用。

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

短延迟

  • 短延迟,等待时间往往小于 1ms。即便是 1000HZ,它的节拍间隔都有 1ms。

  • 内核提供 us、ns、ms 级别的延迟函数:

    // include/linux/delay.h
    static inline void ndelay(unsigned long x) {
    	udelay(DIV_ROUND_UP(x, 1000));
    }
    // arch/xtensa/include/asm/delay.h
    /* For SMP/NUMA systems, change boot_cpu_data to something like
     * local_cpu_data->... where local_cpu_data points to the current
     * cpu. */
    static __inline__ void udelay (unsigned long usecs) {
    	unsigned long start = xtensa_get_ccount();
    	unsigned long cycles = usecs * (loops_per_jiffy / (1000000UL / HZ));
    	/* Note: all variables are unsigned (can wrap around)! */
    	while (((unsigned long)xtensa_get_ccount()) - start < cycles)
    		;
    }
    // include/linux/delay.h
    #define mdelay(n) (\
    	(__builtin_constant_p(n) && (n)<=MAX_UDELAY_MS) ? udelay((n)*1000) : \
    	({unsigned long __ms=(n); while (__ms--) udelay(1000);}))
    

udelay() 依靠执行循环得到延迟效果,而 mdelay() 由依靠 udelay() 实现。因为内核知道处理器 1 秒内能执行多少次循环,所以 udelay() 仅需要根据指定的延迟时间在 1 秒中占的比例,就能决定需要循环多少次可以达到推迟时间。

通常,超过 1ms 延迟的别用 udelay()。

schedule_timeout()

  • 该函数让需要延迟执行的任务睡眠到指定延迟时间后再重新投入运行,即进入运行队列。

  • 用法

    // 将任务设置为可中断睡眠状态
    set_current_state(TASK_INTERRUPTIBLE);
    // 睡眠 s 秒
    schedule_timeout(s * HZ);
    
  • schedule_timeout() 的实现

    /**
     * schedule_timeout - sleep until timeout
     * @timeout: timeout value in jiffies
     *
     * Make the current task sleep until @timeout jiffies have
     * elapsed. The routine will return immediately unless
     * the current task state has been set (see set_current_state()).
     *
     * You can set the task state as follows -
     *
     * %TASK_UNINTERRUPTIBLE - at least @timeout jiffies are guaranteed to
     * pass before the routine returns. The routine will return 0
     *
     * %TASK_INTERRUPTIBLE - the routine may return early if a signal is
     * delivered to the current task. In this case the remaining time
     * in jiffies will be returned, or 0 if the timer expired in time
     *
     * The current task state is guaranteed to be TASK_RUNNING when this
     * routine returns.
     *
     * Specifying a @timeout value of %MAX_SCHEDULE_TIMEOUT will schedule
     * the CPU away without a bound on the timeout. In this case the return
     * value will be %MAX_SCHEDULE_TIMEOUT.
     *
     * In all cases the return value is guaranteed to be non-negative.
     */
    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 %lx\n", 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, false, TIMER_NOT_PINNED);
    	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;
    }
    

你可能感兴趣的:(Linux,Kernel,linux,Linux,Kernel,Linux,内核,Linux,内核设计与实现,定时器和时间管理)