Linux时间子系统之时间维护层(Time Keeper)

article/2025/9/12 12:20:57

时间维护层会收到Tick层的周期调用,每次调用的周期是由内核参数决定的。在此期间,时间维护层可以读取时钟源设备的周期数,从而感知时间的流逝。

目前时间维护层主要负责维护以下几种类型的时间:

  • 实时时间(CLOCK_REALTIME):又称作墙上时间(Wall Time),也就是用户在系统中看到的当前时间。从时钟源设备中只能感知时间经过了,并不能知道当前具体的时间。当前时间一般有两个来源,一种是由一个专门的计时硬件来实现,不管系统是否上电,当中的时间信息都不会丢失,计时会一直持续进行,硬件上通常使用一个电池对其进行单独的供电,该时间有时又称CMOS时间。不过,该时间精度低,一般只有毫秒级;还有一个途径是通过NTP协议从网上的服务器获得。一旦获得了具体时间后,就将其保存在内存中,后面就可以通过时钟源设备周期数将流逝的时间累加到这个具体时间上就可以知道当前的具体时间了。当然,用户也可以手工修改设备上的时间,当用户修改时间时,可能会发生跳变。
  • 单调时间(CLOCK_MONOTONIC):自系统开机后就一直单调增加,但系统休眠时不会递增。这个值有可能会受到NTP(Network Time Protocol)的影响。
  • 原始单调时间(CLOCK_MONOTONIC_RAW):与单调时间类似,也是单调递增的时间,但不会受到NTP时间调整的影响,代表着系统独立时钟硬件对时间的统计。
  • 启动时间(CLOCK_BOOTTIME):与单调时间类似,但是当系统休眠时其值依然会递增,代表系统上电开机后的总时间。
  • 原子时间(CLOCK_TAI):表示原子国际时间(Atomic International Time),该时间和实时时间只是相差一定的偏移。

为了维护以上各种时间,时间维护层使用了timekeeper结构体来记录各种数据:

struct timekeeper {struct tk_read_base	tkr_mono;struct tk_read_base	tkr_raw;u64			xtime_sec;unsigned long		ktime_sec;struct timespec64	wall_to_monotonic;ktime_t			offs_real;ktime_t			offs_boot;ktime_t			offs_tai;s32			tai_offset;unsigned int		clock_was_set_seq;u8			cs_was_changed_seq;ktime_t			next_leap_ktime;u64			raw_sec;struct timespec64	monotonic_to_boot;u64			cycle_interval;u64			xtime_interval;s64			xtime_remainder;u64			raw_interval;u64			ntp_tick;s64			ntp_error;u32			ntp_error_shift;u32			ntp_err_mult;u32			skip_second_overflow;
#ifdef CONFIG_DEBUG_TIMEKEEPING......
#endif
};
  • tkr_mono:记录单调时间的结构体。
  • tkr_raw:记录原始单调时间的结构体。
  • xtime_sec:实时时间当前的秒数。
  • ktime_sec:单调时间当前的秒数。
  • wall_to_monotonic:实时时间和单调时间之间的差值。
  • offs_real:单调时间和实时时间之间的差值,offs_real=-wall_to_monotonic。
  • offs_boot:单调时间和启动时间之间的差值。
  • offs_tai:单调时间和TAI时间之间的差值,offs_tai=offs_real+tai_offset。
  • tai_offset:实时时间和TAI时间之间的差值。
  • clock_was_set_seq:表示时钟被设置的序数。
  • cs_was_changed_seq:表示时钟源更换的序数。
  • next_leap_ktime:下一次需要闰秒(跳变秒)的时间。“闰秒”就是1分钟有61秒, “跳秒”都安排在6月30日,或是12月31日的最后一瞬间。地球自转并非十分均匀,准确的说自转是在不断地在变慢的。每当地球自转变化引起的时间误差积累到与原子钟相关接近1秒时,就要人为地把时钟增加或减少1秒,从而使两者重新协调一致。这增加或减少的1秒称为“跳秒”。若是增加的,就是“正跳秒”(拨慢1秒);若是减少的就是“负跳秒”(拨快1秒),不过负跳秒至今还没有发生过。这样,每逢正跳秒那1分钟自然就是61秒了,正因为这1分钟多1秒,所以又叫“闰秒”。
  • raw_sec:原始单调时间当前的秒数。
  • monotonic_to_boot:单调时间和启动时间之间的差值。
  • cycle_interval:表示一次NTP周期包含多少个时钟周期。
  • xtime_interval:表示一个NTP周期包含多少纳秒,不过这个值是位移过后的,也就是实际的纳秒数向左位移了shift位,而且这个值会根据NTP层的状况做出调整。
  • xtime_remainder:表示从周期数转换成纳秒数时候的精度损失,后面分析代码的时候会解释。
  • raw_interval:也表示了一个NTP周期包含多少纳秒,也是位移过后的,不过这个值不会根据NTP的状况做出调整,一旦设置好后就不会变了。在初始状态下,xtime_interval和raw_interval的值是完全一样的。
  • ntp_tick:记录了NTP周期的纳秒数,这个值也是位移过后的,但其位移的位数不是有时钟源设备决定的,而是一个固定的值。
  • ntp_error:NTP时间和当前实时时间之间的差值,如果ntp_error大于0,表示当前系统的实时时间慢于NTP时间,相反如果小于0则表示快于。
  • ntp_error_shift:存放了NTP的shift和时钟源设备shift之间的差值。NTP层也需要对纳秒数做shift的操作,其值由宏NTP_SCALE_SHIFT定义,现在被定义成了32位。但是时钟源设备的shift值是根据条件计算出来的,所以在两层之间虽然都会shift,但位数是不同的。如果需要转换的话,必须记录下来它们之间的差值。
  • ntp_err_mult:如果ntp_error大于0,则为1,否则都是0。
  • skip_second_overflow:处理闰秒的时候是否需要跳过这一秒。
struct tk_read_base {struct clocksource	*clock;u64			mask;u64			cycle_last;u32			mult;u32			shift;u64			xtime_nsec;ktime_t			base;u64			base_real;
};
  • clock:指向对应底层时钟源设备结构体的指针。
  • cycle_last:记录了最近一次时钟源的周期计数。
  • mask、mult和shift:对应底层时钟源设备的mask、mult和shift的值,用于将时钟周期数和纳秒数之间互相转换。
  • xtime_nsec:实时时间当前的纳秒数,这个值也是移位过后的,也就是实际的纳秒数向左移动了shift位。累积起来会进位。
  • base:单调时间的基准时间。
  • base_real:实时时间的基准时间,base_real=base+offs_real。

下面分场景介绍一下时间维护层的工作过程。

1)初始化

时间维持层的初始化函数是timekeeping_init:

void __init timekeeping_init(void)
{struct timespec64 wall_time, boot_offset, wall_to_mono;struct timekeeper *tk = &tk_core.timekeeper;struct clocksource *clock;unsigned long flags;/* 尝试获得当前的实时时间 */read_persistent_wall_and_boot_offset(&wall_time, &boot_offset);if (timespec64_valid_settod(&wall_time) &&timespec64_to_ns(&wall_time) > 0) {persistent_clock_exists = true;} else if (timespec64_to_ns(&wall_time) != 0) {pr_warn("Persistent clock returned invalid value");wall_time = (struct timespec64){0};}if (timespec64_compare(&wall_time, &boot_offset) < 0)boot_offset = (struct timespec64){0};/* 计算当前实时时间和单调时间之间的差值 */wall_to_mono = timespec64_sub(boot_offset, wall_time);/* 获得自旋锁并关中断 */raw_spin_lock_irqsave(&timekeeper_lock, flags);write_seqcount_begin(&tk_core.seq);/* 初始化NTP层 */ntp_init();/* 获取默认时钟源设备 */clock = clocksource_default_clock();/* 打开默认时钟源设备 */if (clock->enable)clock->enable(clock);/* 设置时钟源设备 */tk_setup_internals(tk, clock);/* 设置实时时间 */tk_set_xtime(tk, &wall_time);tk->raw_sec = 0;/* 设置各种差值 */tk_set_wall_to_mono(tk, wall_to_mono);timekeeping_update(tk, TK_MIRROR | TK_CLOCK_WAS_SET);write_seqcount_end(&tk_core.seq);/* 释放自旋锁并开中断 */raw_spin_unlock_irqrestore(&timekeeper_lock, flags);
}

该函数首先尝试获得系统当前的实时时间,不过不是什么系统都支持这个操作,大多数情况下读出来的值都是空的(全0)。

clocksource_default_clock函数返回的默认时钟源其实就是系统中定义的那个精度最差的基于jiffies的时钟源。在获得默认时钟源设备之后,timekeeping_init函数接着调用了tk_setup_internals函数,将该设备设置进时钟维护层:

static void tk_setup_internals(struct timekeeper *tk, struct clocksource *clock)
{u64 interval;u64 tmp, ntpinterval;struct clocksource *old_clock;++tk->cs_was_changed_seq;old_clock = tk->tkr_mono.clock;/* 设置记录单调时间的结构体变量 */tk->tkr_mono.clock = clock;tk->tkr_mono.mask = clock->mask;/* 读取时钟源当前计数 */tk->tkr_mono.cycle_last = tk_clock_read(&tk->tkr_mono);/* 设置记录原始单调时间的结构体变量 */tk->tkr_raw.clock = clock;tk->tkr_raw.mask = clock->mask;tk->tkr_raw.cycle_last = tk->tkr_mono.cycle_last;/* NTP_INTERVAL_LENGTH表示一个NTP周期有多少纳秒 */tmp = NTP_INTERVAL_LENGTH;/* 计算一个NTP周期包含多少个时钟源周期数并赋值给cycle_interval */tmp <<= clock->shift;ntpinterval = tmp;/* 四舍五入 */tmp += clock->mult/2;do_div(tmp, clock->mult);if (tmp == 0)tmp = 1;interval = (u64) tmp;tk->cycle_interval = interval;/* 设置xtime_interval、xtime_remainder和raw_interval */tk->xtime_interval = interval * clock->mult;tk->xtime_remainder = ntpinterval - tk->xtime_interval;tk->raw_interval = interval * clock->mult;/* 如果新老时钟源设备的shift值不同则相应做出调整 */if (old_clock) {int shift_change = clock->shift - old_clock->shift;if (shift_change < 0) {tk->tkr_mono.xtime_nsec >>= -shift_change;tk->tkr_raw.xtime_nsec >>= -shift_change;} else {tk->tkr_mono.xtime_nsec <<= shift_change;tk->tkr_raw.xtime_nsec <<= shift_change;}}/* 设置shift */tk->tkr_mono.shift = clock->shift;tk->tkr_raw.shift = clock->shift;tk->ntp_error = 0;/* 计算两层shift位之间的差值 */tk->ntp_error_shift = NTP_SCALE_SHIFT - clock->shift;tk->ntp_tick = ntpinterval << tk->ntp_error_shift;/* 设置mult */tk->tkr_mono.mult = clock->mult;tk->tkr_raw.mult = clock->mult;tk->ntp_err_mult = 0;tk->skip_second_overflow = 0;
}

在timekeeper结构体里面凡是跟纳秒相关的变量,基本上都是真实值向左shift过的。因为纳秒数可以通过周期数由下面的公式转换过来:

nms = (cycles * mult) >> shift

所以cycle_interval和xtime_interval的转换关系是:

xtime\_interval=cycle\_interval * mult

而xtime_remainder记录的是将时钟源设备的周期数转换成纳秒数所引入的精度损失。通过前面的代码分析,可以知道cycle_interval的计算公式是:

\frac{NTP\_INTERVAL\_LENGTH << shift}{mult}=cycle\_interval

不过这里的除法是整数除法,肯定是有精度损失的,也就是:

NTP\_INTERVAL\_LENGTH << shift > cycle\_interval * mult

这个精度是cycle_interval累积的,也就是时间维持层每检测到时钟源设备经过了cycle_interval周期,会将代表当前纳秒数位移值的变量xtime_nsec加上xtime_interval,但其实应该不止那么多。因此,就需要一个变量xtime_remainder来记录它们之间的精度差值,也就是:

xtime\_remainder=NTP\_INTERVAL\_LENGTH<<shift-xtime\_interval

在初始设置时,ntp_error被设置成了0,刚开始,累积错误当然是0。ntp_error_shift被设置成了NTP层的shift和时钟源设备shift之间的差值。ntp_tick其实最终被设置成了NTP_INTERVAL_LENGTH<<NTP_SCALE_SHIFT。

完成了时钟源设备的设置之后,timekeeping_init函数接着调用tk_set_wall_to_mono函数,通过前面计算好的wall_to_monotonic值,设置各种其它的差值:

static void tk_set_wall_to_mono(struct timekeeper *tk, struct timespec64 wtm)
{struct timespec64 tmp;/* 首先验证当前的offs_real是否等于-wall_to_monotonic */set_normalized_timespec64(&tmp, -tk->wall_to_monotonic.tv_sec,-tk->wall_to_monotonic.tv_nsec);WARN_ON_ONCE(tk->offs_real != timespec64_to_ktime(tmp));/* 更新wall_to_monotonic */tk->wall_to_monotonic = wtm;/* 根据新的wall_to_monotonic设置offs_real */set_normalized_timespec64(&tmp, -wtm.tv_sec, -wtm.tv_nsec);tk->offs_real = timespec64_to_ktime(tmp);/* offs_tai等于offs_real加上tai_offset */tk->offs_tai = ktime_add(tk->offs_real, ktime_set(tk->tai_offset, 0));
}

完成初始设置之后,初始化函数timekeeping_init最后还调用了timekeeping_update函数用来更新时间维护层的数据,我们接着分析。

2)更新时间维护层的数据

前面提到了,timekeeping_update函数用来更新时间维护层的数据。该函数的第二个参数是action动作,目前共定义了下面三个值:

#define TK_CLEAR_NTP		(1 << 0)
#define TK_MIRROR		(1 << 1)
#define TK_CLOCK_WAS_SET	(1 << 2)
  • TK_CLEAR_NTP:是否需要清除NTP层的状态信息。
  • TK_MIRROR:是否需要复制到影子timekeeper结构体中。
  • TK_CLOCK_WAS_SET:是否需要递增clock_was_set_seq变量,该变量在每次设置时钟后都需要加一。
static void timekeeping_update(struct timekeeper *tk, unsigned int action)
{/* 如果定义了TK_CLEAR_NTP动作则对NTP进行清理 */if (action & TK_CLEAR_NTP) {tk->ntp_error = 0;ntp_clear();}/* 更新闰秒的状态 */tk_update_leap_state(tk);tk_update_ktime_data(tk);/* 更新vdso中关于时间的数据 */update_vsyscall(tk);update_pvclock_gtod(tk, action & TK_CLOCK_WAS_SET);/* base_real = base + offs_real */tk->tkr_mono.base_real = tk->tkr_mono.base + tk->offs_real;update_fast_timekeeper(&tk->tkr_mono, &tk_fast_mono);update_fast_timekeeper(&tk->tkr_raw,  &tk_fast_raw);/* 如果定义了TK_CLOCK_WAS_SET动作则递增clock_was_set_seq */if (action & TK_CLOCK_WAS_SET)tk->clock_was_set_seq++;/* 如果定义了TK_MIRROR动作则将主timekeeper结构体内容拷贝到影子timekeeper中 */if (action & TK_MIRROR)memcpy(&shadow_timekeeper, &tk_core.timekeeper,sizeof(tk_core.timekeeper));
}

该函数除了根据传入的action参数做出相应的操作外,主要就是调用tk_update_ktime_data函数:

static inline void tk_update_ktime_data(struct timekeeper *tk)
{u64 seconds;u32 nsec;/* 更新tkr_mono的base变量 */seconds = (u64)(tk->xtime_sec + tk->wall_to_monotonic.tv_sec);nsec = (u32) tk->wall_to_monotonic.tv_nsec;tk->tkr_mono.base = ns_to_ktime(seconds * NSEC_PER_SEC + nsec);/* 更新ktime_sec */nsec += (u32)(tk->tkr_mono.xtime_nsec >> tk->tkr_mono.shift);/* 是否需要累积到秒上 */if (nsec >= NSEC_PER_SEC)seconds++;tk->ktime_sec = seconds;/* 更新tkr_raw的base变量 */tk->tkr_raw.base = ns_to_ktime(tk->raw_sec * NSEC_PER_SEC);
}

该函数首先更新了代表单调时间的tkr_mono结构体中的base变量,基本上可以理解为单调时间的base=xtime_sec+wall_to_monotonic。这个很好理解,xtime_sec代表了当前实时时间的秒数,而wall_to_monotonic代表了墙上时间也就是实时时间与单调时间的差值,所以直接将它们相加之后再转换成纳秒数,就是单调时间的base值了。同时,该函数还更新了代表原始单调时间的tkr_raw结构体中的base变量,因为原始单调时间是不受NTP调整的,一直都是自顾自的累加,所以不需要通过实时时间转换,而直接就是用代表当前原始单调时间秒数的raw_sec转换成纳秒数就可以了。该函数还更新了代表单调时间当前秒数的ktime_sec,前面说了,单调时间其实是通过实时时间转过来的,那么就有可能当前的纳秒数加上转换偏移的纳秒数之后大于1秒(xtime_nsec >> shift + wall_to_monotonic.tv_nsec > NSEC_PER_SEC),这时候就需要将纳秒数累积到秒上去。

3)更新墙上时间

时间维护层通过调用函数update_wall_time来更新墙上时间:

void update_wall_time(void)
{timekeeping_advance(TK_ADV_TICK);
}

这个函数就是调用了timekeeping_advance函数,传入的参数是timekeeping_adv_mode枚举变量:

enum timekeeping_adv_mode {TK_ADV_TICK,TK_ADV_FREQ
};

共有两种模式:

  1. TK_ADV_TICK:表示由于新的Tick到来了,要相应的更新时间。
  2. TK_ADV_FREQ:表示由于频率改变了,要相应的更新时间。

update_wall_time传递的是TK_ADV_TICK:

static void timekeeping_advance(enum timekeeping_adv_mode mode)
{/* 真的全局timekeeper */struct timekeeper *real_tk = &tk_core.timekeeper;/* 影子时间维护者 */struct timekeeper *tk = &shadow_timekeeper;u64 offset;int shift = 0, maxshift;unsigned int clock_set = 0;unsigned long flags;/* 获得自旋锁并关中断 */raw_spin_lock_irqsave(&timekeeper_lock, flags);/* 如果被挂起了就直接退出 */if (unlikely(timekeeping_suspended))goto out;#ifdef CONFIG_ARCH_USES_GETTIMEOFFSET......
#else/* 计算当前时钟源周期数和上一次之间的差值 */offset = clocksource_delta(tk_clock_read(&tk->tkr_mono),tk->tkr_mono.cycle_last, tk->tkr_mono.mask);/* 如果模式是TK_ADV_TICK且累积的时钟源周期数小于cycle_interval则什么都不做 */if (offset < real_tk->cycle_interval && mode == TK_ADV_TICK)goto out;
#endiftimekeeping_check_update(tk, offset);/* 根据时钟源周期数的差值计算 */shift = ilog2(offset) - ilog2(tk->cycle_interval);shift = max(0, shift);/* 最大shift是不造成tick_length溢出的shift减去1 */maxshift = (64 - (ilog2(ntp_tick_length())+1)) - 1;shift = min(shift, maxshift);/* 一直循环处理到offset<cycle_interval为止 */while (offset >= tk->cycle_interval) {offset = logarithmic_accumulation(tk, offset, shift,&clock_set);/* 如果当前shift都处理完了则减1处理下面的 */if (offset < tk->cycle_interval<<shift)shift--;}/* 调整mult的值来尽量消除累积的NTP偏差 */timekeeping_adjust(tk, offset);/* 累积纳秒数到秒数上 */clock_set |= accumulate_nsecs_to_secs(tk);write_seqcount_begin(&tk_core.seq);/* 更新正真的全局timerkeeper结构体 */timekeeping_update(tk, clock_set);memcpy(real_tk, tk, sizeof(*tk));write_seqcount_end(&tk_core.seq);
out:/* 释放自旋锁并恢复中断 */raw_spin_unlock_irqrestore(&timekeeper_lock, flags);if (clock_set)clock_was_set_delayed();
}

该函数首先读取了当前时钟源设备的周期数,然后将其与记录在tkr_mono结构体变量中的代表上一次周期数的cycle_last值相减,计算两者之间的差值。由于NO_HZ模式的存在,在处于空闲模式的时候,CPU是收不到任何Tick的,因此有可能这个差值会很大,如果一个周期一个周期的累加效率太低了,这里采取了取log然后用shift计算的方法。一共要累积多少个NTP周期可以用下面公式计算:

ntp\_tick\_num=\frac{offset}{cycle\_interval}

两边都取log2之后可以得到:

log_2^{ntp\_tick\_num}=log_2^{\left(\frac{offset}{cycle\_interval}\right )}=log_2^{offset}-log_2^{cycle\_interval}

两边取2的次方后可以得到:

ntp\_tick\_num=1<<(log_2^{offset}-log_2^{cycle\_interval})

上面函数中的shift其实就是计算的最大的那个NTP的周期数,但是由于没有浮点单元,所以ilog2其实算出来的是整数,也就是最高位1所在的位数。得到最高位了,循环调用logarithmic_accumulation函数通过shift累积时间和周期数,如果当前shift位全都累积完了,就接着累积下一位,一直到最低位,也就是offset小于cycle_interval为止。由于对shift的位数有限制,所以最高位的shift有可能要累积多次,下面的每一位最多只会累积一次。

有了Tick数之后,计算过去了多少时间就很简单了,由于xtime_interval表示一个NTP周期包含多少shift后的纳秒数,所以经过了多长时间可以用下面公式计算:

elapsed\_time=xtime\_interval * ntp\_tick\_num=xtime\_interval<<(log_2^{offset}-log_2^{cycle\_interval})

这也就是在logarithmic_accumulation函数中要做的事:

static u64 logarithmic_accumulation(struct timekeeper *tk, u64 offset,u32 shift, unsigned int *clock_set)
{/* 计算一共过了多少周期数 */u64 interval = tk->cycle_interval << shift;u64 snsec_per_sec;/* 如果offset小于位移后的周期数则返回 */if (offset < interval)return offset;/* 将offset减去位移后的周期数 */offset -= interval;/* 将cycle_last加上位移后的周期数 */tk->tkr_mono.cycle_last += interval;tk->tkr_raw.cycle_last  += interval;/* 累积单调时间纳秒数 */tk->tkr_mono.xtime_nsec += tk->xtime_interval << shift;/* 累积纳秒数到实时时间秒数上 */*clock_set |= accumulate_nsecs_to_secs(tk);/* 累积原始单调时间 */tk->tkr_raw.xtime_nsec += tk->raw_interval << shift;snsec_per_sec = (u64)NSEC_PER_SEC << tk->tkr_raw.shift;while (tk->tkr_raw.xtime_nsec >= snsec_per_sec) {tk->tkr_raw.xtime_nsec -= snsec_per_sec;tk->raw_sec++;}/* 累积NTP时间和当前实时时间之间的差值 */tk->ntp_error += tk->ntp_tick << shift;tk->ntp_error -= (tk->xtime_interval + tk->xtime_remainder) <<(tk->ntp_error_shift + shift);return offset;
}

如果offset小于位移后的周期数,则说明这一个shift位不需要累积,直接返回。可以看到,在累积纳秒数xtime_nsec的时候是直接用xtime_interval向左位移shift位计算的。累积完纳秒后,还需要调用accumulate_nsecs_to_secs函数,看是不是纳秒数已经超过一秒了,如果是的话还需要将纳秒数累积成秒数,并将减掉对应一秒的纳秒数:

static inline unsigned int accumulate_nsecs_to_secs(struct timekeeper *tk)
{u64 nsecps = (u64)NSEC_PER_SEC << tk->tkr_mono.shift;unsigned int clock_set = 0;while (tk->tkr_mono.xtime_nsec >= nsecps) {int leap;/* 减掉对应1秒的纳秒数 */tk->tkr_mono.xtime_nsec -= nsecps;/* 将实时时间秒数加1 */tk->xtime_sec++;/* 如果这1秒已经累积过了则跳过下面的执行步骤 */if (unlikely(tk->skip_second_overflow)) {tk->skip_second_overflow = 0;continue;}/* 处理闰秒的情况 */leap = second_overflow(tk->xtime_sec);if (unlikely(leap)) {struct timespec64 ts;tk->xtime_sec += leap;ts.tv_sec = leap;ts.tv_nsec = 0;tk_set_wall_to_mono(tk,timespec64_sub(tk->wall_to_monotonic, ts));__timekeeping_set_tai_offset(tk, tk->tai_offset - leap);clock_set = TK_CLOCK_WAS_SET;}}return clock_set;
}

在logarithmic_accumulation函数的最后,还会计算NTP时间和当前实时时间之间的差值,计算公式为:

ntp\_error=(ntp\_tick-(xtime\_interval+xtime\_remainder)<<ntp\_error\_shift)<<shift

这个误差是按照NTP周期累积的,所以最后还需要左移shift位。前面看到了,一般情况下ntp_tick的值并不会更改,就等于xtime_interval加上xtime_remainder,所以计算出来的误差应该是0。但管理员可以通过命令adjtimex或者是通过系统调用adjtimex对其进行调整。

在完成了时间的累积之后,timekeeping_advance函数会调用timekeeping_adjust函数对mult的值做出调整:

static void timekeeping_adjust(struct timekeeper *tk, s64 offset)
{u32 mult;/* 根据ntp_tick值的改变调整mult的值 */if (likely(tk->ntp_tick == ntp_tick_length())) {/* 如果ntp_tick值没变则还原回原来的mult */mult = tk->tkr_mono.mult - tk->ntp_err_mult;} else {/* 如果ntp_tick值改变了则从新计算mult */tk->ntp_tick = ntp_tick_length();mult = div64_u64((tk->ntp_tick >> tk->ntp_error_shift) -tk->xtime_remainder, tk->cycle_interval);}/* 如果ntp_error大于0说明实时时间晚于NTP时间,需要将mult加1,进行追赶。 */tk->ntp_err_mult = tk->ntp_error > 0 ? 1 : 0;mult += tk->ntp_err_mult;/* 根据mult值的改变对timekeeper结构体中的相应变量进行调整 */timekeeping_apply_adjustment(tk, offset, mult - tk->tkr_mono.mult);/* 检查调整过后的mult的值和原始的mult值之间差值是否超过了最大限度(11%) */if (unlikely(tk->tkr_mono.clock->maxadj &&(abs(tk->tkr_mono.mult - tk->tkr_mono.clock->mult)> tk->tkr_mono.clock->maxadj))) {printk_once(KERN_WARNING"Adjusting %s more than 11%% (%ld vs %ld)\n",tk->tkr_mono.clock->name, (long)tk->tkr_mono.mult,(long)tk->tkr_mono.clock->mult + tk->tkr_mono.clock->maxadj);}/* 处理调整过后xtime_nsec下溢出的情况 */if (unlikely((s64)tk->tkr_mono.xtime_nsec < 0)) {tk->tkr_mono.xtime_nsec += (u64)NSEC_PER_SEC <<tk->tkr_mono.shift;tk->xtime_sec--;tk->skip_second_overflow = 1;}
}

如果ntp_tick的值没有变的话,会将mult还原回原来的值,ntp_err_mult要么是0要么是1,在后面的timekeeping_apply_adjustment函数会将这个值加上,所以在这里还原的时候要减去ntp_err_mult。如果ntp_tick的值改变了的话,要重新计算mult的值。

加大mult的值则实时时间变快,而减小mult的值则实时时间变慢。所以,如果ntp_error的值大于0,说明当前时间晚于NTP时间,因此需要将mult的值加1进行“追赶”。

接着,该函数会调用timekeeping_apply_adjustment函数,针对新的mult值对timekeeper结构体中的其它变量进行修正:

static __always_inline void timekeeping_apply_adjustment(struct timekeeper *tk,s64 offset,s32 mult_adj)
{s64 interval = tk->cycle_interval;/* 当前offset和interval中存的值刚好是mult_adj为1的情况 */if (mult_adj == 0) {/* 如果调整值为0说明不需要调整直接退出 */return;} else if (mult_adj == -1) {/* 如果调整值为-1则直接将offset和interval取负 */interval = -interval;offset = -offset;} else if (mult_adj != 1) {/* 如果不为1则直接对offset和interval计算乘积 */interval *= mult_adj;offset *= mult_adj;}/* 检查调整过后是否会造成mult值的溢出 */if ((mult_adj > 0) && (tk->tkr_mono.mult + mult_adj < mult_adj)) {WARN_ON_ONCE(1);return;}/* 调整timekeeping结构体参数 */tk->tkr_mono.mult += mult_adj;tk->xtime_interval += interval;tk->tkr_mono.xtime_nsec -= offset;
}

该函数根据时钟源设备已经经过的周期数和要调整的mult的差值相应调整xtime_interval和xtime_nsec的值。可以看到,只更改了对应单调时间的结构体变量tkr_mono中的值,对于表示原始单调时间的结构体变量tkr_raw并没有做任何更改,这也符合它们的定义。

具体怎么调整呢,我们先来看xtime_interval,前面说了,xtime_interval和cycle_interval之间的关系是:

xtime\_interval=cycle\_interval * mult

那么调整之后应该就变成了:

xtime\_interval=cycle\_interval * (mult + mult\_adj)

稍微变换之后就是:

xtime\_interval=cycle\_interval * mult + cycle\_interval * mult\_adj

而cycle_interval * mult_adj已经计算过了,就是局部变量interval的值,所以最终就是:

xtime\_interval=xtime\_interval+interval

接着看xtime_nsec的计算,计算它的时候,其实有一个前提条件,就是不希望产生时间的跳变,也就是说改了mult和xtime_nsec的值后和不改它之前通过offset计算出来的时间是一样的。即:

offset*mult+xtime\_nsec_1=offset*(mult+mult\_adj)+xtime\_nsec_2

稍微变换一下就可以得到:

xtime\_nsec_1=offset*mult\_adj+xtime\_nsec_2

而offset * mult_adj也已经在前面计算过了,存放在变量offset中,所以最后就是:

xtime\_nsec_2=xtime\_nsec_1-offset

这又会引入一个新的问题,如果调用timekeeping_apply_adjustment的时候,刚好xtime_nsec的值非常小,小于offset的值,那么当减去offset之后就会造成下溢出,这就是在timekeeping_adjust函数最后所要做的处理。首先是将表示纳秒数的xtime_nsec值加上一秒(当然还要位移),然后将表示秒数的xtime_sec减去1,最后赋值skip_second_overflow为1,因为其实这秒已经在前面累积过了,所以要跳过。这样在后面再次调用accumulate_nsecs_to_secs函数的时候,会跳过这一秒。

更新时间都是首先累积到实时时间也就是墙上时间上的,其它的时间都是通过实时时间转换过来的。

4)切换时钟源

在对时钟源层进行分析的时候,我们提到过其在__clocksource_select函数中选择最佳时钟源的时候,会调用时间维护层的timekeeping_notify函数:

int timekeeping_notify(struct clocksource *clock)
{struct timekeeper *tk = &tk_core.timekeeper;/* 如果要替换的时钟源就是原来的则直接返回 */if (tk->tkr_mono.clock == clock)return 0;/* 调用change_clocksource函数 */stop_machine(change_clocksource, clock, NULL);/* 通知Tick层时钟源设备已改变 */tick_clock_notify();return tk->tkr_mono.clock == clock ? 0 : -1;
}

该函数在“停机”的状态下调用change_clocksource函数切换时钟源:

static int change_clocksource(void *data)
{struct timekeeper *tk = &tk_core.timekeeper;struct clocksource *new, *old;unsigned long flags;new = (struct clocksource *) data;/* 获得自旋锁并关中断 */raw_spin_lock_irqsave(&timekeeper_lock, flags);write_seqcount_begin(&tk_core.seq);/* 立即更新时间 */timekeeping_forward_now(tk);/* 获得时钟源设备的模块 */if (try_module_get(new->owner)) {if (!new->enable || new->enable(new) == 0) {old = tk->tkr_mono.clock;/* 设置新的时钟源 */tk_setup_internals(tk, new);/* 关闭老的时钟源 */if (old->disable)old->disable(old);module_put(old->owner);} else {module_put(new->owner);}}/* 更新数据 */timekeeping_update(tk, TK_CLEAR_NTP | TK_MIRROR | TK_CLOCK_WAS_SET);write_seqcount_end(&tk_core.seq);/* 释放自旋锁并开中断 */raw_spin_unlock_irqrestore(&timekeeper_lock, flags);return 0;
}

该函数先调用timekeeping_forward_now函数,立即读取当前要被替换的老时钟源周期,并用其更新当前时间:

static void timekeeping_forward_now(struct timekeeper *tk)
{u64 cycle_now, delta;/* 读取时钟源设备当前周期数 */cycle_now = tk_clock_read(&tk->tkr_mono);/* 计算其与上一次更新之间的差值 */delta = clocksource_delta(cycle_now, tk->tkr_mono.cycle_last, tk->tkr_mono.mask);/* 更新cycle_last */tk->tkr_mono.cycle_last = cycle_now;tk->tkr_raw.cycle_last  = cycle_now;/* 计算纳秒数 */tk->tkr_mono.xtime_nsec += delta * tk->tkr_mono.mult;......tk->tkr_raw.xtime_nsec += delta * tk->tkr_raw.mult;......tk_normalize_xtime(tk);
}

然后调用tk_setup_internals函数设置新的时钟源设备,最后调用timekeeping_update函数更新时间维护层的数据。


http://chatgpt.dhexx.cn/article/dBHwdSvT.shtml

相关文章

【Linux内核|时间子系统】Linux时间子系统(二)timekeeping简介

文章目录 1. timekeeping2. 计算墙上时间、启动时间差值3. timekeeper初始化3.1. 默认时钟源3.2. tk_setup_internals3.3. 设置时间 4. timekeeping_update4.1. tk_update_ktime_data&#xff1a;tkr_mono和tkr_raw设置4.2. update_fast_timekeeper4.3. shadow_timekeeper 1. t…

matlab 梯度下降 求偏导,通过计算图求梯度下降中各偏导的推导

通过计算图求梯度下降中各偏导的推导 Author: nex3z 2017-08-30 在 Neural Networks and Deep Learning 课程的 Logistic Regression Gradient Descent 一节以逻辑回归为例&#xff0c;介绍了使用计算图(Computation Graph)求梯度下降中各偏导的方法&#xff0c;但没有给出具体…

matlab|求导数/最值

本博文源于matlab求导数求极值求最值&#xff0c;涉及内容极限命令求导/diff求导/一元函数一阶导数&#xff0c;多阶导数&#xff0c;求参数导数/函数极值和最值/不给定区间求最值 用极限命令求导 例子&#xff1a;求函数3sinx4x^2在x0处的导数 >> syms t; limit((3*…

matlab 梯度下降 求偏导,吴恩达机器学习课程课时12梯度下降算法中参数θ0,θ1求偏导...

最近学习吴恩达的机器学习课程。 看到了线性回归的梯度下降算法。课程中将了一个非常简单的线性回归&#xff1a; 比如给出一些房子的size和对应的price&#xff0c;我们可以建立一个模型(在此模型就是线性回归)&#xff0c; 希望之后在给出任意一个房子的size&#xff0c;可以…

matlab求COPULA偏导,matlab note

Update date&#xff1a;2018-01-05 Matlab Note 1 vine-copula 1.1 xlsread() 打开xlsx里面的数据 S3CE xlsread(电表建模数据.xlsx,S3CE&#xff0c;‘G2:G52’) 1.2常见希腊发音总结 image.png 3.prob() 连乘 由于matlab中的元素是以矩阵为单位,prod(x)就是把x向量中所有元…

matlab求解多元函数的偏导数diff

本博文源于matlab求解多元函数导数。涉及求一阶/求多阶/求向量偏导数/求隐函数导数 多元函数的偏导数 diff(f(x,y,z),变量名)例子 求x^2lny根号z的偏导数 >> syms x >> syms y >> syms z >> du_dx diff(x^2log(y)sqrt(z),x)du_dx 2*x>> du_dz…

matlab实现隐函数求偏导数(impldiff函数)

目录 总述函数说明应用举例例1例2 函数实现 总述 由前面给出的算法&#xff0c;可以编写出如下函数来求解隐函数的偏导数。 函数说明 function dy impldiff(f, x, y, n) %impldiff %隐函数求导 % 调用格式&#xff1a; % f1 impldiff(f, x, y, n) % 其中&#xff1a;f…

matlab偏导数方程,利用Matlab求解不同类型的偏微分方程

来源:新浪了凡春秋的博客 在科学技术各领域中,有很多问题都可以归结为偏微分方程问题。在物理专业的力学、热学、电学、光学、近代物理课程中都可遇见偏微分方程。偏微分方程,再加上边界条件、初始条件构成的数学模型,只有在很特殊情况下才可求得解析解。随着计算机技术的发…

【MATLAB】求偏导数

目录 1、示例1 2、示例2 3、MATLAB求偏导数的应用 1、示例1 syms x y; z=(1+x*y)^y; zx=diff (z,’x’)

MATLAB求解偏导数

MATLAB具有多元函数求解偏导数的功能。 例&#xff1a; 1.函数关于x的二阶偏导数 syms x y >> z x^4y^4-4*x^2*y^2; >> zxx diff(z,x,2)zxx 12*x^2 - 8*y^2 2.函数关于y的二阶偏导数 zyy diff(z,y,2)zyy 12*y^2 - 8*x^2 3.函数二阶混合偏导数 zxy diff(dif…

MATLAB常用求导和求偏导函数

matlab求导命令diff调用格式: diff(函数) &#xff0c; 求的一阶导数&#xff1b;diff(函数&#xff0c; n) &#xff0c;求的n阶导数(n是具体整数)&#xff1b;diff(函数&#xff0c;变量名)&#xff0c;求对的偏导数&#xff1b;diff(函数&#xff0c; 变量名&#xff0c;n)…

性能测试流程(完整版)

一、 规范性能测试实施流程的意义 规范的性能测试实施流程能够加强测试工作流程控制&#xff0c;明确性能测试各阶段应完成的工作&#xff0c;指导测试人员正确、有序的开展性能测试工作&#xff0c;提高各角色在性能能测试中的工作效率。本次分享的性能测试实施流程是性能测试…

性能测试流程相关

1、脚本制作流程 脚本制作方法可参考此链接&#xff1a;性能测试脚本实战_HealerLX的博客-CSDN博客 &#xff08;1&#xff09;抓包工具抓包 &#xff08;2&#xff09;录入到性能测试工具中 &#xff08;3&#xff09;调试脚本直到成功 &#xff08;4&#xff09;加上运行逻辑…

性能测试流程、优化、指标

性能测试流程、优化、指标 1、性能测试的流程 1、确定性能测试的目标 之前做的压测性能标准、产品说明书的性能需求部分、运营人员提出的性能指标、通过生产环境换算出的性能指标等 &#xff08;1&#xff09;接口响应时间不能超过3秒&#xff08;每个接口的响应时间&#…

性能测试流程规范

完整的性能测试流程 下午逛一个测试交流群时&#xff0c;聊起性能测试&#xff0c;然后某位群成员说他们用的loadrunner做性能&#xff0c;当时觉得这话有点偏颇&#xff0c;虽然我也是一个性能测试道路上的摸索前进者。。。 诚然&#xff0c;我们在进行性能测试工作的过程中…

Jmeter完整的性能测试流程图,接口测试步骤

第零:300G全套最新软测视频教程分享 链接:https://pan.baidu.com/s/17jkyGn-Wm-zC6QQLkWjrsw 提取码:o94n 第一:测试相关系统准备 1. 验证基本系统功能后 性能测试在哪个阶段开始介入?通常,最好仅在功能测试完成后系统稳定时才开始介入性能测试. 2. 测试团队的组织…

Jmeter性能测试流程

1.关联真机或者模拟器,设置手动代理,要连在一个网络之下执行。 2.通过接口文档或者Charles进行抓包获取接口以及接口参数 3.通过Jmeter进行压力测试:添加线程组,添加http请求,添加响应断言,添加聚合报告 4.添加http请求 5,添加响应断言

性能测试流程及数据分析

性能测试流程 性能测试的工作三大部分性能测试执行阶段测试的主要策略总结阶段&#xff1a; 性能测试的工作三大部分 性能需求调研&#xff1a; 客户能接受的响应时间&#xff0c;每日单交易处理能力&#xff0c;系统资源利用率&#xff0c;系统环境搭建方式、并发用户数、日交…

浅谈Jmeter性能测试流程

不管是Loadrunner还是jmeter进行性能测试&#xff0c;测试流程基本上都是一样的&#xff0c;限制以Jmeter为例分析测试流程&#xff1a; 一、性能测试需求分析 一般而言&#xff0c;被测对象的性能需求&#xff0c;会在用户需求规格说明说中给出&#xff0c;比如单位时间内的访…

性能测试流程 - 即拿即落地(超级详细)

目录&#xff1a;导读 前言一、制定目的二、适用范围三、测试流程四、四大阶段五、总结 前言 性能测试成熟度级别 救火&#xff08;Firefighting&#xff09;&#xff1a;应用程序发布前很少或从来没有进行过性能测试的情况。所有性能缺陷&#xff08;100%&#xff09;都在生…