RT-Thread的內核調度算法
rt-thread的調度算法為基於優先級調度和基於時間片輪轉調度共存的策略。rt-thread內核中存在多個線程優先級,並且支持多個線程具有同樣的線程優先級。線程級別數目在rtconfig.h中以宏定義的方式配置,
當系統存在多個線程時,可能的情況是,某些線程具有不同的線程優先級,但是還有一些線程具有相同的優先級。rt-thread采用的調度策略是:
-
不同優先級的線程,采用可搶占的方式:就緒的高優先級的線程會“立刻”搶占低優先級的線程;
-
同線程優先級別的多個線程則采用時間片輪轉,同級線程依次獲得CPU時間
在上面的情形中,擺在rt-thread面前的問題是,如何從多個線程優先級別中找出當前優先級最高的那個線程,並調度執行。
rt-thread的內核調度算法采用位圖(bitmap)實現,算法時間復雜度為O(1)(注,O(1)定義,請參考數據結構相關【書籍】,即每次調度的時間恆定:無論當前的系統中存在多少個線程,多少個優先級,rt-thread的調度函數總可以在恆定的時間內選擇出最高優先級的線程執行。
線程結構存儲
尋找當前線程優先級最高的線程並調度執行,首先需要解決線程數據結構的存儲問題。下面先來分析rt-thread中如何存儲多個線程的數據結構。
先做幾點說明:
-
每個線程的信息用線程控制塊(Thread Control-Block,縮寫為TCB)表示,它是定義在rtdef.h中的struct結構體,用來描述一個線程所有必要信息;
-
線程的優先級別用非負整數(即無符號整數)表示。數值越小,優先級越高;
-
系統的線程優先級的數目固定,最多支持256級;
-
系統中的線程數目不做任何限制,線程的數目僅受限於系統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, 優先級分別為5、25、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語言程序中直接使用匯編指令實現原本軟件查表實現的功能,代碼更少,性能更好。
