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