菜鸟笔记
提升您的技术认知

Linux mktime 源代码简析

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

 

 

 
  1. /* Converts Gregorian date to seconds since 1970-01-01 00:00:00.

  2. * Assumes input in normal date format, i.e. 1980-12-31 23:59:59

  3. * => year=1980, mon=12, day=31, hour=23, min=59, sec=59.

  4. *

  5. * [For the Julian calendar (which was used in Russia before 1917,

  6. * Britain & colonies before 1752, anywhere else before 1582,

  7. * and is still in use by some communities) leave out the

  8. * -year/100+year/400 terms, and add 10.]

  9. *

  10. * This algorithm was first published by Gauss (I think).

  11. *

  12. * WARNING: this function will overflow on 2106-02-07 06:28:16 on

  13. * machines where long is 32-bit! (However, as time_t is signed, we

  14. * will already get problems at other places on 2038-01-19 03:14:08)

  15. */

  16. unsigned long

  17. mktime(const unsigned int year0, const unsigned int mon0,

  18. const unsigned int day, const unsigned int hour,

  19. const unsigned int min, const unsigned int sec)

  20. {

  21. unsigned int mon = mon0, year = year0;

  22.  
  23. /* 1..12 -> 11,12,1..10 */

  24. if (0 >= (int) (mon -= 2)) {

  25. mon += 12; /* Puts Feb last since it has leap day */

  26. year -= 1;

  27. }

  28.  
  29. return ((((unsigned long)

  30. (year/4 - year/100 + year/400 + 367*mon/12 + day) +

  31. year*365 - 719499

  32. )*24 + hour /* now have hours */

  33. )*60 + min /* now have minutes */

  34. )*60 + sec; /* finally seconds */

  35. }

 

这个函数的功能是获得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都是源码中用到的变量

 

 
  1. 当mon0 > 2 时, mon = mon0 - 2, year = year0 (2a)

  2. 当mon0 <= 2 时, mon = mon0 + 10, year = year0 - 1 (2b)

  3. magic = 367 * mon / 12

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

 

 
  1. /* 1..12 -> 11,12,1..10 */

  2. if (0 >= (int) (mon -= 2)) {

  3. mon += 12; /* Puts Feb last since it has leap day */

  4. year -= 1;

  5. }

 

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

 

 
  1. 当mon0 > 2 时, ydays = magic + day + 28 (3a)

  2. 当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 时

 

 
  1. days = year/4 - year/100 + year/400 + (year - 1) * 365

  2. + 367 * mon / 12 + day + 28 - 719162

 

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

 

 
  1. days = year/4 - year/100 + year/400 + year * 365

  2. + 367 * mon / 12 + day - 365 + 28 - 719162

得到相同的等式:

 

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

  2. + year*365 - 719499

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

 

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

 

367*mon/12

我们再来看看这个算式的计算结果:

 

 
  1. 计算值

  2. mon mon0 value

  3. 1 (3) 30

  4. 2 (4) 61

  5. 3 (5) 91

  6. 4 (6) 122

  7. 5 (7) 152

  8. 6 (8) 183

  9. 7 (9) 214

  10. 8 (10) 244

  11. 9 (11) 275

  12. 10 (12) 305

  13. 11 (1) 336

  14. 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中是通过查表法来获得月份天数信息的

 

 
  1. const unsigned short int __mon_yday[2][13] =

  2. {

  3. /* Normal years. */

  4. { 0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334, 365 },

  5. /* Leap years. */

  6. { 0, 31, 60, 91, 121, 152, 182, 213, 244, 274, 305, 335, 366 }

  7. };

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

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