上個筆記提到調用任務延時函數后,系統將會進行任務切換,否則當前運行任務就會一直霸占著CPU的使用權。那么這個任務延時函數中到底有什么奧秘?調用它為什么能夠讓任務切換自如?這個筆記咱就要揭開uC/OS-II的一大設計精髓——任務切換。 特權同學并非軟件工程或是計算機科班出身,還真沒學過什么操作系統,對于CPU內部架構和工作機制的理解和認識完全靠自身的實踐、摸索加一些教科書的研讀。對于一些概念的闡述或許不夠專業,如果有些偏差也非常歡迎大家提出來加以糾正,但是我想這些“草根”式的圖文或許多少能夠幫助大家快速的理解和認識一些工作機理,但愿“八九不離十”應該是形容這種狀態比較合適的詞匯吧。其實如果能起到這樣的效果,那么對這些文章而言也就足夠。畢竟一板一眼、中規中矩的教科書我們看得太多了,真的是有些審美疲勞了。 因為要說任務調度的機理,那么我們不得不先把幾乎所有嵌入式處理器相關的書籍中都會提及的中斷概念再提一下。雖然講中斷的書滿大街都是,但是我想像圖1這樣一個簡單示意圖就能夠把中斷說清楚的還真不多(怎么有點“王婆賣瓜自賣自夸”的嫌疑,臉紅中~~)。一個“裸奔”的CPU軟件,無非就是一個main函數里面while(1)中包辦所有功能,偶爾來個中斷響應一些實時性要求較高的處理,僅此而已。那么,很顯然,中斷響應時有一個脫離當前main函數的舉動發生,想要讓中斷響應前后CPU回到原有的main函數執行狀態,則必須有一些額外的工作要干,第3和6步的出棧、入棧便是。這個示意圖中,大家需要明白,當某個函數或某些指令占用CPU時,意味著CPU中的寄存器存儲著和當前處理狀態相關的各種中間數據信息;同樣的道理,當中斷函數占有CPU數據時,CPU中的寄存器存儲著和中斷函數相關的各種中間數據。堆棧是專門開辟的一片存儲空間,用于和CPU寄存器相映射。一個在main函數中運行著的程序(包括在它的子函數中運行),如果被一個中斷信號打斷后去執行中斷處理,最后返回,這樣一個過程,發生了以下一些事情: 1. 應用程序(即Main函數)中執行某條指令,此時應用程序控制CPU的使用權,表現為占有CPU寄存器。 2. 一個中斷源產生,應用程序停下了CPU的使用。 3. 應用程序停下來那一刻的CPU寄存器內容被copy到堆棧中,即我們稱之為入棧操作。 4. 程序轉到中斷處理函數執行,表現為即將到來的下一時刻中斷處理函數占有CPU使用權。 5. 中斷處理函數擁有CPU使用權,表現為占有CPU寄存器,直到中斷處理函數執行完成。 6. CPU寄存器恢復執行入棧操作前的狀態,即從堆棧中copy之前入棧的信息,我們稱之為出棧。 7. 應用程序回到中斷前的下一條指令開始執行,雖然經過2-6步的“意外事件”,但是除了應用程序比預期延時了一小段時間執行外,好像這個“意外事件”沒有發生過一樣。 圖1 再來看uC/OS-II中的任務切換是如何實現的,應該說,和傳統CPU的中斷機制有著異曲同工之妙。說白了,uC/OS-II其實也是假借中斷之名偷梁換柱般完成了任務的切換。因為uC/OS-II中的每個task都好比“裸奔”著的軟件程序中的main函數,他們都有機會獨立的占用CPU的使用權。Task的寫法通常有兩種: void user_task(void* pdata) { while (1) { //用戶代碼 } } 或者 void user_task(void* pdata) { //用戶代碼 OSTaskDel(OS_PRIO_SELF); //刪除當前任務 } 前者我們已經接觸過,在用戶代碼的最后我們通常也會加上任務延時函數,讓出CPU的控制權。而后者相當于一次性完成的任務,執行過一次該任務后,自我刪除,從此銷聲匿跡,除非該任務在其他任務函數中被重新建立恢復。 OSTaskDel();函數 INT8U OSTaskDel (INT8U prio); 當任務創建并由OSStart()函數啟動后,要么處于運行態(同一時刻有且只有一個運行態的任務),要么處于就緒態,如果處于某個正在運行的任務使用OSTaskDel()函數刪除了該任務本身或者其他任務(空閑任務OSTaskIdle()是唯一不能被刪除的任務),那么被刪除的任務并不是從存儲代碼的程序中物理消失了,這段代碼還在,只不過它已經不在任務切換優先級列表中了,以后的任務切換中不會考慮運行該任務,我們說這種狀態叫做休眠態,如果要從休眠態喚醒到就緒態,則需要重新創建該函數。 OSTaskIdle()函數 空閑任務是在OSInit();函數中被建立的,看這個函數的具體內容,發現它其實并沒有干什么大事,無非是在那里“消磨時間”,也的確是這樣。但uC/OS-II中必須建立空閑函數,而且它的優先級一定是最低的,至于為什么,我們接下來先講講任務切換的機理,然后大家很容易就能明白的。 void OS_TaskIdle (void *p_arg) { #if OS_CRITICAL_METHOD == 3 /* Allocate storage for CPU status register */ OS_CPU_SR cpu_sr = 0; #endif (void)p_arg; /* Prevent compiler warning for not using 'p_arg' */ for (;;) { OS_ENTER_CRITICAL(); OSIdleCtr++; OS_EXIT_CRITICAL(); OSTaskIdleHook(); /* Call user definable HOOK */ } } 如圖2所示,這便是任務切換的大體流程。和中斷很相似,這里假設task1要切換到task2,task1中一定會調用任務延時函數(上一個筆記已經提到,這是任務切換的必要條件),咱還是簡單的12345把它說明白: 1. Task1擁有CPU的控制權,當前CPU寄存器存儲著和task1當前執行指令相關的信息。 2. Task1調用了任務延時函數。 3. CPU寄存器的數據信息被copy到了堆棧中,即入棧,這個堆棧是task1獨有的,圣神不可侵犯。 4. 把Task2獨有的堆棧數據信息paste到CPU寄存器中,即出棧,這是要恢復task2在上一次擁有CPU控制權的最后一條執行指令留下的現場。當然也可能task2之前未曾擁有過CPU控制權,沒關系,那么默認這個堆棧是應該是個空的。 5. 模擬產生了一個軟中斷源產生,任務延時函數被中斷,停止繼續執行。 6. 模擬中斷處理函數,大概這個函數中什么都不做,為的是有一個中斷返回的動作(還真有點買櫝還珠的味道)。 7. 中斷返回時,當前的CPU寄存器是task2的工作現場,那么這意味著task2已經擁有了CPU的控制權。怎么樣?真得是被“偷梁換柱”了,CPU已經從task1的while(1)里面被“俘虜”到了task2中。至此,一次任務切換完成。 圖2 大家可以好好消化一下,其實任務的切換還真的不是傳說中那么神奇。到這里,還是沒有把任務延時函數的神秘面紗揭開,沒關系,一個一個來,咱要各個擊破,每個知識點都吃透了才行。 我們先給出任務延時的一個最基本函數OSTimeDly()的程序,注意看該函數的最后調用了OS_Sched()函數,該函數便是CPU任務切換的“罪魁禍首”,好奇心強的朋友可不能放過它。 void OSTimeDly (INT16U ticks) { INT8U y; #if OS_CRITICAL_METHOD == 3 /* Allocate storage for CPU status register */ OS_CPU_SR cpu_sr = 0; #endif if (OSIntNesting > 0) { /* See if trying to call from an ISR */ return; } if (ticks > 0) { /* 0 means no delay! */ OS_ENTER_CRITICAL(); y = OSTCBCur->OSTCBY; /* Delay current task */ OSRdyTbl[y] &= ~OSTCBCur->OSTCBBitX; if (OSRdyTbl[y] == 0) { OSRdyGrp &= ~OSTCBCur->OSTCBBitY; } OSTCBCur->OSTCBDly = ticks; /* Load ticks in TCB */ OS_EXIT_CRITICAL(); OS_Sched(); /* Find next task to run! */ } } OS_Sched()函數完成任務級的調度,該函數完成了前一個任務CPU寄存器的入棧和后一個任務CPU寄存器的出棧,并且在最后做了一次“模擬”中斷返回的操作,這個操作是由OS_TASK_SW()函數里完成的。 void OS_Sched (void) { #if OS_CRITICAL_METHOD == 3 /* Allocate storage for CPU status register */ OS_CPU_SR cpu_sr = 0; #endif OS_ENTER_CRITICAL(); if (OSIntNesting == 0) { /* Schedule only if all ISRs done and ... */ if (OSLockNesting == 0) { /* ... scheduler is not locked */ OS_SchedNew(); if (OSPrioHighRdy != OSPrioCur) { /* No Ctx Sw if current task is highest rdy */ OSTCBHighRdy = OSTCBPrioTbl[OSPrioHighRdy]; #if OS_TASK_PROFILE_EN > 0 OSTCBHighRdy->OSTCBCtxSwCtr++; /* Inc. # of context switches to this task */ #endif OSCtxSwCtr++; /* Increment context switch counter */ OS_TASK_SW(); /* Perform a context switch */ } } } OS_EXIT_CRITICAL(); } 函數void OSCtxSw(void);咱還真無法右鍵open打開,通常不是用C語言寫的,而是用匯編來完成這個操作。 再回到 OSTaskIdle()函數,試想想如果系統中只有兩個用戶任務task1和task2,如果他們都調用任務延時函數,那么模擬中斷返回后系統應該到哪里繼續執行程序呢?不得而知,或許程序就要跑飛了,基于此,OSTaskIdle()函數就有存在的必要了,雖然它好像不干什么事,但至少它能保證系統在沒有task可執行的時候處于一個可控的狀態中。除此以外,OSTaskIdle()函數中還做了一件或許大家多少還是有些在意的CPU使用率的計算,它是通過計算空閑時間來推斷每秒鐘CPU的使用率的。 |