0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
会员中心
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

鸿蒙内核源码分析之线程环境下的任务切换

鸿蒙系统HarmonyOS 来源:my.oschina 作者: 鸿蒙内核源码分析 2021-04-25 15:22 次阅读

鸿蒙的内核线程就是任务,系列篇中说的任务和线程当一个东西去理解.

一般二种场景下需要切换任务上下文:

在线程环境下,从当前线程切换到目标线程,这种方式也称为软切换,能由软件控制的自主式切换.哪些情况下会出现软切换呢?

运行的线程申请某种资源(比如各种锁,读/写消息队列)失败时,需要主动释放CPU的控制权,将自己挂入等待队列,调度算法重新调度新任务运行.

每隔10ms就执行一次的OsTickHandler节拍处理函数,检测到任务的时间片用完了,就发起任务的重新调度,切换到新任务运行.

不管是内核态的任务还是用户态的任务,于切换而言是统一处理,一视同仁的,因为切换是需要换栈运行,寄存器有限,需要频繁的复用,这就需要将当前寄存器值先保存到任务自己的栈中,以便别人用完了轮到自己再用时恢复寄存器当时的值,确保老任务还能继续跑下去. 而保存寄存器顺序的结构体叫:任务上下文(TaskContext).

在中断环境下,从当前线程切换到目标线程,这种方式也称为硬切换.不由软件控制的被动式切换.哪些情况下会出现硬切换呢?

由硬件产生的中断,比如 鼠标,键盘外部设备每次点击和敲打,屏幕的触摸,USB的插拔等等这些都是硬中断.同样的需要切换栈运行,需要复用寄存器,但与软切换不一样的是,硬切换会切换工作模式(中断模式).所以会更复杂点,但道理还是一样要保存和恢复切换现场寄存器的值, 而保存寄存器顺序的结构体叫:任务中断上下文(TaskIrqContext).

本篇说清楚在线程环境下切换(软切换)的实现过程.中断切换(硬切换)实现过程将在鸿蒙内核源码分析(总目录)中断切换篇中详细说明.

本篇具体说清楚以下几个问题:

任务上下文(TaskContext)怎么保存的?

代码的实现细节是怎样的?

如何保证切换不会发生错误,指令不会丢失?

在鸿蒙内核源码分析(总目录)系列篇中已经说清楚了调度机制,线程概念,寄存器,CPU,工作模式,这些是读懂本篇的基础,建议先前往翻看,不然理解本篇会费劲.本篇代码量较多,涉及C和汇编代码,代码都添加了注释,试图把任务的整个切换过程逐行逐行说清楚.

前置条件

一个任务要跑起来,需要两个必不可少的硬性条件:

1.从代码段哪个位置取指令? 也就是入口地址,main函数是应用程序的入口地址, run()是new一个线程执行的入口地址.高级语言是这么叫,但到了汇编层的叫法就是PC寄存器.给PC寄存器喂妹值,指令就从哪里取.

2.运行的场地(栈空间)在哪里? ARM有7种工作模式,到了进程层面只需要考虑内核模式和用户模式两种,对应到任务会有内核态栈空间和用户态栈空间.内核模式的任务只有内核态的栈空间,用户模式任务二者都有.栈空间是在初始化一个任务时就分配指定好的.以下是两种栈空间的初始化过程.为了精练省去了部分代码,留下了核心部分.

//任务控制块中对两个栈空间的描述
typedef struct {
    VOID            *stackPointer;      /**< Task stack pointer */  //内核态栈指针,SP位置,切换任务时先保存上下文并指向TaskContext位置.
    UINT32          stackSize;          /**< Task stack size */     //内核态栈大小
    UINTPTR         topOfStack;         /**< Task stack top */      //内核态栈顶 bottom = top + size
    // ....
    UINTPTR         userArea;       //使用区域,由运行时划定,根据运行态不同而不同
    UINTPTR         userMapBase;    //用户态下的栈底位置
    UINT32          userMapSize;    /**< user thread stack size ,real size : userMapSize + USER_STACK_MIN_SIZE */
} LosTaskCB;    
//内核态运行栈初始化
LITE_OS_SEC_TEXT_INIT VOID *OsTaskStackInit(UINT32 taskID, UINT32 stackSize, VOID *topStack, BOOL initFlag)
{
    UINT32 index = 1;
    TaskContext *taskContext = NULL;
    taskContext = (TaskContext *)(((UINTPTR)topStack + stackSize) - sizeof(TaskContext));//上下文存放在栈的底部
    /* initialize the task context */ //初始化任务上下文
    taskContext->PC = (UINTPTR)OsTaskEntry;//程序计数器,CPU首次执行task时跑的第一条指令位置
    taskContext->LR = (UINTPTR)OsTaskExit;  /* LR should be kept, to distinguish it's THUMB or ARM instruction */
    taskContext->resved = 0x0;
    taskContext->R[0] = taskID;             /* R0 */
    taskContext->R[index++] = 0x01010101;   /* R1, 0x01010101 : reg initialed magic word */ //0x55
    for (; index < GEN_REGS_NUM; index++) {//R2 - R12的初始化很有意思
        taskContext->R[index] = taskContext->R[index - 1] + taskContext->R[1]; /* R2 - R12 */
    }
    taskContext->regPSR = PSR_MODE_SVC_ARM;   /* CPSR (Enable IRQ and FIQ interrupts, ARM-mode) */
    return (VOID *)taskContext;
}
//用户态运行栈初始化
LITE_OS_SEC_TEXT_INIT VOID OsUserTaskStackInit(TaskContext *context, TSK_ENTRY_FUNC taskEntry, UINTPTR stack)
{
    context->regPSR = PSR_MODE_USR_ARM;//工作模式:用户模式 + 工作状态:arm
    context->R[0] = stack;//栈指针给r0寄存器
    context->SP = TRUNCATE(stack, LOSCFG_STACK_POINT_ALIGN_SIZE);//给SP寄存器值使用
    context->LR = 0;//保存子程序返回地址 例如 a call b ,在b中保存 a地址
    context->PC = (UINTPTR)taskEntry;//入口函数
}

您一定注意到了TaskContext,说的全是它,这就是任务上下文结构体,理解它是理解任务切换的钥匙.它不仅在C语言层面出现,而且还在汇编层出现,TaskContext是连接或者说打通 C->汇编->C 实现任务切换的最关键概念.本篇全是围绕着它来展开.先看看它张啥样,LOOK!

TaskContext 任务上下文

typedef struct {
#if !defined(LOSCFG_ARCH_FPU_DISABLE)
    UINT64 D[FP_REGS_NUM]; /* D0-D31 */
    UINT32 regFPSCR;       /* FPSCR */
    UINT32 regFPEXC;       /* FPEXC */
#endif
    UINT32 resved;          /* It's stack 8 aligned */
    UINT32 regPSR;          
    UINT32 R[GEN_REGS_NUM]; /* R0-R12 */
    UINT32 SP;              /* R13 */
    UINT32 LR;              /* R14 */
    UINT32 PC;              /* R15 */
} TaskContext;

结构很简单,目的更简单,就是用来保存寄存器现场的值的.鸿蒙内核源码分析(总目录)系列寄存器篇中已经说过了,到了汇编层就是寄存器在玩,当CPU工作在用户和系统模式下时寄存器是复用的,玩的是17个寄存器和内存地址,访问内存地址也是通过寄存器来玩.

哪17个? R0~R15和CPSR. 当调度(主动式)或者中断(被动式)发生时.将这17个寄存器压入任务的内核栈的过程叫保护案发现场.从任务栈中弹出依次填入寄存器的过程叫恢复案发现场.

从栈空间的具体哪个位置开始恢复呢? 答案是:stackPointer,任务控制块(LosTaskCB)的首个变量.对应到汇编层的就是SP寄存器.

而TaskContext(任务上下文)就是一定的顺序来保存和恢复这17个寄存器的.任务上下文在任务还没有开始执行的时候就已经保存在内核栈中了,只不过是一些默认的值,OsTaskStackInit干的就是这个默认的事. 而OsUserTaskStackInit是对用户栈的初始化,改变的是(CPSR)工作模式和SP寄存器.

新任务的运行栈指针(stackPointer)给SP寄存器意味着切换了运行栈,这是本篇最重要的一句话.

以下通过汇编代码逐行分析如何保存和恢复TaskContext(任务上下文)

OsSchedResched 调度算法

//调度算法的实现
VOID OsSchedResched(VOID)
{
    // ...此处省去 ...
    /* do the task context switch */
    OsTaskSchedule(newTask, runTask);//切换任务上下文,注意OsTaskSchedule是一个汇编函数 见于 los_dispatch.s
}

在鸿蒙内核源码分析(总目录)之调度机制篇中,留了一个问题,OsTaskSchedule不是一个C函数,而是个汇编函数,就没有往下分析了,本篇要完成整个分析过程.OsTaskSchedule实现了任务的上下文切换,汇编代码见于los_dispatch.S中

OsTaskSchedule的参数指向的是新老两个任务,这两个参数分别保存在R0,R1寄存器中.

OsTaskSchedule 汇编实现

读这段汇编代码一定要对照上面的TaskContext,不然很难看懂,容易懵圈,但对照着看就秒懂.

/*
 * R0: new task 
 * R1: run task
 */
OsTaskSchedule:	/*任务调度,OsTaskSchedule的目的是将寄存器值按TaskContext的格式保存起来*/
    MRS      R2, CPSR 	/*MRS 指令用于将特殊寄存器(如 CPSR 和 SPSR)中的数据传递给通用寄存器,要读取特殊寄存器的数据只能使用 MRS 指令*/
    STMFD    SP!, {LR}	/*返回地址入栈,LR = PC-4 ,对应TaskContext->PC(R15寄存器)*/
    STMFD    SP!, {LR}	/*再次入栈对应,对应TaskContext->LR(R14寄存器)*/
    /* jump sp */
    SUB      SP, SP, #4 /* 跳的目的是为了,对应TaskContext->SP(R13寄存器)*/
    /* push r0-r12*/
    STMFD    SP!, {R0-R12} 	 @对应TaskContext->R[GEN_REGS_NUM](R0~R12寄存器)。
    STMFD    SP!, {R2}		/*R2 入栈 对应TaskContext->regPSR*/
    /* 8 bytes stack align */
    SUB      SP, SP, #4		@栈对齐,对应TaskContext->resved
    /* save fpu registers */
    PUSH_FPU_REGS   R2	/*保存fpu寄存器*/
    /* store sp on running task */
    STR     SP, [R1] @在运行的任务栈中保存SP,即runTask->stackPointer = sp

OsTaskContextLoad: @加载上下文
    /* clear the flag of ldrex */ @LDREX 可从内存加载数据,如果物理地址有共享TLB属性,则LDREX会将该物理地址标记为由当前处理器独占访问,并且会清除该处理器对其他任何物理地址的任何独占访问标记。
    CLREX @清除ldrex指令的标记
    /* switch to new task's sp */
    LDR     SP, [R0] @ 即:sp =  task->stackPointer
    /* restore fpu registers */
    POP_FPU_REGS    R2 @恢复fpu寄存器,这里用了汇编宏R2是宏的参数
    /* 8 bytes stack align */
    ADD     SP, SP, #4 @栈对齐
    LDMFD   SP!, {R0}  @此时SP!位置保存的是CPSR的内容,弹出到R0
    MOV     R4, R0	@R4=R0,将CPSR保存在r4, 将在OsKernelTaskLoad中保存到SPSR 
    AND     R0, R0, #CPSR_MASK_MODE @R0 =R0&CPSR_MASK_MODE ,目的是清除高16位
    CMP     R0, #CPSR_USER_MODE @R0 和 用户模式比较
    BNE     OsKernelTaskLoad @非用户模式则跳转到OsKernelTaskLoad执行,跳出
    /*此处省去 LOSCFG_KERNEL_SMP 部分*/
    MVN     R3, #CPSR_INT_DISABLE @按位取反 R3 = 0x3F
    AND     R4, R4, R3		@使能中断
    MSR     SPSR_cxsf, R4	@修改spsr值
    /* restore r0-r12, lr */
    LDMFD   SP!, {R0-R12}	@恢复寄存器值
    LDMFD   SP, {R13, R14}^	@恢复SP和LR的值,注意此时SP值已经变了,CPU换地方上班了.
    ADD     SP, SP, #(2 * 4)@sp = sp + 8
    LDMFD   SP!, {PC}^		@恢复PC寄存器值,如此一来 SP和PC都有了新值,完成了上下文切换.完美!
OsKernelTaskLoad: 			@内核任务的加载
    MSR     SPSR_cxsf, R4 	@将R4整个写入到程序状态保存寄存器
    /* restore r0-r12, lr */
    LDMFD   SP!, {R0-R12} 	@出栈,依次保存到 R0-R12,其实就是恢复现场
    ADD     SP, SP, #4 		@sp=SP+4
    LDMFD   SP!, {LR, PC}^ 	@返回地址赋给pc指针,直接跳出.

解读

汇编分成了三段OsTaskSchedule,OsTaskContextLoad,OsKernelTaskLoad.

第一段OsTaskSchedule其实就是在保存现场.代码都有注释,对照着TaskContext来的,它就干了一件事把17个寄存器的值按TaskContext的格式入栈,因为鸿蒙用栈方式采用的是满栈递减的方式,所以存放顺序是从最后一个往前依次入栈.

连着来两句STMFD SP!, {LR}之前让笔者懵圈了很久, 看了TaskContext才恍然大悟,因为三级流水线的原因,LR和PC寄存器之间是差了一条指令的,LR指向了处于译码阶段指令,而PC指向了取指阶段的指令,所以此处做了两次LR入栈,其实是保存了未执行的译码指令地址,确保执行不会丢失一条指令.

R1是正在运行的任务栈,OsTaskSchedule总的理解是在任务R1的运行栈中插入一个TaskContext结构块.而STR SP, [R1],是改变了LosTaskCB->stackPointer的值,这个值只能在汇编层进行精准的改变,而在整个鸿蒙内核C代码层面都没有看到对它有任何修改的地方.这个改变意义极为重要.因为新的任务被调度后的第一件事情就是恢复现场!!!

在OsTaskSchedule执行完成后,因为PC寄存器并没有发生跳转,所以紧接着往下执行OsTaskContextLoad

OsTaskContextLoad的任务就是恢复现场,谁的现场?当然是R0: new task的,所以第一条指令就是CLREX,清除干净后立马执行LDR SP, [R0],所指向的就是LosTaskCB->stackPointer,这个位置存的是新任务的TaskContext结构块,是上一次R0任务被打断时保存下来当时这17个寄存器的值啊,依次出栈就是恢复这17个寄存器的值.

OsTaskContextLoad在开始之前会判断下工作模式,即判断下是内核栈还是用户栈,两种处理方式稍有不同.但都是在恢复现场.

BNE OsKernelTaskLoad是查询CPSR后判断此时为内核栈的现场恢复过程,代码很简单就是恢复17个寄存器. 如此一来,任务执行的两个条件,第一个SP的在LDR SP, [R0]时就有了.第二个条件:PC寄存器的值也在最后一条汇编LDMFD SP!, {LR, PC}^也已经有了.改变了PC和LR有了新值,下一条指令位置一样是上次任务被中断时还没被执行的处于译码阶段的指令地址.

如果是用户态区别是需要恢复中断.因为用户模式的优先级是最低的,必须允许响应中断,也是依次恢复各寄存器的值,最后一句LDMFD SP!, {PC}^结束本次旅行,下一条指令位置一样是上次任务被中断时还没被执行的处于译码阶段的指令地址.

如此,说清楚了任务上下文切换的整个过程,初看可能不太容易理解,建议多看几篇,用笔画下栈的运行过程,脑海中会很清晰的浮现出整个切换过程的运行图.

编辑:hfy

声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • 线程
    +关注

    关注

    0

    文章

    504

    浏览量

    19682
  • 鸿蒙系统
    +关注

    关注

    183

    文章

    2634

    浏览量

    66344
收藏 人收藏

    评论

    相关推荐

    鸿蒙内核源码Task/线程技术分析

    前言 在鸿蒙内核中,广义上可理解为一个Task就是一个线程 一、怎么理解Task 1. 官方文档是怎么描述线程 基本概念 从系统的角度看,线程
    的头像 发表于 10-18 10:42 2218次阅读
    <b class='flag-5'>鸿蒙</b><b class='flag-5'>内核</b><b class='flag-5'>源码</b>Task/<b class='flag-5'>线程</b>技术<b class='flag-5'>分析</b>

    鸿蒙内核源码的中断环境任务切换

    中断环境任务切换鸿蒙内核线程就是
    的头像 发表于 04-30 16:41 2306次阅读
    <b class='flag-5'>鸿蒙</b><b class='flag-5'>内核</b><b class='flag-5'>源码</b>的中断<b class='flag-5'>环境</b><b class='flag-5'>下</b>的<b class='flag-5'>任务</b><b class='flag-5'>切换</b>

    【HarmonyOS】鸿蒙内核源码分析(调度机制篇)

    意义上所理解的线程呢。狭义上的后续有 鸿蒙内核源码分析(启动过程篇) 来说明。不知道大家有没有这种体会,学一个东西的过程中要接触很多新概念,
    发表于 10-14 14:00

    鸿蒙源码分析系列(总目录) | 给HarmonyOS源码逐行加上中文注释

    |-鸿蒙内核源码分析(调度机制篇) | 任务是如何被调度执行的|-鸿蒙
    发表于 11-20 11:24

    鸿蒙内核源码分析(调度机制篇):Task是如何被调度执行的

    (); 就是设置启动任务,但此时啥都还没开始呢,Kprocess 进程都没创建,怎么会有大家一般意义上所理解的线程呢。狭义上的后续有 鸿蒙内核源码
    发表于 11-23 10:53

    鸿蒙内核源码分析(Task管理篇):task是内核调度的单元

    迁移示意图注意官方文档说的是线程,没有提到task(任务),但内核源码中却有大量 task代码,很少有线程(thread)代码 ,这是怎么回
    发表于 11-23 14:01

    鸿蒙内核源码分析(Task管理篇):task是内核调度的单元

    还原回去就能保证task的连续执行,让用户毫无感知。鸿蒙内核给一个任务执行的时间是 20ms ,也就是说有多任务竞争的情况,一秒钟内最多要
    发表于 11-24 10:24

    线程管理线程切换

    线程管理线程切换前言基本信息前言说明PendSV_Handler函数前言基本信息名称描述说明RT-Thread Studio 软件版本版本: 1.1.3RT-Thread 系统版本
    发表于 08-24 08:19

    鸿蒙内核源码线程环境任务切换

    在中断环境,从当前线程切换到目标线程,这种方式也称为硬切换.不由软件控制的被动式
    的头像 发表于 04-25 16:48 1466次阅读
    <b class='flag-5'>鸿蒙</b><b class='flag-5'>内核</b><b class='flag-5'>源码</b><b class='flag-5'>之</b><b class='flag-5'>线程</b><b class='flag-5'>环境</b><b class='flag-5'>下</b>的<b class='flag-5'>任务</b><b class='flag-5'>切换</b>

    鸿蒙内核源码分析任务环境的事件控制块

    一对多同步模型:一个任务等待多个事件的触发。可以是任意一个事件发生时唤醒任务处理事件,也可以是几个事件都发生后才唤醒任务处理事件。
    的头像 发表于 04-26 14:29 1352次阅读
    <b class='flag-5'>鸿蒙</b><b class='flag-5'>内核</b><b class='flag-5'>源码</b><b class='flag-5'>分析</b>多<b class='flag-5'>任务</b><b class='flag-5'>环境</b><b class='flag-5'>下</b>的事件控制块

    鸿蒙内核分析线程中断环境任务切换

     OsSaveSignalContextIrqC函数为止. 中断环境任务切换鸿蒙内核
    的头像 发表于 03-19 14:34 2594次阅读
    <b class='flag-5'>鸿蒙</b><b class='flag-5'>内核</b><b class='flag-5'>分析</b>:<b class='flag-5'>线程</b>中断<b class='flag-5'>环境</b><b class='flag-5'>下</b>的<b class='flag-5'>任务</b><b class='flag-5'>切换</b>

    鸿蒙内核源码分析:task是内核调度的单元

    从系统的角度看,线程是竞争系统资源的最小运行单元。线程可以使用或等待CPU、使用内存空间等系统资源,并独立于其它线程运行。 鸿蒙内核每个进
    发表于 11-23 15:51 22次下载
    <b class='flag-5'>鸿蒙</b><b class='flag-5'>内核</b><b class='flag-5'>源码</b><b class='flag-5'>分析</b>:task是<b class='flag-5'>内核</b>调度的单元

    鸿蒙内核源码分析:进程和Task的就绪队列对调度的作用

    鸿蒙内核代码中有两个源文件是关于队列的,一个是用于调度的队列,另一个是用于线程间通讯的IPC队列。 鸿蒙内核进程和
    发表于 11-23 15:48 31次下载
    <b class='flag-5'>鸿蒙</b><b class='flag-5'>内核</b><b class='flag-5'>源码</b><b class='flag-5'>分析</b>:进程和Task的就绪队列对调度的作用

    鸿蒙内核源码分析内核最重要结构体

    为何鸿蒙内核源码分析系列开篇就说 LOS_DL_LIST ? 因为它在鸿蒙 LOS 内核中无处
    发表于 11-24 17:54 35次下载
    <b class='flag-5'>鸿蒙</b><b class='flag-5'>内核</b><b class='flag-5'>源码</b><b class='flag-5'>分析</b> :<b class='flag-5'>内核</b>最重要结构体

    华为鸿蒙系统内核源码分析上册

    鸿蒙內核源码注释中文版【 Gitee仓】给 Harmoηy○S源码逐行加上中文注解,详细阐述设计细节,助你快速精读 Harmonyos内核源码
    发表于 04-09 14:40 17次下载