setjmp 的正确使用
setjmp 是 C 语言解决 exception 的标准方案。我个人认为,setjmp/longjmp 这组 api 的名字没有取好,导致了许多误解。名字体现的是其行为:跳转,却没能反映其功能:exception 的抛出和捕获。
longjmp 从名字上看,叫做长距离跳转。实际上它能做的事情比名字上看起来的要少得多。跳转并非从静止状态的代码段的某个点跳转到另一个位置(类似在汇编层次的 jmp 指令做的那样),而是在运行态中向前跳转。C 语言的运行控制模型,是一个基于栈结构的指令执行序列。表示出来就是 call / return :调用一个函数,然后用 return 指令从一个函数返回。setjmp/longjmp 实际上是完成的另一种调用返回的模型。setjmp 相当于 call ,longjmp 则是 return 。
重要的区别在于:setjmp 不具备函数调用那样灵活的入口点定义;而 return 不具备 longjmp 那样可以灵活的选择返回点。其次,第一、setjmp 并不负责维护调用栈的数据结构,即,你不必保证运行过程中 setjmp 和 longjmp 层次上配对。如果需要这种层次,则需要程序员自己维护一个调用栈。这个调用栈往往是一个 jmp_buf
的序列;第二、它也不提供调用参数传递的功能,如果你需要,也得自己来实现。
以库形式提供的 setjmp/longjmp 和以语言关键字 return 提供的两套平行的运行流控制放在一起,大大拓展了 C 语言的能力。把 setjmp/longjmp 嵌在单个函数中使用,可以模拟 pascal 中嵌套函数定义:即在函数中定义一个局部函数。ps. GNUC 扩展了 C 语言,也在语法上支持这种定义方法。这种用法可以让几个局部函数有访问和共享 upvalue 的能力。把 setjmp/longjmp 放在大框架上,则多用来模拟 exception 机制。
setjmp 也可以用来模拟 coroutine 。但是会遇到一个难以逾越的难点:正确的 coroutine 实现需要为每个 coroutine 配备一个独立的数据栈,这是 setjmp 无法做到的。虽然有一些 C 的 coroutine 库用 setjmp/longjmp 实现。但使用起来都会有一定隐患。多半是在单一栈上预留一块空间,然后给另一个 coroutine 运行时覆盖使用。当数据栈溢出时,程序会发生许多怪异的现象,很难排除这种溢出 bug 。要正确的实现 coroutine ,还需要 setcontext 库 ,这已经不是 C 语言的标准库了。
在使用 setjmp 时,最常见的一个错误用法就是对 setjmp 做封装,用一个函数去调用它。比如:
int try(breakpoint bp) { return setjmp(bp->jb); } void throw(breakpoint bp) { longjmp(bp->jb,1); }
setjmp 不应该封装在一个函数中。这样写并不讳引起编译错误。但十有八九会引起运行期错误。错误的起源在于 longjmp 的跳转返回点,必须在运行流经过并有效的位置。而如果对 setjmp 做过一层函数调用的封装后。上例中的 setjmp 设置的返回点经过 try 的调用返回后,已经无效。如果要必要封装的话,应该使用宏。
setjmp/longjmp 对于大多数 C 程序员来说比较陌生。正是在于它的定义含糊不清,不太容易弄清楚。使用上容易出问题,运用场合也就变的很狭窄,多用于规模较大的库或框架中。和 C++ 语言提供的 execption 机制一样,很少有构架师愿意把它暴露到外面,那需要对二次开发的程序员有足够清晰的头脑,并充分理解其概念才不会用错。这往往是不可能的。
另外,setjmp/longjmp 的理念和 C++ 本身的 RAII 相冲突。虽然许多编译器为防止 C++ 程序员错误使用 setjmp 都对其做了一定的改进。让它可以正确工作。但大多数情况下,还是在文档中直接声明不推荐在 C++ 程序中使用这个东西。
btw,关于 RAII ,的确是个好东西。但和诸多设计模式一样,不是真理。如果你是一个从 C++ 进化来的 C 程序员,则更应该警惕思维的禁锢,RAII 是一种避免资源泄露的好方案,但不是唯一方案。
Comments
你好,云风。在使用libjpeg时,发现一个问题:使用libjpeg的开源库,cpu并发上不去。(8核的cpu最多只能跑到200%),后来查了下资料,就说道libjpeg中使用了setjmp这个问题。我现在想改一下libjpeg,有什么方案是可以替换libjpeg,比较好的实现多线程并发的?
Posted by: yanbingwei | (20) April 14, 2015 08:40 PM
你好,云风。在使用libjpeg时,发现一个问题:使用libjpeg的开源库,cpu并发上不去。(8核的cpu最多只能跑到200%),后来查了下资料,就说道libjpeg中使用了setjmp这个问题。我现在想改一下libjpeg,有什么方案是可以替换libjpeg,比较好的实现多线程并发的
Posted by: yanbingwei | (19) April 14, 2015 08:39 PM
int try(breakpoint bp)
{
return setjmp(bp->jb);
}
void throw(breakpoint bp)
{
longjmp(bp->jb,1);
}
如果前面加一个inline关键字可以不?
Posted by: Anonymous | (18) December 16, 2014 03:17 PM
LZ你好。
我觉得这篇文章的标题可以改成《正确使用setjmp》,或者《使用setjmp的正确方法》,读起来更自然。
Posted by: silverbullettt | (17) August 27, 2012 02:59 PM
setjmp确实比较麻烦,在一些移动设备上支持也不好。当时移植lua时还折腾了好久,后来才发现是setjum支持的问题。
另外,coroutine 是个很不错的东西。6年前就在一个n-gage游戏中用过,当时是用来做剧情控制的。用起来很顺
Posted by: fenglin | (16) June 24, 2010 10:42 AM
明白了。出栈后,setjmp保存的栈信息已经失效了。
之前直接用上面2个封装函数进行调用试验。运行不出错,但没跳转。是因为这2个函数入栈时信息一样,longjmp 没有跳到setjmp,跳到longjmp自身下面去了。
谢谢云风指点
Posted by: mzfhhhh | (15) June 7, 2010 03:00 PM
@mzfhhhh
把 setjmp 想象成 try
你能把 try 封装进一个函数吗?
Posted by: cloud | (14) June 7, 2010 12:43 PM
而如果对 setjmp 做过一层函数调用的封装后。上例中的 setjmp 设置的返回点经过 try 的调用返回后,已经无效
----------------------
不明白返回点怎么会失效
setjmp,longjmp 可以在函数间跳转,那上面2个封装在函数内跳转,又有何问题?
Posted by: mzfhhhh | (13) June 7, 2010 09:57 AM
不错不错,学习了
Posted by: jhgjh | (12) June 6, 2010 04:42 PM
云风好像很喜欢 coroutine ,看起来这个coroutine 也很诱惑,有点想去钻研学习的欲望,不过好像还没听说过哪个知名的开源产品使用,而且去搜索吧,国内的资料很少,一搜,肯定是搜到云风这里来。
难道是指网游里面比较常见?
Posted by: 小x | (11) May 28, 2010 01:21 PM
setjmp 一般的确用的很少,不过前阵子用 libjpg & libpng,它们都用 setjmp 来做出错处理,感觉还是挺好用的。
但我觉得也就仅限于需要给外部暴露回旋余地的库使用,如果是自己写的东西用这个,个人感觉就是有些没必要了。呵呵
Posted by: cyberscorpio | (10) May 25, 2010 11:35 PM
这样会有很多限制,基本上出了异常就只能释放资源然后关闭程序了。如果想要处理异常然后继续运行的话,要正确的释放临时产生的资源就会非常麻烦了。
看来还是用错误返回值最简单明了。
Posted by: analyst | (9) May 25, 2010 07:05 PM
资源分层次管理, execption 出来后把一大陀资源一起释放就行了。
好比 os 释放 process 的资源那样。启动 process 可以看成 setjmp ,prcoess 异常后可以看成 longjmp 出来。
Posted by: cloud | (8) May 25, 2010 06:37 PM
哦,貌似这样行不通,上一级函数没法释放资源啊。那如果我需要释放资源到底该怎么做?
Posted by: analyst | (7) May 25, 2010 05:27 PM
如果要在longjmp的时候释放资源怎么做?写个Finally lable,然后goto Finally,在里面释放资源然后再longjmp?貌似还是挺麻烦的,万一漏写了就资源泄漏了。
Posted by: analyst | (6) May 25, 2010 05:24 PM
setjmp/longjmp在x86下非常高效,只需要几条指令,而C++的异常处理由于有很多事要做则低效很多
Posted by: Anonymous | (5) May 25, 2010 05:02 PM
longjmp 并非不安全,不安全的是非要在 C 里面套 RAII 模式,然后又使用 longjmp 实现 exception 。
Posted by: cloud | (4) May 25, 2010 05:02 PM
另外,用C++也可以模拟出闭包来,C++允许在函数里定义class,把闭包写成class的形式,upvalue放在class里面就可以了,写法上有些麻烦,但是凑合了还是能用用的。
Posted by: analyst | (3) May 25, 2010 04:50 PM
用setjmp/longjmp搞出个这么不安全的半吊子exception,还不如直接用C++的exception呢,或者干脆只用错误返回值。当然,我对C++的exception也不是很满意,很多时候也就是凑合了能用用。
Posted by: analyst | (2) May 25, 2010 04:45 PM
竟然还可以模拟出闭包,受教了,不过真要用的时候心里还是会有些拿不准
Posted by: xuruoji | (1) May 25, 2010 04:08 PM