从erlang时间函数说到时间校正体系

很多人会注意到这个问题,erlang提供了2个时间函数,erlang:now() 和 os:timestamp()。用法一样,都是返回当前的时间。具体时间是从1970年1月1日零时算起,到现在经过的时间,结果为{MegaSecs, Secs, MicroSecs}。

这两个函数有什么区别?

os:timestamp() 获取到的时间为操作系统的时间,不做任何修正;而erlang:now(),每次获取都会确保生成了唯一的时间,就是说,erlang:now()在实现上对时间做了一个校正,每次都生成一个单调向前的唯一值。

erlang:now()的特点:

Monotonic
erlang:now() never jumps backwards - it always moves forward
Interval correct
The interval between two erlang:now() calls is expected to correspond to the correct time in real life (as defined by an atomic clock, or better)
Absolute correctness
The erlang:now/0 value should be possible to convert to an absolute and correct date-time, corresponding to the real world date and time (the wall clock)
System correspondence
The erlang:now/0 value converted to a date-time is expected to correspond to times given by other programs on the system (or by functions like os:timestamp/0)
Unique
No two calls to erlang:now on one Erlang node should return the same value

主要是这3个特点:


特点

说明

单调向前

erlang:now() 获取的时间是单调向前,就算系统时间倒退了,也不会影响这个函数的使用。(时间依旧是向前的,较之前几乎没有偏差)

唯一性

erlang:now() 获取的值都是唯一的,不会重复出现2个相同的值。

间隔修正

两次 erlang:now() 调用的间隔都可以被利用来修正erlang时间。

到这里,可以看出 erlang 内部实现了一套时间校正的机制,当系统时间出错的时候,就会做修正。(关于这块内容,可以看Erlang相关文档 time correction

erlang 时间校正

时间校正的作用:

在开始这段内容前,讲讲时间校正的作用

1. 时间单调向前:

举个例子,说明时间倒退问题:

比如,游戏中会统计今天和昨天杀怪的总数量,跨零点时要把今天杀怪字段的数量写到昨天的字段,然后将今天的置0。跨零点后,如果时间倒退了几秒钟,然后就会重复跨零点。那么,今天的数量会覆盖昨天的数量,导致昨天的数量被清零。

2. 时间平稳:

同样举个例子,说明时间不平稳问题:

比如,erlang开发中,经常都会出现一个进程call另一个进程的场景,一般是5秒超时,假如时间突然加快了5秒,就相当于没有等待操作完成,就直接超时了。当然这是很不合理的

erlang时间校正的特点:

Monotonic
The clock should not move backwards
Intervals should be near the truth
We want the actual time (as measured by an atomic clock or an astronomer) that passes between two time stamps, T1 and T2, to be as near to T2 - T1 as possible.
Tight coupling to the wall clock
We want a timer that is to be fired when the wall clock reaches a time in the future, to fire as near to that point in time as possible

假如操作系统时间出现了改变,erlang不会立刻改变内部时间为系统时间,而是将时间轻微加快或减慢,最终和系统时间保持一致。就算系统时间突然倒退到以前的某个时间,但时间总是向前这点是不会改变的,所以,erlang只是预期在将来某个时间和系统时间达成一致,而不会倒退时间。

erlang是怎么校正时间的?

erlang内部时间会和系统挂钟时间保持同步,当系统挂钟时间突然改变时,erlang会比较两个时间的差异,让内部的时间的同步值轻微变大或变小,幅度最大是1%,就是说,当系统时间改变了1分钟,erlang会花100分钟来慢慢校正,并最终和系统时间保持同步。

哪些函数受到时间校正影响?

erlang:now/0
The infamous erlang:now/0 function uses time correction so that differences between two "now-timestamps" will correspond to other timeouts in the system. erlang:now/0 also holds other properties, discussed later.
receive ... after
Timeouts on receive uses time correction to determine a stable timeout interval.
The timer module
As the timer module uses other built in functions which deliver corrected time, the timer module itself works with corrected time.
erlang:start_timer/3 and erlang:send_after/3
The timer BIF‘s work with corrected time, so that they will not fire prematurely or too late due to changes in the wall clock time.

源码剖析

erlang:now() 是 bif 实现,代码如下:(以R16B02为例)

/*
 * bif.c now_0函数,实现 erlang:now/0
 * return a timestamp
 */
BIF_RETTYPE now_0(BIF_ALIST_0)
{
    Uint megasec, sec, microsec;
    Eterm* hp;

    get_now(&megasec, &sec, μsec); // 获取当前时间
    hp = HAlloc(BIF_P, 4);
    BIF_RET(TUPLE3(hp, make_small(megasec), make_small(sec),
		   make_small(microsec))); // 返回{MegaSecs, Secs, MicroSecs}
}

再来看下 get_now() 函数。

/*
 * erl_time_sup.c get_now函数,获取当前时间
 * get a timestamp
 */
void get_now(Uint* megasec, Uint* sec, Uint* microsec)
{
    SysTimeval now;

    erts_smp_mtx_lock(&erts_timeofday_mtx);

    get_tolerant_timeofday(&now); // 获取当前时间值
    do_erts_deliver_time(&now);  // 记录当前的时间(用于VM内部读取当前时间,如timer)

    /* 确保时间比上次获取的大 */
    if (then.tv_sec > now.tv_sec ||
	(then.tv_sec == now.tv_sec && then.tv_usec >= now.tv_usec)) {
	now = then;
	now.tv_usec++;
    }
    /* Check for carry from above + general reasonability */
    if (now.tv_usec >= 1000000) {
	now.tv_usec = 0;
	now.tv_sec++;
    }
    then = now;

    erts_smp_mtx_unlock(&erts_timeofday_mtx);

    *megasec = (Uint) (now.tv_sec / 1000000);
    *sec = (Uint) (now.tv_sec % 1000000);
    *microsec = (Uint) (now.tv_usec);

    update_approx_time(&now);//更新「简要」时间(仅用于标记进程启动时间)
}

这里重点看下get_tolerant_timeofday(),实现了时间校正功能。

/*
 * erl_time_sup.c get_tolerant_timeofday函数,获取当前时间
 * 根据系统API不同有两种实现,这里取其中一种做说明
 */
static void get_tolerant_timeofday(SysTimeval *tv)
{
    SysHrTime diff_time, curr;

    if (erts_disable_tolerant_timeofday) {// 时间校正功能被禁用,直接返回系统时间
	sys_gettimeofday(tv);
	return;
    }
    *tv = inittv; // 取VM启动时间

	// 计算从VM启动到现在经过的内部时间(正值,单位微秒)
    diff_time = ((curr = sys_gethrtime()) + hr_correction - hr_init_time) / 1000; 

    if (curr < hr_init_time) {
	erl_exit(1,"Unexpected behaviour from operating system high "
		 "resolution timer");
    }

	// 检查是否刚校正过(两次校正最小间隔 1s)
    if ((curr - hr_last_correction_check) / 1000 > 1000000) {
	/* Check the correction need */
	SysHrTime tv_diff, diffdiff;
	SysTimeval tmp;
	int done = 0;

	// 计算从VM启动到现在经过的实际时间(如果系统时间被调整过,可能是负值,单位微秒)
	sys_gettimeofday(&tmp);
	tv_diff = ((SysHrTime) tmp.tv_sec) * 1000000 + tmp.tv_usec;
	tv_diff -= ((SysHrTime) inittv.tv_sec) * 1000000 + inittv.tv_usec;
	diffdiff = diff_time - tv_diff;// 实际时间与内部时间的差值(缩短这个时间差以赶上实际时间)
	if (diffdiff > 10000) { // 内部时间比外部时间快 0.01s 以上
	    SysHrTime corr = (curr - hr_last_time) / 100; //  两次调用经过的实际时间 * 1%
	    if (corr / 1000 >= diffdiff) {
			++done;
			hr_correction -= ((SysHrTime)diffdiff) * 1000;
			/* 超过diffdiff*1000 * 100,只修正 diffdiff*1000,
			 * 就是1s需要花100s修正,同时标记本次修正完成
			 * 什么情况下会走到这里:就是这个函数很久没调用,超过了时间偏差的100倍
			 * 然后标记修正完成,至此,就没有时间偏差了
			 */
	    } else {
		hr_correction -= corr; // 修正值为两次调用经过的实际时间 * 1%
	    }
		// 重算与VM启动时间的间隔
	    diff_time = (curr + hr_correction - hr_init_time) / 1000;
	} else if (diffdiff < -10000) { // 内部时间比外部时间慢 0.01s 以上
	    SysHrTime corr = (curr - hr_last_time) / 100;
	    if (corr / 1000 >= -diffdiff) {
		++done;
		hr_correction -= ((SysHrTime)diffdiff) * 1000;
	    } else {
		hr_correction += corr;
	    }
	    diff_time = (curr + hr_correction - hr_init_time) / 1000;
	} else {
	    /* 内部时间与外部时间偏差在0.01s 内,标记完成,等1s后修正剩下的时间
	     * 这段代码目的是,如果时间偏差在0.01s内,VM特意等1s后修正这个时间
	     * 另外,如果时间没出差错,就都走到这里,减少时间函数调用开销
         */
	    ++done;
	}
	if (done) {
	    hr_last_correction_check = curr;
	}
    }
    tv->tv_sec += (int) (diff_time / ((SysHrTime) 1000000));
    tv->tv_usec += (int) (diff_time % ((SysHrTime) 1000000));
    if (tv->tv_usec >= 1000000) {
	tv->tv_usec -= 1000000;
	tv->tv_sec += 1;
    }
    hr_last_time = curr;
}

这里,erlang利用一个单调递增的时间函数 sys_gethrtime(),作为参照物来判断VM实际经历的真实时间,然后再轻微的向系统挂钟时间倾斜,以致最终和系统挂钟时间保持同步。至于sys_gethrtime(),我也准备了一点资料,放在拓展阅读分享吧。

拓展阅读

gethrtime()

前面提到的sys_gethrtime(),实际上是一个宏(暂时只讨论linux下的实现,win下类似)

#define sys_gethrtime() gethrtime()

关于 gethrtime() 可以看下unix官方文档说明man page for gethrtime ,写得很详细。

另外,这里参考了 C ++参考指南 C++ Reference Guide - High Resolution Timers


也就是这两个特点:

1. 单调向前,不会倒退

2. 线性增长,不会变快或变慢

所以,Erlang VM利用基于硬件的单调递增时间,取两个时刻的差值来计算VM运行的时间,然后取操作系统的挂钟时间做比较,通过逐渐缩小这个时间差来实现VM时间始终单调向前,从而平稳地把VM时间纠正到用户的挂钟时间。这里的平稳指的是VM的时间校正机制会将时间"轻微"加快或减慢,但最大幅度是1%,也就是说,VM经历 1s 实际上可能就是 0.99s 或者1.01s

时间同步

这里看到《并行与分布仿真系统》的作者写的相关文章Synchronizing Clocks?(Wallclock Time),也很有参考价值

结束语

最后,说下时间校正的副作用。

erlang实现时间校正有计算开销的,而且这个内部校正值是全局变量,不只是所有erlang进程,还是VM所有调度线程都会读写这个时间,所以就要有锁来保证数据。为此,erlang内部设定好了 erlang:now/0 调用频率不会超过1微妙1次。

当然,如果获取时间只是用于测试目的,或者打印错误日志时间,完全可以用 os:timestamp/0 来代替。对于一些有大规模进程的项目,还可以设立一些时间管理进程,用于同步时间,而每个进程只要读取自己的进程字典就好。

如果不想使用,还可以禁用这个功能。

Time correction is enabled or disabled by passing the +c [true|false] command line argument to erl.

R18之后,erlang提供了更多时间校正相关的API,对用户暴露底层时间的校正值,和实际的时间差。这里暂时就不说明了。链接地址

参考:http://blog.csdn.net/mycwq/article/details/45346411

时间: 2024-10-05 06:12:43

从erlang时间函数说到时间校正体系的相关文章

MySQL时间函数-获取当前时间-时间差

MySQL中获取当前时间为now(),不同于sqlserver getdate(). SQLServer转MySQL除变化top 1 -> limit 1之后报错: select 1 from record_visitor where visitor_ip='' and datediff(mi,visitor_time,getdate())<=30 limit 1 [Err] 1582 - Incorrect parameter count in the call to native func

PHP -- 时间函数

PHP – 时间函数 PHP – 时间函数 Table of Contents 时区 date 的时间格式 time date mktime strftime setlocale strtotime DateTime DateInterval 时区 /etc/php.ini date.timezone = Asia/Shanghai date_default_timezone_set('Asia/Shanghai); setlocale(LC_TIME, "C"); echo strf

Sql Server函数全解&lt;四&gt;日期和时间函数

原文:Sql Server函数全解<四>日期和时间函数   日期和时间函数主要用来处理日期和时间值,本篇主要介绍各种日期和时间函数的功能和用法,一般的日期函数除了使用date类型的参数外,也可以使用datetime类型的参数,但会忽略这些值的时间部分.相同的,以time类型值为参数的函数,可以接受datetime类型的参数,但会忽略日期部分. 1.获取系统当前日期的函数getDate();  getDate()函数用于返回当前数据库系统的日期和时间,返回值的类型为datetime.[例]sel

PHP的函数-----生成随机数、日期时间函数

常用的函数 [1]   生成随机数 rand(); 例子: echo rand(); 显示结果: 当刷新时,会有不同的数,默认生成随机数.生成随机数不能控制范围. 如果,想要控制在范围之内,就用: echo rand(最小值,最大值); //两个参数来确定随机数的范围 例子: 生成的随机数控制在1到10之间? echo rand(1,10); 显示结果: 生成随机的数就是1到10之间,不可能出现10以外的数. 如果只写一个参数: 例子: echo rand(9); 显示结果: rand()方法,

php时间函数

实例讲解之前,先来介绍几个核心函数: mktime 函数 mktime() 函数返回一个日期的 Unix 时间戳. 参数总是表示 GMT 日期,因此 is_dst 对结果没有影响. 参数可以从右到左依次空着,空着的参数会被设为相应的当前 GMT 值. 语法:mktime(hour,minute,second,month,day,year,is_dst)参数               描述  hour       可选.规定小时.  minute   可选.规定分钟.  second   可选.

C/C++时间函数的使用

来源:http://blog.csdn.net/apull/article/details/5379819 一.获取日历时间time_t是定义在time.h中的一个类型,表示一个日历时间,也就是从1970年1月1日0时0分0秒到此时的秒数,原型是: typedef long time_t;        /* time value */可以看出time_t其实是一个长整型,由于长整型能表示的数值有限,因此它能表示的最迟时间是2038年1月18日19时14分07秒. 函数time可以获取当前日历时

错误处理和时间函数

错误处理和时间函数 一.错误处理 a)         错误报告级别 语法错误: error 会给一个致命错误  终止程序继续执行 运行时错误: notice warning  运行代码的时候错了 有错误提示,但是他们不会影响程序运行 但是结果不是我们想要的 逻辑错误:逻辑出现错误  最大的难就 就是不报错  不好排除 notice: 本身不是一个错误  只是一个提示 这个错误可以忽略 warning: 警告只要产生warning错误 程序的执行结果就不是我们想要的,但是这个级别的错误,不会终止

mysql学习笔记(五)--- 字符串函数、日期时间函数

一.常见字符串函数: 1.CHAR_LENGTH  获取长度(字符为单位) 2.FORMAT  格式化 3.INSERT  替换的方式插入 4.INSTR  获取位置 5.LEFT/RIGHT  取左.取右 6.LENGTH   获取长度(字节为单位) 7.LTRIM/RTRIM/TRIM 去空格(左/右/自定义) 8.STRCMP  字符串比较 9.CONCAT  字符串拼接 10.SUBSTRING  字符串截取 1.CHAR_LENGTH:获取长度(字符为单位) CHAR_LENGTH()

PHP 中日期时间函数 date() 用法总结

[导读] date()是我们常用的一个日期时间函数,下面我来总结一下关于date()函数的各种形式的用法,有需要学习的朋友可参考.格式化日期date() 函数的第一个参数规定了如何格式化日期 时间.它使用字母来表示日期和时间 格式化日期date() 函数的第一个参数规定了如何格式化日期/时间.它使用字母来表示日期和时间的格式.这里列出了一些可用的字母: •d - 月中的天 (01-31)•m - 当前月,以数字计 (01-12)•Y - 当前的年(四位数)您可以在我们的 PHP Date 参考手