kernel_mktime() 详解 —— Linux-0.11 学习笔记(四)
題目:kernel_mktime() 詳解 —— Linux-0.11 學(xué)習(xí)筆記(四)
在init/main.c文件中,有一個函數(shù)static void time_init(void)
該函數(shù)讀取 CMOS 實(shí)時時鐘信息作為開機(jī)時間,并保存到全局變量startup_time (以秒為單位)中。
static void time_init(void) {struct tm time;do {time.tm_sec = CMOS_READ(0); //當(dāng)前時間的秒值,格式均是BCD碼time.tm_min = CMOS_READ(2); //當(dāng)前時間的分鐘值time.tm_hour = CMOS_READ(4); //當(dāng)前時間的小時值time.tm_mday = CMOS_READ(7); //當(dāng)前的日time.tm_mon = CMOS_READ(8); //當(dāng)前的月time.tm_year = CMOS_READ(9); //當(dāng)前的年,只有后2位數(shù),例如97表示1997年} while (time.tm_sec != CMOS_READ(0));BCD_TO_BIN(time.tm_sec); // 轉(zhuǎn)換成二進(jìn)制數(shù)值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~11startup_time = kernel_mktime(&time); }6~11行:讀出當(dāng)前時間,注意,格式為BCD碼值;
13~18行:把BCD碼值轉(zhuǎn)換成二進(jìn)制;
第19行:time.tm_mon--;這里把月份值減一,為什么這樣做,后文會說明。
第20行:調(diào)用函數(shù)kernel_mktime(),計算從 1970 年 1 月 1 日 0 時起到此次開機(jī)時刻經(jīng)過的秒數(shù),作為開機(jī)時間。
上面的代碼就說到這里,CMOS_READ, BCD_TO_BIN等宏定義以后再說。本文想說說kernel_mktime這個函數(shù)。此函數(shù)在文件kernel/mktime.c 的第 41 行。kernel/mktime.c這個文件很短,僅有58行。
1. 宏定義
#define MINUTE 60 //1分鐘經(jīng)過的秒數(shù) #define HOUR (60*MINUTE) //1小時經(jīng)過的秒數(shù) #define DAY (24*HOUR) //一天經(jīng)過的秒數(shù) #define YEAR (365*DAY) //一年經(jīng)過的秒數(shù)(不考慮閏年)2. 從1.1到x.1經(jīng)過的秒數(shù)
static int month[12] = {0, //[0] 1.1-1.1DAY*(31), //[1] 1.1-2.1DAY*(31+29), //[2] 1.1-3.1DAY*(31+29+31), //[3] 1.1-4.1DAY*(31+29+31+30), //[4] 1.1-5.1 DAY*(31+29+31+30+31), //[5] 1.1-6.1DAY*(31+29+31+30+31+30), //[6] 1.1-7.1DAY*(31+29+31+30+31+30+31), //[7] 1.1-8.1DAY*(31+29+31+30+31+30+31+31), //[8] 1.1-9.1DAY*(31+29+31+30+31+30+31+31+30), //[9] 1.1-10.1DAY*(31+29+31+30+31+30+31+31+30+31), //[10] 1.1-11.1DAY*(31+29+31+30+31+30+31+31+30+31+30) //[11] 1.1-12.1 };假如當(dāng)前是4月,問:從本年1月1日起到4月1日,經(jīng)過了多少秒?
可以先算出經(jīng)過了多少天,再把天數(shù)乘以DAY(見宏定義)。如果用 D(m) 表示月份m的總天數(shù),那么答案就是:
( D(1) + D(2) + D(3) ) * DAY
把上面的問題一般化為:假如當(dāng)前是x月,問:從本年1月1日起到x月1日,經(jīng)過了多少秒?
答案是:
( D(1) + D(2) + D(3) + ... + D(x-1) ) * DAY
思路就是這樣, Linus 用的是查表法,于是就有了上面的數(shù)組。比如從CMOS中讀出的是8月份,那么答案就是month[7];再比如讀出的是12月份,那么答案就是month[11];再來個特殊情況,比如讀出的是1月份,那么就是0,即month[0]. 看出來了吧,索引值比真實(shí)的月份值少1,這就是time.tm_mon--;的原因。
注意,代碼中假設(shè)今年是閏年,即2月份有29天。
3. 結(jié)構(gòu)體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; //夏令時標(biāo)志 };8~10行:這3個成員好像沒有用到。
4. kernel_mktime()函數(shù)
long kernel_mktime(struct tm * tm) {long res;int year;year = tm->tm_year - 70; //計算70年到現(xiàn)在(今年的1.1)經(jīng)過的年數(shù) /* magic offsets (y+1) needed to get leapyears right.*/res = YEAR*year + DAY*((year+1)/4); //把年換算成秒,把閏年多出來的天也換算成秒res += month[tm->tm_mon]; //把今年的1.1到現(xiàn)在的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點(diǎn),再到此刻的時分秒。
第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;推導(dǎo)過程:
20xx?1970=(2000+xx)?(1900+70)=2000+xx?1900?70=100+xx?7020xx?1970=(2000+xx)?(1900+70)=2000+xx?1900?70=100+xx?70
舉例來說,假如是2007年,那么tm->tm_year = 7,執(zhí)行上面的2行語句后,tm->tm_year = 107,再執(zhí)行原來的第6行,year = 37;
第8行:res = YEAR*year + DAY*((year+1)/4);
(year+1)/4表示從1970年1.1到今年的1.1,經(jīng)過了幾個閏年。注意:1972年是閏年。
為什么是這個式子,或者說為什么它是對的,列出來找找規(guī)律就明白了。
| 1970,1971,1972 | 0,1,2 | 0 | 因為截至今年的1.1,所以即使讀出1972年,也不能算是經(jīng)過了閏年,后面的1976、1980等同理 |
| 1973,1974,1975,1976 | 3,4,5,6 | 1 | 如果讀出1973~1976,因為經(jīng)過了1972,所以算為1 |
| 1977,1978,1979,1980 | 7,8,9,10 | 2 | 如果讀出1977~1980,因為經(jīng)過了1972和1976,所以算為2 |
通過上表的中間2列,可以歸納出公式:
經(jīng)過的閏年數(shù)=(year+1)/4經(jīng)過的閏年數(shù)=(year+1)/4
第9行:res += month[tm->tm_mon];在前文第2節(jié)已經(jīng)解釋了。
到目前為止(代碼第10行之前),已經(jīng)計算了1970年1月1日0時到今年本月1日0時經(jīng)歷的秒數(shù)。
11~12行:
if (tm->tm_mon>1 && ((year+2)%4))res -= DAY;如果此表達(dá)式(year+2)%4)取值為0,則說明是閏年(觀察上表中帶下劃線的數(shù)字就可以得出);取值不為0,說明不是閏年;
如果tm->tm_mon>1成立,說明現(xiàn)在的月份是3~12(注意之前的減一);否則現(xiàn)在的月份是1或者2;
以上2個條件,組合起來有4種情況。
| 1,2 | 否 | 因為算到本月1日,所以不牽扯2.29; |
| 1,2 | 是 | 同上 |
| 3-12 | 否 | 多算了2.29,所以要減去1天 |
| 3-12 | 是 | 是閏年,算2.29沒有錯 |
根據(jù)上面的分析,只有表格第3行這種情況需要減去1天,于是就有了上面的代碼。
剩下的代碼就很好理解了,這里不再贅述。
【完】
參考資料
《Linux內(nèi)核完全剖析》(趙炯,機(jī)械工業(yè)出版社,2006)
與50位技術(shù)專家面對面20年技術(shù)見證,附贈技術(shù)全景圖總結(jié)
以上是生活随笔為你收集整理的kernel_mktime() 详解 —— Linux-0.11 学习笔记(四)的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: head.s 分析——Linux-0.1
- 下一篇: 基本类型赋值和普通对象赋值的不同