作者:魚鷹Osprey
ID ??:emOsprey
這篇筆記有如下內(nèi)容:
1、為什么需要計算各個線程的CPU使用率?2、該如何計算線程CPU使用率?3、FreeRTOS線程計算的弊端?如何打破 FreeRTOS 線程計算方式的時間限制?4、關(guān)鍵代碼介紹。
上次介紹了如何計算整個系統(tǒng)的CPU使用率:《單片機里面的CPU使用率是什么鬼?》《實操RT-Thread系統(tǒng)CPU利用率功能添加》但是卻沒有介紹該如何計算每個線程(任務)的CPU使用率。
為什么要計算線程CPU使用率?
首先要問的是,為什么要計算線程的CPU使用率,有啥用?我們知道系統(tǒng)的CPU使用率關(guān)注的是整個系統(tǒng)的使用情況,使用率越低,表示越能更及時的響應外部情況,整個系統(tǒng)的性能也會越好。但這是從系統(tǒng)整體考量的,并不能反映單個線程的執(zhí)行情況。比如雖然整體的CPU使用率是30%,但是有一個線程占據(jù)了25%的使用率,一個線程使用率是5%,那么你肯定會想,為啥這個線程需要占用這么高的CPU使用率,是不是代碼寫的有問題,是不是代碼可以優(yōu)化一下?當系統(tǒng)運行時,如果你能實時觀察各個線程的CPU使用率,那么你就能知道平時這個線程的CPU使用情況是怎樣的,為什么后來又高那么多,那么你就可以由此分析出這個線程可能出現(xiàn)了問題,也就可以針對性的進行檢查了。這點對于合作開發(fā)的項目更是明顯,很多時候因為有些線程的代碼不是自己寫的,所以根本不知道代碼執(zhí)行情況,一旦系統(tǒng)出現(xiàn)問題,那么可能就是互相甩鍋了。而當計算了線程的CPU使用率,一旦發(fā)現(xiàn)某個線程執(zhí)行異常,那么就能交給負責的人去查看了。所以說,使用操作系統(tǒng)的項目是非常有必要計算各個線程(任務)的CPU使用率的。就好比你的電腦,風扇嗡嗡響(CPU高負荷運行),如果只有一個系統(tǒng)CPU使用率,發(fā)現(xiàn)高達90%,但是你卻根本不知道為什么這么高,所以只能重啟。而一旦有了進程CPU使用率,查看一下哪個進程CPU使用率高,把對應的進程關(guān)閉就行了,根本不需要重啟電腦。????????
如何計算線程CPU使用率?
那么現(xiàn)在就來看看該如何計算各個線程的CPU使用率。從前面的筆記,我們其實也可以猜測該如何計算,無非就是獲取每個線程的執(zhí)行時間罷了。比如,1秒時間內(nèi),空閑任務執(zhí)行700毫秒,任務1執(zhí)行200毫秒,任務2執(zhí)行100毫秒,那么各個任務的CPU使用率分別是 70%、20%、10%。以前計算系統(tǒng)的CPU使用率的時候,采用了軟件方法計算空閑任務的運行時間,這必然是不夠準確的,所以最好的方式是采用硬件計時。因為魚鷹采用STM32F103進行測試,所以使用DWT外設進行精確計時,不過麻煩的是,在KEIL 軟件仿真情況下,DWT外設是無法工作的,所以如果要測試的話,必須使用硬件仿真的方式,不過如果真要KEIL軟件仿真的話,也不是沒有辦法,就是使用硬件定時器,這個按下不表。畢竟,DWT外設的功能在這里說白了也就是個定時器而已。既然要獲取線程的執(zhí)行時間,關(guān)鍵一點就是,我們要知道操作系統(tǒng)什么時候會切換到某一個線程運行,什么時候又會從這個線程切出,到另一個線程執(zhí)行呢?這個關(guān)鍵還是在系統(tǒng)內(nèi)置的鉤子函數(shù)。上次的筆記魚鷹介紹過空閑鉤子函數(shù),今天介紹另一個鉤子,任務切換鉤子函數(shù)。這個鉤子函數(shù)的特點就是,每當系統(tǒng)需要切換到下一個任務時,就會先執(zhí)行這個函數(shù)。這個函數(shù)一般有兩個參數(shù),當前任務和即將切換的任務。只要設置任務切換的鉤子函數(shù),并且有時間戳,那么計算一個任務的執(zhí)行時間也就不那么困難了。比如,操作系統(tǒng)在時刻12345 ms 切換到空閑任務執(zhí)行,突然一個任務就緒,開始準備執(zhí)行,所以在時刻12445切換到那個就緒任務執(zhí)行,那么空閑任務的執(zhí)行時間我們也就可以準確計算出來了。12445 – 12345 = 100 ms也就是說,這一次空閑任務執(zhí)行了 100 毫秒。如果我們要計算單位時間(比如1秒內(nèi))空閑任務的執(zhí)行時間,我們只要在每次運行到空閑任務時累計時間即可。比如1秒內(nèi),空閑任務執(zhí)行了 5 次,分別是 10、200、100、200、50,累計時間為10 200 100 200 50 = 560毫秒由此,可計算空閑任務的CPU使用率為 56%,從而可計算出系統(tǒng)的CPU使用率是44%。是的,通過線程的CPU使用率方法,我們其實也可以計算整個系統(tǒng)的CPU使用率。而且這種計算方式比前面所說的計算方法更準確,更科學。前面采用時間戳進行計算,但是時間戳是會溢出的,那個時候,你的時間計算還是準確的嗎?
FreeRTOS線程計算限制?
現(xiàn)在魚鷹就來說說第三個問題,F(xiàn)reeRTOS線程計算的弊端?如何打破 FreeRTOS 線程計算方式的時間限制?從網(wǎng)上查找FreeRTOS任務CPU計算相關(guān)的資料,可以得到以下信息:1、需要開一個定時器,這個定時器中斷頻率是操作系統(tǒng)時鐘的十幾倍(為了保證計算精度)。2、一個64 位的變量在定時器自加更新,一旦變量溢出,時間計算就會出現(xiàn)問題。(相關(guān)細節(jié)可查看安富萊教程)第一個問題會導致系統(tǒng)性能下降(中斷頻率太高,一般是微秒級別的),而第二個問題導致在一段時間內(nèi)(小時級別)線程CPU使用率計算準確,超出時間后,計算會有問題,所以教程中不建議在正式版本加入此功能。第一個問題其實很好解決,就是使用硬件定時器,不再由CPU去更新時間,這樣不會占用CPU時間,第二個問題其實也非常好解決,就是通過《延時功能進化論(合集)》的方式解決溢出問題,這里不再展開說其中的奧妙。
任務切換鉤子函數(shù)的實現(xiàn)總之,魚鷹接下來的實現(xiàn)方式解決了以上兩個痛點,即使無限執(zhí)行下去,也不會影響到計算精度問題,唯一對系統(tǒng)產(chǎn)生的一點影響,只有在任務切換時消耗的一點計算時間(微秒級別)。那么先上任務切換鉤子函數(shù)關(guān)鍵實現(xiàn)代碼(RT-Thread):
void thread_stats_scheduler_hook(struct rt_thread *from, struct rt_thread *to){ static uint32_t schedule_last_time; uint32_t time; time = get_curr_time(); from->user_data = (time - schedule_last_time); schedule_last_time = time;}如何將這個函數(shù)注冊到操作系統(tǒng)中被系統(tǒng)調(diào)用呢?通過這個函數(shù)即可:那么現(xiàn)在來分析這個鉤子函數(shù)實現(xiàn):一個靜態(tài)變量,用于記錄切換時的時間戳。每次任務開始切換時,更新這個時間戳,同時累積時間,這個時間保存在當前任務的user_data里面。難理解?看下圖就清楚了。假設系統(tǒng)調(diào)度是從任務1切換到任務2,即from為任務1,to為任務2,此時獲取的時間戳為 T1。上一次的時間戳我們已經(jīng)通過靜態(tài)變量保留了,這里為T0,那么T1-T0就是from任務即任務1在本次運行的時間,只要下次運行任務1時繼續(xù)不斷的累積這個時間,那么就可以得到任務1的總運行時間。任務2同理。當然我們不可能一直累積下去,不然肯定會溢出,所以隔一段時間就需要清零,這個時間其實就是線程CPU計算的周期。????這里還有一個函數(shù)沒有說,就是 get_curr_time(),在這里使用DWT,為了可以重新實現(xiàn)該函數(shù),魚鷹使用了弱屬性 weak(關(guān)于這個看參考:《困惑多年,為什么 printf 可以重定向??》)。__weakuint32_t get_curr_time() { return DWT->CYCCNT; // don't use the function rt_tick_get()}這里可以看到有個注釋,不要使用 rt_tick_get 函數(shù),為啥?精度太低,有些任務本來執(zhí)行了的,但是因為執(zhí)行時間小于操作系統(tǒng)的時鐘(比如1毫秒),那么就無法累積時間了,那么即使這個任務運行再多,時間累積也為 0,這肯定是我們不希望看到的。然后再說一個點,為了簡化代碼(鉤子函數(shù)代碼只有短短幾行),魚鷹這樣的實現(xiàn)是有兩個問題的。1、首次運行計算有誤,因為靜態(tài)變量應該在運行任務之前就初始化的(不應該初始化為 0),而鉤子函數(shù)是在任務運行之后才調(diào)用的,所以從開機以來的時間被累加到第一個運行任務中了,這肯定是有問題的,不過后面隨著系統(tǒng)的運行,靜態(tài)變量被持續(xù)更新,就不會再出現(xiàn)這個問題了。2、為了減少修改,魚鷹把線程的use_data當成一個變量使用了,實際上這個變量的功能應該是存儲線程私有變量地址的,但是因為魚鷹懶得修改太多代碼,所以直接拿來用了。正因為如此,所以魚鷹添加線程CPU計算時,只要修改很少的代碼就可以了。線程CPU的計算
目前我們已經(jīng)能夠通過鉤子函數(shù)獲取各個線程的CPU執(zhí)行時間,現(xiàn)在就看該如何計算了。為了計算各個線程的CPU使用率,我們需要確定計算周期,這里我們可以設置1秒計算一次。其次,我們需要確定在哪個任務執(zhí)行計算。原理上來說,可以是系統(tǒng)中的任何一個任務,但是為了減少對系統(tǒng)的干擾,可以將計算工作放到優(yōu)先級比較低的任務中進行,比如空閑任務。現(xiàn)在,看看函數(shù)是如何實現(xiàn)的:
// can call the function 1 s (max 60s when stm32f1xx because of dwt)void thread_cal_usage(thread_run_info_def *run_info){ static uint32_t total_time_last; uint32_t time, total_time; struct rt_list_node *node; struct rt_list_node *list; struct rt_thread *thread; uint32_t i; rt_enter_critical(); // 關(guān)閉系統(tǒng)調(diào)度,防止在計算過程中更新線程時間,影響計算 time = get_curr_time(); // 獲取當前時間戳 total_time = time - total_time_last; // 計算運行總時間 total_time_last = time; // 更新時間 list = 




