您好,登錄后才能下訂單哦!
這篇文章主要介紹“TencentOS tiny調(diào)度器的概念和啟動調(diào)度器的方法”,在日常操作中,相信很多人在TencentOS tiny調(diào)度器的概念和啟動調(diào)度器的方法問題上存在疑惑,小編查閱了各式資料,整理出簡單好用的操作方法,希望對大家解答”TencentOS tiny調(diào)度器的概念和啟動調(diào)度器的方法”的疑惑有所幫助!接下來,請跟著小編一起來學(xué)習(xí)吧!
TencentOS tiny
中提供的任務(wù)調(diào)度器是基于優(yōu)先級的全搶占式調(diào)度,在系統(tǒng)運行過程中,當(dāng)有比當(dāng)前任務(wù)優(yōu)先級更高的任務(wù)就緒時,當(dāng)前任務(wù)將立刻被切出
,高優(yōu)先級任務(wù)搶占
處理器運行。
TencentOS tiny
內(nèi)核中也允許創(chuàng)建相同優(yōu)先級的任務(wù)。相同優(yōu)先級的任務(wù)采用時間片輪轉(zhuǎn)方式進行調(diào)度(也就是通常說的分時調(diào)度器),時間片輪轉(zhuǎn)調(diào)度僅在當(dāng)前系統(tǒng)中無更高優(yōu)先級就緒任務(wù)的情況下才有效。
為了保證系統(tǒng)的實時性,系統(tǒng)盡最大可能地保證高優(yōu)先級的任務(wù)得以運行。任務(wù)調(diào)度的原則是一旦任務(wù)狀態(tài)發(fā)生了改變,并且當(dāng)前運行的任務(wù)優(yōu)先級小于優(yōu)先級隊列中任務(wù)最高優(yōu)先級時,立刻進行任務(wù)切換(除非當(dāng)前系統(tǒng)處于中斷處理程序中或禁止任務(wù)切換的狀態(tài))。
調(diào)度器是操作系統(tǒng)的核心
,其主要功能就是實現(xiàn)任務(wù)的切換
,即從就緒列表里面找到
優(yōu)先級最高的任務(wù),然后去執(zhí)行
該任務(wù)。
調(diào)度器的啟動由cpu_sched_start
函數(shù)來完成,它會被tos_knl_start
函數(shù)調(diào)用,這個函數(shù)中主要做兩件事,首先通過readyqueue_highest_ready_task_get
函數(shù)獲取當(dāng)前系統(tǒng)中處于最高優(yōu)先級的就緒任務(wù),并且將它賦值給指向當(dāng)前任務(wù)控制塊的指針k_curr_task
,然后設(shè)置一下系統(tǒng)的狀態(tài)為運行態(tài)KNL_STATE_RUNNING
。
當(dāng)然最重要的是調(diào)用匯編代碼寫的函數(shù)cpu_sched_start
啟動調(diào)度器,該函數(shù)在源碼的arch\arm\arm-v7m
目錄下的port_s.S
匯編文件下,TencentOS tiny
支持多種內(nèi)核的芯片,如M3/M4/M7
等,不同的芯片該函數(shù)的實現(xiàn)方式不同,port_s.S
也是TencentOS tiny
作為軟件與CPU硬件連接的橋梁
。以M4的cpu_sched_start
舉個例子:
__API__ k_err_t tos_knl_start(void) { if (tos_knl_is_running()) { return K_ERR_KNL_RUNNING; } k_next_task = readyqueue_highest_ready_task_get(); k_curr_task = k_next_task; k_knl_state = KNL_STATE_RUNNING; cpu_sched_start(); return K_ERR_NONE; }
port_sched_start CPSID I ; set pendsv priority lowest ; otherwise trigger pendsv in port_irq_context_switch will cause a context swich in irq ; that would be a disaster MOV32 R0, NVIC_SYSPRI14 MOV32 R1, NVIC_PENDSV_PRI STRB R1, [R0] LDR R0, =SCB_VTOR LDR R0, [R0] LDR R0, [R0] MSR MSP, R0 ; k_curr_task = k_next_task MOV32 R0, k_curr_task MOV32 R1, k_next_task LDR R2, [R1] STR R2, [R0] ; sp = k_next_task->sp LDR R0, [R2] ; PSP = sp MSR PSP, R0 ; using PSP MRS R0, CONTROL ORR R0, R0, #2 MSR CONTROL, R0 ISB ; restore r4-11 from new process stack LDMFD SP!, {R4 - R11} IF {FPU} != "SoftVFP" ; ignore EXC_RETURN the first switch LDMFD SP!, {R0} ENDIF ; restore r0, r3 LDMFD SP!, {R0 - R3} ; load R12 and LR LDMFD SP!, {R12, LR} ; load PC and discard xPSR LDMFD SP!, {R1, R2} CPSIE I BX R1
從上面的匯編代碼,我又想介紹一下Cortex-M
內(nèi)核關(guān)中斷指令,唉~感覺還是有點麻煩! 為了快速地開關(guān)中斷, Cortex-M內(nèi)核專門設(shè)置了一條 CPS 指令
,用于操作PRIMASK
寄存器跟FAULTMASK
寄存器的,這兩個寄存器是與屏蔽中斷有關(guān)的,除此之外Cortex-M
內(nèi)核還存在BASEPRI
寄存器也是與中斷有關(guān)的,也順帶介紹一下吧。
CPSID I ;PRIMASK=1 ;關(guān)中斷 CPSIE I ;PRIMASK=0 ;開中斷 CPSID F ;FAULTMASK=1 ;關(guān)異常 CPSIE F ;FAULTMASK=0 ;開異常
寄存器 | 功能 |
---|---|
PRIMASK | 它被置 1 后,就關(guān)掉所有可屏蔽的異常,只剩下 NMI 和HardFault FAULT可以響應(yīng) |
FAULTMASK | 當(dāng)它置 1 時,只有 NMI 才能響應(yīng),所有其它的異常都無法響應(yīng)(包括HardFault FAULT) |
BASEPRI | 這個寄存器最多有 9 位(由表達優(yōu)先級的位數(shù)決定)。它定義了被屏蔽優(yōu)先級的閾值。當(dāng)它被設(shè)成某個值后,所有優(yōu)先級號大于等于此值的中斷都被關(guān)(優(yōu)先級號越大,優(yōu)先級越低)。但若被設(shè)成 0,則不關(guān)閉任何中斷 |
更多具體的描述看我以前的文章:RTOS臨界段知識:https://blog.csdn.net/jiejiemcu/article/details/82534974
在啟動內(nèi)核調(diào)度器過程中需要配置PendSV
的中斷優(yōu)先級為最低,就是往NVIC_SYSPRI14(0xE000ED22)
地址寫入NVIC_PENDSV_PRI(0xFF)
。因為PendSV
都會涉及到系統(tǒng)調(diào)度,系統(tǒng)調(diào)度的優(yōu)先級要低于
系統(tǒng)的其它硬件中斷優(yōu)先級,即優(yōu)先響應(yīng)系統(tǒng)中的外部硬件中斷,所以PendSV的中斷優(yōu)先級要配置為最低,不然很可能在中斷上下文中產(chǎn)生任務(wù)調(diào)度。
PendSV
異常會自動延遲上下文切換的請求,直到其它的 ISR
都完成了處理后才放行。為實現(xiàn)這個機制,需要把 PendSV
編程為最低優(yōu)先級的異常。如果 OS
檢測到某 ISR
正在活動,它將懸起一個 PendSV
異常,以便緩期執(zhí)行上下文切換。也就是說,只要將PendSV
的優(yōu)先級設(shè)為最低的,systick即使是打斷了IRQ,它也不會馬上進行上下文切換,而是等到ISR
執(zhí)行完,PendSV
服務(wù)例程才開始執(zhí)行,并且在里面執(zhí)行上下文切換。過程如圖所示: 然后獲取MSP
主棧指針的地址,在Cortex-M
中,0xE000ED08
是SCB_VTOR
寄存器的地址,里面存放的是向量表的起始地址。
加載k_next_task
指向的任務(wù)控制塊到 R2
,從上一篇文章可知任務(wù)控制塊的第一個成員就是棧頂指針,所以此時R2
等于棧頂指針。
ps : 在調(diào)度器啟動時,
k_next_task
與k_curr_task
是一樣的(k_curr_task = k_next_task
)
加載R2
到R0
,然后將棧頂指針R0
更新到psp
,任務(wù)執(zhí)行的時候使用的棧指針是psp
。
ps:
sp
指針有兩個,分別為psp
和msp
。(可以簡單理解為:在任務(wù)上下文環(huán)境中使用psp
,在中斷上下文環(huán)境使用msp
,也不一定是正確的,這是我個人的理解)
以R0
為基地址,將棧中向上增長的8
個字的內(nèi)容加載到CPU寄存器R4~R11
,同時R0
也會跟著自增
接著需要加載R0 ~ R3、R12以及LR、 PC、xPSR
到CPU寄存器組,PC指針指向的是即將要運行的線程,而LR寄存器則指向任務(wù)的退出。因為這是第一次啟動任務(wù),要全部手動把任務(wù)棧上的寄存器彈到硬件里,才能進入第一個任務(wù)的上下文,因為一開始并沒有第一個任務(wù)運行的上下文環(huán)境,而在進入PendSV的時候需要上文保存,所以需要手動創(chuàng)造任務(wù)上下文環(huán)境(將這些寄存器加載到CPU寄存器組中)
,第一次的時候此匯編入口函數(shù),sp是指向一個選好的任務(wù)的棧頂(k_curr_task
)。
從上面的了解,再來看看任務(wù)棧的初始化,可能會有更深一點的印象。主要了解以下幾點即可:
獲取棧頂指針為stk_base[stk_size]
高地址,Cortex-M
內(nèi)核的棧是向下增長
的。
R0、R1、R2、R3、R12、R14、R15和xPSR的位24
是會被CPU自動
加載與保存的。
xPSR的bit24必須置1
,即0x01000000。
entry是任務(wù)的入口地址,即PC
R14 (LR
)是任務(wù)的退出地址,所以任務(wù)一般是死循環(huán)而不會return
R0: arg是任務(wù)主體的形參
初始化棧時sp指針會自減
__KERNEL__ k_stack_t *cpu_task_stk_init(void *entry, void *arg, void *exit, k_stack_t *stk_base, size_t stk_size) { cpu_data_t *sp; sp = (cpu_data_t *)&stk_base[stk_size]; sp = (cpu_data_t *)((cpu_addr_t)(sp) & 0xFFFFFFF8); /* auto-saved on exception(pendSV) by hardware */ *--sp = (cpu_data_t)0x01000000u; /* xPSR */ *--sp = (cpu_data_t)entry; /* entry */ *--sp = (cpu_data_t)exit; /* R14 (LR) */ *--sp = (cpu_data_t)0x12121212u; /* R12 */ *--sp = (cpu_data_t)0x03030303u; /* R3 */ *--sp = (cpu_data_t)0x02020202u; /* R2 */ *--sp = (cpu_data_t)0x01010101u; /* R1 */ *--sp = (cpu_data_t)arg; /* R0: arg */ /* Remaining registers saved on process stack */ /* EXC_RETURN = 0xFFFFFFFDL Initial state: Thread mode + non-floating-point state + PSP 31 - 28 : EXC_RETURN flag, 0xF 27 - 5 : reserved, 0xFFFFFE 4 : 1, basic stack frame; 0, extended stack frame 3 : 1, return to Thread mode; 0, return to Handler mode 2 : 1, return to PSP; 0, return to MSP 1 : reserved, 0 0 : reserved, 1 */ #if defined (TOS_CFG_CPU_ARM_FPU_EN) && (TOS_CFG_CPU_ARM_FPU_EN == 1U) *--sp = (cpu_data_t)0xFFFFFFFDL; #endif *--sp = (cpu_data_t)0x11111111u; /* R11 */ *--sp = (cpu_data_t)0x10101010u; /* R10 */ *--sp = (cpu_data_t)0x09090909u; /* R9 */ *--sp = (cpu_data_t)0x08080808u; /* R8 */ *--sp = (cpu_data_t)0x07070707u; /* R7 */ *--sp = (cpu_data_t)0x06060606u; /* R6 */ *--sp = (cpu_data_t)0x05050505u; /* R5 */ *--sp = (cpu_data_t)0x04040404u; /* R4 */ return (k_stack_t *)sp; }
一個操作系統(tǒng)如果只是具備了高優(yōu)先級任務(wù)能夠立即
獲得處理器并得到執(zhí)行的特點,那么它仍然不算是實時操作系統(tǒng)。因為這個查找最高優(yōu)先級任務(wù)的過程決定了調(diào)度時間是否具有確定性,可以簡單來說可以使用時間復(fù)雜度
來描述一下吧,如果系統(tǒng)查找最高優(yōu)先級任務(wù)的時間是O(N)
,那么這個時間會隨著任務(wù)個數(shù)的增加而增大,這是不可取的,TencentOS tiny
的時間復(fù)雜度是O(1)
,它提供兩種方法查找最高優(yōu)先級任務(wù),通過TOS_CFG_CPU_LEAD_ZEROS_ASM_PRESENT
宏定義決定。
第一種是使用普通方法,根據(jù)就緒列表中k_rdyq.prio_mask[]
的變量判斷對應(yīng)的位是否被置1。
第二種方法則是特殊方法,利用計算前導(dǎo)零指令CLZ
,直接在k_rdyq.prio_mask[]
這個32
位的變量中直接得出最高優(yōu)先級所處的位置,這種方法比普通方法更快捷,但受限于平臺
(需要硬件前導(dǎo)零指令,在STM32中我們就可以使用這種方法)。
實現(xiàn)過程如下,建議看一看readyqueue_prio_highest_get
函數(shù),他的實現(xiàn)還是非常精妙的~
__STATIC__ k_prio_t readyqueue_prio_highest_get(void) { uint32_t *tbl; k_prio_t prio; prio = 0; tbl = &k_rdyq.prio_mask[0]; while (*tbl == 0) { prio += K_PRIO_TBL_SLOT_SIZE; ++tbl; } prio += tos_cpu_clz(*tbl); return prio; }
__API__ uint32_t tos_cpu_clz(uint32_t val) { #if defined(TOS_CFG_CPU_LEAD_ZEROS_ASM_PRESENT) && (TOS_CFG_CPU_LEAD_ZEROS_ASM_PRESENT == 0u) uint32_t nbr_lead_zeros = 0; if (!(val & 0XFFFF0000)) { val <<= 16; nbr_lead_zeros += 16; } if (!(val & 0XFF000000)) { val <<= 8; nbr_lead_zeros += 8; } if (!(val & 0XF0000000)) { val <<= 4; nbr_lead_zeros += 4; } if (!(val & 0XC0000000)) { val <<= 2; nbr_lead_zeros += 2; } if (!(val & 0X80000000)) { nbr_lead_zeros += 1; } if (!val) { nbr_lead_zeros += 1; } return (nbr_lead_zeros); #else return port_clz(val); #endif }
從前面我們也知道,任務(wù)切換是在PendSV
中斷中進行的,這個中斷中實現(xiàn)的內(nèi)容總結(jié)成一句精髓的話就是 上文保存,下文切換,直接看源代碼:
PendSV_Handler CPSID I MRS R0, PSP _context_save ; R0-R3, R12, LR, PC, xPSR is saved automatically here IF {FPU} != "SoftVFP" ; is it extended frame? TST LR, #0x10 IT EQ VSTMDBEQ R0!, {S16 - S31} ; S0 - S16, FPSCR saved automatically here ; save EXC_RETURN STMFD R0!, {LR} ENDIF ; save remaining regs r4-11 on process stack STMFD R0!, {R4 - R11} ; k_curr_task->sp = PSP MOV32 R5, k_curr_task LDR R6, [R5] ; R0 is SP of process being switched out STR R0, [R6] _context_restore ; k_curr_task = k_next_task MOV32 R1, k_next_task LDR R2, [R1] STR R2, [R5] ; R0 = k_next_task->sp LDR R0, [R2] ; restore R4 - R11 LDMFD R0!, {R4 - R11} IF {FPU} != "SoftVFP" ; restore EXC_RETURN LDMFD R0!, {LR} ; is it extended frame? TST LR, #0x10 IT EQ VLDMIAEQ R0!, {S16 - S31} ENDIF ; Load PSP with new process SP MSR PSP, R0 CPSIE I ; R0-R3, R12, LR, PC, xPSR restored automatically here ; S0 - S16, FPSCR restored automatically here if FPCA = 1 BX LR ALIGN END
將PSP
的值存儲到R0
。當(dāng)進入PendSVC_Handler
時,上一個任務(wù)運行的環(huán)境即: xPSR,PC(任務(wù)入口地址),R14,R12,R3,R2,R1,R0
這些CPU寄存器的值會自動
存儲到任務(wù)的棧中,此時psp指針已經(jīng)被自動更新。而剩下的r4~r11
需要手動
保存,這也是為啥要在PendSVC_Handler
中保存上文(_context_save
)的原因,主要是加載CPU中不能自動保存的寄存器,將其壓入任務(wù)棧中。
接著找到下一個要運行的任務(wù)k_next_task
,將它的任務(wù)棧頂加載到R0
,然后手動將新任務(wù)棧中的內(nèi)容(此處是指R4~R11
)加載到CPU
寄存器組中,這就是下文切換,當(dāng)然還有一些其他沒法自動保存的內(nèi)容也是需要手動加載到CPU
寄存器組的。手動加載完后,此時R0
已經(jīng)被更新了,更新psp的值,在退出PendSVC_Handler
中斷時,會以psp
作為基地址,將任務(wù)棧中剩下的內(nèi)容(xPSR,PC(任務(wù)入口地址),R14,R12,R3,R2,R1,R0
)自動加載到CPU寄存器。
其實在異常發(fā)生時,R14中保存異常返回標(biāo)志,包括返回后進入任務(wù)模式還是處理器模式、使用PSP堆棧指針還是MSP堆棧指針。此時的r14等于0xfffffffd,最表示異常返回后進入任務(wù)模式(畢竟PendSVC_Handler
優(yōu)先級是最低的,會返回到任務(wù)中),SP以PSP作為堆棧指針出棧,出棧完畢后PSP
指向任務(wù)棧的棧頂。當(dāng)調(diào)用 BX R14指令后,系統(tǒng)以PSP
作為SP
指針出棧,把接下來要運行的新任務(wù)的任務(wù)棧中剩下的內(nèi)容加載到CPU寄存器:R0、R1、R2、R3、R12、R14(LR)、R15(PC)和xPSR
,從而切換到新的任務(wù)。
systick是系統(tǒng)的時基,而且它是內(nèi)核時鐘,只要是M0/M3/M4/M7
內(nèi)核它都會存在systick
時鐘,并且它是可以被編程配置的,這就對操作系統(tǒng)的移植提供極大的方便。 TencentOS tiny
會在cpu_init
函數(shù)中將systick
進行初始化,即調(diào)用cpu_systick_init
函數(shù),這樣子就不需要用戶自行去編寫systick
初始化相關(guān)的代碼。
__KERNEL__ void cpu_init(void) { k_cpu_cycle_per_tick = TOS_CFG_CPU_CLOCK / k_cpu_tick_per_second; cpu_systick_init(k_cpu_cycle_per_tick); #if (TOS_CFG_CPU_HRTIMER_EN > 0) tos_cpu_hrtimer_init(); #endif }
__KERNEL__ void cpu_systick_init(k_cycle_t cycle_per_tick) { port_systick_priority_set(TOS_CFG_CPU_SYSTICK_PRIO); port_systick_config(cycle_per_tick); }
SysTick
中斷服務(wù)函數(shù)是需要我們自己編寫的,要在里面調(diào)用一下TencentOS tiny
相關(guān)的函數(shù),更新系統(tǒng)時基以驅(qū)動系統(tǒng)的運行,SysTick_Handler
函數(shù)的移植如下:
void SysTick_Handler(void) { HAL_IncTick(); if (tos_knl_is_running()) { tos_knl_irq_enter(); tos_tick_handler(); tos_knl_irq_leave(); } }
主要是需要調(diào)用tos_tick_handler
函數(shù)將系統(tǒng)時基更新,具體見:
__API__ void tos_tick_handler(void) { if (unlikely(!tos_knl_is_running())) { return; } tick_update((k_tick_t)1u); #if TOS_CFG_TIMER_EN > 0u && TOS_CFG_TIMER_AS_PROC > 0u timer_update(); #endif #if TOS_CFG_ROUND_ROBIN_EN > 0u robin_sched(k_curr_task->prio); #endif }
不得不說TencentOS tiny
源碼的實現(xiàn)非常簡單,我非常喜歡
,在tos_tick_handler
中,首先判斷一下系統(tǒng)是否已經(jīng)開始運行,如果沒有運行將直接返回,如果已經(jīng)運行了,那就調(diào)用tick_update
函數(shù)更新系統(tǒng)時基,如果使能了TOS_CFG_TIMER_EN
宏定義表示使用軟件定時器,則需要更新相應(yīng)的處理,此處暫且不提及。如果使能了TOS_CFG_ROUND_ROBIN_EN
宏定義,還需要更新時間片相關(guān)變量,稍后講解。
__KERNEL__ void tick_update(k_tick_t tick) { TOS_CPU_CPSR_ALLOC(); k_task_t *first, *task; k_list_t *curr, *next; TOS_CPU_INT_DISABLE(); k_tick_count += tick; if (tos_list_empty(&k_tick_list)) { TOS_CPU_INT_ENABLE(); return; } first = TOS_LIST_FIRST_ENTRY(&k_tick_list, k_task_t, tick_list); if (first->tick_expires <= tick) { first->tick_expires = (k_tick_t)0u; } else { first->tick_expires -= tick; TOS_CPU_INT_ENABLE(); return; } TOS_LIST_FOR_EACH_SAFE(curr, next, &k_tick_list) { task = TOS_LIST_ENTRY(curr, k_task_t, tick_list); if (task->tick_expires > (k_tick_t)0u) { break; } // we are pending on something, but tick's up, no longer waitting pend_task_wakeup(task, PEND_STATE_TIMEOUT); } TOS_CPU_INT_ENABLE(); }
tick_update
函數(shù)的主要功能就是將k_tick_count +1
,并且判斷一下時基列表k_tick_list
(也可以成為延時列表吧)的任務(wù)是否超時,如果超時則喚醒該任務(wù),否則就直接退出即可。關(guān)于時間片的調(diào)度也是非常簡單,將任務(wù)的剩余時間片變量timeslice
減一,然后當(dāng)變量減到0時,將該變量進行重裝載timeslice_reload
,然后切換任務(wù)knl_sched()
,其實現(xiàn)過程如下:
__KERNEL__ void robin_sched(k_prio_t prio) { TOS_CPU_CPSR_ALLOC(); k_task_t *task; if (k_robin_state != TOS_ROBIN_STATE_ENABLED) { return; } TOS_CPU_INT_DISABLE(); task = readyqueue_first_task_get(prio); if (!task || knl_is_idle(task)) { TOS_CPU_INT_ENABLE(); return; } if (readyqueue_is_prio_onlyone(prio)) { TOS_CPU_INT_ENABLE(); return; } if (knl_is_sched_locked()) { TOS_CPU_INT_ENABLE(); return; } if (task->timeslice > (k_timeslice_t)0u) { --task->timeslice; } if (task->timeslice > (k_timeslice_t)0u) { TOS_CPU_INT_ENABLE(); return; } readyqueue_move_head_to_tail(k_curr_task->prio); task = readyqueue_first_task_get(prio); if (task->timeslice_reload == (k_timeslice_t)0u) { task->timeslice = k_robin_default_timeslice; } else { task->timeslice = task->timeslice_reload; } TOS_CPU_INT_ENABLE(); knl_sched(); }
到此,關(guān)于“TencentOS tiny調(diào)度器的概念和啟動調(diào)度器的方法”的學(xué)習(xí)就結(jié)束了,希望能夠解決大家的疑惑。理論與實踐的搭配能更好的幫助大家學(xué)習(xí),快去試試吧!若想繼續(xù)學(xué)習(xí)更多相關(guān)知識,請繼續(xù)關(guān)注億速云網(wǎng)站,小編會繼續(xù)努力為大家?guī)砀鄬嵱玫奈恼拢?/p>
免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。