在鴻蒙的內核線程就是任務,系列篇中說的任務和線程當一個東西去理解.
一般二種場景下需要切換任務上下文:
在線程環境下,從當前線程切換到目標線程,這種方式也稱為軟切換,能由軟件控制的自主式切換.哪些情況下會出現軟切換呢?
運行的線程申請某種資源(比如各種鎖,讀/寫消息隊列)失敗時,需要主動釋放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文章
505瀏覽量
19690 -
鴻蒙系統
+關注
關注
183文章
2634瀏覽量
66362
發布評論請先 登錄
相關推薦
評論