中文字幕av专区_日韩电影在线播放_精品国产精品久久一区免费式_av在线免费观看网站

溫馨提示×

溫馨提示×

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

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

用C語言的Setjmp和Longjmp實現異常捕獲和協程

發布時間:2021-06-17 17:49:02 來源:億速云 閱讀:185 作者:chen 欄目:編程語言

本篇內容主要講解“用C語言的Setjmp和Longjmp實現異常捕獲和協程”,感興趣的朋友不妨來看看。本文介紹的方法操作簡單快捷,實用性強。下面就讓小編來帶大家學習“用C語言的Setjmp和Longjmp實現異常捕獲和協程”吧!

  •  一、前言

  • 二、函數語法介紹

    • 與 goto 語句比較

    • 與 fork 函數比較

    • 與 Python 語言中的 yield/resume 比較

  • 三、利用 setjmp/longjmp 實現異常捕獲

  • 四、利用 setjmp/longjmp 實現協程

  • 五、總結

一、前言

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

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

今天,我們就來掰扯掰扯這兩個函數。雖然在一般的程序中使用不上,但是在今后的某個場合,當你需要處理一些比較奇特的程序流程時,也許它們可以給你帶來意想不到的效果。

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

二、函數語法介紹

1. 最簡示例

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

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

執行結果:

用C語言的Setjmp和Longjmp實現異常捕獲和協程

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

用C語言的Setjmp和Longjmp實現異常捕獲和協程

2. 函數說明

首先來看下這個 2 個函數的簽名:

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 函數

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

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

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

longjmp 函數

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

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

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

小結:這 2 個函數是配合使用的,用來實現程序的跳轉。

3. setjmp:保存上下文信息

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

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

用C語言的Setjmp和Longjmp實現異常捕獲和協程

補充一下知識點:

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

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

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

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

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

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

還有一個更簡單的例子:服務器中的快照。快照的作用是什么?當服務器出現錯誤時,可以恢復到某個快照!

4. longjmp: 實現跳轉

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

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

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

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

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

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

用C語言的Setjmp和Longjmp實現異常捕獲和協程

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

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

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

fork 函數返回 0:代表這是新進程;

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

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

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

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

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

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

三、利用 setjmp/longjmp 實現異常捕獲

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

我們來演示一個最簡單的異常捕獲模型,代碼一共 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  // 除數為 0 } ErrorCode;  // 保存上下文的緩沖區 jmp_buf gExcptBuf;  // 可能發生異常的函數 typedef int (*pf)(int, int); int my_div(int a, int b) {     if (0 == b)     {         // 發生異常,跳轉到函數執行之前的位置         // 第2個參數是異常代碼         longjmp(gExcptBuf, ERR_DIV_BY_ZERO);     }     // 沒有異常,返回正確結果     return a / b; }  // 在這個函數中執行可能會出現異常的函數 int try(pf func, int a, int b) {     // 保存上下文,如果發生異常,將會跳入這里     int ret = setjmp(gExcptBuf);     if (0 == ret)     {         // 調用可能發生異常的哈數         func(a, b);         // 沒有發生異常         return ERR_OK;     }     else     {         // 發生了異常,ret 中是異常代碼         return ret;     } }  int main() {     int ret = try(my_div, 8, 0);     // 會發生異常     // int ret = try(my_div, 8, 2);  // 不會發生異常     if (ERR_OK == ret)     {         printf("try ok ! \n");     }     else     {         printf("try excepton. error = %d \n", ret);     }          return 0; }

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

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

用C語言的Setjmp和Longjmp實現異常捕獲和協程

四、利用 setjmp/longjmp 實現協程

1. 什么是協程

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

用C語言的Setjmp和Longjmp實現異常捕獲和協程

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

我們用生產者和消費者來簡單體會一下協程和線程的區別:

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

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

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

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

3. 協程中的生產者和消費者

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

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

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

4. C 語言中的協程實現

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

typedef int     BOOL; #define TRUE    1 #define FALSE   0  // 用來存儲主程和協程的上下文的數據結構 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); \     }  // 在協程中執行的函數 void coroutine_function(void *arg) {     while (TRUE)  // 死循環     {         printf("\n*** coroutine: working \n");         // 模擬耗時操作         for (int i = 0; i < 10; ++i)         {             fprintf(stderr, ".");             usleep(1000 * 200);         }         printf("\n*** coroutine: suspend \n");                  // 讓出 CPU         yield();     } }  // 啟動一個協程 // 參數1:func 在協程中執行的函數 // 參數2:func 需要的參數 typedef void (*pf)(void *); BOOL start_coroutine(pf func, void *arg) {     // 保存主程的跳轉點     if (0 == setjmp(gCtx.mainBuf))     {         func(arg); // 調用函數         return TRUE;     }      return FALSE; }  int main() {     // 啟動一個協程     start_coroutine(coroutine_function, NULL);          while (TRUE) // 死循環     {         printf("\n=== main: working \n");          // 模擬耗時操作         for (int i = 0; i < 10; ++i)         {             fprintf(stderr, ".");             usleep(1000 * 200);         }          printf("\n=== main: suspend \n");                  // 放棄 CPU,讓協程執行         resume();     }      return 0; }

打印信息如下:

用C語言的Setjmp和Longjmp實現異常捕獲和協程

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

五、總結

這篇文章的重點是介紹 setjmp/longjmp 的語法和使用場景,在某些需求場景中,能達到事半功倍的效果。

當然,你還可以發揮想象力,通過執行序列的跳轉來實現更加花哨的功能,一切皆有可能!

到此,相信大家對“用C語言的Setjmp和Longjmp實現異常捕獲和協程”有了更深的了解,不妨來實際操作一番吧!這里是億速云網站,更多相關內容可以進入相關頻道進行查詢,關注我們,繼續學習!

向AI問一下細節

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

AI

开平市| 遂宁市| 东台市| 广宗县| 雷州市| 凌云县| 保亭| 合江县| 梁平县| 德兴市| 平昌县| 三原县| 海伦市| 麟游县| 宜丰县| 宕昌县| 深州市| 驻马店市| 墨江| 垦利县| 天峨县| 辽阳市| 都匀市| 石林| 古交市| 仙桃市| 甘南县| 平南县| 苏尼特左旗| 沧州市| 清河县| 宜都市| 仪征市| 鸡泽县| 常德市| 旺苍县| 上思县| 江源县| 阜平县| 资源县| 盘锦市|