Linux时钟实现和管理(Linux Kernel development 3rd)

删除回忆录丶 提交于 2020-08-09 17:34:14
  1. 简介

    • 时间间隔

      • 这个概念在内核中非常重要。大量多的延时函数都是依赖于时间。

      • 周期性函数

        • 进程调度
        • 屏幕刷新
        • 延时硬盘读写
        • 系统从开机到现在运行了多久
        • 当前的日期
      • 上面列举的都是用到周期的
    • 过去了多久,时间在系统中该怎么衡量核心问题。

    • 相对时间

      • 从某一刻起后多久开始做某事。

      五秒后发射就是相对于现在一段时间。

    • 绝对时间

      日期,经历了多久,往往是一个大的时间。

  2. 周期任务和延迟任务

    • 周期任务

      • 依赖于系统时钟,系统时钟是一个可编程硬件。每隔固定时间就发起中断。
      • 内核知道每秒多少次中断,根据间隔和中断次数,可以用来衡量时间。
    • 系统时钟是核心

      • 这是一个硬件。
      • 一个可编程硬件,即可以配置,可以有内核控制。
      • 用来计量时间流逝。
      • 隔一段时间发起一次中断,中断捕获就更新时间。然后执行对应的函数。
    • 延迟任务

      • 一般是事件,时间发生多久后执行某个响应函数。

      • 主要依赖于动态定时器

    • 动态定时器

      • 从开始计时的那一刻开始,多久后执行某个任务。

      • 这也是后面的主讲。
      • 案例

      软盘驱动在一定时间得不到响应就关闭。

  3. 时间衡量

    • 依赖硬件

      系统时钟

    • 时钟频率

      可配置

    • 转换

      1s = 时钟周期 * 时钟频率
      时钟周期 = 1s / 时钟频率
      总的时间 = 时钟周期 * 次数
      
    • 根据换算方程可以看到时钟周期和过去时间

  4. 动态定时器

    • 定时器

    用来倒计时的,倒计时多久执行某个任务。

    这个也是主要的核心中的核心。

  5. 内核管理和衡量时间

    • 衡量时间

      借助硬件系统时钟的周期性中断和次数来衡量。

      根据配置的频率,按照对应周期触发中断。这个中断由特殊的中断处理器处理。

      • 查看中断
        cat /proc/interrupts
        
      • 系统时间和已经开机时间也是借助中断次数来衡量。
    • 日期

      • 内核需要根据系统时钟来更新系统日期信息。
      • 日期对用户程序非常重要。
    • 系统运行时间

      从成功引导到现在过了多久。

      这个信息对内核和用户程序都很重要。
    • 相对时间

      两个时间戳之间做差就是相对时间。

  6. 时钟中断典型应用

    • 更细系统运行时间

    • 更新系统日期信息

    • 多处理器用于多处理器之间处理任务的负载平衡。

    • 处理动态定时器及其事件

    • 更新资源的使用情况和处理器时间统计

    处理器时间指的是某个进程在这个处理器上执行了多久,是否应该让出资源,让下一个任务执行。

    <span style = "color:red">部分任务在每次触发中断的时候执行,有的则是触发n次中断后才执行。</span>
  7. 时钟频率HZ

    • 配置

      • 时钟中断的频率在系统引导模块被定义,这个值是一个宏,编译的时候已经确定。

        /*
         *  arch/arm/include/asm/param.h
         *
         *  Copyright (C) 1995-1999 Russell King
         *
         * This program is free software; you can redistribute it and/or modify
         * it under the terms of the GNU General Public License version 2 as
         * published by the Free Software Foundation.
         */
        #ifndef __ASM_PARAM_H
        #define __ASM_PARAM_H
        
        #ifdef __KERNEL__
        # define HZ		CONFIG_HZ	/* Internal kernel timer frequency */
        # define USER_HZ	100		/* User interfaces are in "ticks" */
        # define CLOCKS_PER_SEC	(USER_HZ)	/* like times() */
        #else
        # define HZ		100
        #endif
        
        #define EXEC_PAGESIZE	4096
        
        #ifndef NOGROUP
        #define NOGROUP         (-1)
        #endif
        
        /* max length of hostname */
        #define MAXHOSTNAMELEN  64
        
        #endif
        
      • 频率一般来说是不一样的,和架构,甚至和硬件也不一样。
    • 换算关系

      时钟周期 = 1 / 时钟频率
      时间周期是变量
      时钟周期 =  F(时钟频率) = 1 / 时钟频率
      
      • 一般来说是100,当然也可以配置为其他的值。
    • 重要性

      • 影响整个系统的执行效率。
      • 影响整个系统的执行交互性和流畅度。
    • 理想频率

      • Linuxi386版本,时钟中断都是用的100,但是在开发2.5系列的时候,频率提高到了1000,这个提升在当时是有争议的.

      • 现在的频率虽然也是100hz了,但是后来增加了配置选项,允许用户自己设置频率。
      • 时钟频率对整个系统的影响巨大,所以修改之前需要想好。
      • 增大带来的一些影响。

      • 时钟中断处理函数执行得更频繁。

        中断函数要干很多的事情。

      • 时钟频率变大,中段处理器的时间精确度更高,依赖于时钟的定时器时间执行的时间越精确。

      • 定时器相关的事件的精准度也提升。

      • 精确

      • 100hz的时钟频率最小精度为10ms,而1000hz的最小精度为1ms
      • 虽然说内核可以提供1ms的中断,但是整个系统的执行效果并不是说比100hz的就好很多。
      • 精准

      • 定时器在任意时刻启动,执行的误差范围在半个时钟周期内。也就是说,执行超时了,但是想要执行任务需要等时钟中断函数来处理,时钟中断函数的执行需要等到时钟中断触发的时候才能执行。
      • 但是时钟中断是有周期的,就先等着,等着到了时钟中断来了再执行。
      • 所以这种就会有差不多半个周期的误差,这个误差再频率高的时候就越小,频率低的时候越大。
    • 高时钟频率的好处

      • 内核定时器执行得更加精确精准。

      • poll and select这两个依赖于定时器事件的系统调用执行的效率更高。

      • 对于资源的统计,系统执行事件,也更加的精准。

      • 进程的占用处理器的执行时长调度更加准确。

      • 较大提升的有

        • poll select的超时,这种非阻塞的IO,很依赖于超时的准确。前面说的超时等待有误差,对于这种频繁调用系统接口的误差则是更大。
        • 另一个是进程调度,每一次时钟中断的时候就会将那些正在使用处理器的进程的市场减小,当为0,且当有nedd_sched标识置为一的时候就进行进程切换。这个和定时器也有些类似,比如一个进程拥有2ms的执行时长,但是最小精度是10ms,那么有8ms等待时间,这个就是误差。但是这种误差是都有的误差,都可以接收的。而且在有些地方这种误差是良性的。
    • 高频率的坏处

      • 高频率的中断也就意味着频繁的执行中断处理函数,也就代表着高的开销。处理器主要是为用户进程服务的,这个开销太大,导致用户进程占用的时间变少。而且还会导致处理器的缓存命中率降低。频繁的执行中断,会引发用户热数据被不断的置换出三级缓存,导致了执行效率降低。而且耗电。

      • 后面的操作系统和设备的升级1000hz的开销也不是不可以接收。

    • 无频率的操作系统

      • 时钟周期中断真的需要吗?
      • 虽然一直以来都是使用的时钟中断的操作系统。但是也是有不需要时钟中断的操作系统的。而且Linux也支持这种系统。编译的时候配置。
      • Linux编译的时候配置宏CONFIG_HZ,那么内核就是动态的执行中断。也就不是以前的定期的执行,而是想什么时候执行就什么时候执行。比如3ms 后 50ms都是可能的。
      • 这种系统不但减少了开支,而且还省电,尤其是在空转的时候,大家都休息。
      • 标准的时钟中断系统需要为时钟中断服务,哪怕是在空转的时候,而这种系统则不需要,在空转的时候就屏蔽时钟中断。
  8. 时钟计数Jiffies

    • 系统运行时间

      • 系统开机了有多久,这个信息也是很多用户进程和内核都需要的一个信息。

      • 通过计算

      时钟周期 = 1 / 时钟频率
      开机时长 = 时钟周期 * 中断次数
             = 中断次数 / 时钟频率
      
      • 在引导系统的时候赋予初值0,每次时钟中断触发,就会加一。

      • 显示使用中,这个变量比较复杂,这个变量的初始值会设置为一个非0值,很大,让这个变量自动溢出,以此来检测bug.当要求实际的值得时候,就减去初始值就可以了。

    • 定义

      /*
      * The 64-bit value is not atomic - you MUST NOT read it
      * without sampling the sequence number in xtime_lock.
      * get_jiffies_64() will do this for you as appropriate.
      */
      extern u64 __jiffy_data jiffies_64;
      extern unsigned long volatile __jiffy_data jiffies;
      
      #if (BITS_PER_LONG < 64)
      // 如果32位
      u64 get_jiffies_64(void);
      #else
      static inline u64 get_jiffies_64(void)
      {
      	return (u64)jiffies;
      }
      #endif
      
    • 与时间之间的转换

      unsigned long time_stamp = jiffies; /* now */
      unsigned long next_tick = jiffies + 1; /* one tick from now */
      unsigned long later = jiffies + 5*HZ; /* five seconds from now */
      unsigned long fraction = jiffies + HZ / 10; /* a tenth of a second from now */
      
    • 内部的表达方式

      • 溢出问题

        32位100hz系统溢出需要497天,321000hz系统溢出需要49.7天,64就永不溢出。

        linuxunsigned long32位是4字节,64位是8字节,windows不论32还是64都是4字节。
      • 链接实体

        //linux-2.6.39\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
        
      • 原子操作即数据总线携带的数据的位数小于等于总线带宽。

        • 32位的系统,操作64位的数据,则需要用到顺序锁辅助。
        #if (BITS_PER_LONG < 64)
        u64 get_jiffies_64(void)
        {
        	unsigned long seq;
        	u64 ret;
        
        	do {
        		seq = read_seqbegin(&xtime_lock);
        		ret = jiffies_64;
        	} while (read_seqretry(&xtime_lock, seq));
        	return ret;
        }
        EXPORT_SYMBOL(get_jiffies_64);
        #endif
        
        • 上面用到了顺序锁。
      • 32位的时候衡量过去了多久用的是32位的jiffies,而且用的还是64jiffies_64的低32位值,时间管理则用jiffies_64,64的时候就是一个东西了。

      • jiffies值溢出回绕

        #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)
        

        由于溢出可能会带来问题,所以用这种方式来进性比较。

    • 用户应用于频率

      • 2.6以前用户应用也会使用到频率。这个值由内核暴露给用户空间。这些接口永久的不变,即返回固定值。那如果你改变了hz,但是这个函数没有改变,就导致了用户读取错误。比如1000hz2h,用户读取到的就是20h.

      • 为了防止这个问题,内核就进性了等比例收缩jiffies.定义一个用户空间USER_HZ.然后定义了一个HZ用于内核。通过算式转换为等价值。

        clock_t jiffies_to_clock_t(long x)
        {
        #if (TICK_NSEC % (NSEC_PER_SEC / USER_HZ)) == 0
        # if HZ < USER_HZ
        	return x * (USER_HZ / HZ);
        # else
        	return x / (HZ / USER_HZ);
        # endif
        #else
        	return div_u64((u64)x * TICK_NSEC, NSEC_PER_SEC / USER_HZ);
        #endif
        }
        unsigned long clock_t_to_jiffies(unsigned long x)
        {
        #if (HZ % USER_HZ)==0
        	if (x >= ~0UL / (HZ / USER_HZ))
        		return ~0UL;
        	return x * (HZ / USER_HZ);
        #else
        	/* Don't worry about loss of precision here .. */
        	if (x >= ~0UL / HZ * USER_HZ)
        		return ~0UL;
        
        	/* .. but do try to contain it here */
        	return div_u64((u64)x * HZ, USER_HZ);
        #endif
        }
        
    • 硬件时钟和定时器

      • 处理器提供了两种时钟

        • 系统可编程时钟。
        • <span style="color:red">实时时钟</span>
      • 目的差不多,实现差异比较大。

    • <span style="color:red">实时时钟</span>

      • 为系统提供并存储时间的一种稳定设备。

      • 即使系统关机,主板上的纽扣电池也可以为这个硬件供电,以保证正常运行。

      • 电脑中RTCCMOS被继承到一起,这个简单的纽扣电池保证RTC的执行和BIOS的数据被保存。

        • RTC实时时钟,主板上的硬件

        • CMOS存储BIOSRTC值的地方,一种半导体硬件。

      • 系统引导的时候就会读取这个RTC值,主要是用来初始化日期,日期数据存放在xtime中。一般内核就读取以一次。

    • <span style="color:red">系统可编程时钟</span>

      • 主要的中断硬件。
      • 不管什么架构,目的都是相同的

        为系统提供一个周期性的中断。不断的提醒系统时间到了,该做什么了。

      • 时钟中断的两种实现

        • 通过电子时钟即按照给定频率震荡的方式。
        • 另一种就是一个倒计时值,值到了0就发起中断。
      • PIT : programmable interrupt timer

        • 可编程中断时钟,在DOS之后几乎所有的硬件都支持。

        • 引导内核的时候就已经配置好硬件。然后硬件就按照这个频率进性周期性的中断。

  9. 时钟中断处理函数

    • 时钟中断的两部分

    • 依赖于处理器架构部分。

    • 不依赖处理器架构部分。

    • 依赖于处理器架构部分

    • 这个是主体部分,中断触发了,先执行这个部分,具体的工作根据架构不同而不同。
    • 大部分的架构工作都是相似的。

      • 获取xtime_lock这个锁,用于安全访问变量jiffies_64,以及日期变量xtime.
      • 有必要还会通知系统时钟或者重置系统时钟。
      • 周期性的保存实时时钟的值,即真正的日期值,用于校正。
      • 调用与架构无关部分的代码tick_periodic,即通用部分。
    • 不依赖处理器架构部分tick_periodic

    • jiffies_64安全加一。
    • 更新资源使用信息,比如当前运行进程的用户时间执行了多久,以及消耗的系统资源。
    • 执行已经超时了的动态定时器的回调函数。
    • 调用scheduler_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);
    		write_sequnlock(&xtime_lock);
    	}
    
    	update_process_times(user_mode(get_irq_regs()));
    	profile_tick(CPU_PROFILING);
    }
    
    
    /*
    * The 64-bit jiffies value is not atomic - you MUST NOT read it
    * without sampling the sequence number in xtime_lock.
    * jiffies is defined in the linker script...
    */
    void do_timer(unsigned long ticks)
    {
    	jiffies_64 += ticks;
    	update_wall_time();
    	calc_global_load(ticks);
    }
    
    /**
    * update_wall_time - Uses the current clocksource to increment the wall time
    *
    * Called from the timer interrupt, must hold a write on xtime_lock.
    */
    static void update_wall_time(void)
    {
    	struct clocksource *clock;
    	cycle_t offset;
    	int shift = 0, maxshift;
    
    	/* Make sure we're fully resumed: */
    	if (unlikely(timekeeping_suspended))
    		return;
    
    	clock = timekeeper.clock;
    
    #ifdef CONFIG_ARCH_USES_GETTIMEOFFSET
    	offset = timekeeper.cycle_interval;
    #else
    	offset = (clock->read(clock) - clock->cycle_last) & clock->mask;
    #endif
    	timekeeper.xtime_nsec = (s64)xtime.tv_nsec << timekeeper.shift;
    
    	/*
    	 * With NO_HZ we may have to accumulate many cycle_intervals
    	 * (think "ticks") worth of time at once. To do this efficiently,
    	 * we calculate the largest doubling multiple of cycle_intervals
    	 * that is smaller then the offset. We then accumulate that
    	 * chunk in one go, and then try to consume the next smaller
    	 * doubled multiple.
    	 */
    	shift = ilog2(offset) - ilog2(timekeeper.cycle_interval);
    	shift = max(0, shift);
    	/* Bound shift to one less then what overflows tick_length */
    	maxshift = (8*sizeof(tick_length) - (ilog2(tick_length)+1)) - 1;
    	shift = min(shift, maxshift);
    	while (offset >= timekeeper.cycle_interval) {
    		offset = logarithmic_accumulation(offset, shift);
    		if(offset < timekeeper.cycle_interval<<shift)
    			shift--;
    	}
    
    	/* correct the clock when NTP error is too big */
    	timekeeping_adjust(offset);
    
    	/*
    	 * Since in the loop above, we accumulate any amount of time
    	 * in xtime_nsec over a second into xtime.tv_sec, its possible for
    	 * xtime_nsec to be fairly small after the loop. Further, if we're
    	 * slightly speeding the clocksource up in timekeeping_adjust(),
    	 * its possible the required corrective factor to xtime_nsec could
    	 * cause it to underflow.
    	 *
    	 * Now, we cannot simply roll the accumulated second back, since
    	 * the NTP subsystem has been notified via second_overflow. So
    	 * instead we push xtime_nsec forward by the amount we underflowed,
    	 * and add that amount into the error.
    	 *
    	 * We'll correct this error next time through this function, when
    	 * xtime_nsec is not as small.
    	 */
    	if (unlikely((s64)timekeeper.xtime_nsec < 0)) {
    		s64 neg = -(s64)timekeeper.xtime_nsec;
    		timekeeper.xtime_nsec = 0;
    		timekeeper.ntp_error += neg << timekeeper.ntp_error_shift;
    	}
    
    
    	/*
    	 * Store full nanoseconds into xtime after rounding it up and
    	 * add the remainder to the error difference.
    	 */
    	xtime.tv_nsec =	((s64) timekeeper.xtime_nsec >> timekeeper.shift) + 1;
    	timekeeper.xtime_nsec -= (s64) xtime.tv_nsec << timekeeper.shift;
    	timekeeper.ntp_error +=	timekeeper.xtime_nsec <<
    				timekeeper.ntp_error_shift;
    
    	/*
    	 * Finally, make sure that after the rounding
    	 * xtime.tv_nsec isn't larger then NSEC_PER_SEC
    	 */
    	if (unlikely(xtime.tv_nsec >= NSEC_PER_SEC)) {
    		xtime.tv_nsec -= NSEC_PER_SEC;
    		xtime.tv_sec++;
    		second_overflow();
    	}
    
    	/* check to see if there is a new clocksource to use */
    	update_vsyscall(&xtime, &wall_to_monotonic, timekeeper.clock,
    				timekeeper.mult);
    }
    
    
    /*
    * Called from the timer interrupt handler to charge one tick to the current
    * process.  user_tick is 1 if the tick is user time, 0 for system.
    */
    void update_process_times(int user_tick)
    {
    	struct task_struct *p = current;
    	int cpu = smp_processor_id();
    
    	/* Note: this timer irq context must be accounted for as well. */
    	account_process_tick(p, user_tick);
    	run_local_timers();
    	rcu_check_callbacks(cpu, user_tick);
    	printk_tick();
    #ifdef CONFIG_IRQ_WORK
    	if (in_irq())
    		irq_work_run();
    #endif
    	scheduler_tick();
    	run_posix_cpu_timers(p);
    }
    
    
    • 最重要的部分就是do_timer,以及其update_wall_time用于更新日期,而do_timer返回之前,update_process_times函数已经被调用,用于更新进程在过去的时间里使用的各种统计值。

    • update_process_times通过传入参数区分是内核状态下的时间还是用户状态下的时间。

    • 0 表示系统
    • 1表示用户
    • 这个值由tick_periodicupdate_process_times(user_mode(get_irq_regs()));代码决定的。
    • 根据状态
    /*
     * Account a single tick of cpu time.
     * @p: the process that the cpu time gets accounted to
     * @user_tick: indicates if the tick is a user or a system tick
     */
    void account_process_tick(struct task_struct *p, int user_tick)
    {
    	cputime_t one_jiffy_scaled = cputime_to_scaled(cputime_one_jiffy);
    	struct rq *rq = this_rq();
    
    	if (sched_clock_irqtime) {
    		irqtime_account_process_tick(p, user_tick, rq);
    		return;
    	}
    
    	if (user_tick)
    		account_user_time(p, cputime_one_jiffy, one_jiffy_scaled);
    	else if ((p != rq->idle) || (irq_count() != HARDIRQ_OFFSET))
    		account_system_time(p, HARDIRQ_OFFSET, cputime_one_jiffy,
    				    one_jiffy_scaled);
    	else
    		account_idle_time(cputime_one_jiffy);
    }
    
    • 统计对应执行状态下的执行时间,这个不会更新jiffies.

    • 状态有三种

      • 用户态
      • 内核态
      • 空转状态
    • 可以使用指令查看

      time ps
      
      • 这个指令查看指令在各个过程中的消耗。
    • 上面的代码说明了时间上并不严谨

      • 整个时间片都算在一个身上,有可能是内核与用户之间切换了多次。
      • 甚至可能还不是一个进程执行这个时间片。
      • 现在这种程度已经差不多是Unix可以提供的最好的统计方式了。
      • 采用高频率的切换可以提高统计精准度。
    • 执行过期定时器

      /*
       * Called by the local, per-CPU timer interrupt on SMP.
       */
      void run_local_timers(void)
      {
      	hrtimer_run_queues();
      	raise_softirq(TIMER_SOFTIRQ);
      }
      /*
       * Called from hardirq context every jiffy
       */
      void hrtimer_run_queues(void)
      {
      	struct timerqueue_node *node;
      	struct hrtimer_cpu_base *cpu_base = &__get_cpu_var(hrtimer_bases);
      	struct hrtimer_clock_base *base;
      	int index, gettime = 1;
      
      	if (hrtimer_hres_active())
      		return;
      
      	for (index = 0; index < HRTIMER_MAX_CLOCK_BASES; index++) {
      		base = &cpu_base->clock_base[index];
      		if (!timerqueue_getnext(&base->active))
      			continue;
      
      		if (gettime) {
      			hrtimer_get_softirq_time(cpu_base);
      			gettime = 0;
      		}
      
      		raw_spin_lock(&cpu_base->lock);
      
      		while ((node = timerqueue_getnext(&base->active))) {
      			struct hrtimer *timer;
      
      			timer = container_of(node, struct hrtimer, node);
      			if (base->softirq_time.tv64 <=
      					hrtimer_get_expires_tv64(timer))
      				break;
      
      			__run_hrtimer(timer, &base->softirq_time);
      		}
      		raw_spin_unlock(&cpu_base->lock);
      	}
      }
      
      
      • 先遍历队列中的定时器。
      • 然后置位软中断,处理时钟中断下半部。
    • 当前执行进程的时间片减少,合适的时候设置为need_resched

      /*
       * This function gets called by the timer code, with HZ frequency.
       * We call it with interrupts disabled.
       *
       * It also gets called by the fork code, when changing the parent's
       * timeslices.
       */
      void scheduler_tick(void)
      {
      	int cpu = smp_processor_id();
      	struct rq *rq = cpu_rq(cpu);
      	struct task_struct *curr = rq->curr;
      
      	sched_clock_tick();
      
      	raw_spin_lock(&rq->lock);
      	update_rq_clock(rq);
      	update_cpu_load_active(rq);
      	curr->sched_class->task_tick(rq, curr, 0);
      	raw_spin_unlock(&rq->lock);
      
      	perf_event_task_tick();
      
      #ifdef CONFIG_SMP
      	rq->idle_at_tick = idle_cpu(cpu);
      	trigger_load_balance(rq, cpu);
      #endif
      }
      
      • 下面的宏定义表示是多核,如果是多核还会用于处理器负载平衡。
    • tick_periodic处理和架构相关的中断处理函数,一般会进性清理,释放锁。

  10. 日期

    • 相关模块

    /*
    	kernel/time/timekeeping.c
    	struct timespec xtime
    */
    typedef long int __time_t;
    typedef long		__kernel_time_t;
    typedef __kernel_time_t		time_t;
    struct timespec {
    	__kernel_time_t	tv_sec;			/* seconds */
    	long		tv_nsec;		/* nanoseconds */
    };
    
    • 主要还是tv_sec,这个表示时间流逝了多久,时间的其实日期为1970年一月一日。
    • tv_nsec,表示纳秒。
    • 读写安全

    • 需要对应的xtime_lock,这个不是一个互斥锁,而是一个顺序锁。针对的就是写少读多的情况。
    /*
     * Test if reader processed invalid data.
     *
     * If sequence value changed then writer changed data while in section.
     */
    static __always_inline int read_seqretry(const seqlock_t *sl, unsigned start)
    {
    	smp_rmb();
    
    	return unlikely(sl->sequence != start);
    }
    
    u64 get_jiffies_64(void)
    {
    	unsigned long seq;
    	u64 ret;
    
    	do {
    		seq = read_seqbegin(&xtime_lock);
    		ret = jiffies_64;
    	} while (read_seqretry(&xtime_lock, seq));
    	return ret;
    }
    
    /**
     * getnstimeofday - Returns the time of day in a timespec
     * @ts:		pointer to the timespec to be set
     *
     * Returns the time of day in a timespec.
     */
    void getnstimeofday(struct timespec *ts)
    {
    	unsigned long seq;
    	s64 nsecs;
    
    	WARN_ON(timekeeping_suspended);
    
    	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));
    
    	timespec_add_ns(ts, nsecs);
    }
    
    • 获取时间
    SYSCALL_DEFINE2(gettimeofday, struct timeval __user *, tv,
    		struct timezone __user *, tz)
    {
    	if (likely(tv != NULL)) {
    		struct timeval ktv;
    		do_gettimeofday(&ktv);
    		if (copy_to_user(tv, &ktv, sizeof(ktv)))
    			return -EFAULT;
    	}
    	if (unlikely(tz != NULL)) {
    		if (copy_to_user(tz, &sys_tz, sizeof(sys_tz)))
    			return -EFAULT;
    	}
    	return 0;
    }
    
    • 两个入参是用于接收返回值的。
    • 内核提供的gettimeofday,而C库提供了ftime,ctime这些。
    • 内核与日期
    • 内核除了更新xtime以外,几乎没有交集,因为没有必要。
    • 使用日期的是用户应用,指的注意的是文件系统节点,存放的时间戳。
  11. 定时器

    • 用于代码的时间管理控制,特别是延时任务。

    • 中断下半部的延时任务和这个任务有点不一样,前者延时不确定,这个延时更精确。

    • 中断下半部的主要目的是任务现在不执行,抽空执行。

    • 生命周期

      • 设置超时时间
      • 超时后的回调函数。
      • 超时后时钟对象将会被销毁。
      • 时钟动态创建销毁,时钟数量没有上限,内核中时钟广泛使用。
    • 使用

      • 结构体

        struct timer_list {
        	/*
        	 * All fields that change during normal runtime grouped to the
        	 * same cacheline
        	 */
        	struct list_head entry;
        	unsigned long expires;
        	struct tvec_base *base;
        
        	void (*function)(unsigned long);
        	unsigned long data;
        
        	int slack;
        
        #ifdef CONFIG_TIMER_STATS
        	int start_pid;
        	void *start_site;
        	char start_comm[16];
        #endif
        #ifdef CONFIG_LOCKDEP
        	struct lockdep_map lockdep_map;
        #endif
        };
        
        • 声明在linux/timer.h
        • 定义在kernel/timer.c
      • 声明变量

      • 用函数初始化

      • 设置超时,回调函数和参数。

        /* 声明时钟 */
        struct timer_list my_timer;
        /* 初始化时钟 */
        init_timer(&my_timer);
        /* 赋值时钟 */
        my_timer.expires = jiffies + delay; /* timer expires in delay ticks */
        my_timer.data = 0; /* zero is passed to the timer handler */
        my_timer.function = my_function; /* function to run when timer expires */
        /* 添加并激活时钟 */
        add_timer(&my_timer);
        
        • 函数超时后会回调,回调在中断下半部执行。
      • 当实时的jieffies大于等于定义的expires的时候,就会执行回调函数。函数执行没有优先级一说,而且是异步执行,可能有的时钟执行会有一点延迟。

      • 时钟一般在超时之后就会执行,但是也有一些可能会有一定的延迟。这种不可能用于实时的程序。

      • 修改激活的时钟

        • 通过函数mod_timer(&my_timer, jiffies + new_delay); 修改为新的值。
        • 可以修改初始化没有激活的,也可以修改激活的。如果没有激活,这个函数还会激活这个时钟。
      • 删除激活的时钟

        • del_timer(&my_timer);

          • 删除的是非激活的返回0.
          • 否则返回1.
          • 如果删除的是正在执行的,就存在竞争。
          • 删除这样可以等待执行完后删除del_timer_sync。不过不能在中断上下文。
        • 修改定时器不安全。

        • 多处理器删除除非是知道没有在执行。

        • 处理共享数据需要自旋锁保护。

  12. 定时器实现

    • 执行环境

      • 中断上下文,时钟中断。
    • 执行步骤

      • 先处理完时钟相关的上半部分。
      • 再处理完时钟下半部,再处理定时器,所以定时器的执行并不是真的实时。存在一定的延迟。
      • 时钟中断处理进程的统计,等等信息。
      • run_timer_softirq将会执行所有的过期的定时器。
    • 定时器的存储

      • 定时器是链式的,增删查改的效率都不高。插入还要保证有序。
      • 所以内核将定时器根据过期时间分为五个组。
      • 分拆保证了大多数情况下,查找的消耗不会太高。这样就提高效率。
  13. 延时实现

    • 需求

    • 需要精确的时间,而不是使用时钟中断,在中断下半部执行不精确的延迟时间。
    • 采取的是根据硬件的频率,来计算时间。
    • 比如一条指令执行需要1ns,那么1000就是1us。精度远远高于1ms
    • 高精度延迟实现

    • 通过空转,并禁用处理器的中断,获取精确延迟。这种精确延迟一般都不会很长时间的执行。
    • 另一种就不获取处理器,但是精度并不保证。
    • 死循环

    • 使用while条件执行

      while(jiffies<delay);
      
      • jiffies是一个volatile类型,每次执行都会去所在地址读取数据。
    • 切换处理器

      while(jiffies<delay)
      	cond_resched();
      
      • 当有更重要的任务的时候就第一时刻让出处理器。
      • 等待下一次的执行。这个的话精度也不高。但是资源利用率比较高。
      • 有了进程切换所以就不能在中断上下文环境中使用。也不能有锁。
    • 精准计时

    • 精准计时会用处理器频率计时,并且一直占用,不被中断。
    • 一般是短期计时,如果很长的高精度计时,则会进行切换。
    • 计时时睡眠schedule_timeout

    • 可以让出处理器,但是这种精度不高。
    • 到时间了之后内核将唤醒放入执行队列中。
    /* set task’s state to interruptible sleep */
    set_current_state(TASK_INTERRUPTIBLE);
    /* take a nap and wake up in “s” seconds */
    schedule_timeout(s * HZ);
    
    • 唯一参数是相对时间。
    • 设置的是可打断的睡眠模式,遇到信号可以唤醒。
    • 设置了状态还是在运行,切换出去了之后才不会运行。
    • 因为要切换进程所以不能有锁(可能造成死锁),只能在进程上下文中调用。
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!