当前位置:网站首页>任务及任务切换
任务及任务切换
2022-07-31 06:08:00 【南波儿万】
1.概述
这一块的内容有点晦涩,我也是是搞了很久才搞明白的。所以,从自己搞明白到我决定讲明白也经过了漫长的过程。基于以上的原因我踌躇了很久,这部分的内容讲清楚也是一件具有挑战性的事情。
2.任务
什么是任务?理解这个问题对理解bcos任务切换的原理至关重要。对于一个嵌入式操作系统的使用者理解任务的重点在于任务处理函数,因为任务处理函数是实现任务功能的主体。然而,他们往往忽略了在任务创建之初的他们定义的那个用于任务栈的数组。对于bcos来说,每一个任务都有一片独立的内存空间作为任务执行的栈,对于操作系统开发者而言任务的栈几乎是一个任务的全部。
讲到这里几乎所有读者都还是云里雾里的,对于大多数初学者或者已经工作了好几年的开发者而言,特别是那些更加专注于应用开发的嵌入式开发者,栈在他们的印象中一直都是一个比较模糊的概念。其实,我也是去年(2021年)才在工作中对这个概念逐渐清晰,然而我已经从事C语言编程开发工作3年了,如果从2015年开始算起的话已经有六年之久。过去我印象中的栈只是C语言程序中的一篇内存空间,只知道函数的局部变量会保存在这一片空间中,很长一段时间甚至傻傻的分不清此栈和数据结构课程中学习的栈的区别。既然讲清楚任务切换的原理无法绕开栈这个概念,那我就顺着我的理解过程逐步讲解。
3.从计算机的体系结构讲起
如果有的读者研究过《ARM Cortex-M3权威指南》这本书应该对栈有过深入的了解,我就是从研究这本书开始的。到此,决定从头讲起。话说ARM核内大约有十几二十个寄存器,他们都是32位的寄存器所以STM32是32位的单片机。这些寄存器分别是他们:
当然还有几个特殊功能寄存器这里图片中没有给出,这里先关注这15个寄存器就好了。我可以说CPU就是靠着这15个寄存器就实现了软件代码的加载和执行。
R0-R12:通用寄存器
R0-R12都是32位的通用寄存器,用于数据操作。下面有一段汇编代码就很好的体现了通用寄存器的作用。我们看到无论是操作数还是函数地址都要先加载到通用寄存器中才会进一步去使用和执行相应的指令。
; Reset handler routine
Reset_Handler PROC
EXPORT Reset_Handler [WEAK]
IMPORT __main
LDR R0, = SystemInit_ExtMemCtl ; initialize external memory controller
BLX R0
LDR R1, = __initial_sp ; restore original stack pointer
MSR MSP, R1
LDR R0, =__main
BX R0
ENDP
Banked R13: 两个堆栈指针
Cortex‐M3 拥有两个堆栈指针,然而它们是 banked,因此任一时刻只能使用其中的一个。
· 主堆栈指针(MSP):复位后缺省使用的堆栈指针,用于操作系统内核以及异常处理例程(包括中断服务例程),当然一般情况下不适用操作系统的情况下大多数的单片机系统都只使用这一个主堆栈指针就够了。
· 进程堆栈指针(PSP):由用户的应用程序代码使用。一般操作系统会使用PSP实现多线程或多任务的功能。bcos就是使用该堆栈指针实现任务的切换从而实现多任务的功能。
R14:连接寄存器
当CPU调用用一个子函数时(子程序),R14用于保存子函数的返回地址。下面的例子很好的展示了R14的作用:
我们看到上图的程序马上要执行一个叫“SystemInit()”的子程序,它的返回地址是0x08003DF8,当我再向下执行一步后R14应该会保存这个返回地址,下图是我执行一步后的截图:
有的读者可能发现和我们的预期有些不同,此时的R14的值是0x08003DF9不是我们期望的0x08003DF8,由于程序指令都是四字节对其的,这里R14的最后两位有其他特殊的用途,这个后续再做说明。
R15:程序计数寄存器
指向当前的程序地址。如果修改它的值,就能改变程序的执行流。
4.单片机上电是如何执行起来的?
我提出这个问题相比有些读者会笑话。但是,想必百分之六十的读者不甚清楚其中的细节,只有真正掌握了一点汇编的开发者才对这个问题有所了解,从始至终清楚的人已经是嵌入式领域的高级人才了。
通过前面几小节的内容我们已经清楚了CPU核内的几个寄存器的主要作用,下面我们随意打开一个STM32的工程,想一下我们的程序是从哪个文件里的程序开始执行的?想必很多人都是知道的,就是startup.s。有的读者看过一些分析startup.s的博客但终于还是云里雾里不得其解。我们会发现每个启动文件中都有这么一段程序:
; Vector Table Mapped to Address 0 at Reset
AREA RESET, DATA, READONLY
EXPORT __Vectors
EXPORT __Vectors_End
EXPORT __Vectors_Size
__Vectors DCD __initial_spTop ; Top of Stack
DCD Reset_Handler ; Reset Handler
DCD NMI_Handler ; NMI Handler
DCD HardFault_Handler ; Hard Fault Handler
DCD MemManage_Handler ; MPU Fault Handler
DCD BusFault_Handler ; Bus Fault Handler
DCD UsageFault_Handler ; Usage Fault Handler
DCD 0 ; Reserved
DCD 0 ; Reserved
DCD 0 ; Reserved
DCD 0 ; Reserved
DCD SVC_Handler ; SVCall Handler
DCD DebugMon_Handler ; Debug Monitor Handler
DCD 0 ; Reserved
DCD OS_CPU_PendSVHandler ; PendSV Handler
DCD SysTick_Handler ; SysTick Handler
; External Interrupts
DCD WWDG_IRQHandler ; Window Watchdog
DCD PVD_IRQHandler ; PVD through EXTI Line detect
DCD TAMPER_IRQHandler ; Tamper
DCD RTC_IRQHandler ; RTC
DCD FLASH_IRQHandler ; Flash
DCD RCC_IRQHandler ; RCC
DCD EXTI0_IRQHandler ; EXTI Line 0
DCD EXTI1_IRQHandler ; EXTI Line 1
DCD EXTI2_IRQHandler ; EXTI Line 2
DCD EXTI3_IRQHandler ; EXTI Line 3
DCD EXTI4_IRQHandler ; EXTI Line 4
DCD DMA1_Channel1_IRQHandler ; DMA1 Channel 1
DCD DMA1_Channel2_IRQHandler ; DMA1 Channel 2
DCD DMA1_Channel3_IRQHandler ; DMA1 Channel 3
DCD DMA1_Channel4_IRQHandler ; DMA1 Channel 4
DCD DMA1_Channel5_IRQHandler ; DMA1 Channel 5
DCD DMA1_Channel6_IRQHandler ; DMA1 Channel 6
DCD DMA1_Channel7_IRQHandler ; DMA1 Channel 7
DCD ADC1_2_IRQHandler ; ADC1 & ADC2
DCD USB_HP_CAN1_TX_IRQHandler ; USB High Priority or CAN1 TX
DCD USB_LP_CAN1_RX0_IRQHandler ; USB Low Priority or CAN1 RX0
DCD CAN1_RX1_IRQHandler ; CAN1 RX1
DCD CAN1_SCE_IRQHandler ; CAN1 SCE
DCD EXTI9_5_IRQHandler ; EXTI Line 9..5
DCD TIM1_BRK_IRQHandler ; TIM1 Break
DCD TIM1_UP_IRQHandler ; TIM1 Update
DCD TIM1_TRG_COM_IRQHandler ; TIM1 Trigger and Commutation
DCD TIM1_CC_IRQHandler ; TIM1 Capture Compare
DCD TIM2_IRQHandler ; TIM2
DCD TIM3_IRQHandler ; TIM3
DCD TIM4_IRQHandler ; TIM4
DCD I2C1_EV_IRQHandler ; I2C1 Event
DCD I2C1_ER_IRQHandler ; I2C1 Error
DCD I2C2_EV_IRQHandler ; I2C2 Event
DCD I2C2_ER_IRQHandler ; I2C2 Error
DCD SPI1_IRQHandler ; SPI1
DCD SPI2_IRQHandler ; SPI2
DCD USART1_IRQHandler ; USART1
DCD USART2_IRQHandler ; USART2
DCD USART3_IRQHandler ; USART3
DCD EXTI15_10_IRQHandler ; EXTI Line 15..10
DCD RTCAlarm_IRQHandler ; RTC Alarm through EXTI Line
DCD USBWakeUp_IRQHandler ; USB Wakeup from suspend
DCD TIM8_BRK_IRQHandler ; TIM8 Break
DCD TIM8_UP_IRQHandler ; TIM8 Update
DCD TIM8_TRG_COM_IRQHandler ; TIM8 Trigger and Commutation
DCD TIM8_CC_IRQHandler ; TIM8 Capture Compare
DCD ADC3_IRQHandler ; ADC3
DCD FSMC_IRQHandler ; FSMC
DCD SDIO_IRQHandler ; SDIO
DCD TIM5_IRQHandler ; TIM5
DCD SPI3_IRQHandler ; SPI3
DCD UART4_IRQHandler ; UART4
DCD UART5_IRQHandler ; UART5
DCD TIM6_IRQHandler ; TIM6
DCD TIM7_IRQHandler ; TIM7
DCD DMA2_Channel1_IRQHandler ; DMA2 Channel1
DCD DMA2_Channel2_IRQHandler ; DMA2 Channel2
DCD DMA2_Channel3_IRQHandler ; DMA2 Channel3
DCD DMA2_Channel4_5_IRQHandler ; DMA2 Channel4 & Channel5
__Vectors_End
此时,有些读者可能就要说了,这不就是中断向量表吗?有什么神奇的?我已经很熟悉了。其实不然,我们更应该关注的是这段程序开头的部分,我专门对它进行了截图,并将重点圈画出来。
任我们如何观察,我们都会发现每一STM32的程序都只有一个程序段标有RESET,这表明CPU复位后从该段开始执行。另外,我们又发现中断向量表的第二个赋值的是Reset_Handler,复位后CPU会固定从这个地方找到程序的入口函数然后开始执行。知道了这个原理,下面这段程序想必应该可以执行起来, 感兴趣的读者可以尝试执行一下:
; Vector Table Mapped to Address 0 at Reset
AREA RESET, DATA, READONLY
EXPORT __Vectors
EXPORT __Vectors_End
__Vectors DCD 0
DCD test
__Vectors_End
AREA |.text|, CODE, READONLY
test PROC
EXPORT test [WEAK]
MOV R0, #1
MOV R1, #3
ADD R2, R1, R0
B test
ENDP
下面我们继续研究启动文件,我们看看Reset_Handler都做了那些事情:
; Reset handler routine
Reset_Handler PROC
EXPORT Reset_Handler [WEAK]
IMPORT __main
LDR R0, = SystemInit_ExtMemCtl ; initialize external memory controller
BLX R0
LDR R1, = __initial_sp ; restore original stack pointer
MSR MSP, R1
LDR R0, =__main
BX R0
ENDP
我们发现在Reset_Handler里对系统进行了初始化,然后对MSP指针进行了初始化,最后调用了__main并跳转到main函数。这里多说一句,__main的主要作用就是对程序的全局变量进行初始化,后续动态执行程序的内容会重点介绍。
5.栈
栈作为一个计算机术语其有两方面的含义,1.一种先进后出的数据结构;2.一片用来保存cpu调用程序段时的寄存器中的数据的内存空间,其在存取的过程中遵循先进后出的原则。
栈就两种操作:
- push:压栈,向栈内加入数据
- pop :出栈
注:有的人喜欢将栈与堆连起来叫做程序堆栈,其实在C语言中栈是栈,堆是堆,它们本身就是两个东西千万不要混为一谈。
- 相同点:都是一片内存区域;
- 不同点:
- 由编译器分配,存放函数的参数值,局部变量,寄存器组(不同的单片机/处理器各有不同)、函数调用参数传递、中断异常产生时须保存处理器状态的寄存器值等;
- 由程序员分配释放,对于C而言,malloc、realloc/free进行分配/释放;
到此,我们大概了解了栈在cpu执行过程中的作用。前面讲过,“栈几乎是操作系统中任务的全部”到此读者可能还没有一个较为深入的理解,下面给出bcos系统中PendSV中断处理函数的源码,我们详细的分析一下就可以理解这句话的含义了。为了便于读者理解,我将我的分析注释在代码的上面,这种方式可以方便读者对照源码做出理解。
/* * 这个结构体定义了bcos的任务控制块,将栈指针定义成结构体的第一个变量是为了在任务切换时任务控制块结构体变量的地址就是栈指针的地址, * 这样更方便汇编堆栈指针的赋值操作 */
typedef struct bcos_tcb_s {
/* Pointer to current top of stack */
BC_OS_STK *OSTCBStkPtr;
/* 链表头 */
struct list_head list;
/* 优先级 */
uint8_t priority;
/* 延时时间戳 */
BC_OS_TICK delay_stamp;
/* 任务栈顶指针 */
BC_OS_STK *OSTCBStkTopPtr;
} bcos_tcb_t;
OS_CPU_PendSVHandler\
PROC
EXPORT OS_CPU_PendSVHandler ; 对PendSV中断处理函数进行申明
EXTERN OSTCBCur ; 对当前任务控制块变量进行申明
EXTERN OSTCBHighRdy ; 对当前最高优先级任务控制块变量进行申明
CPSID I
MRS R0, PSP ; 获取PSP栈指针寄存器中的值保存到R0
CBZ R0, OS_CPU_PendSVHandler_nosave ; 如果R0中的值为0,则跳转到OS_CPU_PendSVHandler_nosave执行,这时上电初始化时第一次做任务调度时
; 需要做一个特殊的处理
SUBS R0, R0, #0x20 ; 在触发PendSV中断时,CPU会将R0 - R3,LR,SP,PC等寄存器压入栈中,但R4-R11需要程序员手动压栈 (PUSH)
STM R0,{R4-R11} ; 至于为什么要将栈指针值减去0x20,这个后面解释
LDR R1, =OSTCBCur
LDR R1, [R1]
STR R0, [R1]
OS_CPU_PendSVHandler_nosave
LDR R0, =OSTCBCur ; 下面的四行代码就相当于C语言的OSTCBCur = OSTCBHighRdy;操作,从而实现任务的切换
LDR R1, =OSTCBHighRdy
LDR R2, [R1]
STR R2, [R0]
LDR R0, [R2] ; R0 保存的是新任务的SP指针; SP = OSTCBHighRdy->OSTCBStkPtr;
LDM R0, {R4-R11} ; 将 r4-11 进行手动出栈(POP)
ADDS R0, R0, #0x20
MSR PSP, R0 ; 给 PSP赋值新任务的SP,到这里真正完成任务的切换
ORR LR, LR, #0x04 ; Ensure exception return uses process stack
CPSIE I
BX LR ; Exception return will restore remaining context
ENDP
从上面的代码可以看出,bcos任务的切换其实就是对CPU PSP的操作,通过切换PSP指向的栈空间实现任务的切换。每个任务的栈其实保存了各自任务的函数和代码调用的状态。
上面的五张图片清晰的展示了任务切换过程中对任务栈的操作过程及任务栈状态的变化过程,读者可以结合代码和注释并对照这五张图片进行理解bcos操作系统任务切换的完整过程。
6.总结
到此,任务及任务切换的原理就讲的差不多了,现在我们来总结一下。其实,对于用户来讲可能更加关注的是如何利用操作系统来实现其复杂的业务逻辑,操作系统本身只是为更好的实现多任务的功能服务的。但是,开发者了解操作系统原理本身会对功能的实现提供帮助。
其实本节内容其实解释了以下几个问题:
- 什么是任务?任务的本质是什么?
- 单片机是如何从复位运行起来程序的?
- 什么是栈?栈在操作系统或程序中扮演的是什么角色?
- 操作系统如何实现任务的切换?任务的切换其实是栈指针的切换。
读者如果能够对以上问题有一个比较深刻的认识,那么将对嵌入式操作系统的原理的认识将有一个质的改变。
边栏推荐
- 从入门到一位合格的爬虫师,这几点很重要
- 文件 - 07 删除文件: 根据fileIds批量删除文件及文件信息
- 【微服务】 微服务学习笔记二:Eureka注册中心的介绍及搭建
- 2022.07.18_每日一题
- codec2 BlockPool:unreadable libraries
- Chapter 17: go back to find the entrance to the specified traverse, "ma bu" or horse stance just look greedy, no back to search traversal, "ma bu" or horse stance just look recursive search NXM board
- 【科普向】5G核心网架构和关键技术
- 文件 - 02 上传文件:上传临时文件到服务器
- 【云原生】-Docker安装部署分布式数据库 OceanBase
- 【第四章】详解Feign的实现原理
猜你喜欢
DirectExchange交换机简单入门demo
把 VS Code 当游戏机
简单谈谈Feign
MySql的安装配置超详细教程与简单的建库建表方法
Some derivation formulas for machine learning backpropagation
【Go语言入门】一文搞懂Go语言的最新依赖管理:go mod的使用
嵌入式系统驱动初级【2】——内核模块下_参数和依赖
Analysis of pseudo-classes and pseudo-elements
postgresql源码学习(34)—— 事务日志⑩ - 全页写机制
postgresql源码学习(33)—— 事务日志⑨ - 从insert记录看日志写入整体流程
随机推荐
postgresql源码学习(33)—— 事务日志⑨ - 从insert记录看日志写入整体流程
【Go】Go 语言切片(Slice)
QFileInfo常规方法
[PSQL] 复杂查询
CHI论文阅读(1)EmoGlass: an End-to-End AI-Enabled Wearable Platform for Enhancing Self-Awareness of Emoti
2022.07.18_每日一题
文件 - 04 下载文件: 根据文件下载链接下载文件
Automatic translation software - batch batch automatic translation software recommendation
【Star项目】小帽飞机大战(七)
【解决】npm ERR A complete log of this run can be found in npm ERR
【愚公系列】2022年07月 Go教学课程 022-Go容器之字典
How to choose a suitable UI component library in uni-app
深度学习通信领域相关经典论文、数据集整理分享
360 push-360 push tool-360 batch push tool
线程中断方法
4-1-7 二叉树及其遍历 家谱处理 (30 分)
Web浏览器工作流程解析
强化学习科研知识必备(数据库、期刊、会议、牛人)
2022.07.15_每日一题
什么是半波整流器?半波整流器的使用方法