https://m.toutiao.com/is/NrmPKcq/?=操作系统任务调度原理 1.前言操作系统可以为我们运行丰富的应用程序,可以同时满足我们的各种使用需要。操作系统之所以能同时完成我们各种需求,是因为操作系统能并发执行多个用户的应用程序。事实上除了多核处理器系统中是真正的多任务并行之外,其它情况下的并发本质是:宏观并行,微观串行。 操作系统运行多个应用程序时,给用户的宏观体验是多个应用程序同时运行。
2.计算机模型
虽然计算机的硬件实现各不相同,但是它们基本使用了同一套通用的硬件框架设计,计算机的经典硬件模型如下:
嵌入式操作系统通常情况下运行在嵌入式计算机上,为了方便学习我们将嵌入式计算机简化成如下硬件模型(哈佛结构):
2.1程序存储器程序存储器通常可使用ROM,FLASH,RAM等存储介质,在嵌入式计算机通常使用可以片内执行(XIP)的程序存储器。支持片内执行的存储介质有ROM,NOR Flash,SRAM。在低端嵌入式计算机中(如单片机),程序存储器就通常使用NOR Flash。
2.2处理器处理器是计算机的核心部件用于完成运算工作,其内部包括寄存器堆,运算单元,控制单元和总线。处理器的简化模型如下:
运算单元 控制单元 总线 处理器接收程序存储器的数据和指令,完成运算,数据和状态缓存,同时可以输出数据到数据存储器。处理器内部的数据为可变类型。 2.3数据存储器处理器运算后的结果有部分暂存在寄存器中,其它运算结果都存放在数据存储器中。
数据存储器的作用是保存处理器运算结果,其数据类型为可变。 3.嵌入式计算器运行过程
操作区内有厨具和调味品可以食材进行直接处理,有少量的盘子可以存放加工后的食材,有计数器可以记录菜谱序号。这对应处理器读取指令和数据,对数据进行直接运算,得到运算结果缓存在寄存器中,程序寄存器记录了下一步程序地址。 暂存区有大量的盘子,这些盘子分为两类:A类盘子用于存放半成品(如切好的食材,焯水过的食材),B类盘子用于存放做好的菜。这对应数据存储器中的栈区和静态区,栈区用于暂存局部变量,静态区用于存放静态变量。将操作区中半成品食材放到暂存区的A盘子中称为“入栈”,从暂存区的A盘子半成品食材拿回操作区称为“出栈”。 嵌入式计算器运行过程中,处理器中的寄存器堆暂缓了运算结果和处理器运行状态。数据存储器保存了处理器运行结果,其中栈区缓存了处理器运算的中间结果,静态区永久性(程序运行的整个过程)保存了运算结果。其中栈区的数据是动态变化的不仅要关注数据内容还需要关注数据的顺序,栈区的数据不仅反映了运算结果还反映了运算顺序。
4.任务切换原理任务通常以一个无限循环的函数形式存在,假设现在任务中有5条语句,任务代码如下: int task1(void) { while(1) { /* 语句1 */ /* 语句2 */ /* 语句3 */ /* 语句4 */ /* 语句5 */ } } 任务在循环中会依次循环执行语句1->语句2->语句3->语句4->语句5->语句1。假设不考虑静态区,堆区和中断程序,程序每执行一条语句后的嵌入式计算机的总状态如下(每一种颜色代表一种状态,颜色变化说明状态变化):
任务循环执行,处理器和数据存储器的状态也循环周期变化。计算机的总状态只会有P1,P2,P3,P4,P5这五种情况。 假设现在将计算机的总状态设置成P3状态,接下来计算机将会按照P3->P4->P5->P1->P2->P3 的顺序运行,运行图如下:
用这个原理,暂停任务A并记录下计算机运行任务A的总状态,然后装载记录好的计算机运行任务B的总状态,这样就实现了将任务切换。 任务切换步骤 任务切换有三个步骤: 前文提到程序存储器内部的数据不变,因此保存当前任务总状态时不需要对程序存储器进行额外操作。 保存任务的总状态就需要保存处理器寄存器堆的数据和数据存储器的数据,恢复任务的总状态就需要装载处理器寄存器堆的数据和数据存储器的数据。 所以我们只用保存和加载处理器寄存器堆和数据存储器这两个区域的数据就可以了。 处理器数据保存和加载 相信大家对中断程序有一定的了解,当进入中断程序时,处理器会自动将部分寄存器保存到栈区,退出中断程序时将栈区的数据加载到部分寄存器中。 将处理器寄存器堆的数值保存到栈区就可以实现处理器寄存器数据保存,将栈区的数据加载到处理器寄存器堆中就可以实现处理器寄存器数据恢复。所以我们使用这种方法保存和恢复处理器内部寄存器数据。 数据存储器数据保存和加载 栈区数据保存和加载
我们如果在静态区定义一个连续区间(通常情况下创建一个静态数组),并让栈指针寄存器SP执行这个静态区间,这样就实现了“独立”的栈区间。“独立”的栈区间可以不受其它任务的污染,因此使用“独立”栈区间可以不用再实现栈区的保存和恢复。
5.任务切换实现中断程序流程图如下: 发生中断时处理器自动完成“入栈工作”,此时处理器自动将程序指针寄存器,状态寄存器等寄存器依次入栈到栈区,然后更新栈指针寄存器数值,最后执行中断向量位置处的程序。 中断程序完成时处理器使用中断返回指令返回用户程序。中断返回时,处理器将栈区的数据加载到程序指针寄存器,状态寄存器等寄存器,同时更新栈指针寄存器数值。由于加载了程序指针寄存器PC,程序将返回被中断打断的用户程序处继续执行。 过程1:中断进入时处理器自动保存部分寄存器数据到栈区,中断返回时处理器自动从栈区加载部分寄存器数据。 因此只需要在中断进入后,再用代码实现保存其它普通寄存器,保存栈指针寄存器SP到独立任务栈指针中,这样就实现了任务总状态保存。 6.enuo操作系统任务切换实现嵌入式操作系统enuo目前使用的硬件为STM32F4系列MCU(内核为cortex-M4),cortex-M4内核有一个PendSV 异常(可挂起的系统调用),其异常编号为14并且具有可编程的优先级。当用户软件将PendSV设置成挂起时,程序将进入PendSV异常(中断),用户可以根据需要使用软件指令触发PendSV异常,因此可以利用PendSV异常(中断)实现任务切换。
因为涉及到寄存器的操作,所以我们使用汇编语言实现任务保存工作,代码如下:
选择任务 /* 获取next_task 栈指针地址 */LDR R4,=__cpp(&next_task) LDR R4,[R4]/* 读取next_task中的stack_point指针 */LDR R0,[R4] /* 更新current_task */LDR R3, =__cpp(¤t_task) STR R4,[R3] 恢复任务
任务切换全流程
/********************************************************************************************************** @名称 : PendSV_Handler* @描述 : 实现寄存器上下文切换**********************************************************************************************************/__asm void PendSV_Handler(void){ /* 读取当前进程栈指针数值 */ MRS R0,PSP //isb /* 保存R4-R11八个寄存器的值到当前任务栈中 同时将回写的地址写入R0 */ STMDB R0!,{R4-R11} /* 读取current_task 栈指针地址 */ LDR R3, =__cpp(¤t_task) LDR R3, [R3] /* 将当前进程PSP指针值 写入 相应的 current_task */ STR R0,[R3] /* 获取next_task 栈指针地址 */ LDR R4,=__cpp(&next_task) LDR R4,[R4] /* 读取next_task中的stack_point指针 */ LDR R0,[R4] /* 更新current_task */ LDR R3, =__cpp(¤t_task) STR R4,[R3] /* 出栈 R4-R11八个寄存器 */ LDMIA R0!,{R4-R11} /* 设置PSP指针 */ MSR PSP,R0 /* 中断返回 */ BX LR /* 对齐 */ ALIGN 4} 总结:本文讲解了任务切换的原理和步骤,最后展示了enuo嵌入式操作系统使用PendSV中断函数实现任务切换。 希望获取源码的朋友们在评论区里留言。 未完待续… |
|
来自: 山峰云绕 > 《计算机科学(体系结构原理等)》