文章详情

短信预约-IT技能 免费直播动态提醒

请输入下面的图形验证码

提交验证

短信预约提醒成功

利用C语言中的Setjmp和Longjmp,来实现异常捕获和协程

2024-12-03 12:02

关注

在 C 标准库中,有两个威力很猛的函数:setjmp 和 longjmp,不知道各位小伙伴在代码中是否使用过?我问了身体的几位同事,一部分人不认识这两个函数,有一部分人知道这个函数,但从来没有使用过。

从知识点范围来看,这两个函数的功能比较单纯,一个简单的示例代码就能说清楚了。但是,我们需要从这个知识点进行发散、思考,在不同的维度上,把这个知识点与这个编程语言中其它类似的知识进行联想、对比;与其他编程语言中类似的概念进行比较;然后再思考这个知识点可以使用在哪些场合,别人是怎么来使用它的。

今天,我们就来掰扯掰扯这两个函数。虽然在一般的程序中使用不上,但是在今后的某个场合,当你需要处理一些比较奇特的程序流程时,也许它们可以给你带来意想不到的效果。

例如:我们会把 setjmp/longjmp 与 goto 语句进行功能上的比较;与 fork 函数从返回值上进行类比;与 Python/Lua 语言中的协程进行使用场景上的比较。

二、函数语法介绍

1. 最简示例

先不讲道理,直接看一下这个最简单的示例代码,看不懂也没关系,混个脸熟:

  1. int main() 
  2.     // 一个缓冲区,用来暂存环境变量 
  3.     jmp_buf buf; 
  4.     printf("line1 \n"); 
  5.      
  6.     // 保存此刻的上下文信息 
  7.     int ret = setjmp(buf); 
  8.     printf("ret = %d \n", ret); 
  9.      
  10.     // 检查返回值类型 
  11.     if (0 == ret) 
  12.     { 
  13.         // 返回值0:说明是正常的函数调用返回 
  14.         printf("line2 \n"); 
  15.          
  16.         // 主动跳转到 setjmp 那条语句处 
  17.         longjmp(buf, 1); 
  18.     } 
  19.     else 
  20.     { 
  21.         // 返回值非0:说明是从远程跳转过来的 
  22.         printf("line3 \n"); 
  23.     } 
  24.     printf("line4 \n"); 
  25.     return 0; 

执行结果:

 

执行顺序如下(如果不明白就不要深究,看完下面的解释再回过头来看):

 

2. 函数说明

首先来看下这个 2 个函数的签名:

  1. int setjmp(jmp_buf env); 
  2. void longjmp(jmp_buf env, int value); 

它们都在头文件 setjmp.h 中进行声明,维基百科的解释如下:

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

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 个寄存器就能获取到需要执行的指令,如下图:

 

补充一下知识点:

上图中,把代码段寄存器 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 中使用的那些寄存器即可。

 

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 行:

  1. #include  
  2. #include  
  3. #include  
  4. #include  
  5.  
  6. typedef int     BOOL; 
  7. #define TRUE    1 
  8. #define FALSE   0 
  9.  
  10. // 枚举:错误代码 
  11. typedef enum _ErrorCode_ { 
  12.     ERR_OK = 100,         // 没有错误 
  13.     ERR_DIV_BY_ZERO = -1  // 除数为 0 
  14. } ErrorCode; 
  15.  
  16. // 保存上下文的缓冲区 
  17. jmp_buf gExcptBuf; 
  18.  
  19. // 可能发生异常的函数 
  20. typedef int (*pf)(intint); 
  21. int my_div(int a, int b) 
  22.     if (0 == b) 
  23.     { 
  24.         // 发生异常,跳转到函数执行之前的位置 
  25.         // 第2个参数是异常代码 
  26.         longjmp(gExcptBuf, ERR_DIV_BY_ZERO); 
  27.     } 
  28.     // 没有异常,返回正确结果 
  29.     return a / b; 
  30.  
  31. // 在这个函数中执行可能会出现异常的函数 
  32. int try(pf func, int a, int b) 
  33.     // 保存上下文,如果发生异常,将会跳入这里 
  34.     int ret = setjmp(gExcptBuf); 
  35.     if (0 == ret) 
  36.     { 
  37.         // 调用可能发生异常的哈数 
  38.         func(a, b); 
  39.         // 没有发生异常 
  40.         return ERR_OK; 
  41.     } 
  42.     else 
  43.     { 
  44.         // 发生了异常,ret 中是异常代码 
  45.         return ret; 
  46.     } 
  47.  
  48. int main() 
  49.     int ret = try(my_div, 8, 0);     // 会发生异常 
  50.     // int ret = try(my_div, 8, 2);  // 不会发生异常 
  51.     if (ERR_OK == ret) 
  52.     { 
  53.         printf("try ok ! \n"); 
  54.     } 
  55.     else 
  56.     { 
  57.         printf("try excepton. error = %d \n", ret); 
  58.     } 
  59.      
  60.     return 0; 

代码就不需要详细说明了,直接看代码中的注释即可明白。这个代码仅仅是示意性的,在生产代码中肯定需要更完善的包装才能使用。

有一点需要注意:setjmp/longjmp 仅仅是改变了程序的执行顺序,应用程序自己的一些数据如果需要回滚的话,需要我们自己手动处理。

 

四、利用 setjmp/longjmp 实现协程

1. 什么是协程

在 C 程序中,如果需要并发执行的序列一般都是用线程来实现的,那么什么是协程呢?维基百科对于协程的解释是:

 

更详细的信息在这个页面 协程,网页中具体描述了协程与线程、生成器的比较,各种语言中的实现机制。

我们用生产者和消费者来简单体会一下协程和线程的区别:

2. 线程中的生产者和消费者

生产者和消费者是 2 个并行执行的序列,通常用 2 个线程来执行;

生产者在生产商品时,消费者处于等待状态(阻塞)。生产完成后,通过信号量通知消费者去消费商品;

消费者在消费商品时,生产者处于等待状态(阻塞)。消费结束后,通过信号量通知生产者继续生产商品。

3. 协程中的生产者和消费者

生产者和消费者在同一个执行序列中执行,通过执行序列的跳转来交替执行;

生产者在生产商品之后,放弃 CPU,让消费者执行;

消费者在消费商品之后,放弃 CPU,让生产者执行;

4. C 语言中的协程实现

这里给出一个最最简单的模型,通过 setjmp/longjmp 来实现协程的机制,主要是目的是来理解协程的执行序列,没有解决参数和返回值的传递问题。

  1. typedef int     BOOL; 
  2. #define TRUE    1 
  3. #define FALSE   0 
  4.  
  5. // 用来存储主程和协程的上下文的数据结构 
  6. typedef struct _Context_ { 
  7.     jmp_buf mainBuf; 
  8.     jmp_buf coBuf; 
  9. } Context; 
  10.  
  11. // 上下文全局变量 
  12. Context gCtx; 
  13.  
  14. // 恢复 
  15. #define resume() \ 
  16.     if (0 == setjmp(gCtx.mainBuf)) \ 
  17.     { \ 
  18.         longjmp(gCtx.coBuf, 1); \ 
  19.     } 
  20.  
  21. // 挂起 
  22. #define yield() \ 
  23.     if (0 == setjmp(gCtx.coBuf)) \ 
  24.     { \ 
  25.         longjmp(gCtx.mainBuf, 1); \ 
  26.     } 
  27.  
  28. // 在协程中执行的函数 
  29. void coroutine_function(void *arg) 
  30.     while (TRUE)  // 死循环 
  31.     { 
  32.         printf("\n*** coroutine: working \n"); 
  33.         // 模拟耗时操作 
  34.         for (int i = 0; i < 10; ++i) 
  35.         { 
  36.             fprintf(stderr, "."); 
  37.             usleep(1000 * 200); 
  38.         } 
  39.         printf("\n*** coroutine: suspend \n"); 
  40.          
  41.         // 让出 CPU 
  42.         yield(); 
  43.     } 
  44.  
  45. // 启动一个协程 
  46. // 参数1:func 在协程中执行的函数 
  47. // 参数2:func 需要的参数 
  48. typedef void (*pf)(void *); 
  49. BOOL start_coroutine(pf func, void *arg) 
  50.     // 保存主程的跳转点 
  51.     if (0 == setjmp(gCtx.mainBuf)) 
  52.     { 
  53.         func(arg); // 调用函数 
  54.         return TRUE
  55.     } 
  56.  
  57.     return FALSE
  58.  
  59. int main() 
  60.     // 启动一个协程 
  61.     start_coroutine(coroutine_function, NULL); 
  62.      
  63.     while (TRUE) // 死循环 
  64.     { 
  65.         printf("\n=== main: working \n"); 
  66.  
  67.         // 模拟耗时操作 
  68.         for (int i = 0; i < 10; ++i) 
  69.         { 
  70.             fprintf(stderr, "."); 
  71.             usleep(1000 * 200); 
  72.         } 
  73.  
  74.         printf("\n=== main: suspend \n"); 
  75.          
  76.         // 放弃 CPU,让协程执行 
  77.         resume(); 
  78.     } 
  79.  
  80.     return 0; 

 

打印信息如下:

如果想深入研究 C 语言中的协程实现,可以看一下达夫设备这个概念,其中利用 goto 和 switch 语句来实现分支跳转,其中使用的语法比较怪异、但是合法。

五、总结

这篇文章的重点是介绍 setjmp/longjmp 的语法和使用场景,在某些需求场景中,能达到事半功倍的效果。

当然,你还可以发挥想象力,通过执行序列的跳转来实现更加花哨的功能,一切皆有可能!

本文转载自微信公众号「IOT物联网小镇」,可以通过以下二维码关注。转载本文请联系IOT物联网小镇公众号。

 

来源:IOT物联网小镇内容投诉

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

软考中级精品资料免费领

  • 历年真题答案解析
  • 备考技巧名师总结
  • 高频考点精准押题
  • 2024年上半年信息系统项目管理师第二批次真题及答案解析(完整版)

    难度     813人已做
    查看
  • 【考后总结】2024年5月26日信息系统项目管理师第2批次考情分析

    难度     354人已做
    查看
  • 【考后总结】2024年5月25日信息系统项目管理师第1批次考情分析

    难度     318人已做
    查看
  • 2024年上半年软考高项第一、二批次真题考点汇总(完整版)

    难度     435人已做
    查看
  • 2024年上半年系统架构设计师考试综合知识真题

    难度     224人已做
    查看

相关文章

发现更多好内容

猜你喜欢

AI推送时光机
位置:首页-资讯-后端开发
咦!没有更多了?去看看其它编程学习网 内容吧
首页课程
资料下载
问答资讯