linux核心分析筆記—-定時器和時間管理

來源:互聯網
上載者:User

      在這一次裡,主要講講和時間相關的東西,這個我們都比較熟悉,我就直接如主題。

      首先要明白兩個概念:系統定時器和動態定時器。周期性產生的事件都是有系統定時器驅動的,這裡的系統定時器是一種可程式化硬體晶片,它能以固定頻率產生中斷。該中斷就是定時器中斷,它所對應的中斷處理常式負責更新系統時間,也負責執行需要周期行啟動並執行任務。系統定時器和時鐘中斷處理常式是Linux系統核心管理機制中的中樞。動態定時器是用來延遲執行程式的工具。核心可以動態建立或銷毀動態定時器。

       核心必須在硬體的協助下才能計算和行政時間。硬體為核心提供了一個系統定時器用以計算流逝的時間,該時鐘在核心中可看成是一個電子時間資源。系統定時器以某種頻率自行觸發時鐘中斷,該頻率可以通過編程預定稱為節拍率(tick rate).當時鐘中斷髮生時,核心就通過一種特殊的中斷處理常式對其進行處理。系統定時器頻率(節拍率)是通過靜態預先處理定義的,也就是HZ.在系統啟動時按照HZ值對硬體進行設定。體繫結構不一樣,HZ的值也不同,定義在asm/param.h中。剛提到的節拍率就是這個意思。周期是1/HZ秒。最後要說明的是這個HZ值在編寫核心代碼時,不是固定不變的,而是可調的。當然,對於作業系統而言,也並不是一定要這個固定的時鐘中斷。實際上,核心可以使用動態編程定時器操作掛起事件。這裡就不多說了。

       在linux核心裡,有一個叫jiffies的變數(定義在linux/jiffies)記錄了自系統啟動以來產生的節拍的總數。啟動時,核心將該變數初始化為0,此後每次時鐘中斷處理常式都會增加該變數的值。因為一秒內時鐘中斷的次數等於HZ,所以jiffies一秒內增加的值也就為HZ.系統已耗用時間以秒為單位計算,就等於jiffes/HZ.它作為在電腦表示的變數,就總存在大小,當這個變數增加到超出它的表示上限時,就要迴繞到0.這個迴繞看起來很簡單,但實際上還是給我們編程造成了很大的麻煩,比如邊界條件判斷時。幸好,核心提供了四個宏來協助比較節拍計數,這些宏定義在linux/jiffies.h可以很好的處理節拍迴繞的情況:

          說明:unknown參數通常是jiffies,known參數是需要對比的值。

       如果改變核心中的HZ的值則會給使用者空間中某些程式造成異常結果,這是因為核心是以節拍數/秒的形式給使用者空間匯出這個值的,在這個介面穩定了很長一段時間後,應用程式便逐漸依賴於這個特定的HZ的值了。所以如果在核心中更改了HZ的定義值,就打破了使用者空間的常量關係----使用者空間並不知道這個新的HZ的值。為瞭解決這個問題,核心必須更改所有匯出的jiffies的值。核心定義了USER_HZ來代表使用者空間看到的HZ值。核心可以使用宏jiffies_to_clock_t()將一個由HZ表示的節拍計數轉換成一個由USER_HZ表示的節拍數。改宏的用法取決於USER_HZ是否為HZ的整數倍或相反。當是整數倍時,宏的形式相當簡單:

#define jiffies_to_clock_t(x) ((x)/(HZ/USER_HZ));

       如果不是整數倍關係,那麼該宏就得用更為複雜的演算法了。同樣的,如果是64位系統,核心使用函數jiffies_64_to_clock()將64位的jiffies值的單位從HZ轉換為USER_HZ.

       體繫結構提供了兩種裝置進行計時:系統定時器和系統時鐘。系統定時器提供一種周期性觸發中斷機制。系統時鐘(RTC)是用來持久儲存系統時間的裝置,即便系統關閉後,它也可以靠主板上的微型電池提供的電力保護系統的計時。當系統啟動時,核心通過讀取RTC來初始化牆上時間,該時間存放在xtime變數中,系統時鐘最主要的作用是在啟動時初始化xtime變數。

       有了上面的概念基礎,下面就分析時鐘中斷處理常式。它分為兩個部分:體繫結構相關部分和體繫結構無關部分。相關的部分作為系統定時器的中斷處理常式而註冊到核心中,以便在產生時鐘中斷時,它能夠相應地運行。執行的工作如下:

1.獲得xtime_lock鎖,以便對訪問jiffies_64和牆上時間xtime進行保護。
2.需要時應答或重新設定系統時鐘。
3.周期性地使用牆上時間更新系統時鐘。
4.調用體繫結構無關的時間常式:do_timer().
中斷服務程式主要通過調用與體繫結構無關的常式do_timer()執行下面的工作:
1.給jiffies_64變數加1.
2.更新資源消耗的統計值,比如當前進程所消耗的系統時間和使用者時間。
3.執行已經到期的動態定時器.
4.執行scheduler_tick()函數.
5.更新牆上時間,該時間存放在xtime變數中.
6.計算平均負載值.

       do_timer看起來還是很簡單的,應為它的主要工作就是完成上面的架構,具體的讓其它函數做就好了:

void do_timer(struct pt_regs *regs){jiffies_64++;update_process_times(user_mode(regs));update_times();}

       上述user_mode()宏查詢處理器寄存器regs的狀態,如果時鐘中斷髮生在使用者空間,它返回1;如果發生在核心模式,則返回0.update_process_times()函數根據時鐘中斷產生的位置,對使用者或對系統進行相應的時間更新:

void update_process_times(int user_tick){struct task_struct *p=current;int cpu=smp_processor_id();int system=user_tick^1;updata_one_process(p,user_tick,system,cpu);run_local_timers();scheduler_tick(user_tick,system);}

       update_one_process()函數的作用是更新進程時間。它的實現是相當細緻的。但注意,因為使用了XOR操作,所以user_tick和system兩個變數只要其中有一個為1,則另外一個就必須為0,updates_one_process()函數可以通過判斷分支,將user_tick和system加到進程相應的計數上:

p->utime = user;p->stime = system;

       上述操作將適當的計數值增加1,而另外一個值保持不變。也許你已經發現了,這樣做意味著核心對進程時間計數時,是根據中斷髮生時處理器所處的模式進行分類統計的,它把上一個tick全部算給進程。但是事實上進程在上一個節拍器間可能多次進入和退出核心模式,而在在上一個節拍期間,該進程也不一定是唯一一個運行進程,但是這沒辦法。接下來的run_lock_times() 函數標記了一個非強制中斷去處理所有到期的定時器。最後,scheduler_tick()函數負責減少當前運行進程的時間片計數值並且在需要時設定need_resched標誌,在SMP機器中中,該函數還要負責平衡每個處理器上的運行隊列。當update_process_times()函數返回時,do_timer()函數接著會調用update_times()更新牆上時間。

void update_times(void){unsigned long ticks;if(ticks){wall_jiffies += ticks;update_wall_time(ticks);}last_time_offset = 0;calc_load(ticks);}

       這裡的ticks記錄最近一次更新後新產生的節拍數。通常情況下ticks顯然應該等於1.但是時鐘中斷也有可能丟失,因而節拍也會丟失。在中斷長時間被禁止的情況下,就會出現這種現象(這種情況並不常見,往往是個BUG).wall_jiffies值隨後被加上ticks----所以此刻wall_jiffies值就等於更新的牆上時間的更新值jiffies----接著調用update_wall_time()函數更新xtime,最後由calc_load()執行。do_timer()函數執行完畢後返回與體繫結構相關的中斷處理常式,繼續執行後面的工作,釋放xtime_lock鎖,然後退出。以上的工作每1/HZ都要發生一次。

       剛前邊說的牆上時間就是我們常說的實際時間,指變數xtime,由結構體timespec定義(kernel/timer.c),如下:

structtimespec{time_t tv_sec;//秒,存放自1970年7月1日(UTC)以來經過的時間,1970年7月1日稱為紀元long tv_nsec;//納秒,記錄自上一秒開始經過的納秒數}       

讀寫這個xtime變數需要xtime_lock鎖,該鎖是一個順序鎖(seqlock).關於核心讀寫就不說了,注意適當加解鎖就好。回到使用者空間,從使用者空間取得牆上時間的主要介面是gettimeofday(),在核心中對應系統調用為sys_gettimeofday():

asmlinkage long sys_gettimeofday(struct timeval __user *tv, struct timezone __user *tz){         if (likely(tv != NULL)) {                 struct timeval ktv;                 do_gettimeofday(&ktv);                 if (copy_to_user(tv, &ktv, sizeof(ktv)))                         return -EFAULT;         }         if (unlikely(tz != NULL)) {                 if (copy_to_user(tz, &sys_tz, sizeof(sys_tz)))                         return -EFAULT;         }         return 0;}

       分析上面的函數發現,問題就集中在tv上。當tv非空,就調用do_gettimeofday(),它主要完成迴圈讀取xtime的操作。如果tz參數為空白,該函數將把系統時區(存放在sys_tz中)返回使用者。如果給使用者空間拷貝牆上時間或時區發生錯誤,該函數返回-EFAULT;如果成功,則返回0.另外,核心提供的time系統調用,幾乎被gettimeofday()完全取代。C庫函數提供的一些牆上時間相關的庫調用如ftime和ctime。系統的settimeofday()是用來設定目前時間,它需要具有CAP_SYS_TIME許可權。除了更新xtime時間以外,核心不會像使用者空間程式那樣頻繁使用xtime。但也需要注意在檔案系統的實現代碼中存放訪問時間戳記時需要使用xtime。

      上面說完了有關硬時鐘,下面開始新的話題,是關於定時器的(也稱動態定時器或核心定時器)。定時器並不周期執行,它在逾時後就自行銷毀。定義器由定義在linux/timer.h中的time_list表示,如下:

struct timer_list {         struct list_head entry;         unsigned long expires;          spinlock_t lock;         unsigned long magic;          void (*function)(unsigned long);         unsigned long data;          struct tvec_t_base_s *base;};

      核心提供了一組與定時器相關的用來簡化管理定時器的操作。所有這些介面都聲明在檔案linux/timer.h中,大多數介面在檔案kernel/timer.c中獲得實現。有了這些介面,我們要做的事情就很簡單了:

1.建立定時器:struct timer_list my_timer;

2.初始化定時器:init_timer(&my_timer);

3.根據需要,設定定時器了:

            my_timer.expires = jiffies + delay;

            my_timer.data = 0;

            my_timer.function = my_function;

4.啟用定時器:add_timer(&my_timer);

      經過上面的幾步,定時器就可以開始工作了。然而,一般來說,定時器都在逾時後馬上就會執行,但是也有可能被延遲到下一時鐘節拍時才能運行,所以不能使用它來實現硬即時。如果修改定時器,使用mod_timer(&my_timer,jiffies+new_delay)來修改已經啟用的定時器時間。它也可以操作那些已經初始化,但還沒有被啟用的定時器,如果定時器未被啟用,mod_timer會啟用它。如果第啊喲個定時器時未被啟用,該函數返回0;否則返回1。但不論哪種情況,一旦從mod_timer函數返回,定時器都將被啟用而且設定了新的定時值。當然你也可以在超市前刪除定時器用:del_timer(&my_timer);另外需要注意的是在多處理器上定時器中斷可能已經在其它機器上運行了,這是就需要等待可能在其它處理器上啟動並執行定時器處理常式都退出後再刪除該定時器。這是就要使用del_timer_sync()函數執行刪除工作。這個函數參數和上面一個一樣,只是不能在中斷上下文中使用而已。定時器是獨立與當前代碼的,這意味著可能存在競爭條件,這個就要特別小心,從這個意義上講後者刪除比前者更加安全。

      核心在時鐘中斷髮生後執行定時器,定時器作為軟體中斷在下半部上下文中執行。具體來說就是時鐘中斷處理常式會執行update_process_timers()函數,該函數隨即調用run_local_timers()函數:

void run_local_timers(void){raise_softirq(TIMER_SOFTIRQ);}

      這個函數處理非強制中斷TIEMR_SOFTIRQ,從而在當前處理器上運行所有的逾時定時器。所有定時器都以鏈表的形式組織起來,但如果單純的鏈表結構顯然影響效能,因為每次都要順序的的尋找調整,這個時候,核心定時器按它們的逾時時間將他們分為5組,當定時器逾時時間接近時,定時器將隨組一起下移。採用這種方法可以減少搜素逾時定時器所帶來的負擔。

下一話題,核心代碼(尤其是驅動程式)除了使用定時器或下半部機制以外還提供了許多延遲的方法來處理各種延遲請求。下面就來總結一下:

1.忙等待(也叫忙迴圈):通常是最不理想的方法,因為處理器被白白佔用旋轉而無法做別的事情。該方法僅僅在想要延遲的時間是節拍的整數倍或者精確率要求不高時才可以使用。實現起來還是挺簡單的,就是在迴圈中不斷旋轉直到希望的時鐘節拍數耗盡。比如:

unsigned long delay = jiffies+10;   //10個節拍while(time_before(jiffies,delay))cond_resched();

      缺點很明顯,更好的方法是在代碼等待時,允許核心重新調度執行其他任務,如下:

unsigned long delay = jiffies+10;   //10個節拍while(time_before(jiffies,delay))cond_resched();

      cond_resched()函數將調度一個新程式投入運行,但它只有在設定完need_resched標誌後才會生效。換句話說,就是系統中存在更重要的任務需要運行。再由於該方法需要調用發送器,所以它不能在中斷上下文中使用----只能在進程上下文中使用。事實上,所有延遲方法在進程上下文中使用,因為中斷處理常式都應該儘可能快的執行。另外,順延強制不管在哪種情況下都不應該在持有鎖時或者禁止中斷時發生。

      至於說那些需要很短暫的延遲(比時鐘節拍還短)而且還要求延遲的時間很精確,這種情況多發生在和硬體同步時,也就是說需要短暫等待某個動作的完成----等待時間往往小於1ms,所以不可能使用像前面例子中那種基於jiffies的延遲方法。這時,就可以使用在linux/delay.h中定義的兩個函數,它們不使用,這兩個函數可以處理微秒和毫秒層級的延遲的時間,如下所示:

void udelay(unsigned long usecs);void mdelay(unsigned long msecs);

      前者是依靠執行次數迴圈來達到延遲效果的,而mdelay()函數又是通過udelay()函數實現的。因為核心知道處理器在一秒內能執行多少次迴圈,所以udelay()函數僅僅需要根據指定的延遲時間在1秒中占的比例,就能決定需要進行多少次迴圈就能達到需要的延遲時間。udelay()函數僅能在要求的延遲時間很短的情況下執行,而在高速機器中時間很長的延遲會造成溢出,經驗表明,不要試圖在延遲超過1ms的情況下使用這個函數。這兩個函數其實和忙等待一樣,如果不是非常必要,還是不要用了算了。

      前邊說的有點害怕,那咋辦呢?其實更理想的順延強制方法是使用schedule_timeout()函數,該方法會讓需要順延強制的任務睡眠到指定的延遲時間耗盡後再重新運行。但該方法也不能保證睡眠時間正好等於指定的延遲時間----只能盡量是睡眠時間接近指定的延遲時間。當指定的時間到期後,核心喚醒被延遲的任務並將其重新放回運行隊列,如下:

set_current_state(TASK_INTERRUPTIBLE);schedule_timeout(s*HZ);

      唯一的參數是延遲的相對時間,單位是jiffies,上例中將相應的任務推入可中斷睡眠隊列,睡眠s秒。在調用函數schedule_timeout之前,不要要將任務設定成可中斷或不和中斷的一種,否則任務不會休眠。這個函數需要調用發送器,所以調用它的代碼必須保證能夠睡眠,簡而言之,調用代碼必須處於進程上下文中,並且不能持有鎖。有關這個函數的實現細節,可以看下源碼,還是相當簡單的。接下來就是當定時器逾時,process_timeout()函數被調用:

void process_timeout(unsigned long data){wake_up_process((task_t *)data);}

      該函數將任務置為TASK_RUNNING狀態,然後哦將其放入運行隊列。當任務重新被調度時,將傳回碼進入睡眠前的位置繼續執行(正好在調用schedule()後)。如果任務提前被喚醒(比如收到訊號),那麼定時器被銷毀,process_timeout()函數返回剩餘的時間。

      最後,在進程調度那一節我們說過,進程內容相關的代碼為了等待特定時間發生,可以將自己放入等待隊列。但是,等待隊列上的某個任務可能既在等待一個特定事件到來,又在等待一個特定時間到期----就看誰來得更快。這種情況下,代碼可以簡單的使用scedule_timeout()函數代替schedule()函數,這樣一來,當希望指定時間到期後,任務都會被喚醒,當然,代碼需要檢查被喚醒的原因----有可能是被事件喚醒,也有可能是因為延遲的時間到期,還可能是因為接收到了訊號----然後執行相應的操作。

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.