點擊上方 藍色字體 了解更多的嵌入式編程實用技能。
如果你覺得該文章對你有幫助,歡迎點贊+關注
setjmp和longjmp是C語言標準庫頭文件中提供的函數。它們的功能是實現非局部跳轉,可以在程序的不同位置之間進行跳轉,類似于goto語句的擴展。這種非局部跳轉的能力為我們構建查詢式協作多任務系統提供了基礎。
實現前先簡單了解一下相關知識,方便后續開展實現。
setjmp函數:用于保存當前程序狀態,創建一個可以供后續longjmp函數跳轉的上下文環境。
longjmp函數:實現了對保存的上下文環境的跳轉操作。通過傳遞之前由setjmp函數保存的jmp_buf標識符,longjmp函數會將程序的狀態還原到對應的上下文環境,并且會返回到setjmp處繼續執行。
在調用
setjmp時,程序會記錄當前的程序計數器、寄存器和堆棧等狀態信息,并將這些信息保存在一個jmp_buf結構中。同時,setjmp函數返回0作為普通調用的返回值,并將jmp_buf作為標識符存儲起來。不同平臺的
jmp_buf的類型定義不一樣,因為不同平臺的相關寄存器等不一樣,因此占用的大小也不同。
棧指針:每個任務在運行時都有一個棧指針,指向其棧的頂部。任務切換時,需要保存這個指針(jmp_buf會保存),以便在任務恢復時能夠正確訪問該任務的棧數據。
局部變量和返回地址:棧用于存儲任務的局部變量、函數參數和返回地址。在上下文切換時,這些信息也需要被保存,以確保任務能夠在恢復時繼續執行。
獨立棧空間:每個任務都有自己的棧,確保任務之間的局部變量和狀態不會相互干擾。這種隔離使得并發執行的任務能夠獨立運行,提高系統的穩定性。
這里需要用到一點點匯編,即設置棧頂的位置,不同平臺使用的匯編不一樣,這里可以在網上查到或者提供的demo中也能找到,只需要一條語句即可。
如 :
x86 平臺:
#define?COT_OS_SET_STACK(p)????????__asm__?volatile("mov?%0,?%%rsp"?:?:?"r"?(p)?:?"memory");
stm32
#define?COT_OS_SET_STACK(p)????????__set_MSP(p);
利用setjmp和longjmp實現一個任務調度系統(協程),setjmp用于保存當前程序的執行環境,而longjmp用于跳轉到之前保存的執行環境。
具體需要實現三個核心功能。
初始化任務相關變量:申請相關內存,后續儲存任務棧信息等
保存新任務的入口環境:設置新任務棧頂指針后,保存該環境,方便后續任務啟動時從這里開始執行
將新的任務添加到任務列表:任務調度使用
保存當前啟動函數的執行環境:當所有任務都結束后還可以跳轉到這退出該函數
跳轉到第一個任務函數的入口執行環境,開始執行任務
更新保存當前任務函數此時的執行環境:下次任務切換運行時可以恢復到當前位置繼續往下執行
查詢就緒任務并跳轉到就緒任務函數的入口執行環境或者更新后的執行環境
任務函數的流程走向圖:

typedef?struct?stTCB
{
????uint8_t?state;
????uint32_t?nextRunTime;
????jmp_buf?env;
????cotOsTask_f?pfnOsTaskEnter;
????struct?stTCB?*pNext;
}?TCB_t;
#define?COMMON_TASK_INTI????????????????0
#define?COMMON_TASK_RUN?????????????????1
#define?MAIN_TASK_INTI??????????????????0
#define?MAIN_TASK_EXIT??????????????????1
#define?TASK_STATUS_READY???????????????0??//?就緒
#define?TASK_STATUS_RUNNING?????????????1??//?運行
#define?TASK_STATUS_SUSPEND?????????????2??//?掛起
#define?TASK_STATUS_DELETED?????????????3??//?刪除
在函數中,設置新的棧頂后,由于還需要函數中定義的變量,為了防止設置新的堆棧后相關變量生命周期失效,需要使用static修飾定義,保證其生命周期。
cotOsTask_t?cotOs_CreatTask(cotOsTask_f?pfnOsTaskEnter,?void?*pStack,?size_t?stackSize)
{
????//?防止設置新的堆棧后該變量生命周期失效
????static?TCB_t?*s_pNewTCB?=?NULL;
????static?jmp_buf?s_creatTaskEnv;
????if?(pStack?==?NULL?||?stackSize?==?0)
????{
????????return?NULL;
????}
????s_pNewTCB?=?CreatTCB(&sg_OsInfo);
????if?(NULL?==?s_pNewTCB)
????{
????????return?NULL;
????}
????s_pNewTCB->pfnOsTaskEnter?=?pfnOsTaskEnter;
????s_pNewTCB->pNext?=?NULL;
????s_pNewTCB->state?=?TASK_STATUS_READY;
????s_pNewTCB->nextRunTime?=?0;
????if?(0?==?setjmp(s_creatTaskEnv))
????{
????????COT_OS_SET_STACK(((size_t)pStack?+?stackSize));
????????if?(COMMON_TASK_INTI?==?setjmp(s_pNewTCB->env))
????????{
????????????//?設置新的棧頂后記錄創建任務的入口后返回原來的任務棧繼續運行
????????????longjmp(s_creatTaskEnv,?1);
????????}
????????else
????????{
????????????//?任務入口位置
????????????sg_OsInfo.pCurTCB->state?=?TASK_STATUS_RUNNING;
????????????sg_OsInfo.pCurTCB->pfnOsTaskEnter(sg_OsInfo.pCurTCB->param);
????????????sg_OsInfo.pCurTCB->state?=?TASK_STATUS_DELETED;
????????????DestoryTask(&sg_OsInfo,?sg_OsInfo.pCurTCB);
????????????if?(GetTaskNum(&sg_OsInfo)?>?0)
????????????{
????????????????JumpNextTask(&sg_OsInfo);
????????????}
????????????else
????????????{
????????????????//?沒有任務則返回到啟動任務的位置,可以退出
????????????????longjmp(sg_OsInfo.env,?MAIN_TASK_EXIT);
????????????}
????????}
????}
????AddToTCBTaskList(&sg_OsInfo,?s_pNewTCB);
????return?s_pNewTCB;
}
啟動任務,保存該位置的執行環境,方便所有任務退出后這里可以正常退出函數。
int?cotOs_Start(void)
{
????if?(sg_OsInfo.pTCBList?==?NULL)
????{
????????return?-1;
????}
????int?ret?=?setjmp(sg_OsInfo.env);
????if?(MAIN_TASK_INTI?==?ret)
????{
????????sg_OsInfo.pCurTCB?=?sg_OsInfo.pTCBList;
????????longjmp(sg_OsInfo.pCurTCB->env,?COMMON_TASK_RUN);
????}
????//?退出
????return?0;
}
任務休眠,則更新當前執行環境,并查詢可運行的函數進行跳轉
void?cotOs_Wait(uint32_t?time)
{
????sg_OsInfo.pCurTCB->nextRunTime?=?sg_OsInfo.pfnGetTimerMs()?+?time;
????sg_OsInfo.pCurTCB->state?=?TASK_STATUS_SUSPEND;
????if?(COMMON_TASK_RUN?!=?setjmp(sg_OsInfo.pCurTCB->env))
????{
????????JumpNextTask(&sg_OsInfo);
????}
}
上述實現了基本功能,要求每個任務都有自己獨立的棧空間。
為了適應內存資源少的平臺,可以增加共享棧的任務調度,即多個任務使用同一個棧空間運行各自的任務。
共享棧任務的核心有:
任務在運行時獨享該共享棧:單線程運行的,只有待該任務休眠時則釋放該棧空間給到下一個該類型的任務獨享運行。
每個任務都有自己的備用棧:主要用來儲存任務休眠前時儲存在共享棧的數據,不過該備用棧的大小較小,只需要幾十或者上百字節即可。
使用其他獨立棧切換共享棧任務:共享棧任務互相切換時,由于需要將備份棧的數據恢復到共享棧空間,為了防止破環當前任務切換使用的棧數據,需要跳轉到獨立的棧空間中進行數據恢復并切換。
輕量級任務:由于備用棧的空間較小,因此要求該類型任務盡量不在入口函數中定義局部變量(可以定義static修飾的變量,不會占用棧空間),同時只能在入口函數這一層中去休眠任務(在嵌套函數休眠,所使用的棧空間更多,那么需要備份的棧數據就更多)
初始化任務相關變量:申請相關內存,后續儲存任務棧信息等
保存新任務的入口環境:設置新任務棧頂指針后,保存該環境,方便后續任務啟 動時從這里開始執行
將新的任務添加到任務列表:任務調度使用
新增:
區分共享棧和獨立棧的處理
cotOsTask_t?cotOs_CreatTask(cotOsTask_f?pfnOsTaskEnter,?CotOSStackType_e?eStackType,?void?*pStack,?size_t?stackSize)
{
????//?防止設置新的堆棧后該變量生命周期失效
????static?TCB_t?*s_pNewTCB?=?NULL;
????static?jmp_buf?s_creatTaskEnv;
????if?(eStackType?==?COT_OS_UNIQUE_STACK?&&?(pStack?==?NULL?||?stackSize?==?0))
????{
????????return?NULL;
????}
????if?(eStackType?==?COT_OS_SHARED_STACK?&&?sg_OsInfo.sharedStackTop?==?0)
????{
????????return?NULL;
????}
????s_pNewTCB?=?CreatTCB(&sg_OsInfo);
????if?(NULL?==?s_pNewTCB)
????{
????????return?NULL;
????}
????s_pNewTCB->pfnOsTaskEnter?=?pfnOsTaskEnter;
????s_pNewTCB->pNext?=?NULL;
????s_pNewTCB->state?=?TASK_STATUS_READY;
????s_pNewTCB->nextRunTime?=?0;
????s_pNewTCB->pBakStack?=?eStackType?==?COT_OS_SHARED_STACK???CreatTCBStack(&sg_OsInfo)?:?NULL;
????//?這里先判斷類型再保存環境,防止先保存環境后再去判斷類型分別設置棧頂,導致入棧數據錯亂引發異常問題
????if?(eStackType?==?COT_OS_UNIQUE_STACK)
????{
????????if?(0?==?setjmp(s_creatTaskEnv))
????????{
????????????COT_OS_SET_STACK(((size_t)pStack?+?stackSize));
????????????if?(COMMON_TASK_INTI?==?setjmp(s_pNewTCB->env))
????????????{
????????????????longjmp(s_creatTaskEnv,?1);
????????????}
????????????else
????????????{
????????????????RunTask(&sg_OsInfo);
????????????}
????????}
????}
????else
????{
????????if?(s_pNewTCB->pBakStack?==?NULL)
????????{
????????????DestroyTCB(&sg_OsInfo,?sg_OsInfo.pCurTCB);
????????????return?NULL;
????????}
????????if?(0?==?setjmp(s_creatTaskEnv))
????????{
????????????COT_OS_SET_STACK(sg_OsInfo.sharedStackTop);
????????????if?(COMMON_TASK_INTI?==?setjmp(s_pNewTCB->env))
????????????{
????????????????longjmp(s_creatTaskEnv,?1);
????????????}
????????????else
????????????{
????????????????RunTask(&sg_OsInfo);
????????????}
????????}
????}
????AddToTCBTaskList(&sg_OsInfo,?s_pNewTCB);
????return?s_pNewTCB;
}
保存當前啟動函數的執行環境:當所有任務都結束后還可以跳轉到這退出該函數
跳轉到第一個任務函數的入口執行環境,開始執行任務
新增:
共享棧任務需要運行時,先跳轉到該位置,利用main主任務的未使用的棧空間進行任務切換(盡量充分利用未使用的棧空間),先將即將執行的共享棧任務備份數據恢復到共享棧上,然后跳轉過去
這里主要防止共享棧任務切換到下一個共享棧任務,還沒切換時共享棧就被覆蓋破壞導致程序運行異常的問題。
int?cotOs_Start(void)
{
????if?(sg_OsInfo.pTCBList?==?NULL?||?sg_OsInfo.pfnGetTimerMs?==?NULL)
????{
????????return?-1;
????}
????int?ret?=?setjmp(sg_OsInfo.env);
????if?(MAIN_TASK_INTI?==?ret)
????{
????????sg_OsInfo.pCurTCB?=?sg_OsInfo.pTCBList;
????????longjmp(sg_OsInfo.pCurTCB->env,?COMMON_TASK_RUN);
????}
????else?if?(MAIN_TASK_JUMP_SHARED_TASK?==?ret)
????{
????????TcbMemcpy((uint8_t?*)(sg_OsInfo.sharedStackTop?-?COT_OS_MAX_SHARED_BAK_STACK_SIZE),?
????????????sg_OsInfo.pCurTCB->pBakStack,??COT_OS_MAX_SHARED_BAK_STACK_SIZE);
????????longjmp(sg_OsInfo.pCurTCB->env,?COMMON_TASK_RUN);
????}
????return?0;
}
更新保存當前任務函數此時的執行環境:下次任務切換運行時可以恢復到當前位置繼續往下執行
查詢就緒任務并跳轉到就緒任務函數的入口執行環境或者更新后的執行環境
新增:
查詢前如果當前任務是共享棧任務,則先將棧空間保存到該任務的備份棧空間。
void?cotOs_Wait(uint32_t?time)
{
????sg_OsInfo.pCurTCB->nextRunTime?=?sg_OsInfo.pfnGetTimerMs()?+?time;
????sg_OsInfo.pCurTCB->pCondition?=?NULL;
????sg_OsInfo.pCurTCB->state?=?TASK_STATUS_SUSPEND;
????if?(COMMON_TASK_RUN?!=?setjmp(sg_OsInfo.pCurTCB->env))
????{
????????if?(sg_OsInfo.pCurTCB->pBakStack?!=?NULL)
????????{
????????????TcbMemcpy(sg_OsInfo.pCurTCB->pBakStack,?
????????????????(uint8_t?*)(sg_OsInfo.sharedStackTop?-?COT_OS_MAX_SHARED_BAK_STACK_SIZE),?COT_OS_MAX_SHARED_BAK_STACK_SIZE);
????????}
????????JumpNextTask(&sg_OsInfo);
????}
}
至此,已完成一個任務調度系統的實現。
想看完整代碼實現的,程序源碼:查詢協作式多任務系統