十年網(wǎng)站開(kāi)發(fā)經(jīng)驗(yàn) + 多家企業(yè)客戶 + 靠譜的建站團(tuán)隊(duì)
量身定制 + 運(yùn)營(yíng)維護(hù)+專業(yè)推廣+無(wú)憂售后,網(wǎng)站問(wèn)題一站解決
一、前言

為肇源等地區(qū)用戶提供了全套網(wǎng)頁(yè)設(shè)計(jì)制作服務(wù),及肇源網(wǎng)站建設(shè)行業(yè)解決方案。主營(yíng)業(yè)務(wù)為網(wǎng)站建設(shè)、成都網(wǎng)站建設(shè)、肇源網(wǎng)站設(shè)計(jì),以傳統(tǒng)方式定制建設(shè)網(wǎng)站,并提供域名空間備案等一條龍服務(wù),秉承以專業(yè)、用心的態(tài)度為用戶提供真誠(chéng)的服務(wù)。我們深信只要達(dá)到每一位用戶的要求,就會(huì)得到認(rèn)可,從而選擇與我們長(zhǎng)期合作。這樣,我們也可以走得更遠(yuǎn)!
在 C 標(biāo)準(zhǔn)庫(kù)中,有兩個(gè)威力很猛的函數(shù):setjmp 和 longjmp,不知道各位小伙伴在代碼中是否使用過(guò)?我問(wèn)了身體的幾位同事,一部分人不認(rèn)識(shí)這兩個(gè)函數(shù),有一部分人知道這個(gè)函數(shù),但從來(lái)沒(méi)有使用過(guò)。
從知識(shí)點(diǎn)范圍來(lái)看,這兩個(gè)函數(shù)的功能比較單純,一個(gè)簡(jiǎn)單的示例代碼就能說(shuō)清楚了。但是,我們需要從這個(gè)知識(shí)點(diǎn)進(jìn)行發(fā)散、思考,在不同的維度上,把這個(gè)知識(shí)點(diǎn)與這個(gè)編程語(yǔ)言中其它類似的知識(shí)進(jìn)行聯(lián)想、對(duì)比;與其他編程語(yǔ)言中類似的概念進(jìn)行比較;然后再思考這個(gè)知識(shí)點(diǎn)可以使用在哪些場(chǎng)合,別人是怎么來(lái)使用它的。
今天,我們就來(lái)掰扯掰扯這兩個(gè)函數(shù)。雖然在一般的程序中使用不上,但是在今后的某個(gè)場(chǎng)合,當(dāng)你需要處理一些比較奇特的程序流程時(shí),也許它們可以給你帶來(lái)意想不到的效果。
例如:我們會(huì)把 setjmp/longjmp 與 goto 語(yǔ)句進(jìn)行功能上的比較;與 fork 函數(shù)從返回值上進(jìn)行類比;與 Python/Lua 語(yǔ)言中的協(xié)程進(jìn)行使用場(chǎng)景上的比較。
二、函數(shù)語(yǔ)法介紹
1. 最簡(jiǎn)示例
先不講道理,直接看一下這個(gè)最簡(jiǎn)單的示例代碼,看不懂也沒(méi)關(guān)系,混個(gè)臉熟:
- int main()
- {
- // 一個(gè)緩沖區(qū),用來(lái)暫存環(huán)境變量
- jmp_buf buf;
- printf("line1 \n");
- // 保存此刻的上下文信息
- int ret = setjmp(buf);
- printf("ret = %d \n", ret);
- // 檢查返回值類型
- if (0 == ret)
- {
- // 返回值0:說(shuō)明是正常的函數(shù)調(diào)用返回
- printf("line2 \n");
- // 主動(dòng)跳轉(zhuǎn)到 setjmp 那條語(yǔ)句處
- longjmp(buf, 1);
- }
- else
- {
- // 返回值非0:說(shuō)明是從遠(yuǎn)程跳轉(zhuǎn)過(guò)來(lái)的
- printf("line3 \n");
- }
- printf("line4 \n");
- return 0;
- }
執(zhí)行結(jié)果:
執(zhí)行順序如下(如果不明白就不要深究,看完下面的解釋再回過(guò)頭來(lái)看):
2. 函數(shù)說(shuō)明
首先來(lái)看下這個(gè) 2 個(gè)函數(shù)的簽名:
- int setjmp(jmp_buf env);
- void longjmp(jmp_buf env, int value);
它們都在頭文件 setjmp.h 中進(jìn)行聲明,維基百科的解釋如下:
下面我再用自己的理解把上面這段英文解釋一下:
setjmp 函數(shù)
功能:把執(zhí)行這個(gè)函數(shù)時(shí)的各種上下文信息保存起來(lái),主要就是一些寄存器的值;
參數(shù):用來(lái)保存上下文信息的緩沖區(qū),相當(dāng)于把當(dāng)前的上下文信息拍一個(gè)快照保存起來(lái);
返回值:有 2 種返回值,如果是直接調(diào)用 setjmp 函數(shù)時(shí),返回值是 0;如果是調(diào)用 longjmp 函數(shù)跳轉(zhuǎn)過(guò)來(lái)時(shí),返回值是非 0; 這里可以與創(chuàng)建進(jìn)程的函數(shù) fork 進(jìn)行一下類比。
longjmp 函數(shù)
功能:跳轉(zhuǎn)到參數(shù) env 緩沖區(qū)中保存的上下文(快照)中去執(zhí)行;
參數(shù):env 參數(shù)指定跳轉(zhuǎn)到哪個(gè)上下文中(快照)去執(zhí)行, value 用來(lái)給 setjmp 函數(shù)提供返回判斷信息,也就是說(shuō):調(diào)用 longjmp 函數(shù)時(shí),這個(gè)參數(shù) value 將會(huì)作為 setjmp 函數(shù)的返回值;
返回值:沒(méi)有返回值。因?yàn)樵谡{(diào)用這個(gè)函數(shù)時(shí),就直接跳轉(zhuǎn)到其他地方的代碼去執(zhí)行了,不會(huì)再回來(lái)了。
小結(jié):這 2 個(gè)函數(shù)是配合使用的,用來(lái)實(shí)現(xiàn)程序的跳轉(zhuǎn)。
3. setjmp:保存上下文信息
我們知道,C 代碼在編譯成二進(jìn)制文件之后,在執(zhí)行時(shí)被加載到內(nèi)存中,CPU 按照順序到代碼段取出每一條指令來(lái)執(zhí)行。在 CPU 中有很多個(gè)寄存器,用來(lái)保存當(dāng)前的執(zhí)行環(huán)境,比如:代碼段寄存器CS、指令偏移量寄存器IP,當(dāng)然了還有其他很多其它寄存器,我們把這個(gè)執(zhí)行環(huán)境稱作上下文。
CPU 在獲取下一條執(zhí)行指令時(shí),通過(guò) CS 和 IP 這 2 個(gè)寄存器就能獲取到需要執(zhí)行的指令,如下圖:
補(bǔ)充一下知識(shí)點(diǎn):
上圖中,把代碼段寄存器 CS 當(dāng)做一個(gè)基地址來(lái)看待了,也就是說(shuō):CS 指向代碼段在內(nèi)存中的開(kāi)始地址,IP 寄存器代表下一個(gè)要執(zhí)行的指令地址距離這個(gè)基地址的偏移量。因此每次取指令時(shí),只需要把這 2 個(gè)寄存器中的值相加,就得到了指令的地址;
其實(shí),在 x86 平臺(tái)上,代碼段寄存器 CS 并不是一個(gè)基地址,而是一個(gè)選擇子。在操作系統(tǒng)的某個(gè)地方有一個(gè)表格,這個(gè)表格里存儲(chǔ)了代碼段真正的開(kāi)始地址,而 CS 寄存器中 只是存儲(chǔ)了一個(gè)索引值,這個(gè)索引值指向這個(gè)表格中的某個(gè)表項(xiàng),這里涉及到虛擬內(nèi)存的相關(guān)知識(shí)了;
IP 寄存器在獲取一條指令之后,自動(dòng)往下移動(dòng)到下一個(gè)指令的開(kāi)始位置,至于移動(dòng)多少個(gè)字節(jié),那就要看當(dāng)前取出的這條指令占用了多少個(gè)字節(jié)。
CPU 是一個(gè)大傻瓜,它沒(méi)有任何的想法,我們讓它干什么,它就干什么。比如取指令:我們只要設(shè)置 CS 和 IP 寄存器,CPU 就用這 2 個(gè)寄存器里的值去獲取指令。如果把這 2 個(gè)寄存器設(shè)置為一個(gè)錯(cuò)誤的值,CPU 也會(huì)傻不拉幾的去取指令,只不過(guò)在執(zhí)行時(shí)就會(huì)崩潰。
我們可以簡(jiǎn)單的把這些寄存器信息理解為上下文信息,CPU 就根據(jù)這些上下文信息來(lái)執(zhí)行。因此,C 語(yǔ)言為我們準(zhǔn)備了 setjmp 這個(gè)庫(kù)函數(shù)來(lái)把當(dāng)前的上下文信息保存起來(lái),暫時(shí)存儲(chǔ)到一個(gè)緩沖區(qū)中。
保存的目的是什么?為了在以后可以恢復(fù)到當(dāng)前這個(gè)地方繼續(xù)執(zhí)行。
還有一個(gè)更簡(jiǎn)單的例子:服務(wù)器中的快照??煺盏淖饔檬鞘裁?當(dāng)服務(wù)器出現(xiàn)錯(cuò)誤時(shí),可以恢復(fù)到某個(gè)快照!
4. longjmp: 實(shí)現(xiàn)跳轉(zhuǎn)
說(shuō)到跳轉(zhuǎn),腦袋中立刻跳出的概念就是 goto 語(yǔ)句,我發(fā)現(xiàn)很多教程都對(duì) goto 語(yǔ)句很有意見(jiàn),認(rèn)為在代碼中應(yīng)該盡量不要使用它。這樣的觀點(diǎn)出發(fā)點(diǎn)是好的:如果 goto 使用太多,會(huì)影響對(duì)代碼執(zhí)行順序的理解。
但是如果看一下 Linux 內(nèi)核的代碼,可以發(fā)現(xiàn)很多的 goto 語(yǔ)句。還是那句話:在代碼維護(hù)和執(zhí)行效率上要尋找一個(gè)平衡點(diǎn)。
跳轉(zhuǎn)改變了程序的執(zhí)行序列,goto 語(yǔ)句只能在函數(shù)內(nèi)部進(jìn)行跳轉(zhuǎn),如果是跨函數(shù)它就無(wú)能為力了。
因此,C 語(yǔ)言中為我們提供了 longjmp 函數(shù)來(lái)實(shí)現(xiàn)遠(yuǎn)程跳轉(zhuǎn),從它的名字就可以額看出來(lái),也就是說(shuō)可以跨函數(shù)跳轉(zhuǎn)。
從 CPU 的角度看,所謂的跳轉(zhuǎn)就是把上下文中的各種寄存器設(shè)置為某個(gè)時(shí)刻的快照,很顯然,上面的 setjmp 函數(shù)中,已經(jīng)把那個(gè)時(shí)刻的上下文信息(快照)存儲(chǔ)到一個(gè)臨時(shí)緩沖區(qū)中了,如果要跳轉(zhuǎn)到那個(gè)地方去接著執(zhí)行,直接告訴 CPU 就行了。
怎么告訴 CPU 呢?就是把臨時(shí)緩沖區(qū)中的這些寄存器信息覆蓋掉 CPU 中使用的那些寄存器即可。
5. setjmp:返回類型和返回值
在某些需要多進(jìn)程的程序中,我們經(jīng)常使用 fork 函數(shù)來(lái)從當(dāng)前的進(jìn)程中"孵化"一個(gè)新的進(jìn)程,這個(gè)新進(jìn)程從 fork 這個(gè)函數(shù)的下一條語(yǔ)句開(kāi)始執(zhí)行。
對(duì)于主進(jìn)程來(lái)說(shuō),調(diào)用 fork 函數(shù)之后返回,也是繼續(xù)執(zhí)行下一條語(yǔ)句,那么如何來(lái)區(qū)分是主進(jìn)程還是新進(jìn)程呢? fork 函數(shù)提供了一個(gè)返回值給我們來(lái)進(jìn)行區(qū)分:
fork 函數(shù)返回 0:代表這是新進(jìn)程;
fork 函數(shù)返回非 0:代表是原來(lái)的主進(jìn)程,返回?cái)?shù)值是新進(jìn)程的進(jìn)程號(hào)。
類似的,setjmp 函數(shù)也有不同的返回類型。也許用返回類型來(lái)表述不太準(zhǔn)確,可以這樣理解:從 setjmp 函數(shù)返回,一共有 2 個(gè)場(chǎng)景:
主動(dòng)調(diào)用 setjmp 時(shí):返回 0,主動(dòng)調(diào)用的目的是為了保存上下文,建立快照。
通過(guò) longjmp 跳轉(zhuǎn)過(guò)來(lái)時(shí):返回非 0,此時(shí)的返回值是由 longjmp 的第二個(gè)參數(shù)來(lái)指定的。
根據(jù)以上這 2 種不同的值,我們就可以進(jìn)行不同的分支處理了。當(dāng)通過(guò) longjmp 跳轉(zhuǎn)返回的時(shí)候,可以根據(jù)實(shí)際場(chǎng)景,返回不同的非 0 值。有過(guò) Python、Lua 等腳本語(yǔ)言編程經(jīng)驗(yàn)的小伙伴,是不是想到了 yield/resume 函數(shù)?它們?cè)趨?shù)、返回值上的外在表現(xiàn)是一樣的!
小結(jié):到這里,基本上把 setjmp/longjmp 這 2 個(gè)函數(shù)的使用方法講完了,不知道我描述的是否足夠清楚。此時(shí),再看一下文章開(kāi)頭的示例代碼,應(yīng)該一目了然了。
三、利用 setjmp/longjmp 實(shí)現(xiàn)異常捕獲
既然 C 函數(shù)庫(kù)給我們提供了這個(gè)工具,那就肯定存在一定的使用場(chǎng)景。異常捕獲在一些高級(jí)語(yǔ)言中(Java/C++),直接在語(yǔ)法層面進(jìn)行了支持,一般就是 try-catch 語(yǔ)句,但是在 C 語(yǔ)言中需要自己去實(shí)現(xiàn)。
我們來(lái)演示一個(gè)最簡(jiǎn)單的異常捕獲模型,代碼一共 56 行:
- #include
- #include
- #include
- #include
- typedef int BOOL;
- #define TRUE 1
- #define FALSE 0
- // 枚舉:錯(cuò)誤代碼
- typedef enum _ErrorCode_ {
- ERR_OK = 100, // 沒(méi)有錯(cuò)誤
- 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ā)生異常,跳轉(zhuǎn)到函數(shù)執(zhí)行之前的位置
- // 第2個(gè)參數(shù)是異常代碼
- longjmp(gExcptBuf, ERR_DIV_BY_ZERO);
- }
- // 沒(méi)有異常,返回正確結(jié)果
- return a / b;
- }
- // 在這個(gè)函數(shù)中執(zhí)行可能會(huì)出現(xiàn)異常的函數(shù)
- int try(pf func, int a, int b)
- {
- // 保存上下文,如果發(fā)生異常,將會(huì)跳入這里
- int ret = setjmp(gExcptBuf);
- if (0 == ret)
- {
- // 調(diào)用可能發(fā)生異常的哈數(shù)
- func(a, b);
- // 沒(méi)有發(fā)生異常
- return ERR_OK;
- }
- else
- {
- // 發(fā)生了異常,ret 中是異常代碼
- return ret;
- }
- }
- int main()
- {
- int ret = try(my_div, 8, 0); // 會(huì)發(fā)生異常
- // int ret = try(my_div, 8, 2); // 不會(huì)發(fā)生異常
- if (ERR_OK == ret)
- {
- printf("try ok ! \n");
- }
- else
- {
- printf("try excepton. error = %d \n", ret);
- }
- return 0;
- }
代碼就不需要詳細(xì)說(shuō)明了,直接看代碼中的注釋即可明白。這個(gè)代碼僅僅是示意性的,在生產(chǎn)代碼中肯定需要更完善的包裝才能使用。
有一點(diǎn)需要注意:setjmp/longjmp 僅僅是改變了程序的執(zhí)行順序,應(yīng)用程序自己的一些數(shù)據(jù)如果需要回滾的話,需要我們自己手動(dòng)處理。
四、利用 setjmp/longjmp 實(shí)現(xiàn)協(xié)程
1. 什么是協(xié)程
在 C 程序中,如果需要并發(fā)執(zhí)行的序列一般都是用線程來(lái)實(shí)現(xiàn)的,那么什么是協(xié)程呢?維基百科對(duì)于協(xié)程的解釋是:
更詳細(xì)的信息在這個(gè)頁(yè)面 協(xié)程,網(wǎng)頁(yè)中具體描述了協(xié)程與線程、生成器的比較,各種語(yǔ)言中的實(shí)現(xiàn)機(jī)制。
我們用生產(chǎn)者和消費(fèi)者來(lái)簡(jiǎn)單體會(huì)一下協(xié)程和線程的區(qū)別:
2. 線程中的生產(chǎn)者和消費(fèi)者
生產(chǎn)者和消費(fèi)者是 2 個(gè)并行執(zhí)行的序列,通常用 2 個(gè)線程來(lái)執(zhí)行;
生產(chǎn)者在生產(chǎn)商品時(shí),消費(fèi)者處于等待狀態(tài)(阻塞)。生產(chǎn)完成后,通過(guò)信號(hào)量通知消費(fèi)者去消費(fèi)商品;
消費(fèi)者在消費(fèi)商品時(shí),生產(chǎn)者處于等待狀態(tài)(阻塞)。消費(fèi)結(jié)束后,通過(guò)信號(hào)量通知生產(chǎn)者繼續(xù)生產(chǎn)商品。
3. 協(xié)程中的生產(chǎn)者和消費(fèi)者
生產(chǎn)者和消費(fèi)者在同一個(gè)執(zhí)行序列中執(zhí)行,通過(guò)執(zhí)行序列的跳轉(zhuǎn)來(lái)交替執(zhí)行;
生產(chǎn)者在生產(chǎn)商品之后,放棄 CPU,讓消費(fèi)者執(zhí)行;
消費(fèi)者在消費(fèi)商品之后,放棄 CPU,讓生產(chǎn)者執(zhí)行;
4. C 語(yǔ)言中的協(xié)程實(shí)現(xiàn)
這里給出一個(gè)最最簡(jiǎn)單的模型,通過(guò) setjmp/longjmp 來(lái)實(shí)現(xiàn)協(xié)程的機(jī)制,主要是目的是來(lái)理解協(xié)程的執(zhí)行序列,沒(méi)有解決參數(shù)和返回值的傳遞問(wèn)題。
- typedef int BOOL;
- #define TRUE 1
- #define FALSE 0
- // 用來(lái)存儲(chǔ)主程和協(xié)程的上下文的數(shù)據(jù)結(jié)構(gòu)
- typedef struct _Context_ {
- jmp_buf mainBuf;
- jmp_buf coBuf;
- } Context;
- // 上下文全局變量
- Context gCtx;
- // 恢復(fù)
- #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");
- // 模擬耗時(shí)操作
- for (int i = 0; i < 10; ++i)
- {
- fprintf(stderr, ".");
- usleep(1000 * 200);
- }
- printf("\n*** coroutine: suspend \n");
- // 讓出 CPU
- yield();
- }
- }
- // 啟動(dòng)一個(gè)協(xié)程
- // 參數(shù)1:func 在協(xié)程中執(zhí)行的函數(shù)
- // 參數(shù)2:func 需要的參數(shù)
- typedef void (*pf)(void *);
- BOOL start_coroutine(pf func, void *arg)
- {
- // 保存主程的跳轉(zhuǎn)點(diǎn)
- if (0 == setjmp(gCtx.mainBuf))
- {
- func(arg); // 調(diào)用函數(shù)
- return TRUE;
- }
- return FALSE;
- }
- int main()
- {
- // 啟動(dòng)一個(gè)協(xié)程
- start_coroutine(coroutine_function, NULL);
- while (TRUE) // 死循環(huán)
- {
- printf("\n=== main: working \n");
- // 模擬耗時(shí)操作
- for (int i = 0; i < 10; ++i)
- {
- fprintf(stderr, ".");
- usleep(1000 * 200);
- }
- printf("\n=== main: suspend \n");
- // 放棄 CPU,讓協(xié)程執(zhí)行
- resume();
- }
- return 0;
- }
打印信息如下:
如果想深入研究 C 語(yǔ)言中的協(xié)程實(shí)現(xiàn),可以看一下達(dá)夫設(shè)備這個(gè)概念,其中利用 goto 和 switch 語(yǔ)句來(lái)實(shí)現(xiàn)分支跳轉(zhuǎn),其中使用的語(yǔ)法比較怪異、但是合法。
五、總結(jié)
這篇文章的重點(diǎn)是介紹 setjmp/longjmp 的語(yǔ)法和使用場(chǎng)景,在某些需求場(chǎng)景中,能達(dá)到事半功倍的效果。
當(dāng)然,你還可以發(fā)揮想象力,通過(guò)執(zhí)行序列的跳轉(zhuǎn)來(lái)實(shí)現(xiàn)更加花哨的功能,一切皆有可能!
本文轉(zhuǎn)載自微信公眾號(hào)「IOT物聯(lián)網(wǎng)小鎮(zhèn)」,可以通過(guò)以下二維碼關(guān)注。轉(zhuǎn)載本文請(qǐng)聯(lián)系IOT物聯(lián)網(wǎng)小鎮(zhèn)公眾號(hào)。
分享標(biāo)題:利用C語(yǔ)言中的Setjmp和Longjmp,來(lái)實(shí)現(xiàn)異常捕獲和協(xié)程
URL標(biāo)題:http://www.jiaotiyi.com/article/dhgeedp.html