说说Linux下的夏令时问题

需求如下:

Phil
As you know in North America we change time twice a year – 1 hour forward in Spring and 1 hour back in Fall.
This means all terminal users need to go in Date/time settings to change the time manually.
The time change happens 2nd Sunday of March (1 hour forward) at 2 am and 1st Sunday of November at 2 am (1 hour back). 
Can this time change be automated in the kernel or app manager or OS level so the user doesn’t have to do this manually?

Shelly


解决方案

第一步:

copy或者ln -s一个属于夏令时的时区文件(如New_York)到/etc/localtime

cp /usr/share/zoneinfo/America/New_York /etc/localtime  


第二步:
写一个测试程序测试下
int main(int argc, char** argv)
{
	time_t currtime; 
	char ptchTime[30];
	struct tm* pTm;

	time(&currtime); 
	pTm = localtime(&currtime); 
	if (pTm) 
	{ 
		sprintf(ptchTime, "%02d%02d%02d %02d%02d%02d[DST=%d]\n",
			pTm->tm_year%100,  pTm->tm_mon+1, pTm->tm_mday,
			 pTm->tm_hour, pTm->tm_min, pTm->tm_sec, pTm->tm_isdst); 
	} 

	printf(ptchTime);

	struct tm tm_new;
	time_t the_time;
	

	tm_new.tm_year = 113; tm_new.tm_mon = 10; tm_new.tm_mday = 3;
	tm_new.tm_hour = 1; tm_new.tm_min = 59; tm_new.tm_sec = 0;
	

	printf("tm_new est=%d\n", tm_new.tm_isdst);
	the_time=mktime(&tm_new);
	//update system time
	stime((long*)&the_time);
	//update hardware time 
	system("hwclock -w");
	
	time(&currtime); 
	pTm = localtime(&currtime); 
	if (pTm) 
	{ 
		sprintf(ptchTime, "%02d%02d%02d %02d%02d%02d[DST=%d]\n",
			pTm->tm_year%100,  pTm->tm_mon+1, pTm->tm_mday,
			 pTm->tm_hour, pTm->tm_min, pTm->tm_sec, pTm->tm_isdst); 
	} 

	printf(ptchTime);	
	return 0;
}

测试发现3月可自动进一个小时,可是11月时间不变。。原来没初始化:
memset((char*)&tm_new, 0 ,sizeof(struct tm));
同时man mktime查看结构体定义如下:
       Broken-down time is stored in the structure tm which is defined in <time.h> as follows:

           struct tm {
               int tm_sec;         /* seconds */
               int tm_min;         /* minutes */
               int tm_hour;        /* hours */
               int tm_mday;        /* day of the month */
               int tm_mon;         /* month */
               int tm_year;        /* year */
               int tm_wday;        /* day of the week */
               int tm_yday;        /* day in the year */
               int tm_isdst;       /* daylight saving time */
           };

       The members of the tm structure are:

       tm_sec    The number of seconds after the minute, normally in the range 0 to 59, but can be up to 60 to allow for leap seconds.

       tm_min    The number of minutes after the hour, in the range 0 to 59.

       tm_hour   The number of hours past midnight, in the range 0 to 23.

       tm_mday   The day of the month, in the range 1 to 31.

       tm_mon    The number of months since January, in the range 0 to 11.

       tm_year   The number of years since 1900.

       tm_wday   The number of days since Sunday, in the range 0 to 6.

       tm_yday   The number of days since January 1, in the range 0 to 365.

       tm_isdst  A flag that indicates whether daylight saving time is in effect at the time described.  The value is positive if daylight saving time is
                 in effect, zero if it is not, and negative if the information is not available.

尝试修改tm_isdst初始化为0, tm_new.tm_isdst=0,没DST效果;
尝试修改 tm_isdst初始化为1,tm_new.tm_isdst=1, 再次测试发现,全年都被DST影响,设置的时间推后和提前了一个小时,
这不科学啊。。。还可以设为-1,可是-1是神马概念。。。。啊。啊。。

第三步:
奇怪的地方在于,直接用date命令,DST运行正常

# date -s 110301592013
# date
Sun Nov  3 01:59:18 EDT 2013
# date
Sun Nov  3 01:59:54 EDT 2013
# date
Sun Nov  3 01:59:55 EDT 2013
# date
Sun Nov  3 01:59:58 EDT 2013
# date
Sun Nov  3 01:00:01 EST 2013

此时猜测date命令已经正确实现了DST设置,于是读busybox的源码实现,
	/* Now we have parsed all the information except the date format
	   which depends on whether the clock is being set or read */

	if(filename) {
		struct stat statbuf;
		xstat(filename,&statbuf);
		tm=statbuf.st_mtime;
	} else time(&tm);
	memcpy(&tm_time, localtime(&tm), sizeof(tm_time));
	/* Zero out fields - take her back to midnight! */
	if (date_str != NULL) {
		tm_time.tm_sec = 0;
		tm_time.tm_min = 0;
		tm_time.tm_hour = 0;

		/* Process any date input to UNIX time since 1 Jan 1970 */
		if (ENABLE_FEATURE_DATE_ISOFMT && (opt & DATE_OPT_HINT)) {
			strptime(date_str, hintfmt_arg, &tm_time);
		} else if (strchr(date_str, ':') != NULL) {
			date_conv_ftime(&tm_time, date_str);
		} else {
			date_conv_time(&tm_time, date_str);
		}

		/* Correct any day of week and day of year etc. fields */
		tm_time.tm_isdst = -1;	/* Be sure to recheck dst. */
		tm = mktime(&tm_time);
		if (tm < 0) {
			bb_error_msg_and_die(bb_msg_invalid_date, date_str);
		}
		if (utc && putenv("TZ=UTC0") != 0) {
			bb_error_msg_and_die(bb_msg_memory_exhausted);
		}

		/* if setting time, set it */
		if (set_time && stime(&tm) < 0) {
			bb_perror_msg("cannot set date");
		}
	}

发现,意外中的设置:
tm_time.tm_isdst = -1;	/* Be sure to recheck dst. */

比较参数说明:
       tm_isdst  A flag that indicates whether daylight saving time is in effect at the time described.  The value is positive if daylight saving time is
                 in effect, zero if it is not, and negative if the information is not available.
太难理解啊,google之,发现两篇文章:
http://stackoverflow.com/questions/8558919/mktime-and-tm-isdst
http://blog.csdn.net/michaelrun/article/details/4616798
特别是后面这篇国人的分析,分析相当到位,和我的猜测水到渠成:
-1   ,最智能的选项,在这个时候,mktime 函数自动考虑夏令时,为您算出最正确的值。

最后完美的dst代码应该是
	struct tm tm_new;
	time_t the_time;
	memset((char*)&tm_new, 0 ,sizeof(struct tm));

	tm_new.tm_year = 113; tm_new.tm_mon = 10; tm_new.tm_mday = 3;
	tm_new.tm_hour = 1; tm_new.tm_min = 59; tm_new.tm_sec = 0;
	tm_new.tm_isdst=-1;/* Be sure to recheck dst. */

	printf("tm_new est=%d\n", tm_new.tm_isdst);
	the_time=mktime(&tm_new);
	//update system time
	stime((long*)&the_time);


结论:
解决问题看似简单,发现问题需十年功啊。

附赠:线程安全的 localtime_r 函数实现:
 struct tm *localtime_r(const time_t *timer,struct tm *result){
 struct tm *tm;
 tm=localtime(timer);
 memmove(result,tm,sizeof(tm));
 return result;
}

重要后续:

经过继续测试发现,假设时区TZ='America/New_York',
第一次调用
Sys_SetDateTime("11032013", "015950"); 可以正常置为EDT时间,从2:00跳转到1:00
但是如若连续设置时间
Sys_SetDateTime("03112013", "015950");
Sys_SetDateTime("11032013", "015950");
却为EST时间,不再从2:00跳转到1:00,
奇怪的是用busybox的单独date却又可以,审阅代码发现和date命令没有区别,
于是继续研究glibc中的mktime函数,发现了奥妙,

/* Convert *TP to a time_t value, inverting
   the monotonic and mostly-unit-linear conversion function CONVERT.
   Use *OFFSET to keep track of a guess at the offset of the result,
   compared to what the result would be for UTC without leap seconds.
   If *OFFSET's guess is correct, only one CONVERT call is needed.  */
#ifndef _LIBC
static
#endif
time_t
__mktime_internal (struct tm *tp,
		   struct tm *(*convert) (const time_t *, struct tm *),
		   time_t *offset)
{
  time_t t, dt, t0, t1, t2;
  struct tm tm;

  /* The maximum number of probes (calls to CONVERT) should be enough
     to handle any combinations of time zone rule changes, solar time,
     leap seconds, and oscillations around a spring-forward gap.
     POSIX.1 prohibits leap seconds, but some hosts have them anyway.  */
  int remaining_probes = 6;

  /* Time requested.  Copy it in case CONVERT modifies *TP; this can
     occur if TP is localtime's returned value and CONVERT is localtime.  */
  int sec = tp->tm_sec;
  int min = tp->tm_min;
  int hour = tp->tm_hour;
  int mday = tp->tm_mday;
  int mon = tp->tm_mon;
  int year_requested = tp->tm_year;
  int isdst = tp->tm_isdst;

  /* 1 if the previous probe was DST.  */
  int dst2;

  /* Ensure that mon is in range, and set year accordingly.  */
  int mon_remainder = mon % 12;
  int negative_mon_remainder = mon_remainder < 0;
  int mon_years = mon / 12 - negative_mon_remainder;
  int year = year_requested + mon_years;

  /* The other values need not be in range:
     the remaining code handles minor overflows correctly,
     assuming int and time_t arithmetic wraps around.
     Major overflows are caught at the end.  */

  /* Calculate day of year from year, month, and day of month.
     The result need not be in range.  */
  int yday = ((__mon_yday[__isleap (year + TM_YEAR_BASE)]
	       [mon_remainder + 12 * negative_mon_remainder])
	      + mday - 1);

  int sec_requested = sec;

  /* Only years after 1970 are defined.
     If year is 69, it might still be representable due to
     timezone differences.  */
  if (year < 69)
    return -1;

#if LEAP_SECONDS_POSSIBLE
  /* Handle out-of-range seconds specially,
     since ydhms_tm_diff assumes every minute has 60 seconds.  */
  if (sec < 0)
    sec = 0;
  if (59 < sec)
    sec = 59;
#endif

  /* Invert CONVERT by probing.  First assume the same offset as last time.
     Then repeatedly use the error to improve the guess.  */

  tm.tm_year = EPOCH_YEAR - TM_YEAR_BASE;
  tm.tm_yday = tm.tm_hour = tm.tm_min = tm.tm_sec = 0;
  t0 = ydhms_tm_diff (year, yday, hour, min, sec, &tm);

  for (t = t1 = t2 = t0 + *offset, dst2 = 0;
       (dt = ydhms_tm_diff (year, yday, hour, min, sec,
			    ranged_convert (convert, &t, &tm)));
       t1 = t2, t2 = t, t += dt, dst2 = tm.tm_isdst != 0)
    if (t == t1 && t != t2
	&& (tm.tm_isdst < 0
	    || (isdst < 0
		? dst2 <= (tm.tm_isdst != 0)
		: (isdst != 0) != (tm.tm_isdst != 0))))
      /* We can't possibly find a match, as we are oscillating
	 between two values.  The requested time probably falls
	 within a spring-forward gap of size DT.  Follow the common
	 practice in this case, which is to return a time that is DT
	 away from the requested time, preferring a time whose
	 tm_isdst differs from the requested value.  (If no tm_isdst
	 was requested and only one of the two values has a nonzero
	 tm_isdst, prefer that value.)  In practice, this is more
	 useful than returning -1.  */
      break;
    else if (--remaining_probes == 0)
      return -1;

  /* If we have a match, check whether tm.tm_isdst has the requested
     value, if any.  */
  if (dt == 0 && isdst != tm.tm_isdst && 0 <= isdst && 0 <= tm.tm_isdst)
    {
      /* tm.tm_isdst has the wrong value.  Look for a neighboring
	 time with the right value, and use its UTC offset.
	 Heuristic: probe the previous three calendar quarters (approximately),
	 looking for the desired isdst.  This isn't perfect,
	 but it's good enough in practice.  */
      int quarter = 7889238; /* seconds per average 1/4 Gregorian year */
      int i;

      /* If we're too close to the time_t limit, look in future quarters.  */
      if (t < TIME_T_MIN + 3 * quarter)
	quarter = -quarter;

      for (i = 1; i <= 3; i++)
	{
	  time_t ot = t - i * quarter;
	  struct tm otm;
	  ranged_convert (convert, &ot, &otm);
	  if (otm.tm_isdst == isdst)
	    {
	      /* We found the desired tm_isdst.
		 Extrapolate back to the desired time.  */
	      t = ot + ydhms_tm_diff (year, yday, hour, min, sec, &otm);
	      ranged_convert (convert, &t, &tm);
	      break;
	    }
	}
    }

  *offset = t - t0;

#if LEAP_SECONDS_POSSIBLE
  if (sec_requested != tm.tm_sec)
    {
      /* Adjust time to reflect the tm_sec requested, not the normalized value.
	 Also, repair any damage from a false match due to a leap second.  */
      t += sec_requested - sec + (sec == 0 && tm.tm_sec == 60);
      if (! (*convert) (&t, &tm))
	return -1;
    }
#endif

  if (TIME_T_MAX / INT_MAX / 366 / 24 / 60 / 60 < 3)
    {
      /* time_t isn't large enough to rule out overflows in ydhms_tm_diff,
	 so check for major overflows.  A gross check suffices,
	 since if t has overflowed, it is off by a multiple of
	 TIME_T_MAX - TIME_T_MIN + 1.  So ignore any component of
	 the difference that is bounded by a small value.  */

      double dyear = (double) year_requested + mon_years - tm.tm_year;
      double dday = 366 * dyear + mday;
      double dsec = 60 * (60 * (24 * dday + hour) + min) + sec_requested;

      /* On Irix4.0.5 cc, dividing TIME_T_MIN by 3 does not produce
	 correct results, ie., it erroneously gives a positive value
	 of 715827882.  Setting a variable first then doing math on it
	 seems to work.  ([email protected]) */

      const time_t time_t_max = TIME_T_MAX;
      const time_t time_t_min = TIME_T_MIN;

      if (time_t_max / 3 - time_t_min / 3 < (dsec < 0 ? - dsec : dsec))
	return -1;
    }

  if (year == 69)
    {
      /* If year was 69, need to check whether the time was representable
	 or not.  */
      if (t < 0 || t > 2 * 24 * 60 * 60)
	return -1;
    }

  *tp = tm;
  return t;
}


static time_t localtime_offset;

/* Convert *TP to a time_t value.  */
time_t
mktime (tp)
     struct tm *tp;
{
#ifdef _LIBC
  /* POSIX.1 8.1.1 requires that whenever mktime() is called, the
     time zone names contained in the external variable `tzname' shall
     be set as if the tzset() function had been called.  */
  __tzset ();
#endif

  return __mktime_internal (tp, my_mktime_localtime_r, &localtime_offset);
}



其实就是关键这个dst2变量,他会根据上次设置过后的dst推测现在的dst设置,因为我的函数是连续调用2次,在内存中,所以这个dst2保留之前的dst=0,
而连续调date命令,其实是两个不同的进程,dst2并没有保存下来。

至此,耗费一周的研究才把这个来龙去脉搞清楚,通读了busybox和glibc的代码,代码解释一切啊:——)



你可能感兴趣的:(说说Linux下的夏令时问题)