1. 程式人生 > >FreeRTOS開發實戰_作業系統中的任務排程

FreeRTOS開發實戰_作業系統中的任務排程

在一個作業系統的實現中,實現上下文的切換有兩種情況:

  • 執行一個系統呼叫
  • 系統滴答定時器(SYSTICK)中斷,輪轉需要。

5.1.1 SVC系統呼叫

SVC(系統服務呼叫,亦簡稱系統呼叫)用於產生系統函式的呼叫請求。例如,作業系統不讓使用者程式直接訪問硬體,而是通過提供一些系統服務函式,使用者程式使用SVC 發出對系統服務函式的呼叫請求,以這種方法呼叫它們來間接訪問硬體。因此,當用戶程式想要控制特定的硬體時,它就會產生一個SVC 異常,然後作業系統提供的SVC異常服務例程得到執行,它再呼叫相關的作業系統函式,後者完成使用者程式請求的服務。

這種“提出要求——得到滿足”的方式,很好、很強大、很方便、很靈活、很能可持續發展。

首先,它使使用者程式從控制硬體的繁文縟節中解脫出來,而是由OS 負責控制具體的硬體。

第二,OS 的程式碼可以經過充分的測試,從而能使系統更加健壯和可靠。

第三,它使使用者程式無需在特權級下執行,使用者程式無需承擔因誤操作而癱瘓整個系統的風險。

第四,通過SVC 的機制,還讓使用者程式變得與硬體無關,因此在開發應用程式時無需瞭解硬體的操作細節,從而簡化了開發的難度和繁瑣度,並且使應用程式跨硬體平臺移植成為可能。

開發應用程式唯一需要知道的就是作業系統提供的應用程式設計介面(API),並且瞭解各個請求代號和引數表,然後就可以使用SVC 來提出要求了(事實上,為使用方便,作業系統往往會提供一層封皮,以使系統呼叫的形式看起來和普通的函式呼叫一致。各封皮函式會正確使用SVC指令來執行系統呼叫——譯者注)。其實,嚴格地講,操作硬體的工作是由裝置驅動程式完成的,只是對應用程式來說,它們也是作業系統的一部分。

SVC 異常通過執行”SVC”指令來產生。該指令需要一個立即數,充當系統呼叫代號。SVC異常服務例程稍後會提取出此代號,從而解釋本次呼叫的具體要求,再呼叫相應的服務函式。

例如,

SVC 0x3 ;呼叫3 號系統服務

在SVC 服務例程執行後,上次執行的SVC指令地址可以根據自動入棧的返回地址計算出。找到了SVC 指令後,就可以讀取該SVC 指令的機器碼,從機器碼中萃取出立即數,就獲知了請求執行的功能代號。如果使用者程式使用的是PSP,服務例程還需要先執行MRSRn,PSP 指令來獲取應用程式的堆疊指標。

5.1.2 基於SYSTICK任務切換

我們以一個簡單的例子來輔助理解。在一個作業系統呼叫中,任務A和任務B處在就緒狀態。兩個任務將通過SYSTICK簡單輪換的模型如下:

但若在產生SysTick 異常時正在響應一箇中斷,則SysTick 異常會搶佔其ISR。在這種情況下,OS 不得執行上下文切換,否則將使中斷請求被延遲,而且在真實系統中延遲時間還往往不可預知——任何有一丁點實時要求的系統都決不能容忍這種事。因此,在CM3 中也是嚴禁沒商量——如果OS在某中斷活躍時嘗試切入執行緒模式,將觸犯用法fault 異常。整個過程如下圖所示:

為解決此問題,早期的OS 大多會檢測當前是否有中斷在活躍中,只有沒有任何中斷需要響應時,才執行上下文切換(切換期間無法響應中斷)。然而,這種方法的弊端在於,它可以把任務切換動作拖延很久(因為如果搶佔了IRQ,則本次SysTick 在執行後不得作上下文切換,只能等待下一次SysTick 異常),尤其是當某中斷源的頻率和SysTick 異常的頻率比較接近時,會發生“共振”。

Cortext M中引進了PendSV的概念,很好地解決了這個問題。PendSV異常會自動延遲上下文切換的請求,直到其它的ISR都完成了處理後才放行。為實現這個機制,需要把PendSV 程式設計為最低優先順序的異常。如果OS檢測到某IRQ正在活動並且被SysTick搶佔,它將懸起一個PendSV異常,以便緩期執行上下文切換。

        DCD     am_svcall_isr               ; SVCall handler
        DCD     am_debugmon_isr             ; Debug monitor handler
        DCD     0                           ; Reserved
        DCD     am_pendsv_isr               ; The PendSV handler
        DCD     am_systick_isr              ; The SysTick handler

我們來看看一個SysTick異常推動簡單輪轉排程模式圖。

在作業系統中,對於EXC_RETURN 的修改,只是再尋常不過基本需求。在開始排程使用者程式後,一定還伴隨著SysTick異常,它週期性把執行點轉入作業系統,從而使例行的系統管理以及必要輪轉排程得以維持——差不多就是系統的心跳。

假設存在任務A和任務B,使用PendSV控制的上下文切換如下圖:

箇中事件的流水賬記錄如下:

[1] 任務 A 呼叫SVC 來請求任務切換(例如,等待某些工作完成)

[2]  OS接收到請求,做好上下文切換的準備,並且pend一個PendSV 異常。

[3] 當CPU退出SVC後,它立即進入PendSV,從而執行上下文切換。

[4] 當 PendSV 執行完畢後,將返回到任務B,同時進入執行緒模式。

[5] 發生了一箇中斷,並且中斷服務程式開始執行

[6] 在 ISR 執行過程中,發生SysTick 異常,並且搶佔了該ISR。

[7] OS 執行必要的操作,然後pend 起PendSV 異常以作好上下文切換的準備。

[8] 當 SysTick 退出後,回到先前被搶佔的ISR 中,ISR 繼續執行

[9] ISR 執行完畢並退出後,PendSV 服務例程開始執行,並且在裡面執行上下文切換

[10] 當 PendSV 執行完畢後,回到任務A,同時系統再次進入執行緒模式。

在FreeRTOS執行時是不斷在不同的任務間進行切換,而驅動這一排程過程是通過系統tick來驅動的,即每產生一次系統tick則檢查一下當前正在執行的任務的環境判斷是否需要切換任務,即排程,如果需要,則觸發PendSV,通過在PendSV中斷呼叫vTaskSwitchContext()函式來實現任務的排程。

在FreeRTOS/source/portable/rvds/arm_cm4f/Port.c中,有SYSTICK的異常實現函式xPortSysTickHandler()。

void xPortSysTickHandler( void )

{

/* The SysTick runs at the lowest interrupt priority, so when this interrupt

executes all interrupts must be unmasked.  There is therefore no need to

save and then restore the interrupt mask value as its value is already

known. */

( void ) portSET_INTERRUPT_MASK_FROM_ISR();

{

/* Increment the RTOS tick. */

if( xTaskIncrementTick() != pdFALSE )

{

/* A context switch is required.  Context switching is performed in

the PendSV interrupt.  Pend the PendSV interrupt. */

portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT;

}

}

portCLEAR_INTERRUPT_MASK_FROM_ISR( 0 );

}

/*-----------------------------------------------------------*/

另外在同一個檔案還有PendSV具體實現函式xPortPendSVHandler()。通過註釋可以很好了解整個過程。這裡不具體展開咯。

__asm void xPortPendSVHandler( void )

{

extern uxCriticalNesting;

extern pxCurrentTCB;

extern vTaskSwitchContext;

PRESERVE8

mrs r0, psp

isb

/* Get the location of the current TCB. */

ldr  r3, =pxCurrentTCB

ldr  r2, [r3]

/* Is the task using the FPU context?  If so, push high vfp registers. */

tst r14, #0x10

it eq

vstmdbeq r0!, {s16-s31}

/* Save the core registers. */

stmdb r0!, {r4-r11, r14}

/* Save the new top of stack into the first member of the TCB. */

str r0, [r2]

stmdb sp!, {r3}

mov r0, #configMAX_SYSCALL_INTERRUPT_PRIORITY

msr basepri, r0

bl vTaskSwitchContext

mov r0, #0

msr basepri, r0

ldmia sp!, {r3}

/* The first item in pxCurrentTCB is the task top of stack. */

ldr r1, [r3]

ldr r0, [r1]

/* Pop the core registers. */

ldmia r0!, {r4-r11, r14}

/* Is the task using the FPU context?  If so, pop the high vfp registers

too. */

tst r14, #0x10

it eq

vldmiaeq r0!, {s16-s31}

msr psp, r0

isb

#ifdef WORKAROUND_PMU_CM001 /* XMC4000 specific errata */

#if WORKAROUND_PMU_CM001 == 1

push { r14 }

pop { pc }

nop

#endif

#endif

bx r14

nop

nop

}

注:本文部分內容來自網路整理而成。