|
在這一次里,主要講講和時(shí)間相關(guān)的東西,這個(gè)我們都比較熟悉,我就直接如主題。 首 先要明白兩個(gè)概念:系統(tǒng)定時(shí)器和動(dòng)態(tài)定時(shí)器。周期性產(chǎn)生的事件都是有系統(tǒng)定時(shí)器驅(qū)動(dòng)的,這里的系統(tǒng)定時(shí)器是一種可編程硬件芯片,它能以固定頻率產(chǎn)生中斷。 該中斷就是定時(shí)器中斷,它所對(duì)應(yīng)的中斷處理程序負(fù)責(zé)更新系統(tǒng)時(shí)間,也負(fù)責(zé)執(zhí)行需要周期行運(yùn)行的任務(wù)。系統(tǒng)定時(shí)器和時(shí)鐘中斷處理程序是Linux系統(tǒng)內(nèi)核管 理機(jī)制中的中樞。動(dòng)態(tài)定時(shí)器是用來推遲執(zhí)行程序的工具。內(nèi)核可以動(dòng)態(tài)創(chuàng)建或銷毀動(dòng)態(tài)定時(shí)器。 內(nèi)核必須在硬件的幫助下才能計(jì)算和管理時(shí)間。硬件為內(nèi)核提供了一個(gè)系統(tǒng)定時(shí)器用以計(jì)算流逝的時(shí)間,該時(shí)鐘在內(nèi)核中可看成是一個(gè)電子時(shí)間資源。系統(tǒng)定時(shí)器以 某種頻率自行觸發(fā)時(shí)鐘中斷,該頻率可以通過編程預(yù)定稱為節(jié)拍率(tick rate).當(dāng)時(shí)鐘中斷發(fā)生時(shí),內(nèi)核就通過一種特殊的中斷處理程序?qū)ζ溥M(jìn)行處理。系統(tǒng)定時(shí)器頻率(節(jié)拍率)是通過靜態(tài)預(yù)處理定義的,也就是HZ.在系統(tǒng)啟 動(dòng)時(shí)按照HZ值對(duì)硬件進(jìn)行設(shè)置。體系結(jié)構(gòu)不一樣,HZ的值也不同,定義在asm/param.h中。剛提到的節(jié)拍率就是這個(gè)意思。周期是1/HZ秒。最后 要說明的是這個(gè)HZ值在編寫內(nèi)核代碼時(shí),不是固定不變的,而是可調(diào)的。當(dāng)然,對(duì)于操作系統(tǒng)而言,也并不是一定要這個(gè)固定的時(shí)鐘中斷。實(shí)際上,內(nèi)核可以使用 動(dòng)態(tài)編程定時(shí)器操作掛起事件。這里就不多說了。 在linux內(nèi)核里,有一個(gè)叫jiffies的變量(定義在linux/jiffies)記錄了自系統(tǒng)啟動(dòng)以來產(chǎn)生的節(jié)拍的總數(shù)。啟動(dòng)時(shí),內(nèi)核將該變量初 始化為0,此后每次時(shí)鐘中斷處理程序都會(huì)增加該變量的值。因?yàn)橐幻雰?nèi)時(shí)鐘中斷的次數(shù)等于HZ,所以jiffies一秒內(nèi)增加的值也就為HZ.系統(tǒng)運(yùn)行時(shí)間 以秒為單位計(jì)算,就等于jiffes/HZ.它作為在計(jì)算機(jī)表示的變量,就總存在大小,當(dāng)這個(gè)變量增加到超出它的表示上限時(shí),就要回繞到0.這個(gè)回繞看起 來很簡單,但實(shí)際上還是給我們編程造成了很大的麻煩,比如邊界條件判斷時(shí)。幸好,內(nèi)核提供了四個(gè)宏來幫助比較節(jié)拍計(jì)數(shù),這些宏定義在 linux/jiffies.h可以很好的處理節(jié)拍回繞的情況: 如果改變內(nèi)核中的HZ的值則會(huì)給用戶空間中某些程序造成異常結(jié)果,這是因?yàn)閮?nèi)核是以節(jié)拍數(shù)/秒的形式給用戶空間導(dǎo)出這個(gè)值的,在這個(gè)接口穩(wěn)定了很長一段時(shí) 間后,應(yīng)用程序便逐漸依賴于這個(gè)特定的HZ的值了。所以如果在內(nèi)核中更改了HZ的定義值,就打破了用戶空間的常量關(guān)系----用戶空間并不知道這個(gè)新的 HZ的值。為了解決這個(gè)問題,內(nèi)核必須更改所有導(dǎo)出的jiffies的值。內(nèi)核定義了USER_HZ來代表用戶空間看到的HZ值。內(nèi)核可以使用宏 jiffies_to_clock_t()將一個(gè)由HZ表示的節(jié)拍計(jì)數(shù)轉(zhuǎn)換成一個(gè)由USER_HZ表示的節(jié)拍數(shù)。改宏的用法取決于USER_HZ是否為 HZ的整數(shù)倍或相反。當(dāng)是整數(shù)倍時(shí),宏的形式相當(dāng)簡單:
如果不是整數(shù)倍關(guān)系,那么該宏就得用更為復(fù)雜的算法了。同樣的,如果是64位系統(tǒng),內(nèi)核使用函數(shù)jiffies_64_to_clock()將64位的jiffies值的單位從HZ轉(zhuǎn)換為USER_HZ. 體系結(jié)構(gòu)提供了兩種設(shè)備進(jìn)行計(jì)時(shí):系統(tǒng)定時(shí)器和實(shí)時(shí)時(shí)鐘。系統(tǒng)定時(shí)器提供一種周期性觸發(fā)中斷機(jī)制。實(shí)時(shí)時(shí)鐘(RTC)是用來持久存儲(chǔ)系統(tǒng)時(shí)間的設(shè)備,即便 系統(tǒng)關(guān)閉后,它也可以靠主板上的微型電池提供的電力保護(hù)系統(tǒng)的計(jì)時(shí)。當(dāng)系統(tǒng)啟動(dòng)時(shí),內(nèi)核通過讀取RTC來初始化墻上時(shí)間,該時(shí)間存放在xtime變量中, 實(shí)時(shí)時(shí)鐘最主要的作用是在啟動(dòng)時(shí)初始化xtime變量。 有了上面的概念基礎(chǔ),下面就分析時(shí)鐘中斷處理程序。它分為兩個(gè)部分:體系結(jié)構(gòu)相關(guān)部分和體系結(jié)構(gòu)無關(guān)部分。相關(guān)的部分作為系統(tǒng)定時(shí)器的中斷處理程序而注冊(cè)到內(nèi)核中,以便在產(chǎn)生時(shí)鐘中斷時(shí),它能夠相應(yīng)地運(yùn)行。執(zhí)行的工作如下:
do_timer看起來還是很簡單的,應(yīng)為它的主要工作就是完成上面的框架,具體的讓其它函數(shù)做就好了:
上述user_mode()宏查詢處理器寄存器regs的狀態(tài),如果時(shí)鐘中斷發(fā)生在用戶空間,它返回1;如果發(fā)生在內(nèi)核模式,則返回0.update_process_times()函數(shù)根據(jù)時(shí)鐘中斷產(chǎn)生的位置,對(duì)用戶或?qū)ο到y(tǒng)進(jìn)行相應(yīng)的時(shí)間更新:
update_one_process()函數(shù)的作用是更新進(jìn)程時(shí)間。它的實(shí)現(xiàn)是相當(dāng)細(xì)致的。但注意,因?yàn)槭褂昧薠OR操作,所以u(píng)ser_tick和 system兩個(gè)變量只要其中有一個(gè)為1,則另外一個(gè)就必須為0,updates_one_process()函數(shù)可以通過判斷分支,將 user_tick和system加到進(jìn)程相應(yīng)的計(jì)數(shù)上:
上述操作將適當(dāng)?shù)挠?jì)數(shù)值增加1,而另外一個(gè)值保持不變。也許你已經(jīng)發(fā)現(xiàn)了,這樣做意味著內(nèi)核對(duì)進(jìn)程時(shí)間計(jì)數(shù)時(shí),是根據(jù)中斷發(fā)生時(shí)處理器所處的模式進(jìn)行分類 統(tǒng)計(jì)的,它把上一個(gè)tick全部算給進(jìn)程。但是事實(shí)上進(jìn)程在上一個(gè)節(jié)拍器間可能多次進(jìn)入和退出內(nèi)核模式,而在在上一個(gè)節(jié)拍期間,該進(jìn)程也不一定是唯一一個(gè) 運(yùn)行進(jìn)程,但是這沒辦法。接下來的run_lock_times() 函數(shù)標(biāo)記了一個(gè)軟中斷去處理所有到期的定時(shí)器。最后,scheduler_tick()函數(shù)負(fù)責(zé)減少當(dāng)前運(yùn)行進(jìn)程的時(shí)間片計(jì)數(shù)值并且在需要時(shí)設(shè)置 need_resched標(biāo)志,在SMP機(jī)器中中,該函數(shù)還要負(fù)責(zé)平衡每個(gè)處理器上的運(yùn)行隊(duì)列。當(dāng)update_process_times()函數(shù)返回 時(shí),do_timer()函數(shù)接著會(huì)調(diào)用update_times()更新墻上時(shí)間。
這里的ticks記錄最近一次更新后新產(chǎn)生的節(jié)拍數(shù)。通常情況下ticks顯然應(yīng)該等于1.但是時(shí)鐘中斷也有可能丟失,因而節(jié)拍也會(huì)丟失。在中斷長時(shí)間被 禁止的情況下,就會(huì)出現(xiàn)這種現(xiàn)象(這種情況并不常見,往往是個(gè)BUG).wall_jiffies值隨后被加上ticks----所以此刻 wall_jiffies值就等于更新的墻上時(shí)間的更新值jiffies----接著調(diào)用update_wall_time()函數(shù)更新xtime,最后 由calc_load()執(zhí)行。do_timer()函數(shù)執(zhí)行完畢后返回與體系結(jié)構(gòu)相關(guān)的中斷處理程序,繼續(xù)執(zhí)行后面的工作,釋放xtime_lock 鎖,然后退出。以上的工作每1/HZ都要發(fā)生一次。 剛前邊說的墻上時(shí)間就是我們常說的實(shí)際時(shí)間,指變量xtime,由結(jié)構(gòu)體timespec定義(kernel/timer.c),如下:
讀寫這個(gè)xtime變量需要xtime_lock鎖,該鎖是一個(gè)順序鎖(seqlock).關(guān)于內(nèi)核讀寫就不 說了,注意適當(dāng)加解鎖就好。回到用戶空間,從用戶空間取得墻上時(shí)間的主要接口是gettimeofday(),在內(nèi)核中對(duì)應(yīng)系統(tǒng)調(diào)用為 sys_gettimeofday():
分析上面的函數(shù)發(fā)現(xiàn),問題就集中在tv上。當(dāng)tv非空,就調(diào)用do_gettimeofday(),它主要完成循環(huán)讀取xtime的操作。如果tz參數(shù)為 空,該函數(shù)將把系統(tǒng)時(shí)區(qū)(存放在sys_tz中)返回用戶。如果給用戶空間拷貝墻上時(shí)間或時(shí)區(qū)發(fā)生錯(cuò)誤,該函數(shù)返回-EFAULT;如果成功,則返回0. 另外,內(nèi)核提供的time系統(tǒng)調(diào)用,幾乎被gettimeofday()完全取代。C庫函數(shù)提供的一些墻上時(shí)間相關(guān)的庫調(diào)用如ftime和ctime。系 統(tǒng)的settimeofday()是用來設(shè)置當(dāng)前時(shí)間,它需要具有CAP_SYS_TIME權(quán)限。除了更新xtime時(shí)間以外,內(nèi)核不會(huì)像用戶空間程序那 樣頻繁使用xtime。但也需要注意在文件系統(tǒng)的實(shí)現(xiàn)代碼中存放訪問時(shí)間戳?xí)r需要使用xtime。 上面說完了有關(guān)硬時(shí)鐘,下面開始新的話題,是關(guān)于定時(shí)器的(也稱動(dòng)態(tài)定時(shí)器或內(nèi)核定時(shí)器)。定時(shí)器并不周期執(zhí)行,它在超時(shí)后就自行銷毀。定義器由定義在linux/timer.h中的time_list表示,如下:
內(nèi)核提供了一組與定時(shí)器相關(guān)的用來簡化管理定時(shí)器的操作。所有這些接口都聲明在文件linux/timer.h中,大多數(shù)接口在文件kernel/timer.c中獲得實(shí)現(xiàn)。有了這些接口,我們要做的事情就很簡單了:
經(jīng)過上面的幾步,定時(shí)器就可以開始工作了。然而,一般來說,定時(shí)器都在超時(shí)后馬上就會(huì)執(zhí)行,但是也有可能被推遲到下一時(shí)鐘節(jié)拍時(shí)才能運(yùn)行,所以不能使用它 來實(shí)現(xiàn)硬實(shí)時(shí)。如果修改定時(shí)器,使用mod_timer(&my_timer,jiffies+new_delay)來修改已經(jīng)激活的定時(shí)器時(shí) 間。它也可以操作那些已經(jīng)初始化,但還沒有被激活的定時(shí)器,如果定時(shí)器未被激活,mod_timer會(huì)激活它。如果第啊喲個(gè)定時(shí)器時(shí)未被激活,該函數(shù)返回 0;否則返回1。但不論哪種情況,一旦從mod_timer函數(shù)返回,定時(shí)器都將被激活而且設(shè)置了新的定時(shí)值。當(dāng)然你也可以在超市前刪除定時(shí)器 用:del_timer(&my_timer);另外需要注意的是在多處理器上定時(shí)器中斷可能已經(jīng)在其它機(jī)器上運(yùn)行了,這是就需要等待可能在其它 處理器上運(yùn)行的定時(shí)器處理程序都退出后再刪除該定時(shí)器。這是就要使用del_timer_sync()函數(shù)執(zhí)行刪除工作。這個(gè)函數(shù)參數(shù)和上面一個(gè)一樣,只 是不能在中斷上下文中使用而已。定時(shí)器是獨(dú)立與當(dāng)前代碼的,這意味著可能存在競爭條件,這個(gè)就要特別小心,從這個(gè)意義上講后者刪除比前者更加安全。 內(nèi)核在時(shí)鐘中斷發(fā)生后執(zhí)行定時(shí)器,定時(shí)器作為軟件中斷在下半部上下文中執(zhí)行。具體來說就是時(shí)鐘中斷處理程序會(huì)執(zhí)行update_process_timers()函數(shù),該函數(shù)隨即調(diào)用run_local_timers()函數(shù):
這個(gè)函數(shù)處理軟中斷TIEMR_SOFTIRQ,從而在當(dāng)前處理器上運(yùn)行所有的超時(shí)定時(shí)器。所有定時(shí)器都以鏈表的形式組織起來,但如果單純的鏈表結(jié)構(gòu)顯然 影響性能,因?yàn)槊看味家樞虻牡牟檎艺{(diào)整,這個(gè)時(shí)候,內(nèi)核定時(shí)器按它們的超時(shí)時(shí)間將他們分為5組,當(dāng)定時(shí)器超時(shí)時(shí)間接近時(shí),定時(shí)器將隨組一起下移。采用這 種方法可以減少搜素超時(shí)定時(shí)器所帶來的負(fù)擔(dān)。 下一話題,內(nèi)核代碼(尤其是驅(qū)動(dòng)程序)除了使用定時(shí)器或下半部機(jī)制以外還提供了許多延遲的方法來處理各種延遲請(qǐng)求。下面就來總結(jié)一下: 1.忙等待(也叫忙循環(huán)):通常是最不理想的方法,因?yàn)樘幚砥鞅话装渍加眯D(zhuǎn)而無法做別的事情。該方法僅僅在想要延遲的時(shí)間是節(jié)拍的整數(shù)倍或者精確率要求不高時(shí)才可以使用。實(shí)現(xiàn)起來還是挺簡單的,就是在循環(huán)中不斷旋轉(zhuǎn)直到希望的時(shí)鐘節(jié)拍數(shù)耗盡。比如:
缺點(diǎn)很明顯,更好的方法是在代碼等待時(shí),允許內(nèi)核重新調(diào)度執(zhí)行其他任務(wù),如下:
cond_resched()函數(shù)將調(diào)度一個(gè)新程序投入運(yùn)行,但它只有在設(shè)置完need_resched標(biāo)志后才能生效。換句話說,就是系統(tǒng)中存在更重要 的任務(wù)需要運(yùn)行。再由于該方法需要調(diào)用調(diào)度程序,所以它不能在中斷上下文中使用----只能在進(jìn)程上下文中使用。事實(shí)上,所有延遲方法在進(jìn)程上下文中使 用,因?yàn)橹袛嗵幚沓绦蚨紤?yīng)該盡可能快的執(zhí)行。另外,延遲執(zhí)行不管在哪種情況下都不應(yīng)該在持有鎖時(shí)或者禁止中斷時(shí)發(fā)生。 至于說那些需要很短暫的延遲(比時(shí)鐘節(jié)拍還短)而且還要求延遲的時(shí)間很精確,這種情況多發(fā)生在和硬件同步時(shí),也就是說需要短暫等待某個(gè)動(dòng)作的完成---- 等待時(shí)間往往小于1ms,所以不可能使用像前面例子中那種基于jiffies的延遲方法。這時(shí),就可以使用在linux/delay.h中定義的兩個(gè)函 數(shù),它們不使用,這兩個(gè)函數(shù)可以處理微秒和毫秒級(jí)別的延遲的時(shí)間,如下所示:
前者是依靠執(zhí)行次數(shù)循環(huán)來達(dá)到延遲效果的,而mdelay()函數(shù)又是通過udelay()函數(shù)實(shí)現(xiàn)的。因?yàn)閮?nèi)核知道處理器在一秒內(nèi)能執(zhí)行多少次循環(huán),所 以u(píng)delay()函數(shù)僅僅需要根據(jù)指定的延遲時(shí)間在1秒中占的比例,就能決定需要進(jìn)行多少次循環(huán)就能達(dá)到需要的推遲時(shí)間。udelay()函數(shù)僅能在要 求的延遲時(shí)間很短的情況下執(zhí)行,而在高速機(jī)器中時(shí)間很長的延遲會(huì)造成溢出,經(jīng)驗(yàn)表明,不要試圖在延遲超過1ms的情況下使用這個(gè)函數(shù)。這兩個(gè)函數(shù)其實(shí)和忙 等待一樣,如果不是非常必要,還是不要用了算了。 前邊說的有點(diǎn)害怕,那咋辦呢?其實(shí)更理想的延遲執(zhí)行方法是使用schedule_timeout()函數(shù),該方法會(huì)讓需要延遲執(zhí)行的任務(wù)睡眠到指定的延遲 時(shí)間耗盡后再重新運(yùn)行。但該方法也不能保證睡眠時(shí)間正好等于指定的延遲時(shí)間----只能盡量是睡眠時(shí)間接近指定的延遲時(shí)間。當(dāng)指定的時(shí)間到期后,內(nèi)核喚醒 被延遲的任務(wù)并將其重新放回運(yùn)行隊(duì)列,如下:
唯一的參數(shù)是延遲的相對(duì)時(shí)間,單位是jiffies,上例中將相應(yīng)的任務(wù)推入可中斷睡眠隊(duì)列,睡眠s秒。在調(diào)用函數(shù)schedule_timeout之 前,不要要將任務(wù)設(shè)置成可中斷或不和中斷的一種,否則任務(wù)不會(huì)休眠。這個(gè)函數(shù)需要調(diào)用調(diào)度程序,所以調(diào)用它的代碼必須保證能夠睡眠,簡而言之,調(diào)用代碼必 須處于進(jìn)程上下文中,并且不能持有鎖。有關(guān)這個(gè)函數(shù)的實(shí)現(xiàn)細(xì)節(jié),可以看下源碼,還是相當(dāng)簡單的。接下來就是當(dāng)定時(shí)器超 時(shí),process_timeout()函數(shù)被調(diào)用:
該函數(shù)將任務(wù)置為TASK_RUNNING狀態(tài),然后哦將其放入運(yùn)行隊(duì)列。當(dāng)任務(wù)重新被調(diào)度時(shí),將返回代碼進(jìn)入睡眠前的位置繼續(xù)執(zhí)行(正好在調(diào)用 schedule()后)。如果任務(wù)提前被喚醒(比如收到信號(hào)),那么定時(shí)器被銷毀,process_timeout()函數(shù)返回剩余的時(shí)間。 最后,在進(jìn)程調(diào)度那一節(jié)我們說過,進(jìn)程上下文的代碼為了等待特定時(shí)間發(fā)生,可以將自己放入等待隊(duì)列。但是,等待隊(duì)列上的某個(gè)任務(wù)可能既在等待一個(gè)特定事件 到來,又在等待一個(gè)特定時(shí)間到期----就看誰來得更快。這種情況下,代碼可以簡單的使用scedule_timeout()函數(shù)代替 schedule()函數(shù),這樣一來,當(dāng)希望指定時(shí)間到期后,任務(wù)都會(huì)被喚醒,當(dāng)然,代碼需要檢查被喚醒的原因----有可能是被事件喚醒,也有可能是因 為延遲的時(shí)間到期,還可能是因?yàn)榻邮盏搅诵盘?hào)----然后執(zhí)行相應(yīng)的操作。 |
|
|