在鴻蒙的內(nèi)核線程就是任務(wù),系列篇中說的任務(wù)和線程當一個東西去理解.
一般二種場景下需要切換任務(wù)上下文:
在線程環(huán)境下,從當前線程切換到目標線程,這種方式也稱為軟切換,能由軟件控制的自主式切換.哪些情況下會出現(xiàn)軟切換呢?
運行的線程申請某種資源(比如各種鎖,讀/寫消息隊列)失敗時,需要主動釋放CPU的控制權(quán),將自己掛入等待隊列,調(diào)度算法重新調(diào)度新任務(wù)運行.
每隔10ms就執(zhí)行一次的OsTickHandler節(jié)拍處理函數(shù),檢測到任務(wù)的時間片用完了,就發(fā)起任務(wù)的重新調(diào)度,切換到新任務(wù)運行.
不管是內(nèi)核態(tài)的任務(wù)還是用戶態(tài)的任務(wù),于切換而言是統(tǒng)一處理,一視同仁的,因為切換是需要換棧運行,寄存器有限,需要頻繁的復用,這就需要將當前寄存器值先保存到任務(wù)自己的棧中,以便別人用完了輪到自己再用時恢復寄存器當時的值,確保老任務(wù)還能繼續(xù)跑下去. 而保存寄存器順序的結(jié)構(gòu)體叫:任務(wù)上下文(TaskContext).
在中斷環(huán)境下,從當前線程切換到目標線程,這種方式也稱為硬切換.不由軟件控制的被動式切換.哪些情況下會出現(xiàn)硬切換呢?
由硬件產(chǎn)生的中斷,比如 鼠標,鍵盤外部設(shè)備每次點擊和敲打,屏幕的觸摸,USB的插拔等等這些都是硬中斷.同樣的需要切換棧運行,需要復用寄存器,但與軟切換不一樣的是,硬切換會切換工作模式(中斷模式).所以會更復雜點,但道理還是一樣要保存和恢復切換現(xiàn)場寄存器的值, 而保存寄存器順序的結(jié)構(gòu)體叫:任務(wù)中斷上下文(TaskIrqContext).
本篇說清楚在線程環(huán)境下切換(軟切換)的實現(xiàn)過程.中斷切換(硬切換)實現(xiàn)過程將在鴻蒙內(nèi)核源碼分析(總目錄)中斷切換篇中詳細說明.
本篇具體說清楚以下幾個問題:
任務(wù)上下文(TaskContext)怎么保存的?
代碼的實現(xiàn)細節(jié)是怎樣的?
如何保證切換不會發(fā)生錯誤,指令不會丟失?
在鴻蒙內(nèi)核源碼分析(總目錄)系列篇中已經(jīng)說清楚了調(diào)度機制,線程概念,寄存器,CPU,工作模式,這些是讀懂本篇的基礎(chǔ),建議先前往翻看,不然理解本篇會費勁.本篇代碼量較多,涉及C和匯編代碼,代碼都添加了注釋,試圖把任務(wù)的整個切換過程逐行逐行說清楚.
前置條件
一個任務(wù)要跑起來,需要兩個必不可少的硬性條件:
1.從代碼段哪個位置取指令? 也就是入口地址,main函數(shù)是應用程序的入口地址, run()是new一個線程執(zhí)行的入口地址.高級語言是這么叫,但到了匯編層的叫法就是PC寄存器.給PC寄存器喂妹值,指令就從哪里取.
2.運行的場地(棧空間)在哪里? ARM有7種工作模式,到了進程層面只需要考慮內(nèi)核模式和用戶模式兩種,對應到任務(wù)會有內(nèi)核態(tài)棧空間和用戶態(tài)棧空間.內(nèi)核模式的任務(wù)只有內(nèi)核態(tài)的棧空間,用戶模式任務(wù)二者都有.棧空間是在初始化一個任務(wù)時就分配指定好的.以下是兩種棧空間的初始化過程.為了精練省去了部分代碼,留下了核心部分.
//任務(wù)控制塊中對兩個棧空間的描述
typedef struct {
VOID *stackPointer; /**< Task stack pointer */ //內(nèi)核態(tài)棧指針,SP位置,切換任務(wù)時先保存上下文并指向TaskContext位置.
UINT32 stackSize; /**< Task stack size */ //內(nèi)核態(tài)棧大小
UINTPTR topOfStack; /**< Task stack top */ //內(nèi)核態(tài)棧頂 bottom = top + size
// ....
UINTPTR userArea; //使用區(qū)域,由運行時劃定,根據(jù)運行態(tài)不同而不同
UINTPTR userMapBase; //用戶態(tài)下的棧底位置
UINT32 userMapSize; /**< user thread stack size ,real size : userMapSize + USER_STACK_MIN_SIZE */
} LosTaskCB;
//內(nèi)核態(tài)運行棧初始化 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 */ //初始化任務(wù)上下文 taskContext->PC = (UINTPTR)OsTaskEntry;//程序計數(shù)器,CPU首次執(zhí)行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; }
//用戶態(tài)運行棧初始化
LITE_OS_SEC_TEXT_INIT VOID OsUserTaskStackInit(TaskContext *context, TSK_ENTRY_FUNC taskEntry, UINTPTR stack)
{
context->regPSR = PSR_MODE_USR_ARM;//工作模式:用戶模式 + 工作狀態(tài):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;//入口函數(shù)
}
您一定注意到了TaskContext,說的全是它,這就是任務(wù)上下文結(jié)構(gòu)體,理解它是理解任務(wù)切換的鑰匙.它不僅在C語言層面出現(xiàn),而且還在匯編層出現(xiàn),TaskContext是連接或者說打通 C->匯編->C 實現(xiàn)任務(wù)切換的最關(guān)鍵概念.本篇全是圍繞著它來展開.先看看它張啥樣,LOOK!
TaskContext 任務(wù)上下文
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;
結(jié)構(gòu)很簡單,目的更簡單,就是用來保存寄存器現(xiàn)場的值的.鴻蒙內(nèi)核源碼分析(總目錄)系列寄存器篇中已經(jīng)說過了,到了匯編層就是寄存器在玩,當CPU工作在用戶和系統(tǒng)模式下時寄存器是復用的,玩的是17個寄存器和內(nèi)存地址,訪問內(nèi)存地址也是通過寄存器來玩.
哪17個? R0~R15和CPSR. 當調(diào)度(主動式)或者中斷(被動式)發(fā)生時.將這17個寄存器壓入任務(wù)的內(nèi)核棧的過程叫保護案發(fā)現(xiàn)場.從任務(wù)棧中彈出依次填入寄存器的過程叫恢復案發(fā)現(xiàn)場.
從棧空間的具體哪個位置開始恢復呢? 答案是:stackPointer,任務(wù)控制塊(LosTaskCB)的首個變量.對應到匯編層的就是SP寄存器.
而TaskContext(任務(wù)上下文)就是一定的順序來保存和恢復這17個寄存器的.任務(wù)上下文在任務(wù)還沒有開始執(zhí)行的時候就已經(jīng)保存在內(nèi)核棧中了,只不過是一些默認的值,OsTaskStackInit干的就是這個默認的事. 而OsUserTaskStackInit是對用戶棧的初始化,改變的是(CPSR)工作模式和SP寄存器.
新任務(wù)的運行棧指針(stackPointer)給SP寄存器意味著切換了運行棧,這是本篇最重要的一句話.
以下通過匯編代碼逐行分析如何保存和恢復TaskContext(任務(wù)上下文)
OsSchedResched 調(diào)度算法
//調(diào)度算法的實現(xiàn)
VOID OsSchedResched(VOID)
{
// ...此處省去 ...
/* do the task context switch */
OsTaskSchedule(newTask, runTask);//切換任務(wù)上下文,注意OsTaskSchedule是一個匯編函數(shù) 見于 los_dispatch.s
}
在鴻蒙內(nèi)核源碼分析(總目錄)之調(diào)度機制篇中,留了一個問題,OsTaskSchedule不是一個C函數(shù),而是個匯編函數(shù),就沒有往下分析了,本篇要完成整個分析過程.OsTaskSchedule實現(xiàn)了任務(wù)的上下文切換,匯編代碼見于los_dispatch.S中
OsTaskSchedule的參數(shù)指向的是新老兩個任務(wù),這兩個參數(shù)分別保存在R0,R1寄存器中.
OsTaskSchedule 匯編實現(xiàn)
讀這段匯編代碼一定要對照上面的TaskContext,不然很難看懂,容易懵圈,但對照著看就秒懂.
/*
* R0: new task
* R1: run task
*/
OsTaskSchedule: /*任務(wù)調(diào)度,OsTaskSchedule的目的是將寄存器值按TaskContext的格式保存起來*/
MRS R2, CPSR /*MRS 指令用于將特殊寄存器(如 CPSR 和 SPSR)中的數(shù)據(jù)傳遞給通用寄存器,要讀取特殊寄存器的數(shù)據(jù)只能使用 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] @在運行的任務(wù)棧中保存SP,即runTask->stackPointer = sp
OsTaskContextLoad: @加載上下文
/* clear the flag of ldrex */ @LDREX 可從內(nèi)存加載數(shù)據(jù),如果物理地址有共享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是宏的參數(shù)
/* 8 bytes stack align */
ADD SP, SP, #4 @棧對齊
LDMFD SP!, {R0} @此時SP!位置保存的是CPSR的內(nèi)容,彈出到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 @非用戶模式則跳轉(zhuǎn)到OsKernelTaskLoad執(zhí)行,跳出
/*此處省去 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值已經(jīng)變了,CPU換地方上班了.
ADD SP, SP, #(2 * 4)@sp = sp + 8
LDMFD SP!, {PC}^ @恢復PC寄存器值,如此一來 SP和PC都有了新值,完成了上下文切換.完美!
OsKernelTaskLoad: @內(nèi)核任務(wù)的加載
MSR SPSR_cxsf, R4 @將R4整個寫入到程序狀態(tài)保存寄存器
/* restore r0-r12, lr */
LDMFD SP!, {R0-R12} @出棧,依次保存到 R0-R12,其實就是恢復現(xiàn)場
ADD SP, SP, #4 @sp=SP+4
LDMFD SP!, {LR, PC}^ @返回地址賦給pc指針,直接跳出.
解讀
匯編分成了三段OsTaskSchedule,OsTaskContextLoad,OsKernelTaskLoad.
第一段OsTaskSchedule其實就是在保存現(xiàn)場.代碼都有注釋,對照著TaskContext來的,它就干了一件事把17個寄存器的值按TaskContext的格式入棧,因為鴻蒙用棧方式采用的是滿棧遞減的方式,所以存放順序是從最后一個往前依次入棧.
連著來兩句STMFD SP!, {LR}之前讓筆者懵圈了很久, 看了TaskContext才恍然大悟,因為三級流水線的原因,LR和PC寄存器之間是差了一條指令的,LR指向了處于譯碼階段指令,而PC指向了取指階段的指令,所以此處做了兩次LR入棧,其實是保存了未執(zhí)行的譯碼指令地址,確保執(zhí)行不會丟失一條指令.
R1是正在運行的任務(wù)棧,OsTaskSchedule總的理解是在任務(wù)R1的運行棧中插入一個TaskContext結(jié)構(gòu)塊.而STR SP, [R1],是改變了LosTaskCB->stackPointer的值,這個值只能在匯編層進行精準的改變,而在整個鴻蒙內(nèi)核C代碼層面都沒有看到對它有任何修改的地方.這個改變意義極為重要.因為新的任務(wù)被調(diào)度后的第一件事情就是恢復現(xiàn)場!!!
在OsTaskSchedule執(zhí)行完成后,因為PC寄存器并沒有發(fā)生跳轉(zhuǎn),所以緊接著往下執(zhí)行OsTaskContextLoad
OsTaskContextLoad的任務(wù)就是恢復現(xiàn)場,誰的現(xiàn)場?當然是R0: new task的,所以第一條指令就是CLREX,清除干凈后立馬執(zhí)行LDR SP, [R0],所指向的就是LosTaskCB->stackPointer,這個位置存的是新任務(wù)的TaskContext結(jié)構(gòu)塊,是上一次R0任務(wù)被打斷時保存下來當時這17個寄存器的值啊,依次出棧就是恢復這17個寄存器的值.
OsTaskContextLoad在開始之前會判斷下工作模式,即判斷下是內(nèi)核棧還是用戶棧,兩種處理方式稍有不同.但都是在恢復現(xiàn)場.
BNE OsKernelTaskLoad是查詢CPSR后判斷此時為內(nèi)核棧的現(xiàn)場恢復過程,代碼很簡單就是恢復17個寄存器. 如此一來,任務(wù)執(zhí)行的兩個條件,第一個SP的在LDR SP, [R0]時就有了.第二個條件:PC寄存器的值也在最后一條匯編LDMFD SP!, {LR, PC}^也已經(jīng)有了.改變了PC和LR有了新值,下一條指令位置一樣是上次任務(wù)被中斷時還沒被執(zhí)行的處于譯碼階段的指令地址.
如果是用戶態(tài)區(qū)別是需要恢復中斷.因為用戶模式的優(yōu)先級是最低的,必須允許響應中斷,也是依次恢復各寄存器的值,最后一句LDMFD SP!, {PC}^結(jié)束本次旅行,下一條指令位置一樣是上次任務(wù)被中斷時還沒被執(zhí)行的處于譯碼階段的指令地址.
如此,說清楚了任務(wù)上下文切換的整個過程,初看可能不太容易理解,建議多看幾篇,用筆畫下棧的運行過程,腦海中會很清晰的浮現(xiàn)出整個切換過程的運行圖.
編輯:hfy
-
線程
+關(guān)注
關(guān)注
0文章
509瀏覽量
20825 -
鴻蒙系統(tǒng)
+關(guān)注
關(guān)注
183文章
2642瀏覽量
69830
發(fā)布評論請先 登錄
鴻蒙內(nèi)核源碼Task/線程技術(shù)分析
【HarmonyOS】鴻蒙內(nèi)核源碼分析(調(diào)度機制篇)
鴻蒙源碼分析系列(總目錄) | 給HarmonyOS源碼逐行加上中文注釋
鴻蒙內(nèi)核源碼分析(調(diào)度機制篇):Task是如何被調(diào)度執(zhí)行的
鴻蒙內(nèi)核源碼分析(Task管理篇):task是內(nèi)核調(diào)度的單元
鴻蒙內(nèi)核源碼分析(Task管理篇):task是內(nèi)核調(diào)度的單元
線程管理之線程切換
鴻蒙內(nèi)核源碼分析多任務(wù)環(huán)境下的事件控制塊
鴻蒙內(nèi)核分析:線程中斷環(huán)境下的任務(wù)切換
鴻蒙內(nèi)核源碼分析:task是內(nèi)核調(diào)度的單元
鴻蒙內(nèi)核源碼分析:進程和Task的就緒隊列對調(diào)度的作用
鴻蒙內(nèi)核源碼分析 :內(nèi)核最重要結(jié)構(gòu)體
鴻蒙內(nèi)核源碼分析之線程環(huán)境下的任務(wù)切換
評論