@IT·互联网鸿蒙(HarmonyOS)开发知识

鸿蒙内核源码分析(任务切换篇) | 看汇编如何切换任务

2025-05-18  本文已影响0人  迪士尼在逃程序员

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

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

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

前置条件

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

//任务控制块中对两个栈空间的描述
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;

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

OsSchedResched 调度算法

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

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指针,直接跳出.

解读

写在最后

上一篇 下一篇

猜你喜欢

热点阅读