一 前言
在初学c的时候,学到goto这一部分内容,无论是书中还是老师们的讲课一般都告诫学生慎用goto语句,后继的语言多数取消了goto这个关键字,比如java,采用break加标签的形式来实现深层嵌套的退出。原因是乱用goto语句常常让程序乱如面条,非常可怕。C语言的创造者对goto的跳转范围还是做了限制,即只能在同一个函数中跳转,不能跨越函数随便跳转。这次来聊的跳转函数setjmp
和longjmp
,以及sigsetjump
和siglongjump
这两对函数可以完成更加疯狂的举动,在C程序内任意跳转。如果说原来的goto语句还只是如面条在碗里散乱,还好是在一个碗里,而这两对函数就如同面条在不同的碗里乱,想捋顺程序的逻辑将更加困难。
所以虽然初学c的时候就了解了这两个特殊函数,这么多年的开发中却从来没有使用过。这次看CSAPP刚好看到,顺便写几个例子来巩固下,或许以后用的到。
二 简介
上面说的跳转函数被创造出来的初衷是为了C语言的异常控制跳转,可以完成从一个函数直接跳转到另外一个函数,而不要经过层层的返回,简化异常处理的流程。
#include <setjump.h>
int setjump(jmp_buf env);
int sigsetjump(sigjmp_buf env,int savesigs);
我们知道函数的调用其实就是压栈,函数返回就是出栈,上述函数是将函数的栈等信息保存到环境变量中,然后在跳转的时候直接恢复,恢复函数或跳转函数如下:
#include <setjmp.h>
void longjump(jmp_buf env,int retval);
void siglongjump(sigjmp_buf env, int retval);
函数还比较好理解,setjump
保存此时的栈信息等到env中,longjump
跳转到env指定的栈位置。setjump
比较特殊,设置的时候返回0,如果是从longjump
跳过来的,setjump
会返回longjump
中的参数:retval
。这个有点类似fork
返回两次,在子进程中返回0,在父进程中返回子进程的id。
为什么要有另外一对跳转函数那?sigsetjump
和siglongjump
,那是因为如果在信号处理函数里面利用longjump
跳转,就不会清理此信号,就导致这个信号会被程序一直屏蔽(程序在处理一个信号的时候再来相同的信号是被忽略的,处理完毕后,才可以再次接受这个信号),这个就像个bug。savesigs
为非0的时候,表示同时阻塞的信号集合也被保存在env中。
三 实践
我们按照csapp书中的例子练习下。
#include "csapp.h"
jmp_buf buf;
int error1 = 1;
int error2 = 1;
void foo(int*), bar(void);
int main()
{
int a = 123;
int rc=5;
rc = setjmp(buf);
// 第一次设置buf 返回0
if (rc == 0) {
printf("a is %d\n",a);
foo(&a);
} else if (rc == 1) {
printf("longjmp ret a is %d\n",a);
printf("Detect an error1 condition in foo.\n");
} else if (rc == 2) {
printf("Detect an error2 condition in foo.\n");
} else {
printf("unkown eror condition in foo.\n");
}
exit(0);
}
void foo(int * pa)
{
if (error1) {
*pa=456;
longjmp(buf, 1);
}
bar();
}
void bar(void)
{
if (error2) {
longjmp(buf, 2);
}
}
返回:
[root@localhost tinyweb]# ./setjmp
a is 123
longjmp ret a is 456
Detect an error1 condition in foo.
从上面的情况我们可以看出,setjmp只保存栈的信息和寄存器的信息,并没有恢复局部变量的值。
再看一个清理信号跳转:
#include "csapp.h"
sigjmp_buf buf;
int i = 0;
void exithandle(int sig)
{
exit(0);
}
void handler(int sig)
{
if (i++ < 5) {
siglongjmp(buf, 1);
} else {
printf("loop %d times.", i);
signal(SIGINT,exithandle);
}
}
int main()
{
signal(SIGINT, handler);
if (!sigsetjmp(buf, 1)) {
printf("Starting.\n");
} else {
printf("Restarting .\n");
}
while (1) {
sleep(1);
printf("Sleeping..\n");
}
exit(0);
}
运行结果(按ctrl+C发送SIGINT信号),默认行为是程序终止。
[root@localhost tinyweb]# ./sigjmp
Starting.
Sleeping..
Sleeping..
^CRestarting .
Sleeping..
Sleeping..
^CRestarting .
Sleeping..
Sleeping..
^CRestarting .
Sleeping..
^CRestarting .
Sleeping..
^CRestarting .
Sleeping..
^Cloop 6 times.Sleeping..
Sleeping..
Sleeping..
^C
假如我们用不支持信号保存的跳转函数来修改第二个程序,打印内容如下:
[root@localhost tinyweb]# ./sigjmp
Starting.
Sleeping..
Sleeping..
^CRestarting .
Sleeping..
^CSleeping..
^CSleeping..
Sleeping..
^CSleeping..
^CSleeping..
^CSleeping..
^CSleeping..
^CSleeping..
^C^CSleeping..
这是因为用longjmp会跳过信号屏蔽的清理,所以除了第一次收到了信号外,后续的信号都被屏蔽了,无法接收到,只能被kill掉。
四 诗词欣赏
木兰花·拟古决绝词柬友
- 纳兰性德
人生若只如初见,何事秋风悲画扇。
等闲变却故人心,却道故人心易变。
骊山语罢清宵半,泪雨霖铃终不怨。
何如薄幸锦衣郎,比翼连枝当日愿。