RT-Thread的位圖調度算法分析(最新版)

RT-Thread的內核調度算法

rt-thread的調度算法爲基於優先級調度和基於時間片輪轉調度共存的策略。rt-thread內核中存在多個線程優先級,而且支持多個線程具備一樣的線程優先級。線程級別數目在rtconfig.h中以宏定義的方式配置,python

當系統存在多個線程時,可能的狀況是,某些線程具備不一樣的線程優先級,可是還有一些線程具備相同的優先級。rt-thread採用的調度策略是:算法

  • 不一樣優先級的線程,採用可搶佔的方式:就緒的高優先級的線程會「馬上」搶佔低優先級的線程;數組

  • 同線程優先級別的多個線程則採用時間片輪轉,同級線程依次得到CPU時間數據結構

 

在上面的情形中,擺在rt-thread面前的問題是,如何從多個線程優先級別中找出當前優先級最高的那個線程,並調度執行。函數

rt-thread的內核調度算法採用位圖(bitmap)實現,算法時間複雜度爲O(1)(注,O(1)定義,請參考數據結構相關【書籍】,即每次調度的時間恆定:不管當前的系統中存在多少個線程,多少個優先級,rt-thread的調度函數總能夠在恆定的時間內選擇出最高優先級的線程執行。性能

線程結構存儲

尋找當前線程優先級最高的線程並調度執行,首先須要解決線程數據結構的存儲問題。下面先來分析rt-thread中如何存儲多個線程的數據結構。ui

先作幾點說明:spa

  1. 每一個線程的信息用線程控制塊(Thread Control-Block,縮寫爲TCB)表示,它是定義在rtdef.h中的struct結構體,用來描述一個線程全部必要信息;操作系統

  2. 線程的優先級別用非負整數(即無符號整數)表示。數值越小,優先級越高;線程

  3. 系統的線程優先級的數目固定,最多支持256級;

  4. 系統中的線程數目不作任何限制,線程的數目僅受限於系統RAM的大小。

讀者不妨思考最後兩點,當系統存在多個的線程時,如何存儲線程控制塊才能知足要求?

  • 線程的優先級別數目固定。使用數組存儲TCB,數組的長度即爲線程優先級的數目,數組的每一個元素爲一個指向TCB數據結構的指針。

  • 線程數目不受限制。那當某個線程優先級上存在多個線程時,這些TCB顯然沒辦法存儲在上面定義的數組對應的優先級位置上,那麼使用鏈表,鏈表是一種數據結構,每一個元素彼此連接,TCB中有一個連接下一個TCB的「鏈表數據結構」,如同一個鉤子同樣。

這樣就能夠達到上面說起的兩點設計要求,不一樣線程優先級的線程的TCB分別存在線程TCB數組對應優先級的位置上。對於相同優先級別的多個線程,咱們只須要將該優先級的第一個就緒線程的TCB存儲在線程TCB數組中相關位置,後續同級線程經過鏈表依次鏈接。

scheduler.c

...
(1) rt_list_t rt_thread_priority_table[RT_THREAD_PRIORITY_MAX];
(2) struct rt_thread *rt_current_thread;

(3) rt_uint8_t rt_current_priority;

#if RT_THREAD_PRIORITY_MAX > 32
/* maximun priority level, 256 */
(4) rt_uint32_t rt_thread_ready_priority_group;
(5) rt_uint8_t rt_thread_ready_table[32];
#else
/* maximun priority level, 32 */
(6)rt_uint32_t rt_thread_ready_priority_group;
#endif

假定RT_THREAD_PRIORITY_MAX這個宏爲256

  • 語句(1)即定義了線程TCB數組。該數組存儲rt_list_t類型的元素,實際上這就是個鏈表。

  • 語句(2)中定義了一個指針,從名稱上來看,即當前線程,struct rt_thread就是線程TCB數據結構類型。

  • 語句(3)定了當前的線程優先級

  • 語句(4)、(5)爲位圖調度算法的必要數據結構,下文詳細展開

rt-thread中的線程數據結構的存儲問題已經解決,接下來分析位圖調度算法實現。

位圖調度算法

調度算法首先要找出全部線程優先級中優先級最高的那個線程優先級。系統中某些線程優先級上可能不存在線程。也就說,rt_thread_priority_table數組中某些元素爲空,所以要找出該數組中第一個非空的元素。

調度算法1

for(i=0; i<256; i++)
{
    if(rt_thread_priority_table[i] != NULL)
        break;
}
highest_ready_priority = i;

上面策略能夠工做,可是它的問題是運行時間並不固定,若是當前系統中具備最高優先級的線程對應的優先級的數字爲0級,循環一次就能夠找出,若是很不幸,從0級到254級上都沒有就緒的線程,僅在255級上有就緒的線程,這個調度函數不得不在檢查了數組這256個元素以後,才能找出能夠運行的線程。

這個算法雖然直接簡單,可是過低效,並且運行時間也不穩定,做爲嵌入式實時操做系統這是不可接受的。咱們須要尋找一種具備恆定執行時間的調度算法 。

首先來考慮,每個優先級上是否存在線程,這是一個是/否問題,要麼存在線程,要麼不存在,這能夠用一個bit位來表示。咱們規定這個bit爲1表示存在線程,爲0表示不存在線程。

對於256級的線程,則共須要256個bit位。理想的狀況是,建立一個具備256個bit的變量,操做系統使用這個變量來維護整個系統全部對應優先級上是否存在活動的線程。顯然,C語言不支持:-(,可是256個bit也就是32個字節,定義一個32字節長的數組,而後將它看做總體。

如今須要約定,這32個字節和256個線程優先級的對應關係。一個字節的最高位爲bit7,最低位爲bit0,用bit0表示更高的優先級,用BIT7表示稍低的優先級。

來考慮這32個字節中的第一個字節。第一個字節的bit0用來表示優先級0,bit7表示優先級7。第二個字節bit0表示優先級8,bit7表示優先級15。其餘依次類推。以下表格描述了這32個字節的各個bit是和系統的256個優先級的對應關係。

      bit7 6   5   4   3   2    1  0
byte0 |007|006|005|004|003|002|001|000|
byte1 |0l5|014|013|012|011|010|009|008|
.................................
byte32|255|254|253|252|251|250|249|248|

每行對應一個字節,每一列爲各個bit位,單元格中的內容表示對應的優先級。

上面這32個字節所組成的256個bit,他們的排列方式很像一張圖(map),因此這種方式就別稱爲位圖(bit map)。這張圖就是scheduler.c中定義的32個字節的數組:

rt_uint8_t rt_thread_ready_table[32];

舉個例子,咱們建立了一個線程,而且指定了它的優先級是125,而後將它設置爲就緒(READY),實際上在咱們在調用函數將它變爲READY的函數中,RTT就會去上面這256個bit中(也便是這32個字節),找到第125個bit,我稱之爲位圖的BIT125, 也便是字節15的第5個bit,將這個bit置1。 即位圖的BIT125,就是rt_thread_ready_table[125/8]的BIT5.咱們能夠用位代碼表示爲

BITMPA.BIT_125 = rt_thread_ready_table[125/8].BIT5

優先級125 對應那個字節的哪一個bit呢?

這裏有個換算關係。其計算公式 :

優先級別除以8的商取整數即對應位圖中的字節
優先級別除以8的餘數就是對應位圖字節中的bit位

優先級125,125/8=15,125%8=5,位圖的BIT125就是rt_thread_ready_table[15]的BIT5

爲了敘述的方便,作以下說明:

位圖,就指的是數組rt_uint8_t rt_thread_ready_table[32]這32個字節組成的256個bit。

內核須要根據各個線程的狀態實時的更新這個位圖。當優先級爲125的再也不存在就緒的線程時,操做系統就須要將位圖的BIT125清0,當一個線程狀態爲READY後,則須要將這個線程的優先級在位圖中對應的BIT位置1。

尋找優先級最高的線程的問題,就變成從位圖中找出第一個爲1的bit的位置。好比說,內核中存在三個線程A、B、C, 優先級分別爲五、2五、125。即位圖中BIT5,BIT25,BIT125分別爲1,其他bit位所有爲0。調度程序得能找出非零的當前優先級最高的BIT位。也就是BIT5,對應的優先級爲5。

下面是一種顯然的調度思路,即依次遍歷數組rt_thread_priority_table,找出第一個非0的bit,這就是當前存在就緒線程的最高優先級。根據指針取出當前線程TCB,進而調度執行。

調度算法2

for(i=0; i<32; i++)
{
    for(j=0; j<8; j++)
    {
        if (rt_thread_priority_table[i] & (1<<j) ) 
            break;//這就是找到最低的那個爲1的bit位置了。
    }
    //下面就是咱們找到的最高優先級
    highest_ready_priority = i * 8 + j;
}

該調度算法雙層for循環可能只循環一次,也可能會循環256次,這取決於位圖中位圖中爲1的最低BIT的位置。若是BIT0爲1,則執行一次即跳出循環,若是BIT0-BIT254都是0,僅BIT255爲1,則循環256次。 平均來講, 雙層for循環的次數大約是 255/2 次。即與優先級數目N成正比。

每次調度函數執行的時間不恆定,取決於當前線程的優先級分佈情況。這種調度策略從總體上說執行的時間是O(n)的,即調度算法的平均執行時間跟優先級數目成正比。這種方式本質上跟調度算法1同樣,依然不能實如今恆定時間完成調度的目標。

RT-Thread的調度算法

將位圖看做一個變量,並假定當前優先級別爲8,則位圖變量能夠用一個字節表示。考慮位圖變量的取值範圍,當位圖全部BIT0全爲0時,位圖變量的值就是0,當位圖全部BIT位都是1時(表示全部線程優先級上都存在就緒的線程,此時最高優先級爲0級),則位圖變量的值是255。反過來,若是當位圖變量爲1時,此時位圖的BIT0爲1,即最高優先級爲優先級0,一樣,位圖變量爲255時,最高優先級依然是0。 當位圖變量爲6時,BIT2=1,BIT1=1,即最高優先級爲1。所以當位圖變量取0-255之間的任意一個數字時,它的最低爲1的BIT位置都是預知的。能夠預先將這位圖變量的全部取值所對應的最高優先級計算出來,並存成一張表格,而後就能夠避免算法2中的for循環,而只須要查表便可,執行時間天然是恆定的。查表法就是一種經常使用的用空間換取時間的方法。

位圖取值 最低爲1的bit位

0x01 0 (第0個bit位爲1)
0x02 1 (第1個bit位爲1)
0x03 0 (第0個bit位爲1)
....
0xff 0 (第0個bit爲1)

注意0x0比較特殊,所有bit位都是0,返回0但不表示其第0位爲1。只是爲了數組整齊因此填充0。

能夠寫個簡單的程序來生成位圖首BIT表,我寫了個python程序,

gettab.py

#coding=gbk
#打印一個字節的最低bit位,可能的值爲0,1,2,3,4,5,6,7
samples = 256

def getlowbit(byte):
    c = 0
    for i in range(0,8):
        if(byte & 0x01):
            return c
        c = c+1
        byte = byte >> 1
    return 0

line =""
for i in range(0,samples):
    print "%d," %getlowbit(i),
    if((i+1)%16 == 0):
        print "\n

就能夠獲得以下的表了:

const rt_uint8_t rt_lowest_bitmap[] =
{
    /* 00 */ 0, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0,
    /* 10 */ 4, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0,
    /* 20 */ 5, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0,
    /* 30 */ 4, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0,
    /* 40 */ 6, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0,
    /* 50 */ 4, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0,
    /* 60 */ 5, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0,
    /* 70 */ 4, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0,
    /* 80 */ 7, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0,
    /* 90 */ 4, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0,
    /* A0 */ 5, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0,
    /* B0 */ 4, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0,
    /* C0 */ 6, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0,
    /* D0 */ 4, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0,
    /* E0 */ 5, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0,
    /* F0 */ 4, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0
};

當進程優先級爲8時,直接查表就獲得最高優先級了。

當系統存在32個優先級時,若是採用相似方案直接製做表格的話,表格的元素個數將是2**32=4G字節,顯然這是不可接受的。32個優先級,即4個字節,正好能夠用一個uint32_t變量存儲。查找首個非0位,能夠將其分拆爲4個字節,以此查詢上表。

假定u32 rt_thread_priority_bitmap維護着當前系統優先級位圖。

調度算法3-32級優先級查找最高優先級算法

//kservice.c
int __rt_ffs(int value)
{
    if (value == 0) return 0;

    if (value & 0xff)
        return __lowest_bit_bitmap[value & 0xff] + 1;

    if (value & 0xff00)
        return __lowest_bit_bitmap[(value & 0xff00) >> 8] + 9;

    if (value & 0xff0000)
        return __lowest_bit_bitmap[(value & 0xff0000) >> 16] + 17;

    return __lowest_bit_bitmap[(value & 0xff000000) >> 24] + 25;
}

這就解決了32個系統優先級時的調度問題,如今來考慮線程優先級爲256的狀況。讀者可能以爲這沒什麼不一樣,256個bit=32個字節,依然採用算法3的思路,對着32個字節依次查表。問題是,當位圖變量有32個字節時,依次查表耗費的時間就不能夠忽略了,爲了提高系統實時調度的性能,須要對算法3進行改進。

爲了解決這個問題,RT-Thread引入了二級位圖。

即256個bit由32個字節存儲,每個字節的8個bit表明着位圖變量中的8個優先級,若是某個字節非0,則表示其中必有非0的bit位。

rtt中對應的數組爲rt_uint8_t rt_thread_ready_table[32]

所謂二級位圖,即先肯定32個字節中最低的非0的字節。爲了實現這個效果,現對這32個字節引入一個32個bit的位圖變量,每個bit位表示對應的字節是否爲0。例如,這個32bit的位圖變量的BIT5爲0,表示系統線程優先級256bit所分紅的32個字節中的第五個字節非0。爲了區分,稱這個32個bit的位圖變量爲字節位圖變量,這就是rt-thread中使用的是rt_thread_ready_priority_group.

這樣查找系統系統最高優先級時,先肯定非0的最低字節,這實際上依然是算法3,而後再對該字節進行查表,即獲得該字節內最低爲1的bit位,而後二者疊加(注意不是簡單的加)便可。

根據上面的分析,要想使用這個二級位圖算法,rt-thread在跟蹤線程的狀態轉換時,不只須要維護256bit的位圖變量數組rt_thread_ready_table[thread->number] |= thread->high_mask,還須要維護32bit的字節位圖變量 rt_thread_ready_priority_group。參看以下代碼。

// thread.c
rt_err_t rt_thread_startup(rt_thread_t thread)
{
    ...
    /* set current priority to init priority */
    thread->current_priority = thread->init_priority;

(1) thread->number      = thread->current_priority >> 3; /* 5bit */
(2) thread->number_mask = 1L << thread->number;
(3) thread->high_mask   = 1L << (thread->current_priority & 0x07); /* 3bit */
    ...
}

void rt_schedule_insert_thread(struct rt_thread *thread) 
{
    ...
#if RT_THREAD_PRIORITY_MAX > 32
(4) rt_thread_ready_table[thread->number] |= thread->high_mask;
#endif
(5) rt_thread_ready_priority_group |= thread->number_mask;
    ....
}

初始化線程時,指定了線程的優先級別thread->init_priority,因爲線程優先級爲0到255,一個字節就能夠表示。可是bitmap是32個字節。爲了調高效率,最好能快速向位圖的對應的bit寫1。

  • 語句(1)thread->current_priority >> 3,等價於除以8,移位效率效率更高。

  • 上面除法的餘數,就表示這個優先級在上面字節中的第幾個bit。這個餘數可使用 (thread->current_priority & 0x07)來表示。

  • 語句(3)是獲得該bit對應的權值。例如一個字節的bit7對應的權值即 (1<<7),這樣作是爲了使用「位與,或,非」等位運算,能夠提升運行速度,即語句(4)。

  • 語句(4)表示了這幾個變量做用。可見,根據某個表示優先級的數字向位圖中相應的bit位寫入了1。

  • 那麼語句(2)和(5)是作什麼用的呢? 這個number_mask其實是爲了加快查找位圖的速度而建立的。它將在rt_schedule函數中發揮做用。

thread->number表示當前線程優先級在32個字節的位圖數組中的字節位置。爲了提升效率,rt-thread另外使用了一個u32類型的變量rt_thread_ready_priority_group來加快速度。若是這32個bit中某一個bit爲1,就表示對應的某個字節非0(想一想看,這意味着該字節所表示的8個優先級中存在就緒線程)。

rt_thread_ready_priority_group變量爲32位寬度,長度上等於4個字節,所以能夠對每個字節查表(上面生成的表格)就能夠獲得爲1的最低的bit位置。

歸納起來就是,rt-thread首先肯定32個字節的位圖中,非0的最低的那個字節,而後再查表獲得這個字節非0的最低那個bit。這兩步驟正好能夠利用兩次上面的表格rt_lowest_bitmap

下面是rt_schedule的核心邏輯,非必要的代碼被我隱去。讀者能夠對比下面的代碼理解思路

// scheduler.c
void rt_schedule(void)
{
    ....
    register rt_ubase_t highest_ready_priority;

#if RT_THREAD_PRIORITY_MAX == 8
    highest_ready_priority = rt_lowest_bitmap[rt_thread_ready_priority_group];
#else
#if RT_THREAD_PRIORITY_MAX <= 32
        highest_ready_priority = __rt_ffs(rt_thread_ready_priority_group) - 1;
#else
        register rt_ubase_t number;

        number = __rt_ffs(rt_thread_ready_priority_group) - 1;
        highest_ready_priority = (number << 3) + __rt_ffs(rt_thread_ready_table[number]) - 1;
#endif
    ....
}

// kservice.c
int __rt_ffs(int value)
{
    if (value == 0) return 0;

    if (value & 0xff)
        return __lowest_bit_bitmap[value & 0xff] + 1;

    if (value & 0xff00)
        return __lowest_bit_bitmap[(value & 0xff00) >> 8] + 9;

    if (value & 0xff0000)
        return __lowest_bit_bitmap[(value & 0xff0000) >> 16] + 17;

    return __lowest_bit_bitmap[(value & 0xff000000) >> 24] + 25;
}

one more thing

能夠看出位圖調度算法的核心就是查找字節最低非0 bit位的查表法軟件實現,是整個位圖調度算法的核心。ARM公司提供專門的指令獲取寄存器最低位,只要幾條彙編語句就能夠完成一樣的功能,並且性能更好。

rt-thread做爲一款成熟商用的RTOS內核,也支持使用CPU指令實現查找字節最低非0位,這部分代碼在libcpu/arm//cpuport.c中,以cortex-m3的爲例,代碼以下

// libcpu/arm/cortex-m3/cpuport.c

__asm int __rt_ffs(int value)
{
    CMP     r0, #0x00
    BEQ     exit

    RBIT    r0, r0
    CLZ     r0, r0
    ADDS    r0, r0, #0x01

exit
    BX      lr
}

經過編譯器提供的內聯彙編功能,在C語言程序中直接使用匯編指令實現本來軟件查表實現的功能,代碼更少,性能更好。

相關文章
相關標籤/搜索