kernel_mktime() 詳解 —— Linux-0.11 學習筆記(四)

ARM的程式設計師敲著詩歌的夢發表於2020-04-04

題目:kernel_mktime() 詳解 —— Linux-0.11 學習筆記(四)

init/main.c檔案中,有一個函式static void time_init(void)

該函式讀取 CMOS 實時時鐘資訊作為開機時間,並儲存到全域性變數startup_time (以秒為單位)中。

static void time_init(void)
{
    struct tm time;

    do {
        time.tm_sec = CMOS_READ(0); //當前時間的秒值,格式均是BCD碼
        time.tm_min = CMOS_READ(2); //當前時間的分鐘值
        time.tm_hour = CMOS_READ(4); //當前時間的小時值
        time.tm_mday = CMOS_READ(7); //當前的日
        time.tm_mon = CMOS_READ(8);  //當前的月
        time.tm_year = CMOS_READ(9); //當前的年,只有後2位數,例如97表示1997年
    } while (time.tm_sec != CMOS_READ(0));
    BCD_TO_BIN(time.tm_sec);  // 轉換成二進位制數值
    BCD_TO_BIN(time.tm_min);
    BCD_TO_BIN(time.tm_hour);
    BCD_TO_BIN(time.tm_mday);
    BCD_TO_BIN(time.tm_mon);
    BCD_TO_BIN(time.tm_year);
    time.tm_mon--;           // 減一後月份範圍是0~11
    startup_time = kernel_mktime(&time);
}

6~11行:讀出當前時間,注意,格式為BCD碼值;
13~18行:把BCD碼值轉換成二進位制;

第19行:time.tm_mon--;這裡把月份值減一,為什麼這樣做,後文會說明。

第20行:呼叫函式kernel_mktime(),計算從 1970 年 1 月 1 日 0 時起到此次開機時刻經過的秒數,作為開機時間。

上面的程式碼就說到這裡,CMOS_READ, BCD_TO_BIN等巨集定義以後再說。本文想說說kernel_mktime這個函式。此函式在檔案kernel/mktime.c 的第 41 行。kernel/mktime.c這個檔案很短,僅有58行。

1. 巨集定義

#define MINUTE 60        //1分鐘經過的秒數
#define HOUR (60*MINUTE) //1小時經過的秒數
#define DAY (24*HOUR)    //一天經過的秒數
#define YEAR (365*DAY)   //一年經過的秒數(不考慮閏年)

2. 從1.1x.1經過的秒數

static int month[12] = {
    0,                                        //[0]  1.1-1.1
    DAY*(31),                                 //[1]  1.1-2.1
    DAY*(31+29),                              //[2]  1.1-3.1
    DAY*(31+29+31),                           //[3]  1.1-4.1
    DAY*(31+29+31+30),                        //[4]  1.1-5.1              
    DAY*(31+29+31+30+31),                     //[5]  1.1-6.1
    DAY*(31+29+31+30+31+30),                  //[6]  1.1-7.1
    DAY*(31+29+31+30+31+30+31),               //[7]  1.1-8.1
    DAY*(31+29+31+30+31+30+31+31),            //[8]  1.1-9.1
    DAY*(31+29+31+30+31+30+31+31+30),         //[9]  1.1-10.1
    DAY*(31+29+31+30+31+30+31+31+30+31),     //[10]  1.1-11.1
    DAY*(31+29+31+30+31+30+31+31+30+31+30)   //[11]  1.1-12.1
};

假如當前是4月,問:從本年1月1日起到4月1日,經過了多少秒?

可以先算出經過了多少天,再把天數乘以DAY(見巨集定義)。如果用 D(m) 表示月份m的總天數,那麼答案就是:

( D(1) + D(2) + D(3) ) * DAY

把上面的問題一般化為:假如當前是x月,問:從本年1月1日起到x月1日,經過了多少秒?

答案是:

( D(1) + D(2) + D(3) + ... + D(x-1) ) * DAY

思路就是這樣, Linus 用的是查表法,於是就有了上面的陣列。比如從CMOS中讀出的是8月份,那麼答案就是month[7];再比如讀出的是12月份,那麼答案就是month[11];再來個特殊情況,比如讀出的是1月份,那麼就是0,即month[0]. 看出來了吧,索引值比真實的月份值少1,這就是time.tm_mon--;的原因。

注意,程式碼中假設今年是閏年,即2月份有29天。

3. 結構體struct tm

struct tm {
    int tm_sec;
    int tm_min;
    int tm_hour;
    int tm_mday;
    int tm_mon;
    int tm_year;  //以上6行不用多說,用來儲存讀出來的年月日時分秒
    int tm_wday;
    int tm_yday;
    int tm_isdst; //夏令時標誌
};

8~10行:這3個成員好像沒有用到。

4. kernel_mktime()函式

long kernel_mktime(struct tm * tm)
{
    long res;
    int year;

    year = tm->tm_year - 70; //計算70年到現在(今年的1.1)經過的年數
/* magic offsets (y+1) needed to get leapyears right.*/
    res = YEAR*year + DAY*((year+1)/4); //把年換算成秒,把閏年多出來的天也換算成秒
    res += month[tm->tm_mon]; //把今年的1.1到現在的x.1換算成秒
/* and (y+2) here. If it wasn't a leap-year, we have to adjust */
    if (tm->tm_mon>1 && ((year+2)%4))
        res -= DAY;
    res += DAY*(tm->tm_mday-1); //不算今天
    res += HOUR*tm->tm_hour;   
    res += MINUTE*tm->tm_min;
    res += tm->tm_sec;
    return res;
}

總的來說,計算的方法是先整後零:從1970.1.1算到今年的1.1,再算到本月1日,再算到今天的0點,再到此刻的時分秒。

第6行:因為是從1970年算起,且tm->tm_year中是年份的末2位,所以要減去70。舉例來說,如果是1998年,那麼tm->tm_year = 98year = 28.

注意:因為年份是 2 位表示方式,所以會有2000年問題。我們可以簡單地在最前面(比如第5行)新增一條語句來解決這個問題:

if(tm->tm_year < 70)
    tm->tm_year += 100;

推導過程:

20xx1970=(2000+xx)(1900+70)=2000+xx190070=100+xx70
20xx-1970 =(2000+xx) - (1900+70) = 2000+xx-1900 - 70 = 100 + xx - 70

舉例來說,假如是2007年,那麼tm->tm_year = 7,執行上面的2行語句後,tm->tm_year = 107,再執行原來的第6行,year = 37

第8行:res = YEAR*year + DAY*((year+1)/4);

(year+1)/4表示從1970年1.1到今年的1.1,經過了幾個閏年。注意:1972年是閏年。

為什麼是這個式子,或者說為什麼它是對的,列出來找找規律就明白了。

讀出的年份 year的值 經過的閏年數 備註
1970,1971,1972 0,1,2 0 因為截至今年的1.1,所以即使讀出1972年,也不能算是經過了閏年,後面的1976、1980等同理
1973,1974,1975,1976 3,4,5,6 1 如果讀出1973~1976,因為經過了1972,所以算為1
1977,1978,1979,1980 7,8,9,10 2 如果讀出1977~1980,因為經過了1972和1976,所以算為2

通過上表的中間2列,可以歸納出公式:

=(year+1)/4
經過的閏年數 = (year+1)/4

第9行:res += month[tm->tm_mon];在前文第2節已經解釋了。

到目前為止(程式碼第10行之前),已經計算了1970年1月1日0時到今年本月1日0時經歷的秒數。

11~12行:

if (tm->tm_mon>1 && ((year+2)%4))
        res -= DAY;

如果此表示式(year+2)%4)取值為0,則說明是閏年(觀察上表中帶下劃線的數字就可以得出);取值不為0,說明不是閏年;

如果tm->tm_mon>1成立,說明現在的月份是3~12(注意之前的減一);否則現在的月份是1或者2;

以上2個條件,組合起來有4種情況。

現在的月份 今年是閏年嗎? 結論
1,2 因為算到本月1日,所以不牽扯2.29;
1,2 同上
3-12 多算了2.29,所以要減去1天
3-12 是閏年,算2.29沒有錯

根據上面的分析,只有表格第3行這種情況需要減去1天,於是就有了上面的程式碼。

剩下的程式碼就很好理解了,這裡不再贅述。

【完】

參考資料

《Linux核心完全剖析》(趙炯,機械工業出版社,2006)

相關文章