一個操作系統如果只是具備了高優先級任務能夠“立即”獲得處理器並得到執行的特點,那么它仍然不算是實時操作系統。因為這個查找最高優先級線程的過程決定了調度時間是否具有確定性,例如一個包含n個就緒任務的系統中,如果僅僅從頭找到尾,那么這個時間將直接和n相關,而下一個就緒線程抉擇時間的長短將會極大的影響系統的實時性。當所有就緒線程都鏈接在它們對應的優先級隊列中時,抉擇過程就將演變為在優先級數組中尋找具有最高優先級線程的非空鏈表。
RT-Thread內核中采用了基於位圖(bitmap)的優先級算法(時間復雜度O(1),即與就緒線程的多少無關),通過位圖的定位快速的獲得優先級最高的線程。大致來說,就是每次調度的時間是恆定的:無論當前的系統中存在多少個線程,多少個優先級,rt-thread的調度函數總是可以在一個恆定的時間內選擇出最高優先級的那個線程來執行。對不同優先級的線程,RT-Thread采用可搶占的方式:即高優先級的線程會“立刻”搶占低優先級的線程。
RT-Thread內核中也允許創建相同優先級的線程。相同優先級的線程采用時間片輪轉方式進行調度(也就是通常說的分時調度器),時間片輪轉調度僅在當前系統中無更高優先級就緒線程存在的情況下才有效。每個線程的時間片大小都可以在初始化或創建這個線程時指定。在src/scheduler.c中:
rt_list_t rt_thread_priority_table[RT_THREAD_PRIORITY_MAX];//就緒線程優先級鏈表數組(在rt_schedule_insert_thread函數中將線程設置為就緒狀態后,將當前線程鏈表節點插入對應優先級線程鏈表中)
struct rt_thread *rt_current_thread; //保存當前運行的線程(在線程跳轉時設置為目標線程to_thread)
rt_uint8_t rt_current_priority; //保存當前運行線程優先級(在線程跳轉時設置為目標線程to_thread的優先級)
#if RT_THREAD_PRIORITY_MAX > 32
/* Maximum priority level, 256 */
rt_uint32_t rt_thread_ready_priority_group;//32位二級位圖,用於查找一級位圖中32個字節的最低非0字節(即當前所有就緒線程中最高優先級對應的字節)
rt_uint8_t rt_thread_ready_table[32]; //256位一級位圖,代表32個字節,分別對應256個線程優先級。比如第一個字節的bit0表示優先級0,bit7表示優先級7。第二個字節bit0表示優先級8,bit7表示優先級15
#else
/* Maximum priority level, 32 */
rt_uint32_t rt_thread_ready_priority_group;/32位位圖變量,當Maximum priority level==32時,可看成一級位圖,該變量32bit分別對應32個線程優先級(0-31)
#endif
1、當最大優先級為8時:
若最大優先級取8,則位圖變量可用一個字節表示,且取值范圍為0-255,字節的每一位分別對應優先級0-7。當位圖變量取0-255之間的任意一個數字時,它的最低為1的BIT位置都是預知的。我們可以預先將這位圖變量的所有取值對應的最低為1的BIT位置(最高優先級)計算出來,並存成一張表格,而只需要查表即可,這個執行時間自然是恆定的。實際上,查表法就是一種常用的用空間換取時間的方法。在src/kservice.c中:
const rt_uint8_t __lowest_bit_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 };
上表可由下面的python簡單程序生成:
#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"
2、當最大優先級為32時:
當進程優先級為8時,我們可以通過查表直接解決,但是當系統存在32個優先級時,如果直接制作表格的話,這個表格的元素個數將是 2^32 = 4294967296L= 4G字節,顯然這是不可接受的。若當前最大優先級為32,即優先級位圖變量可以使用u32型,也就是等價於4個字節,我們可以對這4個字節從字節0開始依次查表,如果字節0中非0,則最高優先級一定存在於字節0中,我們對字節0查表rt_lowest_bitmap,即可以得到最高優先級。 如果字節0為0,字節1非0,我們對字節1查表得到的是字節1中為1的最低bit位,然后加上8,就是系統的最高優先級。對字節2,字節3同樣處理。當Maximum priority level==32時,則位圖變量為:
/* Maximum priority level, 32 */ rt_uint32_t rt_thread_ready_priority_group; //32位位圖變量,當Maximum priority level==32時,該變量32bit分別對應32個線程優先級
/* * rt_thread_ready_priority_group 用來表示當前系統優先級位圖。 * highest_ready_priority表示當前系統中最高優先級 */
這里僅僅說明如何獲取最高優先級,在實際源碼中可能有一些小改動。
if (rt_thread_ready_priority_group & 0xff) { highest_ready_priority = __lowest_bit_bitmap[rt_thread_ready_priority_group & 0xff]; } else if (rt_thread_ready_priority_group & 0xff00) { highest_ready_priority = __lowest_bit_bitmap[(rt_thread_ready_priority_group >> 8) & 0xff] + 8; } else if (rt_thread_ready_priority_group & 0xff0000) { highest_ready_priority = __lowest_bit_bitmap[(rt_thread_ready_priority_group >> 16) & 0xff] + 16; } else { highest_ready_priority = __lowest_bit_bitmap[(rt_thread_ready_priority_group >> 24) & 0xff] + 24; }
3、當最大優先級為256時:
現在我們解決了32個系統優先級時的調度問題,現在來考慮線程優先級為256的情況。讀者可能會想了,這沒什么不同,256個bit=32個字節,依然采用算法3的思路,對着32個字節依次查表。問題是,當位圖變量有32個字節時,對這32個字節依次查表耗費的時間就不可以忽略了,為了提升系統實時調度的性能,我們需要對算法3進行改進。為了解決這個問題,我們使用二級位圖。即,256個bit由32個字節存儲,每一個字節的8個bit代表着位圖變量中的8個優先級,如果某個字節非0,則表示其中必有非0的bit位。rtt中對應的數組為:
rt_uint8_t rt_thread_ready_table[32]; //256位一級位圖,代表32個字節,分別對應256個線程優先級。比如第一個字節的bit0表示優先級0,bit7表示優先級7。第二個字節bit0表示優先級8,bit7表示優先級15。
所謂二級位圖,即我們首先確定32個字節中最低的非0的字節。為了實現這個效果,我們需要對這32個字節引入一個32個bit的位圖變量,每一個bit位表示對應的字節是否為0。例如,這個32bit的位圖變量的bit5為0,表示系統線程優先級256bit所分成的32個字節中的byte5為非0。為了區分,稱這個32個bit的位圖變量-字節位圖變量 ,rt-thread中使用的是:
/* Maximum priority level, 256 */ rt_uint32_t rt_thread_ready_priority_group;//32位二級位圖,用於查找一級位圖就緒表中32個字節的最低非0字節(即當前所有就緒線程中最高優先級對應的字節)
顯然我們查找系統系統最高優先級時,先確定非0的最低字節,這實際上依然是算法3,然后再對該字節進行查表,即得到該字節內最低為1的bit位,然后兩者疊加(注意不是簡單的加)即可。根據上面的分析,要想使用這個二級位圖算法,rtt在跟蹤線程的狀態轉換時,不僅需要維護256bit的位圖變量數組rt_thread_ready_table[thread->number] |= thread->high_mask,還需要維護32bit的字節位圖變量 rt_thread_ready_priority_group。參看如下代碼
在rtdef.h中定義的線程控制發塊
/* priority */ rt_uint8_t current_priority; /**< current priority */ rt_uint8_t init_priority; /**< initialized priority */ #if RT_THREAD_PRIORITY_MAX > 32 rt_uint8_t number; rt_uint8_t high_mask; #endif rt_uint32_t number_mask;
注:只有當用戶定義的最大優先級大於32個時,才會存在number和high_mask兩個成員變量,這兩個成員變量及另一個成員變量number_mask都是用來作位圖運算用的,
只不過后面那個成員變量number_mask不管用戶定義的優先級個數大於32還是在32個優先級以內都會存在。
在thread.c中_rt_thread_init函數:
thread->init_priority = priority; thread->current_priority = priority;
在thread.c中rt_thread_startup函數: /* set current priority to init priority */ thread->current_priority = thread->init_priority;//將當前優先級設置為初始值 /* calculate priority attribute */ #if RT_THREAD_PRIORITY_MAX > 32 thread->number = thread->current_priority >> 3; /* high-5bit *///右移3位就是除以8,因為一個字節表示8個優先級。這樣就可以得到當前這個優先級對應一級位圖(32個字節)中的第幾個字節 thread->number_mask = 1L << thread->number; //thread->number范圍是0到31,將當前線程優先級所對應的一級位圖字節在二級(32bit)位圖變量中對應的bit置1,表示該字節代表的8個優先級至少存在一個就緒線程 thread->high_mask = 1L << (thread->current_priority & 0x07); /* low-3bit */ //current_priority的低3位表示這個優先級在上面字節中的第幾個bit #else thread->number_mask = 1L << thread->current_priority; //將當前線程的優先級在位圖變量rt_thread_ready_priority_group中對應的bit置1,表示該優先級存在就緒線程 #endif
在scheduler.c中rt_schedule_insert_thread函數:
在rt_thread_startup函數首先調用rt_thread_resume函數,在resume函數中調用rt_schedule_insert_thread函數,然后在insert函數中將線程狀態設置為就緒狀態,即所有在調度器中的線程均為就緒狀態。
#if RT_THREAD_PRIORITY_MAX > 32
rt_thread_ready_table[thread->number] |= thread->high_mask;//將當前線程優先級所處一級位圖(32個字節)中對應字節(由thread->number確定)的對應位(由thread->high_mask確定)置1,表示該優先級存在就緒線程
#endif
rt_thread_ready_priority_group |= thread->number_mask; //若最大優先級為32,則將當前線程優先級在位圖變量rt_thread_ready_priority_group中對應的bit置1,表示該優先級存在就緒線程 //若最大優先級為256,則將當前線程優先級所對應的一級位圖字節在二級(32bit)位圖變量中對應的bit置1,表示該字節代表的8個優先級至少存在一個冰緒線程
上文已說明,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位置。概括起來就是,rtt首先確定32個字節的位圖中,非0的最低的那個字節,然后再查表得到這個字節非0的最低那個bit。這兩步驟正好可以利用兩次上面的表格__lowest_bit_bitmap。
4、在線程調度時獲取所有就緒線程優先級的最高優先級:
在ksevicer.c中: int __rt_ffs(int value)//該函數用於獲取32位value第一個bit位為1的bit值加1。以低8位為例,0x00--0(特殊情況),0x01--0+1,0x02--1+1,0x03--0+1,0x04--2+1 { 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; }
在scheduler.c中rt_system_scheduler_start函數: #if RT_THREAD_PRIORITY_MAX > 32 register rt_ubase_t number;
number = __rt_ffs(rt_thread_ready_priority_group) - 1;//number為一中間參數,表示根據二級位圖rt_thread_ready_priority_group查表得到一級位圖32個字節中最低非0字節,取值范圍為0-31
highest_ready_priority = (number << 3) + __rt_ffs(rt_thread_ready_table[number]) - 1;//查表得到最低非0字節所代表的8位中最低為1的位(取值范圍為0-7),再與最低非0字節乘以8相加得到最高優先級(0-255) #else highest_ready_priority = __rt_ffs(rt_thread_ready_priority_group) - 1;//若最大優先級為32,則直接根據32位位圖變量查表得到32位中最低為1的位(取值范圍為0-31),即最高優先級 #endif 在scheduler.c中rt_schedule函數: #if RT_THREAD_PRIORITY_MAX <= 32 highest_ready_priority = __rt_ffs(rt_thread_ready_priority_group) - 1;//若最大優先級為32,則直接根據32位位圖變量查表得到32位中最低為1的位(取值范圍為0-31),即最高優先級
#else register rt_ubase_t number; number = __rt_ffs(rt_thread_ready_priority_group) - 1;//number為一中間參數,表示根據二級位圖rt_thread_ready_priority_group查表得到一級位圖32個字節中最低非0字節,取值范圍為0-31
highest_ready_priority = (number << 3) + __rt_ffs(rt_thread_ready_table[number]) - 1;//查表得到最低非0字節所代表的8位中最低為1的位(取值范圍為0-7),再與最低非0字節乘以8相加得到最高優先級(0-255)
#endif
5、線程失去占用cpu時參數變化:
主動失去cpu:(1)調用sleep,delay函數使用線程放棄CPU;(2)等待信號量,互斥鎖,事件,郵箱或消息隊列過程中調用suspend使線程掛起。
在線程主動失去CPU時,程序都會在rt_thread_suspend函數中執行rt_schedule_remove_thread函數,將當前線程從調度器中移除。
在rt_schedule_remove_thread函數中執行rt_list_remove(&(thread->tlist));將當前線程從調度器中移除,同時將該線程優先級對應的位圖變量所在位清0。
被動失去cpu:(1)線程的時間片耗盡,被迫放棄CPU;(2)系統產生中斷,線程暫時失去CPU,一旦中斷例程執行完,還是會還原,這些是由硬件自動完成的。
被動失去CPU時調用線程讓出rt_thread_yield函數(這里指(1),(2)完全由硬件來完成,不需要軟件干預),此函數中程序會執行rt_list_remove(&(thread->tlist));即將當前線程從調度器中移除,
然后再執行rt_list_insert_before((rt_thread_priority_table[thread->current_priority]),&(thread->tlist));將當前線程加入到調度器中對應優先級的就緒線程鏈表末尾
緊接着執行rt_schedule();重新調度線程。在被動失去CPU的過程中,程序並未操作與獲取線程最高優先級算法相關的幾個參數。
在scheduler.c中rt_schedule_remove_thread函數:
該函數在rt_thread_suspend函數中調用,調用之前在suspend函數中會將線程狀態設置為掛起狀態,即所有從調度器中移除的線程(不包括detach和delete函數中的移除)均為掛起狀態。 /* remove thread from ready list */ rt_list_remove(&(thread->tlist));//重置線程鏈表節點為初始值,即節點next與prev均指向自身節點,即將當前線程從調度器中移除 if (rt_list_isempty(&(rt_thread_priority_table[thread->current_priority])))//若當前優先級線程鏈表中不存在就緒線程 { #if RT_THREAD_PRIORITY_MAX > 32 rt_thread_ready_table[thread->number] &= ~thread->high_mask;//將該線程優先級在一級位圖中對應字節的對應位清0 if (rt_thread_ready_table[thread->number] == 0) //若該線程優先級在一級位圖中對應字節的對應位清0后,該對應字節仍然為0,則說明該對應字節所代表的8個優先級均不存在就緒線程 { rt_thread_ready_priority_group &= ~thread->number_mask; //若該線程優先級在一級位圖中對應字節所代表的8個優先級均不存在就緒線程,則將二級位圖中對應位清0 } #else rt_thread_ready_priority_group &= ~thread->number_mask; //若最大優先級為32,則將32位圖變量中當前線程優先級的對應位清0,表示當前優先級不存在就緒線程 #endif }
整個調度算法分析完畢,具體算法分析可參考http://blog.csdn.net/prife/article/details/7077120
