孔子于鄉黨,恂恂如也,似不能言者。其在宗廟朝廷,便便言,唯謹爾。 《論語》:鄉黨篇
百篇部落格系列篇.本篇為:
v45.xx 鴻蒙核心源碼分析(Fork篇) | 一次調用,兩次傳回
程序管理相關篇為:
- v02.06 鴻蒙核心源碼分析(程序管理) | 誰在管理核心資源
- v24.03 鴻蒙核心源碼分析(程序概念) | 程序在管理哪些資源
- v45.05 鴻蒙核心源碼分析(Fork) | 一次調用,兩次傳回
- v46.05 鴻蒙核心源碼分析(特殊程序) | 老鼠生兒會打洞
- v47.02 鴻蒙核心源碼分析(程序回收) | 臨終前如何向老祖宗托孤
- v48.05 鴻蒙核心源碼分析(信号生産) | 年過半百,依然活力十足
- v49.03 鴻蒙核心源碼分析(信号消費) | 誰讓CPU連續四次換棧運作
- v71.03 鴻蒙核心源碼分析(Shell編輯) | 兩個任務,三個階段
- v72.01 鴻蒙核心源碼分析(Shell解析) | 應用窺伺核心的視窗
筆者第一次看到fork時,說是一次調用,兩次傳回,當時就懵圈了,多新鮮,真的很難了解.因為這足以颠覆了以往對函數的認知, 函數調用還能這麼玩,父程序調用一次,父子程序各傳回一次.而且隻能通過傳回值來判斷是哪個程序的傳回.是以一直有幾個問題纏繞在腦海中.
- fork是什麼? 外部如何正确使用它.
- 為什麼要用fork這種設計? fork的本質和好處是什麼?
- 怎麼做到的? 調用fork()使得父子程序各傳回一次,怎麼做到傳回兩次的,其中到底發生了什麼?
- 為什麼
代表了是子程序的傳回? 為什麼父程序不需要傳回 0 ?pid = 0
直到看了linux核心源碼後才搞明白,但系列篇的定位是挖透鴻蒙的核心源碼,是以本篇将深入fork函數,用鴻蒙核心源碼去說明白這些問題.在看本篇之前建議要先看系列篇的其他篇幅.如(任務切換篇,寄存器篇,工作模式篇,系統調用篇 等),有了這些基礎,會很好了解fork的實作過程.
fork是什麼
先看一個網上經常拿來說fork的一個代碼片段.
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
pid_t pid;
char *message;
int n;
pid = fork();
if (pid < 0) {
perror("fork failed");
exit(1);
}
if (pid == 0) {
message = "This is the child\n";
n = 6;
} else {
message = "This is the parent\n";
n = 3;
}
for(; n > 0; n--) {
printf(message);
sleep(1);
}
return 0;
}
-
fork 失敗pid < 0
-
fork成功,是子程序的傳回pid == 0
-
fork成功,是父程序的傳回pid > 0
-
的傳回值這樣規定是有道理的。fork
在子程序中傳回0,子程序仍可以調用fork
函數得到自己的程序id,也可以調用getpid
函數得到父程序的id。在父程序中用getppid
可以得到自己的程序id,然而要想得到子程序的id,隻有将getpid
的傳回值記錄下來,别無它法。fork
- 子程序并沒有真正執行
,而是核心用了一個很巧妙的方法獲得了傳回值,并且将傳回值硬生生的改寫成了0,這是筆者認為fork()
的實作最精彩的部分.fork
運作結果
$ ./a.out
This is the child
This is the parent
This is the child
This is the parent
This is the child
This is the parent
This is the child
$ This is the child
This is the child
這個程式的運作過程如下圖所示。
解讀
-
是一個系統調用,是以會切換到SVC模式運作.在SVC棧中父程序複制出一個子程序,父程序和子程序的PCB資訊相同,使用者态代碼和資料也相同.fork()
- 從案例的執行上可以看出,fork 之後的代碼父子程序都會執行,即代碼段指向(PC寄存器)是一樣的.實際上fork隻被父程序調用了一次,子程序并沒有執行
函數,但是卻獲得了一個傳回值,fork
,這個非常重要.這是本篇說明的重點.pid == 0
- 從執行結果上看,父程序列印了三次(This is the parent),因為 n = 3. 子程序列印了六次(This is the child),因為 n = 6. 而子程式并沒有執行以下代碼:
子程序是從pid_t pid; char *message; int n;
後開始執行的,按理它不會在新任務棧中出現這些變量,而實際上後面又能順利的使用這些變量,說明父程序目前任務的使用者态的資料也複制了一份給子程序的新任務棧中.pid = fork()
- 被fork成功的子程序跑的首條代碼指令是
,這裡的0是傳回值,存放在pid = 0
寄存器中.說明父程序的任務上下文也進行了一次拷貝,父程序從核心态回到使用者态時恢複的上下文和子程序的任務上下文是一樣的,即 PC寄存器指向是一樣的,如此才能確定在代碼段相同的位置執行.R0
- 執行
後 第一條列印的是./a.out
說明This is the child
中發生了一次排程,CPU切到了子程序的任務執行,fork()
的本質在系列篇中多次說過是任務主動放棄CPU的使用權,将自己挂入任務等待連結清單,由此發生一次任務排程,CPU切到父程序執行,才有了列印第二條的sleep(1)
,父程序的This is the parent
又切到子程序如此往返,直到 n = 0, 結束父子程序.sleep(1)
- 但這個例子和筆者的解讀隻解釋了fork是什麼的使用說明書,并猜測其中做了些什麼,并沒有說明為什麼要這樣做和代碼是怎麼實作的. 正式結合鴻蒙的源碼說清楚為什麼和怎麼做這兩個問題?
為什麼是fork
fork函數的特點概括起來就是“調用一次,傳回兩次”,在父程序中調用一次,在父程序和子程序中各傳回一次。從上圖可以看出,一開始是一個控制流程,調用fork之後發生了分叉,變成兩個控制流程,這也就是“fork”(分叉)這個名字的由來了。
系列篇已經寫了40+多篇,已經很容易了解一個程式運作起來就需要各種資源(記憶體,檔案,ipc,監控資訊等等),資源就需要管理,程序就是管理資源的容器.這些資源相當于幹活需要各種工具一樣,幹活的工具都差不多,實在沒必再走流程一一申請,而且申請下來會發現和别人手裡已有的工具都一樣, 别人有直接拿過來使用它不香嗎? 是以最簡單的辦法就是認個幹爹,讓幹爹拷貝一份幹活工具給你.這樣隻需要專心的幹好活(任務)就行了. fork的本質就是copy,具體看代碼.
fork怎麼實作的?
//系統調用之fork ,建議去 https://gitee.com/weharmony/kernel_liteos_a_note fork 一下? :P
int SysFork(void)
{
return OsClone(CLONE_SIGHAND, 0, 0);//本質就是克隆
}
LITE_OS_SEC_TEXT INT32 OsClone(UINT32 flags, UINTPTR sp, UINT32 size)
{
UINT32 cloneFlag = CLONE_PARENT | CLONE_THREAD | CLONE_VFORK | CLONE_VM;
if (flags & (~cloneFlag)) {
PRINT_WARN("Clone dont support some flags!\n");
}
return OsCopyProcess(cloneFlag & flags, NULL, sp, size);
}
STATIC INT32 OsCopyProcess(UINT32 flags, const CHAR *name, UINTPTR sp, UINT32 size)
{
UINT32 intSave, ret, processID;
LosProcessCB *run = OsCurrProcessGet();//擷取目前程序
LosProcessCB *child = OsGetFreePCB();//從程序池中申請一個程序控制塊,鴻蒙程序池預設64
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);//拷貝程序的資源,包括虛拟空間,檔案,安全,IPC ==
if (ret != LOS_OK) {
goto ERROR_TASK;
}
ret = OsChildSetProcessGroupAndSched(child, run);//設定程序組和加入程序排程就緒隊列
if (ret != LOS_OK) {
goto ERROR_TASK;
}
LOS_MpSchedule(OS_MP_CPU_ALL);//給各CPU發送準備接受排程信号
if (OS_SCHEDULER_ACTIVE) {//目前CPU core處于活動狀态
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;
}
### OsForkInitPCB
STATIC UINT32 (UINT32 flags, LosProcessCB *child, const CHAR *name, UINTPTR sp, UINT32 size)
{
UINT32 ret;
LosProcessCB *run = OsCurrProcessGet();//擷取目前程序
ret = OsInitPCB(child, run->processMode, OS_PROCESS_PRIORITY_LOWEST, LOS_SCHED_RR, name);//初始化PCB資訊,程序模式,優先級,排程方式,名稱 == 資訊
if (ret != LOS_OK) {
return ret;
}
ret = OsCopyParent(flags, child, run);//拷貝父親大人的基因資訊
if (ret != LOS_OK) {
return ret;
}
return OsCopyTask(flags, child, name, sp, size);//拷貝任務,設定任務入口函數,棧大小
}
//初始化PCB塊
STATIC UINT32 OsInitPCB(LosProcessCB *processCB, UINT32 mode, UINT16 priority, UINT16 policy, const CHAR *name)
{
UINT32 count;
LosVmSpace *space = NULL;
LosVmPage *vmPage = NULL;
status_t status;
BOOL retVal = FALSE;
processCB->processMode = mode; //使用者态程序還是核心态程序
processCB->processStatus = OS_PROCESS_STATUS_INIT; //程序初始狀态
processCB->parentProcessID = OS_INVALID_VALUE; //爸爸程序,外面指定
processCB->threadGroupID = OS_INVALID_VALUE; //所屬線程組
processCB->priority = priority; //程序優先級
processCB->policy = policy; //排程算法 LOS_SCHED_RR
processCB->umask = OS_PROCESS_DEFAULT_UMASK; //掩碼
processCB->timerID = (timer_t)(UINTPTR)MAX_INVALID_TIMER_VID;
LOS_ListInit(&processCB->threadSiblingList);//初始化孩子任務/線程連結清單,上面挂的都是由此fork的孩子線程 見于 OsTaskCBInit LOS_ListTailInsert(&(processCB->threadSiblingList), &(taskCB->threadList));
LOS_ListInit(&processCB->childrenList); //初始化孩子程序連結清單,上面挂的都是由此fork的孩子程序 見于 OsCopyParent LOS_ListTailInsert(&parentProcessCB->childrenList, &childProcessCB->siblingList);
LOS_ListInit(&processCB->exitChildList); //初始化記錄退出孩子程序連結清單,上面挂的是哪些exit 見于 OsProcessNaturalExit LOS_ListTailInsert(&parentCB->exitChildList, &processCB->siblingList);
LOS_ListInit(&(processCB->waitList)); //初始化等待任務連結清單 上面挂的是處于等待的 見于 OsWaitInsertWaitLIstInOrder LOS_ListHeadInsert(&processCB->waitList, &runTask->pendList);
for (count = 0; count < OS_PRIORITY_QUEUE_NUM; ++count) { //根據 priority數 建立對應個數的隊列
LOS_ListInit(&processCB->threadPriQueueList[count]); //初始化一個個線程隊列,隊列中存放就緒狀态的線程/task
}//在鴻蒙核心中 task就是thread,在鴻蒙源碼分析系列篇中有詳細闡釋 見于 https://my.oschina.net/u/3751245
if (OsProcessIsUserMode(processCB)) {// 是否為使用者模式程序
space = LOS_MemAlloc(m_aucSysMem0, sizeof(LosVmSpace));//配置設定一個虛拟空間
if (space == NULL) {
PRINT_ERR("%s %d, alloc space failed\n", __FUNCTION__, __LINE__);
return LOS_ENOMEM;
}
VADDR_T *ttb = LOS_PhysPagesAllocContiguous(1);//配置設定一個實體頁用于存儲L1頁表 4G虛拟記憶體分成 (4096*1M)
if (ttb == NULL) {//這裡直接擷取實體頁ttb
PRINT_ERR("%s %d, alloc ttb or space failed\n", __FUNCTION__, __LINE__);
(VOID)LOS_MemFree(m_aucSysMem0, space);
return LOS_ENOMEM;
}
(VOID)memset_s(ttb, PAGE_SIZE, 0, PAGE_SIZE);//記憶體清0
retVal = OsUserVmSpaceInit(space, ttb);//初始化虛拟空間和程序mmu
vmPage = OsVmVaddrToPage(ttb);//通過虛拟位址拿到page
if ((retVal == FALSE) || (vmPage == NULL)) {//異常處理
PRINT_ERR("create space failed! ret: %d, vmPage: %#x\n", retVal, vmPage);
processCB->processStatus = OS_PROCESS_FLAG_UNUSED;//程序未使用,幹淨
(VOID)LOS_MemFree(m_aucSysMem0, space);//釋放虛拟空間
LOS_PhysPagesFreeContiguous(ttb, 1);//釋放實體頁,4K
return LOS_EAGAIN;
}
processCB->vmSpace = space;//設為程序虛拟空間
LOS_ListAdd(&processCB->vmSpace->archMmu.ptList, &(vmPage->node));//将空間映射頁表挂在 空間的mmu L1頁表, L1為表頭
} else {
processCB->vmSpace = LOS_GetKVmSpace();//核心共用一個虛拟空間,核心程序 常駐記憶體
}
#ifdef LOSCFG_SECURITY_VID
status = VidMapListInit(processCB);
if (status != LOS_OK) {
PRINT_ERR("VidMapListInit failed!\n");
return LOS_ENOMEM;
}
#endif
#ifdef LOSCFG_SECURITY_CAPABILITY
OsInitCapability(processCB);
#endif
if (OsSetProcessName(processCB, name) != LOS_OK) {
return LOS_ENOMEM;
}
return LOS_OK;
}
//拷貝一個Task過程
STATIC UINT32 OsCopyTask(UINT32 flags, LosProcessCB *childProcessCB, const CHAR *name, UINTPTR entry, UINT32 size)
{
LosTaskCB *childTaskCB = NULL;
TSK_INIT_PARAM_S childPara = { 0 };
UINT32 ret;
UINT32 intSave;
UINT32 taskID;
OsInitCopyTaskParam(childProcessCB, name, entry, size, &childPara);//初始化Task參數
ret = LOS_TaskCreateOnly(&taskID, &childPara);//隻建立任務,不排程
if (ret != LOS_OK) {
if (ret == LOS_ERRNO_TSK_TCB_UNAVAILABLE) {
return LOS_EAGAIN;
}
return LOS_ENOMEM;
}
childTaskCB = OS_TCB_FROM_TID(taskID);//通過taskId擷取task實體
childTaskCB->taskStatus = OsCurrTaskGet()->taskStatus;//任務狀态先同步,注意這裡是指派操作. ...01101001
if (childTaskCB->taskStatus & OS_TASK_STATUS_RUNNING) {//因隻能有一個運作的task,是以如果一樣要改4号位
childTaskCB->taskStatus &= ~OS_TASK_STATUS_RUNNING;//将四号位清0 ,變成 ...01100001
} else {//非運作狀态下會發生什麼?
if (OS_SCHEDULER_ACTIVE) {//克隆線程發生錯誤未運作
LOS_Panic("Clone thread status not running error status: 0x%x\n", childTaskCB->taskStatus);
}
childTaskCB->taskStatus &= ~OS_TASK_STATUS_UNUSED;//幹淨的Task
childProcessCB->priority = OS_PROCESS_PRIORITY_LOWEST;//程序設為最低優先級
}
if (OsProcessIsUserMode(childProcessCB)) {//是否是使用者程序
SCHEDULER_LOCK(intSave);
OsUserCloneParentStack(childTaskCB, OsCurrTaskGet());//拷貝目前任務上下文給新的任務
SCHEDULER_UNLOCK(intSave);
}
OS_TASK_PRI_QUEUE_ENQUEUE(childProcessCB, childTaskCB);//将task加入子程序的就緒隊列
childTaskCB->taskStatus |= OS_TASK_STATUS_READY;//任務狀态貼上就緒标簽
return LOS_OK;
}
//把父任務上下文克隆給子任務
LITE_OS_SEC_TEXT VOID OsUserCloneParentStack(LosTaskCB *childTaskCB, LosTaskCB *parentTaskCB)
{
TaskContext *context = (TaskContext *)childTaskCB->stackPointer;
VOID *cloneStack = (VOID *)(((UINTPTR)parentTaskCB->topOfStack + parentTaskCB->stackSize) - sizeof(TaskContext));
//cloneStack指向 TaskContext
LOS_ASSERT(parentTaskCB->taskStatus & OS_TASK_STATUS_RUNNING);//目前任務一定是正在運作的task
(VOID)memcpy_s(childTaskCB->stackPointer, sizeof(TaskContext), cloneStack, sizeof(TaskContext));//直接把任務上下文拷貝了一份
context->R[0] = 0;//R0寄存器為0,這個很重要, pid = fork() pid == 0 是子程序傳回.
}
- 系統調用是通過
的方式建立子程序的.具體有哪些建立方式如下:CLONE_SIGHAND
此處不展開細說,程序之間發送信号用于異步通訊,系列篇有專門的篇幅說信号(signal),請自行翻看.#define CLONE_VM 0x00000100 //子程序與父程序運作于相同的記憶體空間 #define CLONE_FS 0x00000200 //子程序與父程序共享相同的檔案系統,包括root、目前目錄、umask #define CLONE_FILES 0x00000400 //子程序與父程序共享相同的檔案描述符(file descriptor)表 #define CLONE_SIGHAND 0x00000800 //子程序與父程序共享相同的信号處理(signal handler)表 #define CLONE_PTRACE 0x00002000 //若父程序被trace,子程序也被trace #define CLONE_VFORK 0x00004000 //父程序被挂起,直至子程序釋放虛拟記憶體資源 #define CLONE_PARENT 0x00008000 //建立的子程序的父程序是調用者的父程序,新程序與建立它的程序成了“兄弟”而不是“父子” #define CLONE_THREAD 0x00010000 //Linux 2.4中增加以支援POSIX線程标準,子程序與父程序共享相同的線程群
- 可以看出fork的主體函數是
,先申請一個幹淨的PCB,相當于申請一個容器裝資源.OsCopyProcess
- 初始化這個容器
,OsForkInitPCB
先把容器打掃幹淨,虛拟空間,位址映射表(L1表),各種連結清單初始化好,為接下來的内容拷貝做好準備.OsInitPCB
-
把家族基因/關系傳遞給子程序,誰是你的老祖宗,你的七大姑八大姨是誰都得告訴你知道,這些都将挂到你已經初始化好的連結清單上.OsCopyParent
-
這個很重要,拷貝父程序目前執行的任務資料給子程序的新任務,系列篇中已經說過,真正讓CPU幹活的是任務(線程),是以子程序需要建立一個新任務OsCopyTask
來接受目前任務的資料,這個資料包括棧的資料,運作代碼段指向,LOS_TaskCreateOnly
将使用者态的上下文資料OsUserCloneParentStack
拷貝到子程序新任務的棧底位置, 也就是說新任務運作棧中此時隻有上下文的資料.而且有最最最重要的一句代碼TaskContext
強制性的将未來恢複上下文context->R[0] = 0;
寄存器的資料改成了0, 這意味着排程算法切到子程序的任務後, 任務幹的第一件事是恢複上下文,屆時R0
寄存器的值變成0,而R0
意味着什麼? 同時R0=0
寄存器的值也和父程序的一樣.這又意味着什麼?LR/SP
- 系列篇寄存器篇中以說過傳回值就是存在R0寄存器中,
,A拿B的傳回值隻認A()->B()
的資料,讀到什麼就是什麼傳回值,而R0寄存器值等于0,等同于獲得傳回值為0, 而LR寄存器所指向的指令是R0
, sp寄存器記錄了棧中的開始計算的位置,如此完全還原了父程序調用pid=傳回值
前的運作場景,唯一的差別是改變了fork()
寄存器的值,是以才有了R0
由此確定了這是子程序的傳回.這是pid = 0;//fork()的傳回值,注意子程序并沒有執行fork(),它隻是通過恢複上下文獲得了一個傳回值. if (pid == 0) { message = "This is the child\n"; n = 6; }
最精彩的部分.一定要好好了解.fork()
的代碼細節.會讓你醍醐灌頂,永生難忘.OsCopyTask``OsUserCloneParentStack
- 父程序的傳回是
是子程序的ID,任何子程序的ID是不可能等于0的,成功了隻能是大于0. 失敗了就是負數processID = child->processID;
return -ret;
-
用于指派各種資源,包括拷貝虛拟空間記憶體,拷貝打開的檔案清單,IPC等等.OsCopyProcessResources
-
設定子程序組和排程的準備工作,加入排程隊列,準備排程.OsChildSetProcessGroupAndSched
-
是個核間中斷,給所有CPU發送排程信号,讓所有CPU發生一次排程.由此父程序讓出CPU使用權,因為子程序的排程優先級和父程序是平級,而同級情況下子程序的任務已經插到就緒隊列的頭部位置LOS_MpSchedule
排在了父程序任務的前面,是以在沒有比他們更高優先級的程序和任務出現之前,下一次被排程到的任務就是子程序的任務.也就是在本篇開頭看到的OS_PROCESS_PRI_QUEUE_ENQUEUE
$ ./a.out This is the child This is the parent This is the child This is the parent This is the child This is the parent This is the child $ This is the child This is the child
- 以上為fork在鴻蒙核心的整個實作過程,務必結合系列篇其他篇了解,一次了解透徹,終生不忘.
百篇部落格分析.深挖核心地基
- 給鴻蒙核心源碼加注釋過程中,整理出以下文章。内容立足源碼,常以生活場景打比方盡可能多的将核心知識點置入某種場景,具有畫面感,容易了解記憶。說别人能聽得懂的話很重要! 百篇部落格絕不是百度教條式的在說一堆诘屈聱牙的概念,那沒什麼意思。更希望讓核心變得栩栩如生,倍感親切.确實有難度,自不量力,但已經出發,回頭已是不可能的了。 😛
- 與代碼有bug需不斷debug一樣,文章和注解内容會存在不少錯漏之處,請多包涵,但會反複修正,持續更新,v**.xx 代表文章序号和修改的次數,精雕細琢,言簡意赅,力求打造精品内容。
按功能子產品:
基礎工具 | 加載運作 | 程序管理 | 編譯建構 |
---|---|---|---|
雙向連結清單 位圖管理 用棧方式 定時器 原子操作 時間管理 | ELF格式 ELF解析 靜态連結 重定位 程序映像 | 程序概念 Fork 特殊程序 程序回收 信号生産 信号消費 Shell編輯 Shell解析 | 編譯環境 編譯過程 環境腳本 建構工具 gn應用 忍者ninja |
程序通訊 | 記憶體管理 | 前因後果 | 任務管理 |
自旋鎖 互斥鎖 信号量 事件控制 消息隊列 | 記憶體配置設定 記憶體彙編 記憶體映射 記憶體規則 實體記憶體 | 總目錄 排程故事 記憶體主奴 源碼注釋 源碼結構 靜态站點 | 時鐘任務 任務排程 排程隊列 排程機制 線程概念 并發并行 CPU 系統調用 任務切換 |
檔案系統 | 硬體架構 | ||
檔案概念 索引節點 挂載目錄 根檔案系統 字元裝置 VFS 檔案句柄 管道檔案 | 彙編基礎 彙編傳參 工作模式 寄存器 異常接管 彙編彙總 中斷切換 中斷概念 中斷管理 |
百萬漢字注解.精讀核心源碼
四大碼倉中文注解 . 定期同步官方代碼
鴻蒙研究站( weharmonyos ) | 每天死磕一點點,原創不易,歡迎轉載,請注明出處。若能支援點贊更好,感謝每一份支援。