在一个操作系统的实现中,实现上下文的切换有两种情况:
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 } 注:本文部分内容来自网络整理而成。 |