7.10 setjmp和longjmp函数
在C中,goto语句是不能跨越函数的。而执行这种跳转功能的是函数setjmp和longjmp。这两个函数对于处理发生在很深的嵌套函数调用中的出错情况非常有用。
考虑一下程序7 - 4的骨干部分。其主循环是从标准输入读1行,然后调用do_line处理每一输入行。该函数然后调用get_token从该输入行中取下一个记号。一行中的第一个记号假定是某种形式的一条命令,于是switch语句就实现命令选择。我们的程序只处理一条命令,对此命令调用cmd_add函数。
程序7.4 进行命令处理的典型程序骨架
#include <stdio.h>
#include <stdlib.h>
#include <setjmp.h>
#define TOK_ADD 5
#define MAXLINE 128
void do_line(char *);
void cmd_add(void);
int get_token(void);
int main(void)
{
char line[MAXLINE];
while (fgets(line, MAXLINE, stdin) != NULL)
do_line(line);
exit(0);
}
char *tok_ptr; //global pointer for get_token
void do_line(char *ptr) //process one line of input
{
int cmd;
tok_ptr = ptr;
while ((cmd = get_token()) > 0) {
switch (cmd) {
case TOK_ADD:
cmd_add();
break;
}
}
}
void cmd_add(void)
{
int token;
token = get_token();
//reset of processing for this command
}
int get_token(void)
{
//fetch next token from line pointer to by tok_ptr
}
程序7 - 4在读命令、确定命令的类型、然后调用相应函数处理每一条命令这类程序中是非常典型的。图7 - 4显示了调用了cmd_add之后栈的大致使用情况。
自动变量的存储单元在每个函数的栈桢中。数组line在main的栈帧中,整型cmd在do_line的栈帧中,整型token在cmd_add的栈帧中。
如上所述,这种形式的栈安排是非常典型的,但并不要求非如此不可。栈并不一定要向低地址方向扩充。某些系统对栈并没有提供特殊的硬件支持,此时一个C实现可能要用连接表实现栈帧。
在编写如程序7 - 4这样的程序中经常会遇到的一个问题是,如何处理非致命性的错误。例如,若cmd_add函数发现一个错误,譬如说一个无效的数,那么它可能先打印一个出错消息,然后希望忽略输入行的余下部分,返回main函数并读下一输入行。用C语言比较难做到这一点。(在本例中, cmd_add函数只比main低两个层次,在有些程序中往往低五或更多层次。)如果我们不得不以检查返回值的方法逐层返回,那就会变得很麻烦。
解决这种问题的方法就是使用非局部goto跳转—— setjmp和longjmp函数。非局部表示这不是在一个函数内的普通的C语言g o t o语句,而是在栈上跳过若干调用帧,返回到当前函数调用路径上的一个函数中。
#include <setjmp.h>
int setjmp(jmp_buf env) ;
返回:若直接调用则为0,若从longjmp返回则为非0
void longjmp(jmp_buf env, int val) ;
在希望返回到的位置调用setjmp,在本例中,此位置在main函数中。因为我们直接调用该函数,所以其返回值为0。setjmp的参数env是一个特殊类型j m p _ b u f。这一数据类型是某种形式的数组,其中存放在调用longjmp时能用来恢复栈状态的所有信息。一般, env变量是个全局变量,因为需从另一个函数中引用它。
当检查到一个错误时,例如在cmd_add函数中,则以两个参数调用longjmp函数。第一个就是在调用setjmp时所用的env;第二个v a l,是个非0值,它成为从setjmp处返回的值。使用第二个参数的原因是对于一个setjmp可以有多个longjmp。例如,可以在cmd_add中以v a l为1调用longjmp,也可在get_token中以v a l为2调用longjmp。在main函数中,setjmp的返回值就会是1或2,通过测试返回值就可判断是从cmd_add还是从get_token来的longjmp。
再回到程序实例中,程序7 - 5中给出了经修改过后的main和cmd_add函数(其他两个函数,do_line和get_token未经更改)。
#include <stdio.h>
#include <stdlib.h>
#include <setjmp.h>
#define TOK_ADD 5
#define MAXLINE 128
jmp_buf jmpbuffer;
void do_line(char *);
void cmd_add(void);
int get_token(void);
int main(void)
{
char line[MAXLINE];
if (setjmp(jmpbuffer) != 0)
printf("error");
while (fgets(line, MAXLINE, stdin) != NULL)
do_line(line);
exit(0);
}
char *tok_ptr; //global pointer for get_token
void do_line(char *ptr) //process one line of input
{
int cmd;
tok_ptr = ptr;
while ((cmd = get_token()) > 0) {
switch (cmd) {
case TOK_ADD:
cmd_add();
break;
}
}
}
void cmd_add(void)
{
int token;
token = get_token();
//reset of processing for this command
if (token < 0) //an error has occurred
longjmp(jmpbuffer, 1);
//reset of processing for this command
}
int get_token(void)
{
//fetch next token from line pointer to by tok_ptr
}
执行main时,调用setjmp,它将所需的信息记入变量j m p b u ff e r中并返回0。然后调用do_line,它又调用c m _ a d d,假定在其中检测到一个错误。在cmd_add中调用longjmp之前,栈的形式如图7 - 4中所示。但是longjmp使栈反绕到执行main函数时的情况,也就是抛弃了cmd_add和do_line的栈帧。调用longjmp造成main中setjmp的返回,但是,这一次的返回值是1 ( longjmp的第二个参数)。
那么此时的栈帧结构就是只有main的栈帧,其他函数的都已经被抛弃了。
7.10.1 自动、寄存器和易失变量
下一个问题是:“在main函数中,自动变量和寄存器变量的状态如何?”当longjmp返回到main函数时,这些变量的值是否能恢复到以前调用setjmp时的值(即滚回原先值),或者这些变量的值保持为调用do_line时的值( do_line调用cmd_add,cmd_add又调用longjmp ) ?不幸的是,对此问题的回答是“看情况”。大多数实现并不滚回这些自动变量和寄存器变量的值,而所有标准则说它们的值是不确定的。如果你有一个自动变量,而又不想使其值滚回,则可定义其为具有volatile属性。说明为全局和静态变量的值在执行longjmp时保持不变。
实例
下面我们通过程序7 - 6说明在调用longjmp后,自动变量、寄存器变量和易失变量的不同情况。如果以不带优化和带优化对此程序分别进行编译,然后运行它们,则得到的结果是不同的:
$ cc testjmp.c 不进行任何优化的编译
$ a . o u t
in f1(): count = 97, val = 98, sum = 99
after longjmp: count = 97, val = 98, sum = 99
$ cc -O testjmp.c 进行全部优化的编译
$ a . o u t
in f1(): count = 97, val = 98, sum = 99
after longjmp: count = 2, val = 3, sum = 99
注意,易失变量( s u m )不受优化的影响,在longjmp之后的值,是它在调用f 1时的值。在我们所使用的setjmp ( 3 )手册页上说明存放在存储器中的变量将具有longjmp时的值,而在CPU和浮点寄存器中的变量则恢复为调用setjmp时的值。这确实就是在运行程序7 - 5时所观察到的值。
不进行优化时,所有这三个变量都存放在存储器中(亦即对v a l的寄存器存储类被忽略)。而进行优化时,c o u n t和v a l都存放在寄存器中(即使c o u n t并末说明为register ) , volatile变量则仍存放在存储器中。通过这一例子要理解的是,如果要编写一个使用非局部跳转的可移植程序,则必须使用volatile属性。
程序7-6 longjmp对自动,寄存器和易失变量的影响
#include <stdio.h>
#include <stdlib.h>
#include <setjmp.h>
static void f1(int, int, int, int);
static void f2(void);
static jmp_buf jmpbuffer;
static int globval;
int main(void)
{
int autoval;
register int regival;
volatile int volaval;
static int statval;
globval = 1;
autoval = 2;
regival = 3;
volaval = 4;
statval = 5;
if (setjmp(jmpbuffer) != 0) {
printf("after longjmp : \n");
printf("globval = %d, autoval = %d, regival = %d,"
"volaval = %d, statval = %d\n", globval, autoval,
regival, volaval, statval);
exit(0);
}
//change varivables after setjmp ,but before longjmp
globval = 95;
autoval = 96;
regival = 97;
volaval = 98;
statval = 99;
f1(autoval, regival, volaval, statval); //never returns
exit(0);
}
static void f1(int i, int j, int k, int l)
{
printf("in f1():\n");
printf("globval = %d, autoval = %d, regival = %d,"
"volaval = %d, statval = %d\n", globval, i, j, k, l);
f2();
}
static void f2(void)
{
longjmp(jmpbuffer, 1);
}
注意,全局、静态和易失变量不受优化的影响。在调用longjmp后,他们的值是最近所呈现的值。存放在存储器中的变量将具有longjmp时的值,而在CPU和浮点数寄存器中的变量则恢复为调用setjmp时的值。
在程序中,某些printf的格式字符串可能不适宜安排在程序文本的一行中,我们没有将其分成多个printf调用,而是使用了ISO C的字符串连接功能,于是两个字符串序列:
“string1” “string2”
等价于
“string1string2”
7.10.2 自动变量的潜在问题
前面已经说明了处理栈帧的一般方式,与此相关我们现在可以分析一下自动变量的一个潜在出错情况。基本规则是说明自动变量的函数已经返回后,就不能再引用这些自动变量。在整个UNIX手册中,关于这一点有很多警告。
程序7 - 7是一个名为open_data的函数,它打开了一个标准I / O流,然后为该流设置缓存。程序7-6 自动变量的不正确使用
#include <stdio.h>
#define DATAFILE "datafile"
FILE *open_data(void)
{
FILE *fp;
char databuf[BUFSIZ]; //setvbuf makes this the stdio buffer
if ((fp = fopen(DATAFILE, "r")) == NULL)
return NULL;
if (setvbuf(fp, databuf, _IOLBF, BUFSIZ) != 0)
return NULL;
return fp;
}
int main(int argc, char *argv[])
{
FILE *fp;
fp = open_data();
return 0;
}
问题是:当open_data返回时,它在栈上所使用的空间将由下一个被调用函数的栈帧使用。但是,标准I / O库函数仍将使用原先为databuf在栈上分配的存储空间作为该流的缓存。这就产生了冲突和混乱。为了改正这一问题,应在全局存储空间静态地(如static或extern ),或者动态地(使用一种a l l o c函数)为数组databuf分配空间。