上一节的代码,虽然成功的用 C 语言模拟了 python 的 try 功能,但是因为使用全局变量保存现场信息(env),所以有两个致命问题:
- 覆盖了原有的信号处理函数。
- 信号处理函数不能传递参数,因此只能使用全局变量,但是这样一来,模拟 try 功能就不能支持嵌套使用了。
本节将尽力解决这两个问题。下面再贴一下上一节的代码。
#include <stdio.h>
#include <setjmp.h>
#include <signal.h>
jmp_buf mark;
void sigHandle(int sig)
{
siglongjmp(mark, -1);
}
int main()
{
int ret = 0, a = 0;
signal(SIGFPE, sigHandle);
ret = sigsetjmp(mark, 1);
if(0==ret){
a = 8/0;
printf("ret == 0 is true\n");
}else{
printf("ERROR: division by zero\n");
}
return 0;
}
为什么会覆盖原有的信号处理函数
这个问题真的非常简单,假设之前程序定义和绑定了其他的SIGFPE
的信号处理函数。现在为了捕获SIGFPE
异常,却需要将SIGFPE
的信号处理函数修改为我们的sigHandle
函数,这不就覆盖了吗?如果不知道原来的信号处理函数是什么,那么在捕获异常动作结束后,也无法恢复,这就是问题。
为什么用全局变量保存现场(env ),为什么不能支持嵌套调用?
一句话,这是因为 linux 的信号处理函数不接受额外的参数,也无法接收出信号外的其他参数。
请看信号处理函数 sigHandle
,因为无法把除信号外的任何参数传递给它,所以 siglongjmp
的 mark 参数只能通过一些特殊手段传递。比较简单的方法就是使用全局变量。上面的 demo 使用了全局变量 mark
保存现场,这样就无法支持嵌套捕获异常了。举个例子:
...
ret = sigsetjmp(mark, 1);
if(0==ret){
ret2 = sigsetjmp(mark, 1); // 嵌套保存现场
a = 8/0;
printf("ret == 0 is true\n");
}else{
printf("ERROR: division by zero\n");
}
...
第一次保存的现场,被第二次的 sigsetjmp
函数覆盖了,所以执行 a=8/0
出错时,程序会返回到
ret2 = sigsetjmp(mark, 1); // 嵌套保存现场
这并不是我们期望的。
解决办法
知道了问题的症结所在,就可以解决它了。
- 记录原信号处理函数,在捕获异常结束后,再改回去。这样就解决了 C 语言模拟 try 功能会覆盖原信号处理函数的问题。
- 建立多元全局变量,将每次
sigsetjmp
保存的现场环境分开保存,就有望解决因现场被覆盖而不能嵌套调用的问题。
经过上面的分析,现知道不仅要保存现场环境,还需要保存其他信息,所以,可以如下建立结构体(LCC是我的名字缩写,没别的意思):
typedef struct __LCC_TRY
{
jmp_buf mark;
int markRet;
int sig;
struct sigaction oldAct;
}LCC_TRY;
mark 和 markRet 用于记录 sigsetjmp
保存的现场和返回值,sig 和 oldAct 则用来保存要捕获的信号和该信号原来的信号处理函数,方便之后恢复。
因为要支持多级嵌套使用,所以 LCC_TRY
要建立多个。仔细分析一下,知道其实 sigsetjmp
保存的现场在捕获结束后就没用了,并不需要一直保存。所以建立栈结构
的多元 LCC_TRY
数据结构非常合适。
linux c 语言利用循环数组建立环形栈
不多话,直接放出代码:
#include <stdio.h>
#include <setjmp.h>
#include <signal.h>
#include <stdlib.h>
#include <string.h>
#include "lcc_try.h"
// ****************
LCC_STACK g_lcc_try_stack = {0}; // = {0} 就相当于 init 了
void lcc_stack_init(LCC_STACK* s)
{
s->top = 0;
s->pushTimes = 0;
memset(s->tryInfo, 0, sizeof(LCC_STACK));
}
int lcc_stack_push(LCC_STACK* s, LCC_TRY* iTryInfo)
{
memcpy(&(s->tryInfo[(s->top++)%LCC_TRY_MAX_NEST]), iTryInfo, sizeof(LCC_TRY));
s->pushTimes = (s->pushTimes<LCC_TRY_MAX_NEST)?(s->pushTimes+1):LCC_TRY_MAX_NEST;
return 1;
}
int lcc_stack_pop(LCC_STACK* s, LCC_TRY* oTryInfo)
{
if( 0<s->pushTimes )
s->pushTimes--;
else{
printf("ERROR: pop empty stack\n");
exit(-1);
}
memcpy(oTryInfo, &(s->tryInfo[(--s->top)%LCC_TRY_MAX_NEST]), sizeof(LCC_TRY));
return 1;
}
void __lcc_SigHandle(int sig)
{
LCC_TRY tmp = {0};
lcc_stack_pop(&g_lcc_try_stack, &tmp);
siglongjmp(tmp.mark, -1);
}
int lcc_stack_set_try(LCC_STACK* s, LCC_TRY* iTryInfo)
{
memcpy(&(s->tryInfo[(s->top)%LCC_TRY_MAX_NEST]), iTryInfo, sizeof(LCC_TRY));
return 0;
}
int lcc_stack_get_try(LCC_STACK* s, LCC_TRY* oTryInfo)
{
memcpy(oTryInfo, &(s->tryInfo[(s->top)%LCC_TRY_MAX_NEST]), sizeof(LCC_TRY));
return 1;
}
代码非常简单,也比较粗糙,抛砖引玉了。建立的栈
不会“满”,当栈保存的数据超过一定的长度,最旧的数据会被顶出栈。LCC_STACK
的结构如下定义:
typedef struct __LCC_STACK
{
LCC_TRY tryInfo[LCC_TRY_MAX_NEST];
int top;
unsigned int pushTimes;
}LCC_STACK;
C语言模拟python的try-catch功能
既然使用C语言模拟python的 try
功能,当然也要尽量保持语法结构也一致,所以采用了宏定义。下面直接上代码:
// 这里两个细节,一是利用函数形参做局部变量。一是利用 if(a&&b)的 a,b 执行顺序和执行个数。
#define lcc_try(tryInfo, sigs) \
sigaction((sigs), NULL, &((tryInfo).oldAct)); \
signal((sigs), __lcc_SigHandle); \
(tryInfo).sig = (sigs); \
(tryInfo).markRet = sigsetjmp((tryInfo).mark, 1); \
if(0!=(tryInfo).markRet){ \
lcc_stack_get_try(&g_lcc_try_stack, &(tryInfo)); \
(tryInfo).markRet = -1;} \
if( ( \
(lcc_stack_push(&g_lcc_try_stack, &(tryInfo))) \
)&& \
(0==(tryInfo).markRet) )
#define lcc_catch(tryInfo) \
else if( (lcc_stack_pop(&g_lcc_try_stack, &(tryInfo)))&& \
(lcc_stack_push(&g_lcc_try_stack, &(tryInfo)))&& \
(0!=(tryInfo).markRet) )
#define lcc_try_end(tryInfo) \
lcc_stack_pop(&g_lcc_try_stack, &(tryInfo)); \
signal((tryInfo).sig, (tryInfo).oldAct.sa_handler);
测试
经过宏定义的封装,终于有点像 try 功能了。下面放出一段测试代码:
#include <stdio.h>
#include <signal.h>
#include "pthread.h"
#include "unistd.h"
#include "lcc_try.h"
void* thread(void* param)
{
LCC_TRY tryInfo;
char* p = NULL;
lcc_try(tryInfo, SIGSEGV){
printf("thread running...\n");
*p = 1;
}lcc_catch(tryInfo){
printf("thread ERROR\n");
}lcc_try_end(tryInfo);
}
void defHandle(int sig)
{
printf("recieved SIGSEGV!\n");
raise(9);
}
int main()
{
char* buf = NULL;
int tmp = 0;
LCC_TRY tryInfo;
signal(SIGSEGV, defHandle); // 设定默认的信号处理函数
putchar('\n');
lcc_try(tryInfo, SIGSEGV){
printf("main running...\n");
lcc_try(tryInfo, SIGFPE){
printf("inner 1 running...\n");
tmp = 8/0;
}lcc_catch(tryInfo){
printf("inner 1 ERROR\n");
}lcc_try_end(tryInfo);
*buf = NULL;
}lcc_catch(tryInfo){
printf("main ERROR\n");
}lcc_try_end(tryInfo);
pthread_t pid;
pthread_create(&pid,NULL,thread,NULL);
sleep(1);
printf("finished\n\n");
*buf = 1; // 测试是否恢复为原来的信号处理函数
return 0;
}
编译,执行之,得到结果如下:
$ ./a.out
main running...
inner 1 running...
inner 1 ERROR
main ERROR
thread running...
thread ERROR
finished
recieved SIGSEGV!
Killed
可以看出,我们成功了。嵌套的异常被成功捕获了,线程里的异常也被捕获了。
还有问题吗
当然是有的,既然使用了全局变量,那么就应该考虑在多线程,多进程的情况下的竞争问题。
相信能解决,这篇文章就到这里了。