溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

怎么用C語言的Setjmp和Longjmp實現(xiàn)異常捕獲和協(xié)程

發(fā)布時間:2022-04-02 14:08:31 來源:億速云 閱讀:306 作者:iii 欄目:開發(fā)技術

這篇文章主要介紹了怎么用C語言的Setjmp和Longjmp實現(xiàn)異常捕獲和協(xié)程的相關知識,內容詳細易懂,操作簡單快捷,具有一定借鑒價值,相信大家閱讀完這篇怎么用C語言的Setjmp和Longjmp實現(xiàn)異常捕獲和協(xié)程文章都會有所收獲,下面我們一起來看看吧。

一、前言

在 C 標準庫中,有兩個威力很猛的函數(shù):setjmp 和  longjmp,不知道各位小伙伴在代碼中是否使用過?我問了身體的幾位同事,一部分人不認識這兩個函數(shù),有一部分人知道這個函數(shù),但從來沒有使用過。

從知識點范圍來看,這兩個函數(shù)的功能比較單純,一個簡單的示例代碼就能說清楚了。但是,我們需要從這個知識點進行發(fā)散、思考,在不同的維度上,把這個知識點與這個編程語言中其它類似的知識進行聯(lián)想、對比;與其他編程語言中類似的概念進行比較;然后再思考這個知識點可以使用在哪些場合,別人是怎么來使用它的。

例如:我們會把 setjmp/longjmp 與 goto 語句進行功能上的比較;與 fork 函數(shù)從返回值上進行類比;與 Python/Lua  語言中的協(xié)程進行使用場景上的比較。

二、函數(shù)語法介紹

1. 最簡示例

先不講道理,直接看一下這個最簡單的示例代碼,看不懂也沒關系,混個臉熟:

int main() {     // 一個緩沖區(qū),用來暫存環(huán)境變量     jmp_buf buf;     printf("line1 \n");          // 保存此刻的上下文信息     int ret = setjmp(buf);     printf("ret = %d \n", ret);          // 檢查返回值類型     if (0 == ret)     {         // 返回值0:說明是正常的函數(shù)調用返回         printf("line2 \n");                  // 主動跳轉到 setjmp 那條語句處         longjmp(buf, 1);     }     else     {         // 返回值非0:說明是從遠程跳轉過來的         printf("line3 \n");     }     printf("line4 \n");     return 0; }

執(zhí)行結果:

怎么用C語言的Setjmp和Longjmp實現(xiàn)異常捕獲和協(xié)程

執(zhí)行順序如下(如果不明白就不要深究,看完下面的解釋再回過頭來看):

怎么用C語言的Setjmp和Longjmp實現(xiàn)異常捕獲和協(xié)程

2. 函數(shù)說明

首先來看下這個 2 個函數(shù)的簽名:

int setjmp(jmp_buf env); void longjmp(jmp_buf env, int value);

它們都在頭文件 setjmp.h 中進行聲明,維基百科的解釋如下:

  • setjmp: Sets up the local jmp_buf buffer and initializes it for the jump.  This routine saves the program's calling environment in the environment buffer  specified by the env argument for later use by longjmp. If the return is from a  direct invocation, setjmp returns 0. If the return is from a call to longjmp,  setjmp returns a nonzero value。

  • longjmp:Restores the context of the environment buffer env that was saved by  invocation of the setjmp routine in the same invocation of the program. Invoking  longjmp from a nested signal handler is undefined. The value specified by value  is passed from longjmp to setjmp. After longjmp is completed, program execution  continues as if the corresponding invocation of setjmp had just returned. If the  value passed to longjmp is 0, setjmp will behave as if it had returned 1;  otherwise, it will behave as if it had returned value。

下面我再用自己的理解把上面這段英文解釋一下:

setjmp 函數(shù)

功能:把執(zhí)行這個函數(shù)時的各種上下文信息保存起來,主要就是一些寄存器的值;

參數(shù):用來保存上下文信息的緩沖區(qū),相當于把當前的上下文信息拍一個快照保存起來;

返回值:有 2 種返回值,如果是直接調用 setjmp 函數(shù)時,返回值是 0;如果是調用 longjmp 函數(shù)跳轉過來時,返回值是非 0;  這里可以與創(chuàng)建進程的函數(shù) fork 進行一下類比。

longjmp 函數(shù)

功能:跳轉到參數(shù) env 緩沖區(qū)中保存的上下文(快照)中去執(zhí)行;

參數(shù):env 參數(shù)指定跳轉到哪個上下文中(快照)去執(zhí)行, value 用來給 setjmp 函數(shù)提供返回判斷信息,也就是說:調用 longjmp  函數(shù)時,這個參數(shù) value 將會作為 setjmp 函數(shù)的返回值;

返回值:沒有返回值。因為在調用這個函數(shù)時,就直接跳轉到其他地方的代碼去執(zhí)行了,不會再回來了。

小結:這 2 個函數(shù)是配合使用的,用來實現(xiàn)程序的跳轉。

3. setjmp:保存上下文信息

我們知道,C 代碼在編譯成二進制文件之后,在執(zhí)行時被加載到內存中,CPU 按照順序到代碼段取出每一條指令來執(zhí)行。在 CPU  中有很多個寄存器,用來保存當前的執(zhí)行環(huán)境,比如:代碼段寄存器CS、指令偏移量寄存器IP,當然了還有其他很多其它寄存器,我們把這個執(zhí)行環(huán)境稱作上下文。

CPU 在獲取下一條執(zhí)行指令時,通過 CS 和 IP 這 2 個寄存器就能獲取到需要執(zhí)行的指令,如下圖:

怎么用C語言的Setjmp和Longjmp實現(xiàn)異常捕獲和協(xié)程

補充一下知識點:

上圖中,把代碼段寄存器 CS 當做一個基地址來看待了,也就是說:CS 指向代碼段在內存中的開始地址,IP  寄存器代表下一個要執(zhí)行的指令地址距離這個基地址的偏移量。因此每次取指令時,只需要把這 2 個寄存器中的值相加,就得到了指令的地址;

其實,在 x86 平臺上,代碼段寄存器 CS 并不是一個基地址,而是一個選擇子。在操作系統(tǒng)的某個地方有一個表格,這個表格里存儲了代碼段真正的開始地址,而  CS 寄存器中 只是存儲了一個索引值,這個索引值指向這個表格中的某個表項,這里涉及到虛擬內存的相關知識了;

IP 寄存器在獲取一條指令之后,自動往下移動到下一個指令的開始位置,至于移動多少個字節(jié),那就要看當前取出的這條指令占用了多少個字節(jié)。

CPU 是一個大傻瓜,它沒有任何的想法,我們讓它干什么,它就干什么。比如取指令:我們只要設置 CS 和 IP 寄存器,CPU 就用這 2  個寄存器里的值去獲取指令。如果把這 2 個寄存器設置為一個錯誤的值,CPU 也會傻不拉幾的去取指令,只不過在執(zhí)行時就會崩潰。

我們可以簡單的把這些寄存器信息理解為上下文信息,CPU 就根據(jù)這些上下文信息來執(zhí)行。因此,C 語言為我們準備了 setjmp  這個庫函數(shù)來把當前的上下文信息保存起來,暫時存儲到一個緩沖區(qū)中。

保存的目的是什么?為了在以后可以恢復到當前這個地方繼續(xù)執(zhí)行。

還有一個更簡單的例子:服務器中的快照??煺盏淖饔檬鞘裁?當服務器出現(xiàn)錯誤時,可以恢復到某個快照!

4. longjmp: 實現(xiàn)跳轉

說到跳轉,腦袋中立刻跳出的概念就是 goto 語句,我發(fā)現(xiàn)很多教程都對 goto  語句很有意見,認為在代碼中應該盡量不要使用它。這樣的觀點出發(fā)點是好的:如果 goto 使用太多,會影響對代碼執(zhí)行順序的理解。

但是如果看一下 Linux 內核的代碼,可以發(fā)現(xiàn)很多的 goto 語句。還是那句話:在代碼維護和執(zhí)行效率上要尋找一個平衡點。

跳轉改變了程序的執(zhí)行序列,goto 語句只能在函數(shù)內部進行跳轉,如果是跨函數(shù)它就無能為力了。

因此,C 語言中為我們提供了 longjmp 函數(shù)來實現(xiàn)遠程跳轉,從它的名字就可以額看出來,也就是說可以跨函數(shù)跳轉。

從 CPU 的角度看,所謂的跳轉就是把上下文中的各種寄存器設置為某個時刻的快照,很顯然,上面的 setjmp  函數(shù)中,已經把那個時刻的上下文信息(快照)存儲到一個臨時緩沖區(qū)中了,如果要跳轉到那個地方去接著執(zhí)行,直接告訴 CPU 就行了。

怎么告訴 CPU 呢?就是把臨時緩沖區(qū)中的這些寄存器信息覆蓋掉 CPU 中使用的那些寄存器即可。

怎么用C語言的Setjmp和Longjmp實現(xiàn)異常捕獲和協(xié)程

5. setjmp:返回類型和返回值

在某些需要多進程的程序中,我們經常使用 fork 函數(shù)來從當前的進程中"孵化"一個新的進程,這個新進程從 fork 這個函數(shù)的下一條語句開始執(zhí)行。

對于主進程來說,調用 fork 函數(shù)之后返回,也是繼續(xù)執(zhí)行下一條語句,那么如何來區(qū)分是主進程還是新進程呢? fork  函數(shù)提供了一個返回值給我們來進行區(qū)分:

fork 函數(shù)返回 0:代表這是新進程;

fork 函數(shù)返回非 0:代表是原來的主進程,返回數(shù)值是新進程的進程號。

類似的,setjmp 函數(shù)也有不同的返回類型。也許用返回類型來表述不太準確,可以這樣理解:從 setjmp 函數(shù)返回,一共有 2 個場景:

主動調用 setjmp 時:返回 0,主動調用的目的是為了保存上下文,建立快照。

通過 longjmp 跳轉過來時:返回非 0,此時的返回值是由 longjmp 的第二個參數(shù)來指定的。

根據(jù)以上這 2 種不同的值,我們就可以進行不同的分支處理了。當通過 longjmp 跳轉返回的時候,可以根據(jù)實際場景,返回不同的非 0 值。有過  Python、Lua 等腳本語言編程經驗的小伙伴,是不是想到了 yield/resume 函數(shù)?它們在參數(shù)、返回值上的外在表現(xiàn)是一樣的!

小結:到這里,基本上把 setjmp/longjmp 這 2  個函數(shù)的使用方法講完了,不知道我描述的是否足夠清楚。此時,再看一下文章開頭的示例代碼,應該一目了然了。

三、利用 setjmp/longjmp 實現(xiàn)異常捕獲

既然 C  函數(shù)庫給我們提供了這個工具,那就肯定存在一定的使用場景。異常捕獲在一些高級語言中(Java/C++),直接在語法層面進行了支持,一般就是 try-catch  語句,但是在 C 語言中需要自己去實現(xiàn)。

我們來演示一個最簡單的異常捕獲模型,代碼一共 56 行:

#include <unistd.h> #include <stdio.h> #include <stdlib.h> #include <setjmp.h>  typedef int     BOOL; #define TRUE    1 #define FALSE   0  // 枚舉:錯誤代碼 typedef enum _ErrorCode_ {     ERR_OK = 100,         // 沒有錯誤     ERR_DIV_BY_ZERO = -1  // 除數(shù)為 0 } ErrorCode;  // 保存上下文的緩沖區(qū) jmp_buf gExcptBuf;  // 可能發(fā)生異常的函數(shù) typedef int (*pf)(int, int); int my_div(int a, int b) {     if (0 == b)     {         // 發(fā)生異常,跳轉到函數(shù)執(zhí)行之前的位置         // 第2個參數(shù)是異常代碼         longjmp(gExcptBuf, ERR_DIV_BY_ZERO);     }     // 沒有異常,返回正確結果     return a / b; }  // 在這個函數(shù)中執(zhí)行可能會出現(xiàn)異常的函數(shù) int try(pf func, int a, int b) {     // 保存上下文,如果發(fā)生異常,將會跳入這里     int ret = setjmp(gExcptBuf);     if (0 == ret)     {         // 調用可能發(fā)生異常的哈數(shù)         func(a, b);         // 沒有發(fā)生異常         return ERR_OK;     }     else     {         // 發(fā)生了異常,ret 中是異常代碼         return ret;     } }  int main() {     int ret = try(my_div, 8, 0);     // 會發(fā)生異常     // int ret = try(my_div, 8, 2);  // 不會發(fā)生異常     if (ERR_OK == ret)     {         printf("try ok ! \n");     }     else     {         printf("try excepton. error = %d \n", ret);     }          return 0; }

代碼就不需要詳細說明了,直接看代碼中的注釋即可明白。這個代碼僅僅是示意性的,在生產代碼中肯定需要更完善的包裝才能使用。

有一點需要注意:setjmp/longjmp 僅僅是改變了程序的執(zhí)行順序,應用程序自己的一些數(shù)據(jù)如果需要回滾的話,需要我們自己手動處理。

怎么用C語言的Setjmp和Longjmp實現(xiàn)異常捕獲和協(xié)程

四、利用 setjmp/longjmp 實現(xiàn)協(xié)程

1. 什么是協(xié)程

在 C 程序中,如果需要并發(fā)執(zhí)行的序列一般都是用線程來實現(xiàn)的,那么什么是協(xié)程呢?維基百科對于協(xié)程的解釋是:

怎么用C語言的Setjmp和Longjmp實現(xiàn)異常捕獲和協(xié)程

更詳細的信息在這個頁面 協(xié)程,網頁中具體描述了協(xié)程與線程、生成器的比較,各種語言中的實現(xiàn)機制。

我們用生產者和消費者來簡單體會一下協(xié)程和線程的區(qū)別:

2. 線程中的生產者和消費者

生產者和消費者是 2 個并行執(zhí)行的序列,通常用 2 個線程來執(zhí)行;

生產者在生產商品時,消費者處于等待狀態(tài)(阻塞)。生產完成后,通過信號量通知消費者去消費商品;

消費者在消費商品時,生產者處于等待狀態(tài)(阻塞)。消費結束后,通過信號量通知生產者繼續(xù)生產商品。

3. 協(xié)程中的生產者和消費者

生產者和消費者在同一個執(zhí)行序列中執(zhí)行,通過執(zhí)行序列的跳轉來交替執(zhí)行;

生產者在生產商品之后,放棄 CPU,讓消費者執(zhí)行;

消費者在消費商品之后,放棄 CPU,讓生產者執(zhí)行;

4. C 語言中的協(xié)程實現(xiàn)

這里給出一個最最簡單的模型,通過 setjmp/longjmp  來實現(xiàn)協(xié)程的機制,主要是目的是來理解協(xié)程的執(zhí)行序列,沒有解決參數(shù)和返回值的傳遞問題。

typedef int     BOOL; #define TRUE    1 #define FALSE   0  // 用來存儲主程和協(xié)程的上下文的數(shù)據(jù)結構 typedef struct _Context_ {     jmp_buf mainBuf;     jmp_buf coBuf; } Context;  // 上下文全局變量 Context gCtx;  // 恢復 #define resume() \     if (0 == setjmp(gCtx.mainBuf)) \     { \         longjmp(gCtx.coBuf, 1); \     }  // 掛起 #define yield() \     if (0 == setjmp(gCtx.coBuf)) \     { \         longjmp(gCtx.mainBuf, 1); \     }  // 在協(xié)程中執(zhí)行的函數(shù) void coroutine_function(void *arg) {     while (TRUE)  // 死循環(huán)     {         printf("\n*** coroutine: working \n");         // 模擬耗時操作         for (int i = 0; i < 10; ++i)         {             fprintf(stderr, ".");             usleep(1000 * 200);         }         printf("\n*** coroutine: suspend \n");                  // 讓出 CPU         yield();     } }  // 啟動一個協(xié)程 // 參數(shù)1:func 在協(xié)程中執(zhí)行的函數(shù) // 參數(shù)2:func 需要的參數(shù) typedef void (*pf)(void *); BOOL start_coroutine(pf func, void *arg) {     // 保存主程的跳轉點     if (0 == setjmp(gCtx.mainBuf))     {         func(arg); // 調用函數(shù)         return TRUE;     }      return FALSE; }  int main() {     // 啟動一個協(xié)程     start_coroutine(coroutine_function, NULL);          while (TRUE) // 死循環(huán)     {         printf("\n=== main: working \n");          // 模擬耗時操作         for (int i = 0; i < 10; ++i)         {             fprintf(stderr, ".");             usleep(1000 * 200);         }          printf("\n=== main: suspend \n");                  // 放棄 CPU,讓協(xié)程執(zhí)行         resume();     }      return 0; }

打印信息如下:

怎么用C語言的Setjmp和Longjmp實現(xiàn)異常捕獲和協(xié)程

如果想深入研究 C 語言中的協(xié)程實現(xiàn),可以看一下達夫設備這個概念,其中利用 goto 和 switch  語句來實現(xiàn)分支跳轉,其中使用的語法比較怪異、但是合法。

關于“怎么用C語言的Setjmp和Longjmp實現(xiàn)異常捕獲和協(xié)程”這篇文章的內容就介紹到這里,感謝各位的閱讀!相信大家對“怎么用C語言的Setjmp和Longjmp實現(xiàn)異常捕獲和協(xié)程”知識都有一定的了解,大家如果還想學習更多知識,歡迎關注億速云行業(yè)資訊頻道。

向AI問一下細節(jié)

免責聲明:本站發(fā)布的內容(圖片、視頻和文字)以原創(chuàng)、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關證據(jù),一經查實,將立刻刪除涉嫌侵權內容。

AI