Linux mktime 源代码简析

之前已经有人对这个源代码做过分析了,参见http://blog.csdn.net/axx1611/article/details/1792827?reload


这里选择从另外一个角度再次解析这部分代码,建议先阅读上面的博客内容:


/* Converts Gregorian date to seconds since 1970-01-01 00:00:00.
 * Assumes input in normal date format, i.e. 1980-12-31 23:59:59
 * => year=1980, mon=12, day=31, hour=23, min=59, sec=59.
 *
 * [For the Julian calendar (which was used in Russia before 1917,
 * Britain & colonies before 1752, anywhere else before 1582,
 * and is still in use by some communities) leave out the
 * -year/100+year/400 terms, and add 10.]
 *
 * This algorithm was first published by Gauss (I think).
 *
 * WARNING: this function will overflow on 2106-02-07 06:28:16 on
 * machines where long is 32-bit! (However, as time_t is signed, we
 * will already get problems at other places on 2038-01-19 03:14:08)
 */
unsigned long
mktime(const unsigned int year0, const unsigned int mon0,
       const unsigned int day, const unsigned int hour,
       const unsigned int min, const unsigned int sec)
{
	unsigned int mon = mon0, year = year0;

	/* 1..12 -> 11,12,1..10 */
	if (0 >= (int) (mon -= 2)) {
		mon += 12;	/* Puts Feb last since it has leap day */
		year -= 1;
	}

	return ((((unsigned long)
		  (year/4 - year/100 + year/400 + 367*mon/12 + day) +
		  year*365 - 719499
	    )*24 + hour /* now have hours */
	  )*60 + min /* now have minutes */
	)*60 + sec; /* finally seconds */
}

这个函数的功能是获得1970年1月1日至今的秒数,我设这个数为totol_sec

首先需要获得公元元年1月1日至今的天数,我们设这个数为days

显然:

totol_sec = ((days * 24 + hour) * 60 + min) * 60 + sec

后面的部分比较简单,难点在于获取days

设leapdays为公元0年到今天之前(不包含今天)的闰天数
设ydays为今年1月1日至今的天数
另: 公元0年到1970年1月1日的天数为719162天

因此:

days = leapdays + (year0 - 1) * 365 + ydays - 719162    (1)

为了更好的理解上面的源代码,我们引入变量mon, year, magic,其中mon和year都是源码中用到的变量

当mon0 > 2 时, mon = mon0 - 2, year = year0             (2a)
当mon0 <= 2 时, mon = mon0 + 10, year = year0 - 1       (2b)
magic = 367 * mon / 12

上面的(2a) (2b)等价于源码中的

/* 1..12 -> 11,12,1..10 */
if (0 >= (int) (mon -= 2)) {
    mon += 12;	/* Puts Feb last since it has leap day */
    year -= 1;
}

遍历mon0(参见上面的博客内容)我们可以发现magic有如下的性质

当mon0 > 2 时, ydays = magic + day + 28                 (3a)
当mon0 <= 2 时, ydays = magic + day - 365 + 28          (3b)

同时,无论mon0为何值,year是否等于year0都有

leapdays = year/4 - year/100 + year/400                 (4)

结合(1)(2a)(3a)(4), 当mon0 > 2 时

days = year/4 - year/100 + year/400 + (year - 1) * 365
        + 367 * mon / 12 + day + 28 - 719162

结合(1)(2b)(3b)(4), 当mon0 <= 2 时

days = year/4 - year/100 + year/400 + year * 365
        + 367 * mon / 12 + day - 365 + 28 - 719162

得到相同的等式:

days = (year/4 - year/100 + year/400 + 367*mon/12 + day)
          + year*365 - 719499

这也就是源码中获取经过天数的公式。


可以看到源码中最精彩最难以理解的部分就是

367*mon/12
我们再来看看这个算式的计算结果:

计算值
        mon   mon0     value
        1     (3)      30
        2     (4)      61
        3     (5)      91
        4     (6)      122
        5     (7)      152
        6     (8)      183
        7     (9)      214
        8     (10)     244
        9     (11)     275
        10    (12)     305
        11    (1)      336
        12    (2)      367

可以发现这个计算结果实际上隐含了一个信息就是,它恰好代表着之前月份天数的和month_day_in_year ,只是这个和是一个经过偏转的值即

value = month_day_in_year - 29

1月和2月比较特殊,因为他们被加上了12个月,也就是1年,他们的value实际上应该先减去365,也就是

value - 365 = month_day_in_year - 29

也就是说367 * mon / 12这个算式经过取整运算刚好能得出月份天数和的信息,这应该是一个数学上的巧合。

据说是高斯最先提出这种算法,实在是佩服这些天才。

我也参看了glibc相同功能的代码(参见glicb源码time/mktime.c),在glibc中是通过查表法来获得月份天数信息的

const unsigned short int __mon_yday[2][13] =
{
    /* Normal years.  */
    { 0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334, 365 },
    /* Leap years.  */
    { 0, 31, 60, 91, 121, 152, 182, 213, 244, 274, 305, 335, 366 }
};

因为linux源码中已经处理了闰年的情况,所以我们只需要关注Normal years的部分就行了。根据上面的分析倒算回去对比一下,其实是一样的。

从效率上来说,linux和glibc的mktime都是差不多的,但是linux版本的mktime不需要依靠全局变量。


  • 2
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
static __time64_t __cdecl _make__time64_t ( struct tm *tb, int ultflag ) { __time64_t tmptm1, tmptm2, tmptm3; struct tm tbtemp; long dstbias = 0; long timezone = 0; _VALIDATE_RETURN( ( tb != NULL ), EINVAL, ( ( __time64_t )( -1 ) ) ) /* * First, make sure tm_year is reasonably close to being in range. */ if ( ((tmptm1 = tb->tm_year) < _BASE_YEAR - 1) || (tmptm1 > _MAX_YEAR64 + 1) ) goto err_mktime; /* * Adjust month value so it is in the range 0 - 11. This is because * we don't know how many days are in months 12, 13, 14, etc. */ if ( (tb->tm_mon < 0) || (tb->tm_mon > 11) ) { tmptm1 += (tb->tm_mon / 12); if ( (tb->tm_mon %= 12) < 0 ) { tb->tm_mon += 12; tmptm1--; } /* * Make sure year count is still in range. */ if ( (tmptm1 < _BASE_YEAR - 1) || (tmptm1 > _MAX_YEAR64 + 1) ) goto err_mktime; } /***** HERE: tmptm1 holds number of elapsed years *****/ /* * Calculate days elapsed minus one, in the given year, to the given * month. Check for leap year and adjust if necessary. */ tmptm2 = _days[tb->tm_mon]; if ( _IS_LEAP_YEAR(tmptm1) && (tb->tm_mon > 1) ) tmptm2++; /* * Calculate elapsed days since base date (midnight, 1/1/70, UTC) * * * 365 days for each elapsed year since 1970, plus one more day for * each elapsed leap year. no danger of overflow because of the range * check (above) on tmptm1. */ tmptm3 = (tmptm1 - _BASE_YEAR) * 365 + _ELAPSED_LEAP_YEARS(tmptm1); /* * elapsed days to current month (still no possible overflow) */ tmptm3 += tmptm2; /* * elapsed days to current date. */ tmptm1 = tmptm3 + (tmptm2 = (__time64_t)(tb->tm_mday)); /***** HERE: tmptm1 holds number of elapsed days *****/ /* * Calculate elapsed hours since base date */ tmptm2 = tmptm1 * 24; tmptm1 = tmptm2 + (tmptm3 = (__time64_t)tb->tm_hour); /***** HERE: tmptm1 holds number of elapsed hours *****/ /* * Calculate elapsed minutes since base date */ tmptm2 = tmptm1 * 60; tmptm1 = tmptm2 + (tmptm3 = (__time64_t)tb->tm_min); /***** HERE: tmptm1 holds number of elapsed minutes *****/ /* * Calculate elapsed seconds since base date */ tmptm2 = tmptm1 * 60; tmptm1 = tmptm2 + (tmptm3 = (__time64_t)tb->tm_sec); /***** HERE: tmptm1 holds number of elapsed seconds *****/ if ( ultflag ) { /* * Adjust for timezone. No need to check for overflow since * localtime() will check its arg value */ __tzset(); _ERRCHECK(_get_dstbias(&dstbias;)); _ERRCHECK(_get_timezone(&timezone;)); tmptm1 += timezone; /* * Convert this second count back into a time block structure. * If localtime returns NULL, return an error. */ if ( _localtime64_s(&tbtemp;, &tmptm1;) != 0 ) goto err_mktime; /* * Now must compensate for DST. The ANSI rules are to use the * passed-in tm_isdst flag if it is non-negative. Otherwise, * compute if DST applies. Recall that tbtemp has the time without * DST compensation, but has set tm_isdst correctly. */ if ( (tb->tm_isdst > 0) || ((tb->tm_isdst < 0) && (tbtemp.tm_isdst > 0)) ) { tmptm1 += dstbias; if ( _localtime64_s(&tbtemp;, &tmptm1;) != 0 ) goto err_mktime; } } else { if ( _gmtime64_s(&tbtemp;, &tmptm1;) != 0) goto err_mktime; } /***** HERE: tmptm1 holds number of elapsed seconds, adjusted *****/ /***** for local time if requested *****/ *tb = tbtemp; return tmptm1; err_mktime: /* * All errors come to here */ errno = EINVAL; return (__time64_t)(-1); }

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值