實操RT-Thread系統CPU利用率功能添加

來源:公衆號【魚鷹談單片機】

作者:魚鷹Osprey

ID   :emOsprey

首先要對各位道友說聲抱歉,上週就準備寫的,然後有些事情耽誤了,導致食言了,主要是魚鷹這個人比較佛系,如果狀態好,又有自己想寫的內容,就會寫的很舒服,質量也有保證,否則的話,寫的時候就是一種折磨了,這樣寫出來的文章對讀者而言,也是比較不負責的。所以魚鷹寧願留着這周狀態好再寫(那什麼時候好嘞?喫飽喝足睡夠就行啦)。

好了,不廢話了。

上週提到爲什麼我們需要關注CPU利用率的問題,總結一句話就是,利用率越低,你的系統效率越高、響應越快,實時性越高。但是並沒有具體說該如何計算CPU利用率。

今天,藉助國產操作系統RT-Thread,我們開始實操一番。

在實操之前,需要簡單瞭解幾個概念。

鉤子函數,即以hook命名的那些函數。那麼什麼是鉤子函數呢?說白了,就是一個函數指針,只是這個函數比較特殊一點。

特殊在哪?操作系統某些指定位置纔會設置鉤子函數,比如程序運行到空閒任務了,爲了不修改系統源碼(沒事別修改源碼,很危險的事情,除非你是真大佬),系統會提供一個設置鉤子函數的函數接口給你,當你需要在空閒任務中執行某些功能時,用這個函數設置你的需要功能函數就可以了,等系統運行到空閒任務,他就會幫你調用這個函數了。

這個功能看着是不是有點眼熟,對的,和所謂的回調函數是一個道理(我也不明白爲啥叫鉤子函數,可能是因爲和系統有關,和通用的回調函數又有點區別,所以就稱之爲鉤子函數吧,不過你不要管名稱,只要知道意思就行了)。

除了在空閒任務可以設置鉤子函數,還有可能在任務切換、系統啓動、任務創建等等關鍵的地方設置,當然了,這裏的每一個鉤子函數都是一個單獨的函數指針。

前面也說了,設置鉤子函數的目的只有一個,那就是可以讓你在不修改系統源碼的情況下達到私人目的,讓系統的擴展性更強,比如今天說的內容(還有下次介紹的線程CPU使用率問題),如果系統沒有空閒鉤子函數的存在,你只能去修改系統源碼才能達到目的啦。

還有文章所說的線程(task)、任務(thread),其實在RTOS中都是一樣的。在 uCOS、FreeRTOS 中,叫任務,RT-Thread 叫線程,只是叫的名稱不一樣,內容都是差不多的。

然後再大概說說怎麼計算的問題。也就是在空閒鉤子函數裏面,我們需要幹什麼事情才能到達CPU計算的目的。

首先,第一步肯定是設置鉤子函數,其次就是鉤子函數該怎麼寫的問題。

這個網上一搜就出現了(魚鷹也是網上搜的代碼),然後就要分析爲什麼這麼寫。

前面說過,CPU利用率其實是首先計算一段時間內空閒任務執行時間,然後反推其他任務的執行時間。

這裏有兩個問題,一段時間是多少?空閒任務的執行時間怎麼計算?

先說第二個問題。用定時器時間掐?好像不好,因爲你不知道什麼時候程序就離開了空閒任務跑去執行其他任務了,而即使你可以知道它什麼時候離開空閒任務的,那也會增加計算難度,不是好的方式。

那怎麼辦?還記得剛學單片機時你是怎麼進行軟件延時的嗎?對,就是用這個方法,軟件延時!

只要程序執行到空閒任務了,就用一個變量不停自加。這樣就可以根據變量值來大概計算空閒任務的執行時間。

但是這裏又存在一個問題:如果這個變量一直自加,肯定會溢出,該怎麼解決。

加大變量的大小,比如原先使用一個字節、兩個字節的,那麼如果溢出,就用四個字節、八個字節。

但32位系統最大能支持的也就8個字節了,如果還是溢出了咋辦?再套一個循環,一個循環的數加完了,再加另一數就行了。

但是還有一個問題,如果說自加的時間不做限制,那麼再多的變量也不行,而且還會影響CPU計算的實時性,也就不能實時反映CPU利用率了;而如果時間太短,如果剛好有任務的執行時間在這個範圍,那麼很可能你計算CPU利用率就直接是100%了。

比如說你一個任務需要執行10毫秒,然後你計算CPU的週期也是10毫秒,那麼可能剛好開始計算時跳到了那個任務執行,那麼你的變量就沒有自加了,也就會顯示100%利用率了。

這裏其實說的是前面的第一個問題,一段時間是多少?

對於這個時間,因爲魚鷹看的書籍比較少,所以也沒有理論支撐(如果有道友知道的,不如留言)。

但是肯定既要考慮變量溢出(這個可以通過加循環方式解決),又要考慮實時性,還要考慮其他任務的最大執行時間,否則本來系統沒有問題的,但是因爲你追求實時性,導致CPU利用率80%、90%的,那就很尷尬了。

以上討論如果沒有經驗可能比較難理解,所以建議大家在看完後面內容,實操過後,再回頭重新看一遍,這樣纔有更深的理解。

現在再看CPU計算公式:

cpu_usage = (total_count – count)/ total_count × 100 %

(滑動查看)

cpu_usage: CPU利用率;

total_count:單位時間內全速運行下的變量值;

count:單位時間內空閒任務自加的變量值。

total_count這個值表現了單片機全速運行下,所能達到的最大值。所謂全速運行,即不響應中斷,也不去執行其他任務,就單純讓它在一個地方持續運行一段時間,這個值可以體現CPU的算力有多大。

比如,51單片機,可能這個值自加10毫秒之後只有100,STM32F1單片機自加能到1000,而STM32F4單片機能到2000,這樣就能體現他們之間的算力差別了。

這個值可以是動態的,也可以是靜態的。靜態有靜態的好處,動態有動態的好處。

所謂的靜態是指,在系統沒有運行任務時,關閉所有的中斷,自加這個值。這樣,這個值比較準確,但是如果一開始這個值計算錯了,那麼後面的計算肯定也是有問題的,而且如果系統啓動後長時間既不啓動任務,也不響應中斷,肯定對系統有一定的影響。但是好處是,系統消耗更少,因爲他只計算一次。

而動態計算,則是在空閒任務中,當這個值爲零時,計算一次,之後只會在空閒任務自加的變量值超過這個數時,纔會更新這個值,這樣一來,最終還是能準確反映CPU利用率的。好處是,不需要在開機時關閉所有中斷,當然壞處是,前期可能不是很準,因爲可能由於中斷原因導致計算的值較小(中斷處理時消耗了算力)。

廢話太多了一些,直接開始幹吧。新建一個文件,拷貝如下代碼:

#include <rtthread.h>
#include <rthw.h>


#define CPU_USAGE_CALC_TICK    10
#define CPU_USAGE_LOOP        100


static rt_uint8_t  cpu_usage_major = 0, cpu_usage_minor= 0;
static rt_uint32_t total_count = 0;


static void cpu_usage_idle_hook(void)
{
    rt_tick_t tick;
    rt_uint32_t count;
    volatile rt_uint32_t loop;


    if (total_count == 0)
    {
        /* get total count */
        rt_enter_critical();
        tick = rt_tick_get();
        while(rt_tick_get() - tick < CPU_USAGE_CALC_TICK)
        {
            total_count ++;
            loop = 0;


            while (loop < CPU_USAGE_LOOP) loop ++;
        }
        rt_exit_critical();
    }


    count = 0;
    /* get CPU usage */
    tick = rt_tick_get();
    while (rt_tick_get() - tick < CPU_USAGE_CALC_TICK)
    {
        count ++;
        loop  = 0;
        while (loop < CPU_USAGE_LOOP) loop ++;
    }


    /* calculate major and minor */
    if (count < total_count)
    {
        count = total_count - count;
        cpu_usage_major = (count * 100) / total_count;
        cpu_usage_minor = ((count * 100) % total_count) * 100 / total_count;
    }
    else
    {
        total_count = count;


        /* no CPU usage */
        cpu_usage_major = 0;
        cpu_usage_minor = 0;
    }
}


void cpu_usage_get(rt_uint8_t *major, rt_uint8_t *minor)
{
    RT_ASSERT(major != RT_NULL);
    RT_ASSERT(minor != RT_NULL);


    *major = cpu_usage_major;
    *minor = cpu_usage_minor;
}


void cpu_usage_init(void)
{
    /* set idle thread hook */
    rt_thread_idle_sethook(cpu_usage_idle_hook);
}

以上的代碼網上找的,首先分析這兩個宏,第二個宏就是前面所說的防止變量溢出用的,而第一個值就是CPU計算週期,這個值比較關鍵,後面再說。

首先在系統啓動前設置鉤子函數:

然後,就沒有然後了。

對的,設置完之後就可以了,但爲了讓我們能觀察到,可以打印出來。

我們可以觀察效果如何,開始設置計算週期和任務延時函數一樣,10毫秒。

測試結果:

可以看到,因爲是動態計算的,所以開始爲0,因爲系統首先運行其他任務,只有其它任務不運行時,纔會開始運行空閒任務,所以CPU利用率爲0。

但是即使後面有值了,你也會發現CPU利用率變化很大,0.82%~1.5%。而且你會發現除了開始的0.0%,後面又再次出現了,這又是怎麼回事?

通過設置斷點分析,發現,這是因爲計算值超出了開始的值,重新設置了:

這就是動態計算的一些問題了,它在一開始的一段時間裏,因爲無法完全表現算力,只能通過後面不停的修正該值才能達到穩定。

現在修改計算週期 20 毫秒:

發現它的表現更差勁,4.3%~11.61%,而且會週期性出現低利用率的情況。

再改,100毫秒:

可以看到這個比較穩定了,13.71%~14.35%。

那麼這個測試代碼實際情況的CPU利用率是多少呢?

我們可以通過前面的筆記《KEIL 下如何準確測量代碼執行時間?》大概計算線程執行時間:

1.59毫秒,10毫秒執行週期,如果只有這個任務執行,大概1.59/10=15.9%(準確計算應該是 1.59/(10 + 1.59) =13.7%)。

和前面的100毫秒類似。

我們先不管前面的結果,先理解一下里面的計算方法。

首先,如果total_count開始爲0,那麼開始第一次計算。這次計算會關閉調度器。

計算過後,就不再進入。

之後就是動態計算過程:

和第一次計算一樣,都是在一定時間內自加計數器,不同的是,這次不會關閉調度器,也就是說,如果有高優先級任務就緒,那麼是可以執行其他任務的。

並且計時時間使用的是系統函數rt_tick_get(),單位爲系統調度時間。測試環境中,系統調度時間爲 1 毫秒。

有意思的是,在進行最終的計算時,採用了分步計算,首先計算整數,再計算小數。

爲什麼要這樣做?效率!

這樣的計算方法,可以將浮點運算轉化成整型運算,這在沒有浮點運算單元的單片機中,能大大減少計算時間。

另外,爲了防止溢出,還使用了一個循環結構。

理解了以上內容,現在開始進行魚鷹式深度思考:

  1. 上面的分步計算是否存在問題?

  2. 關調度器只關閉了任務調度,但還是會響應中斷,這能夠體現單片機最大算力嗎?

  3. 使用rt_tick_get() 函數進行計時,精度是多少,會影響最終的計時嗎?

  4. 有必要使用循環體嗎?如果單位時間內不溢出,是否不用循環體會更好?

  5. 前面的CPU使用率爲什麼會跳動,按理說任務的執行時間應該是確定的,也只有一個任務在運行,不應該跳動纔對?

  6. 10毫秒的計算和100毫秒的計算差別在哪?

    7. 終極問題,如何精確計算CPU使用率?

上面的問題,如果只是粗略計算,其實都可以不用考慮,本着對技術的熱愛,還是聊一聊好了。

1、分步計算,不知道你想到了什麼BUG?這個問題其實在以往的筆記都提過,這次再說一次。

當你在獲取CPU使用率時,如果剛好在更新這兩個值,那麼可能整數部分是上一次計算的值,而小數部分卻是這次計算的值,那麼肯定有問題。

這就涉及到數據完整性獲取的問題。怎麼解決。關調度器、關中斷都可以。

但是因爲是粗略計算,那麼小數部分即使是錯誤的,也沒事。

2、因爲只關調度器,所以對於中斷還是會響應,比如說你設定計算週期爲100毫秒,那麼1毫秒一次的systick中斷肯定會執行,那麼在100毫秒中,有100次進入中斷執行,而這些算力在上述算法中是無法體現的。

3、rt_tick_get() 函數精度問題,因爲這個是系統的軟件計時器,所以在測試環境中爲1毫秒遞增一次,也就是說它的精度在1毫秒。因此,在100毫秒的計算週期裏面,有1% 的誤差存在,在10毫秒的計算週期裏面,誤差10%!

4、有沒有必要用循環體?在1秒計算一次的情況下,即使不用循環體,也不會導致溢出問題。而且使用了循環體,還會導致精度降低,畢竟樣本少了。比如使用循環體最大值爲100,不使用時爲10000,哪個精度高?

5、CPU使用率跳動問題。因爲是測試,所以只有一個任務在運行,而且任務很簡單。

這個任務的執行時間應該是固定的纔對,但即使是使用了後面的高精度計算方式,CPU使用率還是會跳動,這是爲什麼?

第一,rt_kprintf函數執行時間是不固定的,不固定在哪,比如要顯示的變量開始是1,後面是1000,因此它輸出的字符串不一樣,並且打印時間也不一樣,因爲是查詢方式打印,所以差別很大!這就是我爲什麼推薦DMA打印的原因,未使用前是10%,使用後可能就是1%,甚至更低。

第二點,也是非常容易忽視的一點,插入的中斷執行時間。

系統每隔1毫秒需要進入systick執行一次(或者其他中斷執行時間),如果說任務的執行時間超過1毫秒,那麼中間必然會先執行中斷,再執行任務,這樣一來,因爲中斷的插入,導致時間不再那麼準確了。而當你把打印的時間控制在 1 毫秒以內,那麼CPU使用率會變的非常穩定。

第三:延時rt_thread_delay()函數本身的誤差,受到系統精度的影響,這個延時時間其實也不是固定的,會有一定的浮動。

6、10毫秒和100毫秒計算的差別?

如果說你的任務執行時間小於1毫秒,那麼在10毫秒和100毫秒的計算差別不是很大,但是如果說計算週期變成了5毫秒,即使任務執行時間小於1毫秒的情況下,計算值也是會在最大最小之間來回跳動的。而執行時間一旦超過1毫秒,那麼10毫秒和100毫秒的計算就有較大的差別。

並且測試的時候,因爲系統延時時間是10毫秒,而計算的時候也是10毫秒的週期,所以出現了比較詭異的事情,因爲按理說延時10毫秒,任務執行時間2.56毫秒,任務運行週期爲12 毫秒(還記得前面所說的延時誤差嗎),CPU 使用率按理應該是 21.3 左右,實際上卻是 6.5% 左右,相差太大了,這就非常奇怪了。而且如果更改執行時間爲1.5毫秒時(通過修改代碼修改執行時間),發現計算值又正常了;而即使不修改執行時間,修改計算時間爲100毫秒,又正常了,這是怎麼回事?

通過深入分析發現,剛好在主任務延時10毫秒的時候,切換到了空閒任務進行空閒時間計算,執行了9.4毫秒的時候,又切回到了主任務,所以計算時,得到了6.5%的計算值。

粗略表示如下所示:

通過這個分析,你應該知道,計算CPU的時候,儘量不要使用和任務延時時間一樣的計算週期,否則會出現莫名其妙的事情;還有一點就是,任務的執行週期 = 任務執行時間 + 系統延時,而前面所介紹的計算方法只是粗略的表示,嚴格來說是有問題的。

7、終極問題,如何提高計算精度?

通過以上分析,我們其實已經知道了計算時的一些問題點。首先,計算週期問題,這個可以根據系統來確定,但是千萬要注意前面的提到的問題。如果說500毫秒計算週期可以滿足要求的話,就沒必要使用50毫秒,不然你會發現計算值跳動很大。

其次,時間精度問題,這個問題老生常談了,魚鷹建議是DWT,如果沒有,找一個定時器代替也是可以的。

最後是單位時間算力問題,爲了保證精確,可以關閉中斷進行第一次計算,或者用短一點的時間,比如1毫秒得到一個算力,如果計算週期爲100毫秒,那這個算力乘以100就行了。當然如果系統時鐘不經常變的話,也可以通過靜態方式先得到單位時間的算力,之後就以它爲標準就可以了。這樣就不會有長時間關中斷的情況出現了。

但是計算算力的時候,千萬千萬要注意一點的是,C語言轉化爲彙編代碼時,可能一樣的代碼,在不同的地方執行時間是不一樣的(比如前面代碼的第一次計算和後面的計算,看似一樣,但實際上有較大差別,原因就在於執行效率不一樣),這個涉及到寄存器比內存效率更高的問題,所以計算算力時,可以把它封裝成一個函數,這樣,只要優化等級不變,那麼函數的執行時間就可以認爲是確定的。

推薦閱讀:

終極串口接收方式,極致效率

爲什麼說你一定要掌握 KEIL 調試方法?

延時功能進化論(合集)

指針,很難嗎?| 解析指針的過程與意義(一)

如何寫一個健壯且高效的串口接收程序?

KIEL 調試那些事兒之窗口展示——變量(二)

打了多年的單片機調試斷點到底應該怎麼設置?| 顛覆認知

-THE END-


如果對你有幫助,記得轉發分享哦

微信公衆號「魚鷹談單片機

每週一更單片機知識

長按後前往圖中包含的公衆號關注

魚鷹,一個被嵌入式耽誤的暢銷書作家

個人微信「EmbeddedOsprey

長按後打開對方的名片關注

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章