« 太空堡垒卡拉狄加 | 返回首页 | 千呼万唤始出来,结果是这么白痴的设定 »

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,比较好的实现多线程并发的?
你好,云风。在使用libjpeg时,发现一个问题:使用libjpeg的开源库,cpu并发上不去。(8核的cpu最多只能跑到200%),后来查了下资料,就说道libjpeg中使用了setjmp这个问题。我现在想改一下libjpeg,有什么方案是可以替换libjpeg,比较好的实现多线程并发的
int try(breakpoint bp) { return setjmp(bp->jb); } void throw(breakpoint bp) { longjmp(bp->jb,1); } 如果前面加一个inline关键字可以不?
LZ你好。 我觉得这篇文章的标题可以改成《正确使用setjmp》,或者《使用setjmp的正确方法》,读起来更自然。
setjmp确实比较麻烦,在一些移动设备上支持也不好。当时移植lua时还折腾了好久,后来才发现是setjum支持的问题。 另外,coroutine 是个很不错的东西。6年前就在一个n-gage游戏中用过,当时是用来做剧情控制的。用起来很顺
明白了。出栈后,setjmp保存的栈信息已经失效了。 之前直接用上面2个封装函数进行调用试验。运行不出错,但没跳转。是因为这2个函数入栈时信息一样,longjmp 没有跳到setjmp,跳到longjmp自身下面去了。 谢谢云风指点
@mzfhhhh 把 setjmp 想象成 try 你能把 try 封装进一个函数吗?
而如果对 setjmp 做过一层函数调用的封装后。上例中的 setjmp 设置的返回点经过 try 的调用返回后,已经无效 ---------------------- 不明白返回点怎么会失效 setjmp,longjmp 可以在函数间跳转,那上面2个封装在函数内跳转,又有何问题?
不错不错,学习了
云风好像很喜欢 coroutine ,看起来这个coroutine 也很诱惑,有点想去钻研学习的欲望,不过好像还没听说过哪个知名的开源产品使用,而且去搜索吧,国内的资料很少,一搜,肯定是搜到云风这里来。 难道是指网游里面比较常见?
setjmp 一般的确用的很少,不过前阵子用 libjpg & libpng,它们都用 setjmp 来做出错处理,感觉还是挺好用的。 但我觉得也就仅限于需要给外部暴露回旋余地的库使用,如果是自己写的东西用这个,个人感觉就是有些没必要了。呵呵
这样会有很多限制,基本上出了异常就只能释放资源然后关闭程序了。如果想要处理异常然后继续运行的话,要正确的释放临时产生的资源就会非常麻烦了。 看来还是用错误返回值最简单明了。
资源分层次管理, execption 出来后把一大陀资源一起释放就行了。 好比 os 释放 process 的资源那样。启动 process 可以看成 setjmp ,prcoess 异常后可以看成 longjmp 出来。
哦,貌似这样行不通,上一级函数没法释放资源啊。那如果我需要释放资源到底该怎么做?
如果要在longjmp的时候释放资源怎么做?写个Finally lable,然后goto Finally,在里面释放资源然后再longjmp?貌似还是挺麻烦的,万一漏写了就资源泄漏了。
setjmp/longjmp在x86下非常高效,只需要几条指令,而C++的异常处理由于有很多事要做则低效很多
longjmp 并非不安全,不安全的是非要在 C 里面套 RAII 模式,然后又使用 longjmp 实现 exception 。
另外,用C++也可以模拟出闭包来,C++允许在函数里定义class,把闭包写成class的形式,upvalue放在class里面就可以了,写法上有些麻烦,但是凑合了还是能用用的。
用setjmp/longjmp搞出个这么不安全的半吊子exception,还不如直接用C++的exception呢,或者干脆只用错误返回值。当然,我对C++的exception也不是很满意,很多时候也就是凑合了能用用。
竟然还可以模拟出闭包,受教了,不过真要用的时候心里还是会有些拿不准

Post a comment

非这个主题相关的留言请到:留言本