kernel_mktime() 詳解 —— Linux-0.11 學習筆記(四)
題目: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.1
到x.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 = 98
,year = 28
.
注意:因為年份是 2 位表示方式,所以會有2000年問題。我們可以簡單地在最前面(比如第5行)新增一條語句來解決這個問題:
if(tm->tm_year < 70)
tm->tm_year += 100;
推導過程:
舉例來說,假如是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列,可以歸納出公式:
第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)
相關文章
- bootsect.s 分析—— Linux-0.11 學習筆記(一)bootLinux筆記
- setup.s 分析—— Linux-0.11 學習筆記(二)Linux筆記
- Go學習筆記-GMP詳解Go筆記
- main函式解析(一)——Linux-0.11 學習筆記(五)AI函式Linux筆記
- main 函式解析(二)—— Linux-0.11 學習筆記(六)AI函式Linux筆記
- JVM學習筆記(3)---OutOfMemory詳解JVM筆記
- goLang學習筆記(四)Golang筆記
- Javascript 學習 筆記四JavaScript筆記
- TS學習筆記(四)筆記
- mysql學習筆記-底層原理詳解MySql筆記
- Nginx變數詳解(學習筆記十九)Nginx變數筆記
- ONNXRuntime學習筆記(四)筆記
- activiti學習筆記(四)managementService筆記
- Android學習筆記四Android筆記
- Spss 學習筆記(四)SPSS筆記
- 四元數 學習筆記筆記
- c++學習筆記(四)C++筆記
- MIT 6.824 學習筆記(一)--- RPC 詳解MIT筆記RPC
- git checkout 命令詳解—— Git 學習筆記 16Git筆記
- JavaWeb學習筆記_Day03_JavaScript詳解Web筆記JavaScript
- JMeter學習筆記--詳解JMeter定時器JMeter筆記定時器
- ES6 學習筆記四筆記
- TS學習筆記(四):函式筆記函式
- springboot 學習筆記(四)Spring Boot筆記
- 大資料學習筆記(四)大資料筆記
- NET Core-學習筆記(四)筆記
- git reset 命令詳解(二)—— Git 學習筆記 08Git筆記
- git reset 命令詳解(一)—— Git 學習筆記 07Git筆記
- git cherry-pick 詳解 —— Git 學習筆記 18Git筆記
- 【Java學習筆記之五】java陣列詳解Java筆記陣列
- IOS學習筆記——iOS元件之UIScrollView詳解iOS筆記元件UIView
- MySQL學習筆記之資料型別詳解MySql筆記資料型別
- 註解學習筆記筆記
- Gradle外掛學習筆記(四)Gradle筆記
- 李巨集毅深度學習 筆記(四)深度學習筆記
- Java學習筆記 第四天Java筆記
- MySql 學習筆記四:表的拆分MySql筆記
- OS學習筆記四:同步機制筆記