5

linux下C语言开发18,使用C语言自制try模块

 3 years ago
source link: https://blog.popkx.com/linux%E4%B8%8Bc%E8%AF%AD%E8%A8%80%E5%BC%80%E5%8F%9118-%E4%BD%BF%E7%94%A8c%E8%AF%AD%E8%A8%80%E8%87%AA%E5%88%B6try%E6%A8%A1%E5%9D%97/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

上一节介绍了 linux 操作系统中信号的概念,我们知道了崩溃性错误通常会伴随着信号的产生。例如段错误引发的 SIGSEGV 信号,0 做除数引发的 SIGFPE 信号等等。

ea3f82d6756071c35d35d9c26aa206b2.png

不仅如此,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
781b25194d3348a0e4a0af40be4293e3.png

当然,现实中谁也不会白痴的写出 0 做除数的代码,但是有些时候 0 做除数是非常隐蔽的,相信常写代码的朋友一定遇到过。

针对这类崩溃性错误,python 非常贴心的提供了 try 语句。现在使用 try 语句改写一下上面的代码,请看:

#encoding=utf8

if __name__=="__main__":
    try:
        a = 8/0
    except Exception, e:
        print 'ERROR: ', e

    print "python exit normally"

c5e2f5bfb7a7c029675465897bef6eba.png
执行代码,会发现虽然仍无法计算 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 函数的说明:

bdcc03954f737655e40f733b8ec03406.png

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

8a6a4cf89f04d5a2be7caaf5e04fc6af.png
居然真假两条信息都被打印出来了。真叫人吃惊,现在来分析一下这个程序:

  • 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 为例,我们可以写如下代码:

a0ae9f605d0ca06a1805fe49f0db07b7.png

编译并执行,发现 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;
}
bb0add9e89c4bfc01c1c7d73f65ff35f.png

现在经过封装,是不是很像 python 中的 try 语句了呢?编译执行,结果和预期一样:
# ./a.out 
ERROR: division by zero
main exit normally
721f8a1e9987ec6ea71e0948293ca151.png

这样我们就使用C语言自制了类似python的try语句。不过它还是有局限的,例如使用了 genv 全局变量,以及 try 只是简单的代码封装,不能嵌套使用。再例如,这种封装会修改默认的信号处理函数,可能会为其他功能模块带来不便,等等。这些不足当然是能够解决的,限于篇幅,下一节或以后再继续讨论。

其实主要就是借助于栈的数据结构特点,感兴趣的朋友可以自己先试一试,完全可以封装一个强大的 C语言 try 库。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK