為什么學個東西要學那么多的概念?
鴻蒙的內核中 Task和 線程在廣義上可以理解為是一個東西,但狹義上肯定會有區別,區別在于管理體系的不同,Task是調度層面的概念,線程是進程層面概念。比如 main()函數中首個函數 OsSetMainTask();就是設置啟動任務,但此時啥都還沒開始呢,Kprocess進程都沒創建,怎么會有大家一般意義上所理解的線程呢。狹義上的后續有鴻蒙內核源碼分析(啟動過程篇)來說明。不知道大家有沒有這種體會,學一個東西的過程中要接觸很多新概念,尤其像 Java/android 的生態,概念賊多,很多同學都被繞在概念中出不來,痛苦不堪。那問題是為什么需要這么多的概念呢?
舉個例子就明白了:
假如您去深圳參加一個面試老板問你哪里人?你會說是江西人,湖南人...而不會說是張家村二組的張全蛋,這樣還誰敢要你。但如果你參加同鄉會別人問你同樣問題,你不會說是來自東北那旮沓的,卻反而要說張家村二組的張全蛋。明白了嗎?張全蛋還是那個張全蛋,但因為場景變了,您的說法就得必須跟著變,否則沒法愉快的聊天。程序設計就是源于生活,歸于生活,大家對程序的理解就是要用生活中的場景去打比方,更好的理解概念。
那在內核的調度層面,咱們只說task,task是內核調度的單元,調度就是圍著它轉。
進程和線程的狀態遷移圖
先看看task從哪些渠道產生:
渠道很多,可能是shell的一個命令,也可能由內核創建,更多的是大家編寫應用程序new出來的一個線程。
調度的內容task已經有了,那他們是如何被有序調度的呢?答案:是32個進程和線程就緒隊列,各32個哈,為什么是32個,鴻蒙系統源碼分析(總目錄) 文章里有詳細說明,自行去翻。這張進程狀態遷移示意圖一定要看明白.
注意:進程和線程的隊列內的內容只針對就緒狀態,其他狀態內核并沒有用隊列去描述它,(線程的阻塞狀態用的是pendlist鏈表),因為就緒就意味著工作都準備好了就等著被調度到CPU來執行了。所以理解就緒隊列很關鍵,有三種情況會加入就緒隊列。
Init→Ready:
進程創建或fork時,拿到該進程控制塊后進入Init狀態,處于進程初始化階段,當進程初始化完成將進程插入調度隊列,此時進程進入就緒狀態。
Pend→Ready / Pend→Running:
阻塞進程內的任意線程恢復就緒態時,進程被加入到就緒隊列,同步轉為就緒態,若此時發生進程切換,則進程狀態由就緒態轉為運行態。
Running→Ready:
進程由運行態轉為就緒態的情況有以下兩種:
有更高優先級的進程創建或者恢復后,會發生進程調度,此刻就緒列表中最高優先級進程變為運行態,那么原先運行的進程由運行態變為就緒態。
若進程的調度策略為SCHED_RR,且存在同一優先級的另一個進程處于就緒態,則該進程的時間片消耗光之后,該進程由運行態轉為就緒態,另一個同優先級的進程由就緒態轉為運行態。
誰來觸發調度工作?
就緒隊列讓task各就各位,在其生命周期內不停的進行狀態流轉,調度是讓task交給CPU處理,那又是什么讓調度去工作的呢?它是如何被觸發的?
筆者能想到的觸發方式是以下四個:
Tick(時鐘管理),類似于JAVA的定時任務,時間到了就觸發。系統定時器是內核時間機制中最重要的一部分,它提供了一種周期性觸發中斷機制,即系統定時器以HZ(時鐘節拍率)為頻率自行觸發時鐘中斷。當時鐘中斷發生時,內核就通過時鐘中斷處理程序OsTickHandler對其進行處理。鴻蒙內核默認是10ms觸發一次,執行以下中斷函數:
/* * Description : Tick interruption handler */ LITE_OS_SEC_TEXT VOID OsTickHandler(VOID) { UINT32 intSave; TICK_LOCK(intSave); g_tickCount[ArchCurrCpuid()]++; TICK_UNLOCK(intSave); #ifdef LOSCFG_KERNEL_VDSO OsUpdateVdsoTimeval(); #endif #ifdef LOSCFG_KERNEL_TICKLESS OsTickIrqFlagSet(OsTicklessFlagGet()); #endif #if (LOSCFG_BASE_CORE_TICK_HW_TIME == YES) HalClockIrqClear(); /* diff from every platform */ #endif OsTimesliceCheck();//時間片檢查 OsTaskScan(); /* task timeout scan *///任務掃描,發起調度 #if (LOSCFG_BASE_CORE_SWTMR == YES) OsSwtmrScan();//軟時鐘掃描檢查 #endif }
里面對任務進行了掃描,時間片到了或就緒隊列有高或同級task,會執行調度。
第二個是各種軟硬中斷,如何USB插拔,鍵盤,鼠標這些外設引起的中斷,需要去執行中斷處理函數。
第三個是程序主動中斷,比如運行過程中需要申請其他資源,而主動讓出控制權,重新調度。
最后一個是創建一個新進程或新任務后主動發起的搶占式調度,新進程會默認創建一個main task, task的首條指令(入口函數)就是我們上層程序的main函數,它被放在代碼段的第一的位置。
哪些地方會申請調度?看一張圖。
這里提下圖中的OsCopyProcess(),這是fork進程的主體函數,可以看出fork之后立即申請了一次調度。
LITE_OS_SEC_TEXT INT32 LOS_Fork(UINT32 flags, const CHAR *name, const TSK_ENTRY_FUNC entry, UINT32 stackSize) { UINT32 cloneFlag = CLONE_PARENT | CLONE_THREAD | CLONE_VFORK | CLONE_FILES; if (flags & (~cloneFlag)) { PRINT_WARN("Clone dont support some flags!\n"); } flags |= CLONE_FILES; return OsCopyProcess(cloneFlag & flags, name, (UINTPTR)entry, stackSize); } STATIC INT32 OsCopyProcess(UINT32 flags, const CHAR *name, UINTPTR sp, UINT32 size) { UINT32 intSave, ret, processID; LosProcessCB *run = OsCurrProcessGet(); LosProcessCB *child = OsGetFreePCB(); if (child == NULL) { return -LOS_EAGAIN; } processID = child->processID; ret = OsForkInitPCB(flags, child, name, sp, size); if (ret != LOS_OK) { goto ERROR_INIT; } ret = OsCopyProcessResources(flags, child, run); if (ret != LOS_OK) { goto ERROR_TASK; } ret = OsChildSetProcessGroupAndSched(child, run); if (ret != LOS_OK) { goto ERROR_TASK; } LOS_MpSchedule(OS_MP_CPU_ALL); if (OS_SCHEDULER_ACTIVE) { LOS_Schedule();// 申請調度 } return processID; ERROR_TASK: SCHEDULER_LOCK(intSave); (VOID)OsTaskDeleteUnsafe(OS_TCB_FROM_TID(child->threadGroupID), OS_PRO_EXIT_OK, intSave); ERROR_INIT: OsDeInitPCB(child); return -ret; }
原來創建一個進程這么簡單,真的就是在COPY!
源碼告訴你調度過程是怎樣的
以上是需要提前了解的信息,接下來直接上源碼看調度過程吧,文件就三個函數,主要就是這個了:
VOID OsSchedResched(VOID) { LOS_ASSERT(LOS_SpinHeld(&g_taskSpin));//調度過程要上鎖 newTask = OsGetTopTask(); //獲取最高優先級任務 OsSchedSwitchProcess(runProcess, newProcess);//切換進程 (VOID)OsTaskSwitchCheck(runTask, newTask);//任務檢查 OsCurrTaskSet((VOID*)newTask);//*設置當前任務 if (OsProcessIsUserMode(newProcess)) {//判斷是否為用戶態,使用用戶空間 OsCurrUserTaskSet(newTask->userArea);//設置任務空間 } /* do the task context switch */ OsTaskSchedule(newTask, runTask); //切換CPU任務上下文,匯編代碼實現 }
函數有點長,筆者留了最重要的幾行,看這幾行就夠了,流程如下:
調度過程要自旋鎖,多核情況下只能被一個CPU core執行. 不允許任何中斷發生, 沒錯,說的是任何事是不能去打斷它,否則后果太嚴重了,這可是內核在切換進程和線程的操作啊。
在就緒隊列里找個最高優先級的task
切換進程,就是task歸屬的那個進程設為運行進程,這里要注意,老的task和老進程只是讓出了CPU指令執行權,其他都還在內存,資源也都沒有釋放.
設置新任務為當前任務
用戶模式下需要設置task運行空間,因為每個task棧是不一樣的.空間部分具體在系列篇內存中查看
是最重要的,切換任務上下文,參數是新老兩個任務,一個要保存現場,一個要恢復現場。
什么是任務上下文?鴻蒙內核源碼分析(總目錄)任務切換篇已有詳細的描述,請自行翻看.
請讀懂OsGetTopTask()
讀懂OsGetTopTask(),就明白了就緒隊列是怎么回事了。這里提下goto語句,幾乎所有內核代碼都會大量的使用goto語句,鴻蒙內核有617個goto遠大于264個break,還有人說要廢掉goto,你知道內核開發者青睞goto的真正原因嗎?
LITE_OS_SEC_TEXT_MINOR LosTaskCB *OsGetTopTask(VOID) { UINT32 priority, processPriority; UINT32 bitmap; UINT32 processBitmap; LosTaskCB *newTask = NULL; #if (LOSCFG_KERNEL_SMP == YES) UINT32 cpuid = ArchCurrCpuid(); #endif LosProcessCB *processCB = NULL; processBitmap = g_priQueueBitmap; while (processBitmap) { processPriority = CLZ(processBitmap); LOS_DL_LIST_FOR_EACH_ENTRY(processCB, &g_priQueueList[processPriority], LosProcessCB, pendList) { bitmap = processCB->threadScheduleMap; while (bitmap) { priority = CLZ(bitmap); LOS_DL_LIST_FOR_EACH_ENTRY(newTask, &processCB->threadPriQueueList[priority], LosTaskCB, pendList) { #if (LOSCFG_KERNEL_SMP == YES) if (newTask->cpuAffiMask & (1U << cpuid)) { #endif newTask->taskStatus &= ~OS_TASK_STATUS_READY; OsPriQueueDequeue(processCB->threadPriQueueList, &processCB->threadScheduleMap, &newTask->pendList); OsDequeEmptySchedMap(processCB); goto OUT; #if (LOSCFG_KERNEL_SMP == YES) } #endif } bitmap &= ~(1U << (OS_PRIORITY_QUEUE_NUM - priority - 1)); } } processBitmap &= ~(1U << (OS_PRIORITY_QUEUE_NUM - processPriority - 1)); } OUT: return newTask; } #ifdef __cplusplus #if __cplusplus }
編輯:hfy
-
cpu
+關注
關注
68文章
10824瀏覽量
211137 -
線程
+關注
關注
0文章
504瀏覽量
19651 -
進程
+關注
關注
0文章
201瀏覽量
13947
發布評論請先 登錄
相關推薦
評論