歡迎來到Linux教程網
Linux教程網
Linux教程網
Linux教程網
Linux教程網 >> Unix知識 >> 關於Unix >> 第七章 Linux內核的時鐘中斷 (上)

第七章 Linux內核的時鐘中斷 (上)

日期:2017/3/6 15:19:09   编辑:關於Unix
摘要:本文主要從內核實現的角度分析了Linux 2.4.0內核的時鐘中斷、內核對時間的表示等。本文是為那些想要了解Linux I/O子系統的讀者和Linux驅動程序 開發 人員而寫的。 關鍵詞:Linux、時鐘、定時器 第七章 Linux內核的時鐘中斷 (By 詹榮開,NUDT) Copyri
摘要:本文主要從內核實現的角度分析了Linux 2.4.0內核的時鐘中斷、內核對時間的表示等。本文是為那些想要了解Linux I/O子系統的讀者和Linux驅動程序開發人員而寫的。
關鍵詞:Linux、時鐘、定時器

第七章 Linux內核的時鐘中斷
(By 詹榮開,NUDT)





Copyright © 2003 by 詹榮開
E-mail:[email protected]
Linux-2.4.0
Version 1.0.0,2003-2-14




摘要:本文主要從內核實現的角度分析了Linux 2.4.0內核的時鐘中斷、內核對時間的表示等。本文是為那些想要了解Linux I/O子系統的讀者和Linux驅動程序開發人員而寫的。
關鍵詞:Linux、時鐘、定時器

申明:這份文檔是按照自由軟件開放源代碼的精神發布的,任何人可以免費獲得、使用和重新發布,但是你沒有限制別人重新發布你發布內容的權利。發布本文的目的是希望它能對讀者有用,但沒有任何擔保,甚至沒有適合特定目的的隱含的擔保。更詳細的情況請參閱GNU通用公共許可證(GPL),以及GNU自由文檔協議(GFDL)。

你應該已經和文檔一起收到一份GNU通用公共許可證(GPL)的副本。如果還沒有,寫信給:
The Free Software Foundation, Inc., 675 Mass Ave, Cambridge,MA02139, USA

歡迎各位指出文檔中的錯誤與疑問。
前言
時間在一個操作系統內核中占據著重要的地位,它是驅動一個OS內核運行的“起博器”。一般說來,內核主要需要兩種類型的時間:
1.在內核運行期間持續記錄當前的時間與日期,以便內核對某些對象和事件作時間標記(timestamp,也稱為“時間戳”),或供用戶通過時間syscall進行檢索。
2.維持一個固定周期的定時器,以提醒內核或用戶一段時間已經過去了。
PC機中的時間是有三種時鐘硬件提供的,而這些時鐘硬件又都基於固定頻率的晶體振蕩器來提供時鐘方波信號輸入。這三種時鐘硬件是:(1)實時時鐘(Real Time Clock,RTC);(2)可編程間隔定時器(Programmable IntervalTimer,PIT);(3)時間戳計數器(Time Stamp Counter,TSC)。

7.1 時鐘硬件
7.1.1 實時時鐘RTC
自從IBM PCAT起,所有的PC機就都包含了一個叫做實時時鐘(RTC)的時鐘芯片,以便在PC機斷電後仍然能夠繼續保持時間。顯然,RTC是通過主板上的電池來供電的,而不是通過PC機電源來供電的,因此當PC機關掉電源後,RTC仍然會繼續工作。通常,CMOSRAM和RTC被集成到一塊芯片上,因此RTC也稱作“CMOSTimer”。最常見的RTC芯片是MC146818(Motorola)和DS12887(maxim),DS12887完全兼容於MC146818,並有一定的擴展。本節內容主要基於MC146818這一標准的RTC芯片。具體內容可以參考MC146818的Datasheet。

7.1.1.1 RTC寄存器
MC146818 RTC芯片一共有64個寄存器。它們的芯片內部地址編號為0x00~0x3F(不是I/O端口地址),這些寄存器一共可以分為三組:
(1)時鐘與日歷寄存器組:共有10個(0x00~0x09),表示時間、日歷的具體信息。在PC機中,這些寄存器中的值都是以BCD格式來存儲的(比如23dec=0x23BCD)。
(2)狀態和控制寄存器組:共有4個(0x0A~0x0D),控制RTC芯片的工作方式,並表示當前的狀態。
(3)CMOS配置數據:通用的CMOS RAM,它們與時間無關,因此我們不關心它。
時鐘與日歷寄存器組的詳細解釋如下:
AddressFunction
00Current second for RTC
01Alarm second
02Current minute
03Alarm minute
04Current hour
05Alarm hour
06Current day of week(01=Sunday)
07Current date of month
08Current month
09Current year(final two digits,eg:93)

狀態寄存器A(地址0x0A)的格式如下:
其中:
(1)bit[7]——UIP標志(Update in Progress),為1表示RTC正在更新日歷寄存器組中的值,此時日歷寄存器組是不可訪問的(此時訪問它們將得到一個無意義的漸變值)。
(2)bit[6:4]——這三位是“除法器控制位”(divider-control bits),用來定義RTC的操作頻率。各種可能的值如下:
Divider bitsTime-base frequencyDivider ResetOperation Mode
DV2DV1DV0
0004.194304 MHZNOYES
0011.048576 MHZNOYES
01032.769 KHZNOYES
110/1任何YESNO
PC機通常將Divider bits設置成“010”。
(3)bit[3:0]——速率選擇位(Rate Selection bits),用於周期性或方波信號輸出。
RS bits4.194304或1.048578 MHZ32.768 KHZ
RS3RS2RS1RS0周期性中斷方波周期性中斷方波
0000NoneNoneNoneNone
000130.517μs32.768 KHZ3.90625ms256 HZ
001061.035μs16.384 KHZ
0011122.070μs8.192KHZ
0100244.141μs4.096KHZ
0101488.281μs2.048KHZ
0110976.562μs1.024KHZ
01111.953125ms512HZ
10003.90625ms256HZ
10017.8125ms128HZ
101015.625ms64HZ
101131.25ms32HZ
110062.5ms16HZ
1101125ms8HZ
1110250ms4HZ
1111500ms2HZ
PC機BIOS對其默認的設置值是“0110”。

狀態寄存器B的格式如下所示:
各位的含義如下:
(1)bit[7]——SET標志。為1表示RTC的所有更新過程都將終止,用戶程序隨後馬上對日歷寄存器組中的值進行初始化設置。為0表示將允許更新過程繼續。
(2)bit[6]——PIE標志,周期性中斷使能標志。
(3)bit[5]——AIE標志,告警中斷使能標志。
(4)bit[4]——UIE標志,更新結束中斷使能標志。
(5)bit[3]——SQWE標志,方波信號使能標志。
(6)bit[2]——DM標志,用來控制日歷寄存器組的數據模式,0=BCD,1=BINARY。BIOS總是將它設置為0。
(7)bit[1]——24/12標志,用來控制hour寄存器,0表示12小時制,1表示24小時制。PC機BIOS總是將它設置為1。
(8)bit[0]——DSE標志。BIOS總是將它設置為0。

狀態寄存器C的格式如下:
(1)bit[7]——IRQF標志,中斷請求標志,當該位為1時,說明寄存器B中斷請求發生。
(2)bit[6]——PF標志,周期性中斷標志,為1表示發生周期性中斷請求。
(3)bit[5]——AF標志,告警中斷標志,為1表示發生告警中斷請求。
(4)bit[4]——UF標志,更新結束中斷標志,為1表示發生更新結束中斷請求。

狀態寄存器D的格式如下:
(1)bit[7]——VRT標志(Valid RAM and Time),為1表示OK,為0表示RTC已經掉電。
(2)bit[6:0]——總是為0,未定義。

7.1.1.2 通過I/O端口訪問RTC
在PC機中可以通過I/O端口0x70和0x71來讀寫RTC芯片中的寄存器。其中,端口0x70是RTC的寄存器地址索引端口,0x71是數據端口。
讀RTC芯片寄存器的步驟是:
mov al, addr
out 70h, al ; Select reg_addr in RTC chip
jmp $+2 ; a slight delay to settle thing
in al, 71h ;
寫RTC寄存器的步驟如下:
mov al, addr
out 70h, al ; Select reg_addr in RTC chip
jmp $+2 ; a slight delay to settle thing
mov al, value
out 71h, al

7.1.2 可編程間隔定時器PIT
每個PC機中都有一個PIT,以通過IRQ0產生周期性的時鐘中斷信號。當前使用最普遍的是Intel 8254 PIT芯片,它的I/O端口地址是0x40~0x43。
Intel 8254 PIT有3個計時通道,每個通道都有其不同的用途:
(1)通道0用來負責更新系統時鐘。每當一個時鐘滴答過去時,它就會通過IRQ0向系統產生一次時鐘中斷。
(2)通道1通常用於控制DMAC對RAM的刷新。
(3)通道2被連接到PC機的揚聲器,以產生方波信號。
每個通道都有一個向下減小的計數器,8254PIT的輸入時鐘信號的頻率是1193181HZ,也即一秒鐘輸入1193181個clock-cycle。每輸入一個clock-cycle其時間通道的計數器就向下減1,一直減到0值。因此對於通道0而言,當他的計數器減到0時,PIT就向系統產生一次時鐘中斷,表示一個時鐘滴答已經過去了。當各通道的計數器減到0時,我們就說該通道處於“Terminal count”狀態。
通道計數器的最大值是10000h,所對應的時鐘中斷頻率是1193181/(65536)=18.2HZ,也就是說,此時一秒鐘之內將產生18.2次時鐘中斷。

7.1.2.1 PIT的I/O端口
在i386平台上,8254芯片的各寄存器的I/O端口地址如下:
PortDescription
40hChannel 0 counter(read/write)
41hChannel 1 counter(read/write)
42hChannel 2 counter(read/write)
43hPIT control word(write only)
其中,由於通道0、1、2的計數器是一個16位寄存器,而相應的端口卻都是8位的,因此讀寫通道計數器必須進行進行兩次I/O端口讀寫操作,分別對應於計數器的高字節和低字節,至於是先讀寫高字節再讀寫低字節,還是先讀寫低字節再讀寫高字節,則由PIT的控制寄存器來決定。8254PIT的控制寄存器的格式如下:
(1)bit[7:6]——Select Counter,選擇對那個計數器進行操作。“00”表示選擇Counter0,“01”表示選擇Counter 1,“10”表示選擇Counter 2,“11”表示Read-BackCommand(僅對於8254,對於8253無效)。
(2)bit[5:4]——Read/Write/Latch格式位。“00”表示鎖存(Latch)當前計數器的值;“01”只讀寫計數器的高字節(MSB);“10”只讀寫計數器的低字節(LSB);“11”表示先讀寫計數器的LSB,再讀寫MSB。
(3)bit[3:1]——Mode bits,控制各通道的工作模式。“000”對應Mode 0;“001”對應Mode 1;“010”對應Mode 2;“011”對應Mode 3;“100”對應Mode 4;“101”對應Mode 5。
(4)bit[0]——控制計數器的存儲模式。0表示以二進制格式存儲,1表示計數器中的值以BCD格式存儲。

7.1.2.2 PIT通道的工作模式
PIT各通道可以工作在下列6種模式下:
1.Mode 0:當通道處於“Terminal count”狀態時產生中斷信號。
2.Mode 1:Hardware retriggerable one-shot。
3. Mode 2:RateGenerator。這種模式典型地被用來產生實時時鐘中斷。此時通道的信號輸出管腳OUT初始時被設置為高電平,並以此持續到計數器的值減到1。然後在接下來的這個clock-cycle期間,OUT管腳將變為低電平,直到計數器的值減到0。當計數器的值被自動地重新加載後,OUT管腳又變成高電平,然後重復上述過程。通道0通常工作在這個模式下。
4.Mode 3:方波信號發生器。
5.Mode 4:Software triggered strobe。
6.Mode 5:Hardware triggered strobe。

7.1.2.3 鎖存計數器(Latch Counter)
當控制寄存器中的bit[5:4]設置成0時,將把當前通道的計數器值鎖存。此時通過I/O端口可以讀到一個穩定的計數器值,因為計數器表面上已經停止向下計數(PIT芯片內部並沒有停止向下計數)。NOTE!一旦發出了鎖存命令,就要馬上讀計數器的值。

7.1.3 時間戳記數器TSC
從Pentium開始,所有的Intel 80x86 CPU就都又包含一個64位的時間戳記數器(TSC)的寄存器。該寄存器實際上是一個不斷增加的計數器,它在CPU的每個時鐘信號到來時加1(也即每一個clock-cycle輸入CPU時,該計數器的值就加1)。
匯編指令rdtsc可以用於讀取TSC的值。利用CPU的TSC,操作系統通常可以得到更為精准的時間度量。假如clock-cycle的頻率是400MHZ,那麼TSC就將每2.5納秒增加一次。
7.2 Linux內核對RTC的編程
MC146818RTC芯片(或其他兼容芯片,如DS12887)可以在IRQ8上產生周期性的中斷,中斷的頻率在2HZ~8192HZ之間。與MC146818RTC對應的設備驅動程序實現在include/linux/rtc.h和drivers/char/rtc.c文件中,對應的設備文件是/dev/rtc(major=10,minor=135,只讀字符設備)。因此用戶進程可以通過對她進行編程以使得當RTC到達某個特定的時間值時激活IRQ8線,從而將RTC當作一個鬧鐘來用。
而Linux內核對RTC的唯一用途就是把RTC用作“離線”或“後台”的時間與日期維護器。當Linux內核啟動時,它從RTC中讀取時間與日期的基准值。然後再運行期間內核就完全拋開RTC,從而以軟件的形式維護系統的當前時間與日期,並在需要時將時間回寫到RTC芯片中。
Linux在include/linux/mc146818rtc.h和include/asm-i386/mc146818rtc.h頭文件中分別定義了mc146818RTC芯片各寄存器的含義以及RTC芯片在i386平台上的I/O端口操作。而通用的RTC接口則聲明在include/linux/rtc.h頭文件中。





7.2.1 RTC芯片的I/O端口操作
Linux在include/asm-i386/mc146818rtc.h頭文件中定義了RTC芯片的I/O端口操作。端口0x70被稱為“RTC端口0”,端口0x71被稱為“RTC端口1”,如下所示:
#ifndef RTC_PORT
#define RTC_PORT(x)(0x70 + (x))
#define RTC_ALWAYS_BCD1/* RTC operates in binary mode */
#endif
顯然,RTC_PORT(0)就是指端口0x70,RTC_PORT(1)就是指I/O端口0x71。
端口0x70被用作RTC芯片內部寄存器的地址索引端口,而端口0x71則被用作RTC芯片內部寄存器的數據端口。再讀寫一個RTC寄存器之前,必須先把該寄存器在RTC芯片內部的地址索引值寫到端口0x70中。根據這一點,讀寫一個RTC寄存器的宏定義CMOS_READ()和CMOS_WRITE()如下:
#define CMOS_READ(addr) ({
outb_p((addr),RTC_PORT(0));
inb_p(RTC_PORT(1));
})
#define CMOS_WRITE(val, addr) ({
outb_p((addr),RTC_PORT(0));
outb_p((val),RTC_PORT(1));
})
#define RTC_IRQ 8
在上述宏定義中,參數addr是RTC寄存器在芯片內部的地址值,取值范圍是0x00~0x3F,參數val是待寫入寄存器的值。宏RTC_IRQ是指RTC芯片所連接的中斷請求輸入線號,通常是8。

7.2.2 對RTC寄存器的定義
Linux在include/linux/mc146818rtc.h這個頭文件中定義了RTC各寄存器的含義。

(1)寄存器內部地址索引的定義
Linux內核僅使用RTC芯片的時間與日期寄存器組和控制寄存器組,地址為0x00~0x09之間的10個時間與日期寄存器的定義如下:
#define RTC_SECONDS0
#define RTC_SECONDS_ALARM1
#define RTC_MINUTES2
#define RTC_MINUTES_ALARM3
#define RTC_HOURS4
#define RTC_HOURS_ALARM5
/* RTC_*_alarm is always true if 2 MSBs are set */
# define RTC_ALARM_DONT_CARE 0xC0

#define RTC_DAY_OF_WEEK6
#define RTC_DAY_OF_MONTH7
#define RTC_MONTH8
#define RTC_YEAR9

四個控制寄存器的地址定義如下:
#define RTC_REG_A10
#define RTC_REG_B11
#define RTC_REG_C12
#define RTC_REG_D13

(2)各控制寄存器的狀態位的詳細定義
控制寄存器A(0x0A)主要用於選擇RTC芯片的工作頻率,因此也稱為RTC頻率選擇寄存器。因此Linux用一個宏別名RTC_FREQ_SELECT來表示控制寄存器A,如下:
#define RTC_FREQ_SELECTRTC_REG_A
RTC頻率寄存器中的位被分為三組:①bit[7]表示UIP標志;②bit[6:4]用於除法器的頻率選擇;③bit[3:0]用於速率選擇。它們的定義如下:
# define RTC_UIP0x80
# define RTC_DIV_CTL0x70
/* Periodic intr. / Square wave rate select. 0=none, 1=32.8kHz,... 15=2Hz */
# define RTC_RATE_SELECT 0x0F
正如7.1.1.1節所介紹的那樣,bit[6:4]有5中可能的取值,分別為除法器選擇不同的工作頻率或用於重置除法器,各種可能的取值如下定義所示:
/* divider control: refclock values 4.194 / 1.049 MHz / 32.768 kHz */
# define RTC_REF_CLCK_4MHZ0x00
# define RTC_REF_CLCK_1MHZ0x10
# define RTC_REF_CLCK_32KHZ0x20
/* 2 values for divider stage reset, others for "testing purposes only" */
# define RTC_DIV_RESET10x60
# define RTC_DIV_RESET20x70

寄存器B中的各位用於使能/禁止RTC的各種特性,因此控制寄存器B(0x0B)也稱為“控制寄存器”,Linux用宏別名RTC_CONTROL來表示控制寄存器B,它與其中的各標志位的定義如下所示:
#define RTC_CONTROLRTC_REG_B
# define RTC_SET 0x80/* disable updates for clock setting */
# define RTC_PIE 0x40/* periodic interrupt enable */
# define RTC_AIE 0x20/* alarm interrupt enable */
# define RTC_UIE 0x10/* update-finished interrupt enable */
# define RTC_SQWE 0x08/* enable square-wave output */
# define RTC_DM_BINARY 0x04/* all time/date values are BCD if clear */
# define RTC_24H 0x02/* 24 hour mode - else hours bit 7 means pm */
# define RTC_DST_EN 0x01/* auto switch DST - works f. USA only */

寄存器C是RTC芯片的中斷請求狀態寄存器,Linux用宏別名RTC_INTR_FLAGS來表示寄存器C,它與其中的各標志位的定義如下所示:
#define RTC_INTR_FLAGSRTC_REG_C
/* caution - cleared by read */
# define RTC_IRQF 0x80/* any of the following 3 is active */
# define RTC_PF 0x40
# define RTC_AF 0x20
# define RTC_UF 0x10

寄存器D僅定義了其最高位bit[7],以表示RTC芯片是否有效。因此寄存器D也稱為RTC的有效寄存器。Linux用宏別名RTC_VALID來表示寄存器D,如下:
#define RTC_VALIDRTC_REG_D
# define RTC_VRT 0x80/* valid RAM and time */

(3)二進制格式與BCD格式的相互轉換
由於時間與日期寄存器中的值可能以BCD格式存儲,也可能以二進制格式存儲,因此需要定義二進制格式與BCD格式之間的相互轉換宏,以方便編程。如下:
#ifndef BCD_TO_BIN
#define BCD_TO_BIN(val) ((val)=((val)&15) + ((val)>>4)*10)
#endif

#ifndef BIN_TO_BCD
#define BIN_TO_BCD(val) ((val)=(((val)/10)<<4) + (val)%10)
#endif

7.2.3 內核對RTC的操作
如前所述,Linux內核與RTC進行互操作的時機只有兩個:(1)內核在啟動時從RTC中讀取啟動時的時間與日期;(2)內核在需要時將時間與日期回寫到RTC中。為此,Linux內核在arch/i386/kernel/time.c文件中實現了函數get_cmos_time()來進行對RTC的第一種操作。顯然,get_cmos_time()函數僅僅在內核啟動時被調用一次。而對於第二種操作,Linux則同樣在arch/i386/kernel/time.c文件中實現了函數set_rtc_mmss(),以支持向RTC中回寫當前時間與日期。下面我們將來分析這二個函數的實現。
在分析get_cmos_time()函數之前,我們先來看看RTC芯片對其時間與日期寄存器組的更新原理。

(1)Update In Progress
當控制寄存器B中的SET標志位為0時,MC146818芯片每秒都會在芯片內部執行一個“更新周期”(UpdateCycle),其作用是增加秒寄存器的值,並檢查秒寄存器是否溢出。如果溢出,則增加分鐘寄存器的值,如此一致下去直到年寄存器。在“更新周期”期間,時間與日期寄存器組(0x00~0x09)是不可用的,此時如果讀取它們的值將得到未定義的值,因為MC146818在整個更新周期期間會把時間與日期寄存器組從CPU總線上脫離,從而防止軟件程序讀到一個漸變的數據。
在MC146818的輸入時鐘頻率(也即晶體增蕩器的頻率)為4.194304MHZ或1.048576MHZ的情況下,“更新周期”需要花費248us,而對於輸入時鐘頻率為32.768KHZ的情況,“更新周期”需要花費1984us=1.984ms。控制寄存器A中的UIP標志位用來表示MC146818是否正處於更新周期中,當UIP從0變為1的那個時刻,就表示MC146818將在稍後馬上就開更新周期。在UIP從0變到1的那個時刻與MC146818真正開始UpdateCycle的那個時刻之間時有一段時間間隔的,通常是244us。也就是說,在UIP從0變到1的244us之後,時間與日期寄存器組中的值才會真正開始改變,而在這之間的244us間隔內,它們的值並不會真正改變。如下圖所示:

(2)get_cmos_time()函數
該函數只被內核的初始化例程time_init()和內核的APM模塊所調用。其源碼如下:
/* not static: needed by APM */
unsigned long get_cmos_time(void)
{
unsigned int year, mon, day, hour, min, sec;
int i;

/* The Linux interpretation of the CMOS clock register contents:
* When the Update-In-Progress (UIP) flag goes from 1 to 0, the
* RTC registers show the second which has precisely just started.
* Let's hope other operating systems interpret the RTC the same way.
*/
/* read RTC exactly on falling edge of update flag */
for (i = 0 ; i < 1000000 ; i++)/* may take up to 1 second... */
if (CMOS_READ(RTC_FREQ_SELECT) & RTC_UIP)
break;
for (i = 0 ; i < 1000000 ; i++)/* must try at least 2.228 ms */
if (!(CMOS_READ(RTC_FREQ_SELECT) & RTC_UIP))
break;
do { /* Isn't this overkill ? UIP above should guarantee consistency */
sec = CMOS_READ(RTC_SECONDS);
min = CMOS_READ(RTC_MINUTES);
hour = CMOS_READ(RTC_HOURS);
day = CMOS_READ(RTC_DAY_OF_MONTH);
mon = CMOS_READ(RTC_MONTH);
year = CMOS_READ(RTC_YEAR);
} while (sec != CMOS_READ(RTC_SECONDS));
if (!(CMOS_READ(RTC_CONTROL) & RTC_DM_BINARY) || RTC_ALWAYS_BCD)
{
BCD_TO_BIN(sec);
BCD_TO_BIN(min);
BCD_TO_BIN(hour);
BCD_TO_BIN(day);
BCD_TO_BIN(mon);
BCD_TO_BIN(year);
}
if ((year += 1900) < 1970)
year += 100;
return mktime(year, mon, day, hour, min, sec);
}
對該函數的注釋如下:
(1)在從RTC中讀取時間時,由於RTC存在UpdateCycle,因此軟件發出讀操作的時機是很重要的。對此,get_cmos_time()函數通過UIP標志位來解決這個問題:第一個for循環不停地讀取RTC頻率選擇寄存器中的UIP標志位,並且只要讀到UIP的值為1就馬上退出這個for循環。第二個for循環同樣不停地讀取UIP標志位,但他只要一讀到UIP的值為0就馬上退出這個for循環。這兩個for循環的目的就是要在軟件邏輯上同步RTC的UpdateCycle,顯然第二個for循環最大可能需要2.228ms(TBUC+max(TUC)=244us+1984us=2.228ms)
(2)從第二個for循環退出後,RTC的UpdateCycle已經結束。此時我們就已經把當前時間邏輯定准在RTC的當前一秒時間間隔內。也就是說,這是我們就可以開始從RTC寄存器中讀取當前時間值。但是要注意,讀操作應該保證在244us內完成(准確地說,讀操作要在RTC的下一個更新周期開始之前完成,244us的限制是過分偏執的:-)。所以,get_cmos_time()函數接下來通過CMOS_READ()宏從RTC中依次讀取秒、分鐘、小時、日期、月份和年分。這裡的do{}while(sec!=CMOS_READ(RTC_SECOND))循環就是用來確保上述6個讀操作必須在下一個Update Cycle開始之前完成。
(3)接下來判定時間的數據格式,PC機中一般總是使用BCD格式的時間,因此需要通過BCD_TO_BIN()宏把BCD格式轉換為二進制格式。
(4)接下來對年分進行修正,以將年份轉換為“19XX”的格式,如果是1970以前的年份,則將其加上100。
(5)最後調用mktime()函數將當前時間與日期轉換為相對於1970-01-01 00:00:00的秒數值,並將其作為函數返回值返回。

函數mktime()定義在include/linux/time.h頭文件中,它用來根據Gauss算法將以year/mon/day/hour/min/sec(如1980-12-31 23:59:59)格式表示的時間轉換為相對於1970-01-0100:00:00這個UNIX時間基准以來的相對秒數。其源碼如下:
static inline unsigned long
mktime (unsigned int year, unsigned int mon,
unsigned int day, unsigned int hour,
unsigned int min, unsigned int sec)
{
if (0 >= (int) (mon -= 2)) {/* 1..12 -> 11,12,1..10 */
mon += 12;/* Puts Feb last since it has leap day */
year -= 1;
}

return (((
(unsigned long) (year/4 - year/100 + year/400 + 367*mon/12 + day) +
year*365 - 719499
)*24 + hour /* now have hours */
)*60 + min /* now have minutes */
)*60 + sec; /* finally seconds */
}

(3)set_rtc_mmss()函數
該函數用來更新RTC中的時間,它僅有一個參數nowtime,是以秒數表示的當前時間,其源碼如下:
static int set_rtc_mmss(unsigned long nowtime)
{
int retval = 0;
int real_seconds, real_minutes, cmos_minutes;
unsigned char save_control, save_freq_select;

/* gets recalled with irq locally disabled */
spin_lock(&rtc_lock);
save_control = CMOS_READ(RTC_CONTROL); /* tell the clock it's being set */
CMOS_WRITE((save_control|RTC_SET), RTC_CONTROL);

save_freq_select = CMOS_READ(RTC_FREQ_SELECT); /* stop and reset prescaler */
CMOS_WRITE((save_freq_select|RTC_DIV_RESET2), RTC_FREQ_SELECT);

cmos_minutes = CMOS_READ(RTC_MINUTES);
if (!(save_control & RTC_DM_BINARY) || RTC_ALWAYS_BCD)
BCD_TO_BIN(cmos_minutes);

/*
* since we're only adjusting minutes and seconds,
* don't interfere with hour overflow. This avoids
* messing with unknown time zones but requires your
* RTC not to be off by more than 15 minutes
*/
real_seconds = nowtime % 60;
real_minutes = nowtime / 60;
if (((abs(real_minutes - cmos_minutes) + 15)/30) & 1)
real_minutes += 30;/* correct for half hour time zone */
real_minutes %= 60;

if (abs(real_minutes - cmos_minutes) < 30) {
if (!(save_control & RTC_DM_BINARY) || RTC_ALWAYS_BCD) {
BIN_TO_BCD(real_seconds);
BIN_TO_BCD(real_minutes);
}
CMOS_WRITE(real_seconds,RTC_SECONDS);
CMOS_WRITE(real_minutes,RTC_MINUTES);
} else {
printk(KERN_WARNING
"set_rtc_mmss: can't update from %d to %d\n",
cmos_minutes, real_minutes);
retval = -1;
}

/* The following flags have to be released exactly in this order,
* otherwise the DS12887 (popular MC146818A clone with integrated
* battery and quartz) will not reset the oscillator and will not
* update precisely 500 ms later. You won't find this mentioned in
* the Dallas Semiconductor data sheets, but who believes data
* sheets anyway ... -- Markus Kuhn
*/
CMOS_WRITE(save_control, RTC_CONTROL);
CMOS_WRITE(save_freq_select, RTC_FREQ_SELECT);
spin_unlock(&rtc_lock);

return retval;
}
對該函數的注釋如下:
(1)首先對自旋鎖rtc_lock進行加鎖。定義在arch/i386/kernel/time.c文件中的全局自旋鎖rtc_lock用來串行化所有CPU對RTC的操作。
(2)接下來,在RTC控制寄存器中設置SET標志位,以便通知RTC軟件程序隨後馬上將要更新它的時間與日期。為此先把RTC_CONTROL寄存器的當前值讀到變量save_control中,然後再把值(save_control |RTC_SET)回寫到寄存器RTC_CONTROL中。
(3)然後,通過RTC_FREQ_SELECT寄存器中bit[6:4]重啟RTC芯片內部的除法器。為此,類似地先把RTC_FREQ_SELECT寄存器的當前值讀到變量save_freq_select中,然後再把值(save_freq_select |RTC_DIV_RESET2)回寫到RTC_FREQ_SELECT寄存器中。
(4)接著將RTC_MINUTES寄存器的當前值讀到變量cmos_minutes中,並根據需要將它從BCD格式轉化為二進制格式。
(5)從nowtime參數中得到當前時間的秒數和分鐘數。分別保存到real_seconds和real_minutes變量。注意,這裡對於半小時區的情況要修正分鐘數real_minutes的值。
(6)然後,在real_minutes與RTC_MINUTES寄存器的原值cmos_minutes二者相差不超過30分鐘的情況下,將real_seconds和real_minutes所表示的時間值寫到RTC的秒寄存器和分鐘寄存器中。當然,在回寫之前要記得把二進制轉換為BCD格式。
(7)最後,恢復RTC_CONTROL寄存器和RTC_FREQ_SELECT寄存器原來的值。這二者的先後次序是:先恢復RTC_CONTROL寄存器,再恢復RTC_FREQ_SELECT寄存器。然後在解除自旋鎖rtc_lock後就可以返回了。

最後,需要說明的一點是,set_rtc_mmss()函數盡可能在靠近一秒時間間隔的中間位置(也即500ms處)左右被調用。此外,Linux內核對每一次成功的更新RTC時間都留下時間軌跡,它用一個系統全局變量last_rtc_update來表示內核最近一次成功地對RTC進行更新的時間(單位是秒數)。該變量定義在arch/i386/kernel/time.c文件中:
/* last time the cmos clock got updated */
static long last_rtc_update;
每一次成功地調用set_rtc_mmss()函數後,內核都會馬上將last_rtc_update更新為當前時間(具體請見7.4.3節)

Copyright © Linux教程網 All Rights Reserved