Linux系統中時間區域和API


1、問題

在開發雲平台程序的時候,經常會碰到時間區域轉換的問題。比如,任何網絡存儲的文檔的metadata都自己記錄了編輯時間。但是,雲平台記錄時需要把這個時間轉成標准時間,便於管理。但是用戶使用的時候卻是根據他自己的時間來的。比如,

  • 某人需要在北京時間12/31:11:59把新年短信發給女朋友。太早發,太晚發都會惹人不高興。因此,系統搜索或安排任務的時候,需要根據某個時區,或是新年短信內部時間設定。
  • 再比如,在搜索電郵或短信的時候,根據RFC的定義,時間的搜索需要按照收到時間的字面數值。比如一個電郵的收到時間為Mon, 14 May 2018 23:23:27 -0700,那么搜索1 SEARCH BEFORE 14-May-2018必須返回這個電郵。但是,如果我們在標准時區,當地時間已經是5月15了。

2、概念與API

在最早的Unix中,一台機器只能處理UTC以外的一個時區。時區偏移量和名稱字符串(實際上是一對名稱字符串,一個用於夏季,一個用於冬季)已配置到內核中,並可供C程序使用。后來(1982)System III通過修改名為TZ的環境變量並調用函數tzset(3),可以設置session時區。從此,解釋TZ值區域規范的規則成為POSIX標准的一部分。

V7和較早的BSD Unix有其他各種配置本地時區的方法,這些方法不涉及使用或解釋TZ。他們與POSIX TZ的解釋有一個相同的致命缺陷,那就是它們不是為了應對時區系統的歷史不穩定而設計的。他們無法解釋一整套歷史位移/ DST的規則,從而正確表達過去本地時間和現在時刻。

在現代Unix系統上,TZ變量可能根本就沒有設置,但在任何進程中可以通過明確設置TZ來覆蓋系統默認時區。在啟動或通過重寫值TZ時,可以根據地理位置配置時區指示符(通常但不總是地區/主要城市對),例如“America / New_York”或“Europe / Vienna”或“Asia /台北”。如果標識符是通過TZ設置的,為了與POSIX標准向后兼容,可能需要以冒號開頭,以區別於舊式的時區規范。 

基於位置的區域命名方案[IANA-ZONES]由互聯網號碼分配機構IANA管理。

2.1、Unix中時間日期的格式

作為現在大多數操作系統的鼻祖,Unix中時間日期的format是相當亂的。這也反映了系統發展的歷程和不同貢獻者的設計偏好:

時間日期

解釋

1526924458

Unix UTC seconds

Mon May 21 11:20:32 PDT 2018

date(1) output

2018-05-21 11:23:27-07:00

$ date --rfc-3339=seconds

 

Mon, 21 May 2018 11:23:27 -0700

e-mail RFC-822/RFC-2822 format

Fri, 24 Oct 2014 19:32:27 GMT

HTTP (RFC-2616/RFC-7231) format

20141024192327.000000Z

LDAP (RFC-2252/X.680/X.208) format

2014-10-24 15:32:27

Modified ISO-8601 local time

2014-10-24T15:32:27

Strict ISO-8601 local time

2014-10-24T19:32:27Z

RFC-3339 time, always UTC and marked Z

在用unix程序時,兩個使用最多的數據結構是time_t和struct tm,time_t其實就是int32_t,而struct tm的定義如下,我們發現這個結構有個問題,沒有時區信息:

struct tm
{
    int    tm_sec;   /* seconds [0,60] (60 for + leap second) */
    int    tm_min;   /* minutes [0,59] */
    int    tm_hour;  /* hour [0,23] */
    int    tm_mday;  /* day of month [1,31] */
    int    tm_mon ;  /* month of year [0,11] */
    int    tm_year;  /* years since 1900 */
    int    tm_wday;  /* day of week [0,6] (Sunday = 0) */
    int    tm_yday;  /* day of year [0,365] */
    int    tm_isdst; /* daylight saving flag */
};

時區是通過環境變量"TZ"來定義的,使之生效需要調用tzset函數,查看時區需要使用tzname:

setenv("TZ", "Australia/Currie", 1);
tzset();
printf ("tz: [%s:%s]\n", tzname[0], tzname[1]);

特別需要注意的是 char * tzname [2]

數組tzname包含兩個字符串,它們是用戶選擇的時區(標准和夏令時)的標准名稱。 tzname [0]是標准時區的名稱(例如“EST”),tzname [1]是使用夏令時的時區名稱(例如“EDT”)。這些對應於來自TZ環境變量的std和dst字符串(分別)。如果從不使用夏令時,則tzname [1]是空字符串。

tzname數組在tzset,ctime,strftime,mktime或localtime被調用時從TZ環境變量初始化。如果使用了多個縮寫(例如美國東部時間和東部夏令時的“EWT”和“EDT”),則該數組包含最近的縮寫。

tzname數組對於POSIX.1兼容性是必需的,但在GNU程序中,最好使用分解時間結構的tm_zone成員,因為即使它不是最新的縮寫,tm_zone也會報告正確的縮寫。

2.2、API

  • Unix時間: 
#include <time.h>

/* Obtain current time. */
time_t current_time = time(NULL);

C標准定義time()返回的time_t值不是特定時區的,自1970年1月1日00:00 UTC時刻以來流逝的秒數。使用時不能假設其內部實現,需要使用C標准庫中適當的函數(如gmtime和localtime)將time_t轉換為struct tm並獲得對時間戳細節的訪問。  

  • 設置時區:tzset
#include <time.h>
void tzset (void);
extern char *tzname[2]; /* time zone name */
extern long timezone;   /* seconds west of UTC, *not* DST-corrected */
extern int daylight;    /* nonzero if DST is ever in effect here */
  • 獲取標准或本地時間
    • gmtime/gmtime_r:獲取標准時間,不會修改時區變量
    • localtime/localtime_r:獲取本地時間,如果本地時間未設置,會根據地理位置修改時區變量
#include <time.h>
struct tm *gmtime(const time_t *);
struct tm *gmtime_r(const time_t *, struct tm *);
struct tm *localtime(const time_t *);
struct tm *localtime_r(const time_t *, struct tm *);
  • 從本地時間轉為Unix時間(秒):mktime
#include <time.h>

time_t mktime(struct tm *tm);

 mktime函數是一個標准函數,是localtime的反函數,將輸入本地時間struct tm轉換為Unix時間time_t。

  • 其它函數

其它還有輔助函數幫助時間轉換為字符串或從字符串轉換成時間,因為和時區的關系不大,我們不詳細解釋。

    • asctime將struct tm對象轉換為文本表示(不建議使用)
    • ctime將time_t值轉換為文本表示
    • strftime將struct tm對象轉換為自定義文本表示
    • wcsftime將struct tm對象轉換為自定義寬字符串文本表示

3、用例

 我們有一個已知時間和時區位移,需要輸出在該時區的時間,返回值為該天的年月日(yyyyMMdd)。

int timestamp_to_timezonedate(time_t date_timestamp, int32_t tz_offset, char *buf, size_t bufsize) {
    struct tm dat = {0};

date_timestamp += tz_offset * 60;
gmtime_r(&date_timestamp, &dat); if (buf) strftime(buf, bufsize, "%Y-%m-%dT%H:%M:%S", &dat); return ((dat.tm_year+1900) * 10000) + ((dat.tm_mon+1) * 100) + (dat.tm_mday); }

再看看如何獲取當前時區和標准時間的差別(分鍾):

//return local timezone offset in minutes
int store_local_tz_offset(void) {
    time_t gmt, rawtime = time(NULL);
    struct tm gbuf;

    gmtime_r(&rawtime, &gbuf);

    // Request that mktime() looks up dst in timezone database
    gbuf.tm_isdst = -1;

    //gmt = rawtime - offset 
    //gbuf is used as local tm, therefore gmt will be shifted back with offset
    gmt = mktime(&gbuf);

    //rawtime - gmt
    return (int)difftime(rawtime, gmt)/60;
}

4、One more thing

一般的時區位移是分鍾。比如Mon, 21 May 2018 11:23:27 -0700,最后幾位是時區位移:[+|-]HHMM。

有些地方的時區位移是半小時,所以為了支持這些地區,在小時后面會有分鍾的參數,比如印度 Tuesday 2018-05-22|04:56:39 +0630

 

參考文獻

[1]https://www.gnu.org/software/libc/manual/html_node/Time-Zone-Functions.html

[2]http://www.catb.org/esr/time-programming/


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM