上一节介绍了 linux 操作系统中信号的概念,我们知道了崩溃性错误通常会伴随着信号的产生。例如段错误引发的 SIGSEGV 信号,0 做除数引发的 SIGFPE 信号等等。
不仅如此,linux 中的信号也是可以被“截获”的,甚至还能够被修改处理动作。例如上一节,我们使用了 C语言编程截获了 SIGSEGV 信号,并将处理动作由默认的打印“segmentation fault”并退出,修改为我们自定义的动作,所以程序在遇到段错误时也没有退出。
python 中的错误处理—— try 语句
知道了 linux 中信号的概念,其实我们可以做些更好玩的事,在讨论这个之前,先来看看使用 python 计算 8 除以 0 的值会怎样:
#encoding=utf8
if __name__=="__main__":
a = 8/0
print a
不出所料,无论使用何种工具计算 8 除以 0 都是一样的没有意义,python 也是如此,在遇到 0 做除数也会崩溃退出:
# python2 t.py
Traceback (most recent call last):
File "t.py", line 4, in <module>
a = 8/0
ZeroDivisionError: integer division or modulo by zero
当然,现实中谁也不会白痴的写出 0 做除数的代码,但是有些时候 0 做除数是非常隐蔽的,相信常写代码的朋友一定遇到过。
针对这类崩溃性错误,python 非常贴心的提供了 try 语句。现在使用 try 语句改写一下上面的代码,请看:
#encoding=utf8
if __name__=="__main__":
try:
a = 8/0
except Exception, e:
print 'ERROR: ', e
print "python exit normally"
执行代码,会发现虽然仍无法计算 8/0,但是程序却避免了崩溃,先是提示“ERROR: integer division or modulo by zero”,然后打印出了“python exit normally”的程序正常退出信息。
# python2 t.py
ERROR: integer division or modulo by zero
python exit normally
python 的 try 语法有利于写出更加健壮的程序。但是遗憾的是 C语言并没有这样的语法,不过在了解了 linux 中信号的概念之后,我们完全可以自己实现一套 C语言的 try 语句。
setjmp 和 longjmp 函数
C 语言虽然没有直接提供类似于 python 中的 try 语句,但是却提供了 setjmp 和 longjmp 函数用于保存和恢复现场,再结合我们已经了解的 linux 中的信号机制,自己来实现一套 C语言 try 语句也并不是什么难事。
先来看看 setjmp 和 longjmp 函数的说明:
setjmp 和 longjmp 函数非常适合解决底层的错误或者冲突,setjmp 可以保存栈上下文和环境表等信息,之后 longjmp 函数可以使用这些信息,就像时光倒流回到过去一样,修改程序的执行流程。这么说有些虚,来看一下实例,请看下面的代码:
#include <stdio.h>
#include <setjmp.h>
int main()
{
jmp_buf mark;
int ret = 0;
ret = setjmp(mark);
if(0==ret){
printf("ret == 0 is true\n");
longjmp(mark, -1);
}else{
printf("ret == 0 is false\n");
}
return 0;
}
分析一下这个程序,会打印“ret == 0 is true”,还是“ret == 0 is false”呢?编译并执行,却得到如下结果:
# ./a.out
ret == 0 is true
ret == 0 is false
居然真假两条信息都被打印出来了。真叫人吃惊,现在来分析一下这个程序:
- setjmp 函数第一次执行时,会将程序执行到这句所产生的现场环境保存到 mark 里,接着返回 0。
- 因为 ret 为 0,所以程序打印出了ret == 0 is true”。
- 接着执行了 longjmp 函数,它会恢复 mark 保存的现场,也就是程序又跳回 setjmp 这一行了,但是 longjmp 函数小小的修改了一点历史:将 setjmp 的返回值修改为 -1 了。
- 因为这次 ret 为 -1,所以程序又打印了“ret == 0 is false”。
- 程序执行了 return 0。
现在应该都清楚了,可能已经有朋友发现了,把这两个函数和 linux 中的信号处理函数结合起来,不就实现了类似 python 中的 try 语句吗?
使用C语言,自制类似 python 的 try 语句
既然 longjmp 函数能够回到过去修改历史,那实现 try 语句就太简单了。还是以计算 8 除以 0 为例,我们可以写如下代码:
编译并执行,发现 C语言程序不但没有崩溃退出,输出了“main exit normally”信息,而且还贴心的提示了“ERROR: division by zero”错误提示信息。
# ./a.out
ERROR: division by zero
main exit normally
虽然功能大体实现了,但是上面的代码并不像 try 语法那么简洁。这时可以借助于 define 宏封装,现在将代码做适当调整,请看:
#include <stdio.h>
#include <setjmp.h>
#include <signal.h>
jmp_buf genv;
#define try if(({ \
int __ret = 0; \
__ret = sigsetjmp(genv,1); \
0==__ret; \
}))
#define except else
void signal_handle(int sig)
{
siglongjmp(genv, -1);
}
int main()
{
int ret = 0, a = 0;
signal(SIGFPE, signal_handle);
try{
a = 8/0;
}except{
printf("ERROR: division by zero\n");
}
printf("main exit normally\n");
return 0;
}
现在经过封装,是不是很像 python 中的 try 语句了呢?编译执行,结果和预期一样:
# ./a.out
ERROR: division by zero
main exit normally
这样我们就使用C语言自制了类似python的try语句。不过它还是有局限的,例如使用了 genv 全局变量,以及 try 只是简单的代码封装,不能嵌套使用。再例如,这种封装会修改默认的信号处理函数,可能会为其他功能模块带来不便,等等。这些不足当然是能够解决的,限于篇幅,下一节或以后再继续讨论。
其实主要就是借助于栈的数据结构特点,感兴趣的朋友可以自己先试一试,完全可以封装一个强大的 C语言 try 库。