浅谈RTOS中的多任务切换(基于UC/OS iii)

浅谈RTOS中的多任务切换(基于UC/OS iii)

一. 简介

  RTOS中的多任务切换是操作系统与裸机编程的一个非常大的区别,一般逻辑变成运行在一个循环内,裸机编程很难实现两个事件的并行(这里的并行指的是宏观的并行),但是在操作系统中我们可以在逻辑上面实现两个任务的并行。每一个任务在操作系统层面可以看成一个进程,大学有修过操作系统的朋友肯定不陌生,其中一个非常重要的概念——PCB进程控制块,在单片机RTOS编程中我们称之为TCB即任务控制块,记录了一个任务的很多关键信息比如该任务函数的函数地址,任务函数的长度,任务的存活周期,任务函数的形参等。
  笔者的实验与代码均基于野火的嵌入式系列,不做广告,提出感谢。毕竟在互联网泡沫巨大的今天,愿意踏踏实实写一本几十万字详细介绍操作系统底层的书也是很不容易,笔者虽然即将进入研究生进行cv方面的学习,但还是感觉当今国内应该多一些进行应用层以下的计算机底层研究人员,毕竟如果只以资本导向为研究方向的话,未来国内的人工智能可能会达到世界顶端,但是计算机底层的研究会严重滞后,闲聊至此。在本篇文章为了方便观察结果,设计了两个任务Task1,Task2.如下代码所示:

/*main.c*/
void Task1( void *p_arg )
{
	for( ;; )
	{
		flag1 = 1;
		delay( 100 );		
		flag1 = 0;
		delay( 100 );
		
		/* 任务切换,这里是手动切换 */		
		OSSched();
	}
}

/* 任务2 */
void Task2( void *p_arg )
{
	for( ;; )
	{
		flag2 = 1;
		delay( 100 );		
		flag2 = 0;
		delay( 100 );
		
		/* 任务切换,这里是手动切换 */
		OSSched();
	}
}

其中flag1和flag2都在一个小延时函数后取反,OSSched函数的作用我们后面再说,本文后序内容便是研究如何做到Task1和Task2成为逻辑上面对等的两个任务并做到宏观上面的并行。

二.主要变量

1.全局变量

/*os.h*/
OS_EXT    OS_TCB         *OSTCBCurPtr;
OS_EXT    OS_TCB         *OSTCBHighRdyPtr;
OS_EXT    OS_RDY_LIST    OSRdyList[OS_CFG_PRIO_MAX];
OS_EXT    OS_STATE       OSRunning;

这里说明一下,OS_EXT是自定义的外部声明,可以直接看作extern。OS_TCB,OS_RDY_LIST,OS_STATE是三个自定义的数据类型,其中OS_TCB表示一个32位的无符号整数,表示stm32中的一个地址,OS_RDY_LIST是一个结构体,表示就绪队列,如下图:

/*os.h*/
typedef  struct  os_rdy_list         OS_RDY_LIST;
struct os_rdy_list
{
	OS_TCB        *HeadPtr;
	OS_TCB        *TailPtr;
};

OS_STATE是一个8位的字符。
  OSTCBCurPtr是指向正在运行的任务的TCB的指针,OSTCBHighRdyPtr是指向当前优先级最高,即即将运行的任务的指针,OSRdyList是任务的就绪队列(有操作系统知识基础的朋友一定不陌生),OSRunning是操作当前的运行状态。

2.局部变量

  

/*main.c*/
#define  TASK1_STK_SIZE       20
#define  TASK2_STK_SIZE       20

static   CPU_STK   Task1Stk[TASK1_STK_SIZE];
static   CPU_STK   Task2Stk[TASK2_STK_SIZE];

static   OS_TCB    Task1TCB;
static   OS_TCB    Task2TCB;

void     Task1( void *p_arg );
void     Task2( void *p_arg );

任务的局部变量声明在main.c文件中其中TaskxStk是一个TASK1_STK_SIZE*32b大小的任务堆栈,Task1TCBx是指向任务栈顶的指针。这里将Taskx函数以指针变量的形式声明出来是因为在进行上下文切换时要将该地址传给cpu的pc(程序计数器)中。

三.主要函数

1.C语言函数

  由于篇幅的限制,函数具体的实现过程在这里不加赘述,主要介绍几个函数的功能以及函数的参数。
(1)

void OSInit (OS_ERR *p_err)

用于初始化操作系统的全局变量,参数为程序错误代码的枚举类型
(2)

 void OSTaskCreate (OS_TCB        *p_tcb, 
                   OS_TASK_PTR   p_task, 
                   void          *p_arg,
                   CPU_STK       *p_stk_base,
                   CPU_STK_SIZE  stk_size,
                   OS_ERR        *p_err)

用于创建操作系统的任务,其中形参从上到下一次是:任务tcb栈顶指针,任务函数指针,任务参数,任务栈起始地址,任务栈大小,任务错误代码

(3)

CPU_STK *OSTaskStkInit (OS_TASK_PTR  p_task,
                      void         *p_arg,
                      CPU_STK      *p_stk_base,
                      CPU_STK_SIZE stk_size)

用于初始化任务的tcb,参数依次是:任务函数指针,任务函数参数,任务栈起始地址,任务栈大小

(4)

void OSStart (OS_ERR *p_err)

用于启动操作系统,参数只有任务错误代码

2.ARM汇编函数

  笔者对于汇编语言的了解仅限于在计算机组成原理中学到的一些最基本的x86汇编指令,但还是非常勉强的看完了野火给的上下文切换的函数(-_ -!)不过自己实践时如果对于arm的汇编不太了解不妨跳过这一部分,只需要知道每一个汇编函数的意义基本就可以了,因为一个人想要完全实现操作系统不仅工程量巨大,所涉及的知识也涵盖硬件组成+汇编语言+c语言+操作系统知识+。。。。。,所以这部分个人感觉当前学习的意义并不大,因为学习成本太高,很容易在繁杂的计算机底层知识中迷茫,这里给出每一个函数的实现以及作用,完整的代码会在文末给出。

/*os_cpu_a.s*/
;********************************************************************************************************
;                                          开始第一次上下文切换
; 1、配置PendSV异常的优先级为最低
; 2、在开始第一次上下文切换之前,设置psp=0
; 3、触发PendSV异常,开始上下文切换
;********************************************************************************************************
OSStartHighRdy
	LDR		R0, = NVIC_SYSPRI14              ; 设置  PendSV 异常优先级为最低
	LDR     R1, = NVIC_PENDSV_PRI
	STRB    R1, [R0]
	
	MOVS    R0, #0                           ; 设置psp的值为0,开始第一次上下文切换
	MSR     PSP, R0
	
	LDR     R0, =NVIC_INT_CTRL               ; 触发PendSV异常
	LDR     R1, =NVIC_PENDSVSET
	STR     R1, [R0]
	
	CPSIE   I                                 ; 开中断
	
OSStartHang
	B       OSStartHang                       ; 程序应永远不会运行到这里	

;********************************************************************************************************
;                                          PendSVHandler异常
;********************************************************************************************************
PendSV_Handler
; 任务的保存,即把CPU寄存器的值存储到任务的堆栈中	
	CPSID   I                                 ; 关中断,NMI和HardFault除外,防止上下文切换被中断	
	MRS     R0, PSP                           ; 将psp的值加载到R0
	CBZ     R0, OS_CPU_PendSVHandler_nosave   ; 判断R0,如果值为0则跳转到OS_CPU_PendSVHandler_nosave
	                                          ; 进行第一次任务切换的时候,R0肯定为0
	
	; 在进入PendSV异常的时候,当前CPU的xPSR,PC(任务入口地址),R14,R12,R3,R2,R1,R0会自动存储到当前任务堆栈,同时递减PSP的值
	STMDB   R0!, {R4-R11}                     ; 手动存储CPU寄存器R4-R11的值到当前任务的堆栈
	
	LDR     R1, = OSTCBCurPtr                 ; 加载 OSTCBCurPtr 指针的地址到R1,这里LDR属于伪指令
	LDR     R1, [R1]                          ; 加载 OSTCBCurPtr 指针到R1,这里LDR属于ARM指令
	STR     R0, [R1]                          ; 存储R0的值到	OSTCBCurPtr->OSTCBStkPtr,这个时候R0存的是任务空闲栈的栈顶

; 任务的切换,即把下一个要运行的任务的堆栈内容加载到CPU寄存器中
OS_CPU_PendSVHandler_nosave  
	; OSTCBCurPtr = OSTCBHighRdyPtr;
	LDR     R0, = OSTCBCurPtr                 ; 加载 OSTCBCurPtr 指针的地址到R0,这里LDR属于伪指令
	LDR     R1, = OSTCBHighRdyPtr             ; 加载 OSTCBHighRdyPtr 指针的地址到R1,这里LDR属于伪指令
	LDR     R2, [R1]                          ; 加载 OSTCBHighRdyPtr 指针到R2,这里LDR属于ARM指令
	STR     R2, [R0]                          ; 存储 OSTCBHighRdyPtr 到 OSTCBCurPtr
	
	LDR     R0, [R2]                          ; 加载 OSTCBHighRdyPtr 到 R0
	LDMIA   R0!, {R4-R11}                     ; 加载需要手动保存的信息到CPU寄存器R4-R11
	
	MSR     PSP, R0                           ; 更新PSP的值,这个时候PSP指向下一个要执行的任务的堆栈的栈底(这个栈底已经加上刚刚手动加载到CPU寄存器R4-R11的偏移)
	ORR     LR, LR, #0x04                     ; 确保异常返回使用的堆栈指针是PSP,即LR寄存器的位2要为1
	CPSIE   I                                 ; 开中断
	BX      LR                                ; 异常返回,这个时候任务堆栈中的剩下内容将会自动加载到xPSR,PC(任务入口地址),R14,R12,R3,R2,R1,R0(任务的形参)
	                                          ; 同时PSP的值也将更新,即指向任务堆栈的栈顶。在STM32中,堆栈是由高地址向低地址生长的。
	
	NOP                                       ; 为了汇编指令对齐,不然会有警告
	
	
	END                                       ; 汇编文件结束

这里讲一下上述的三个函数的具体含义,首先是OSStartHighRdy函数,该函数在执行时并不进行上下文切换,而是触发一次中断寄存器PendSV,在触发中断寄存器后,程序会自动跳到下面的中断服务函数PendSV_Handler,此函数是用来TCB上文的,通俗的说就是将在cpu寄存器中的任务控制块写回到RAM的任务栈中,很容易看出R0-R14寄存器是需要手动保存的,其余寄存器自动保存,这里给出任务堆栈的结构:
在这里插入图片描述
有两个地方需要我们特别注意一个是R15寄存器,它存储了一个指向任务函数的指针,在寄存器中每次任务开始前存储在程序计数器即PC中,R0寄存器存储了任务函数所传参数的地址,为了方便观察结果这里将参数设置成了void类型,在正常操作系统编写时由于不同的任务函数的参数不相同,一般设置为一个指向结构体的指针。
  OS_CPU_PendSVHandler_nosave函数的作用就是将即将执行的任务的TCB写入到cpu的寄存器中以完成上下文的切换。

四.总流程

  在程序执行时的流程比较简单,如图所示:
在这里插入图片描述
在系统初始化,全局变量初始化,任务堆栈初始化后操作系统便进入了运行状态,首先会产生一次PendSV中断,系统检测到当前任务堆栈指针尚处于初始化状态后会自动将第一次要执行的任务写入,在上文中,每一次任务执行到末尾时会调用OSSched()函数,其目便是让操作系统产生一次PendSV中断,然后执行终端服务函数中的上下文其切换,在完成上下文切换后,此时cpu的PC寄存器已经是另一个任务函数的首地址,程序会进入到另一个任务中运行,在另一个任务执行后系统又会切换过来,并循环这个过程。当然这个过程知识逻辑上面的并行,用裸机编程的NVIC也可以实现这个效果,但是在引入时间片等其他元素后,RTOS将会大大简化编程难度,该试验仅演示多任务切换的过程,笔者说是并行也不准确,还需要后续的优化。

五.结果

在keil5中使用调试功能,观察flag1和flag2的运行情况如图:
在这里插入图片描述
虽然flag1和flag2并没有在同一时间进行切换,但是仍然可将Task1和Task2视作宏观的并行,在一些更为复杂的操作系统(如windows,Linux)中,上下文切换会更加的细化,每日一次切换后的pc值并不是下一个要执行任务的首地址,而是该任务执行的最近一条指令的下一条指令,这就可以让两个任务的执行事件差缩小到一个时间片内,这样这样可以很大程度上的提升多个任务在执行时的并行性,但频繁进行上下文的切换也会让操作系统的开销更大,RTOS的作用其中之一便是平衡并行性和上下文切换之间的矛盾,找到一个适中的办法。

六.后记(含代码链接)

  上文中的进程切换可作为一个参考,由于代码比较多,所以看起来可能感觉很乱,有真正想要了解其过程的朋友可以下载下面的源码

链接:https://pan.baidu.com/s/1xTmXt4h-r7-r0jJLMTUkKA
提取码:p4cw

最后说一两句题外话吧,笔者未来的学习方向和从事工作可能都和操作系统没有生么太大关系,但是在科技竞争愈演愈烈的今天,想要打破发达国家对国内的封锁,我们在很多领域都需要有一批人才站出来,手机soc的研发,FPGA芯片的研发,操作系统的研发这些其实都是国内目前的短板,互联网泡沫终有过去的那天,可能若干年后中国在建立在应用层之上的人工智能领域有着百万千万的人才储备,但计算机基础领域在北上广深的核心人才都很难凑成三位数。我们都不希望看到那一天,所以在此向每一个从事计算机基础研究工作的学者,工程师以及学生致以最诚挚的敬意!

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

贪睡熊猫

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值