Setjmp 和 longjmp 在 C 语言中的实际应用

有人能解释一下 setjmp()longjmp()函数在嵌入式编程中的具体应用吗?我知道这些是用于错误处理的。但我想知道一些用例。

61248 次浏览

错误处理
假设在嵌套在许多其他函数中的函数内部存在一个错误,错误处理只有在顶级函数中才有意义。

如果中间的所有函数都必须正常返回并计算返回值或全局错误变量,以确定进一步处理没有意义,甚至是不好的,那将是非常繁琐和尴尬的。

在这种情况下,setjmp/longjmp 是有意义的。 这些情况类似于其他语言(C + + ,Java)中的异常有意义的情况。

协奏曲
除了错误处理之外,我还可以想到在 C 语言中需要 setjmp/longjmp 的另一种情况:

当您需要实现 协同动作时,就是这种情况。

下面是一个小小的演示示例。 我希望它能满足 Sivaprasad Palas 对一些示例代码的要求,并回答了 TheBlastOne 关于 setjmp/longjmp 如何支持协议实现的问题(尽管我看到它没有基于任何非标准或新的行为)。

编辑:
它可能是实际上 未定义的行为,做一个 longjmp 放下的调用堆栈(见 MikeMB 的评论; 虽然我还没有机会验证)。

#include <stdio.h>
#include <setjmp.h>


jmp_buf bufferA, bufferB;


void routineB(); // forward declaration


void routineA()
{
int r ;


printf("(A1)\n");


r = setjmp(bufferA);
if (r == 0) routineB();


printf("(A2) r=%d\n",r);


r = setjmp(bufferA);
if (r == 0) longjmp(bufferB, 20001);


printf("(A3) r=%d\n",r);


r = setjmp(bufferA);
if (r == 0) longjmp(bufferB, 20002);


printf("(A4) r=%d\n",r);
}


void routineB()
{
int r;


printf("(B1)\n");


r = setjmp(bufferB);
if (r == 0) longjmp(bufferA, 10001);


printf("(B2) r=%d\n", r);


r = setjmp(bufferB);
if (r == 0) longjmp(bufferA, 10002);


printf("(B3) r=%d\n", r);


r = setjmp(bufferB);
if (r == 0) longjmp(bufferA, 10003);
}




int main(int argc, char **argv)
{
routineA();
return 0;
}

下图显示了执行流程:
flow of execution

警告
在使用 setjmp/longjmp 时,应该意识到它们对局部变量的有效性有影响,而这些影响通常是不被考虑的。
参考我的 关于这个话题的问题

其理论是,您可以使用它们进行错误处理,这样您就可以跳出深度嵌套的调用链,而无需处理链中每个函数中的处理错误。

就像每一个聪明的理论一样,当遇到现实的时候,这个理论就分崩离析了。您的中间函数将分配内存、抓取锁、打开文件并执行需要清理的各种不同操作。所以在实践中,setjmp/longjmp通常是一个坏主意,除非在非常有限的情况下,您可以完全控制您的环境(一些嵌入式平台)。

根据我的经验,在大多数情况下,只要你认为使用 setjmp/longjmp可行,你的程序就足够清晰和简单,以至于调用链中的每个中间函数调用都可以执行错误处理,或者它是如此混乱和不可能修复,以至于当你遇到错误时,你应该执行 exit

setjmplongjmp的组合是“超强 goto”。小心使用。然而,正如其他人所解释的那样,当您想要快速地执行 get me back to the beginning而不是为18层函数逐层返回错误消息时,longjmp对于摆脱令人讨厌的错误情况非常有用。

然而,就像 goto一样,但更糟糕的是,您必须非常小心地使用它。longjmp只会让你回到代码的开头。它不会影响在 setjmp和返回到 setjmp开始的地方之间可能已经改变的所有其他状态。因此,当您返回到调用 setjmp的位置时,分配、锁、半初始化的数据结构等仍然是分配、锁和半初始化的。这意味着,你必须真正关心的地方,你这样做,这是真的可以调用 longjmp没有造成更多的问题。当然,如果接下来要做的事情是“重启”(也许是在存储有关错误的消息之后)——例如,在嵌入式系统中,您发现硬件处于糟糕的状态,那么没问题。

我还看到过 setjmp/longjmp用于提供非常基本的线程机制。但这是非常特殊的情况——而且绝对不是“标准”线程的工作方式。

编辑: 当然可以添加代码来“处理清理”,就像 C + + 在已编译的代码中存储异常点,然后知道什么给出了异常,什么需要清理一样。这将涉及到某种函数指针表,并将“如果我们从这里跳出来,使用这个参数调用这个函数”存储起来。就像这样:

struct
{
void (*destructor)(void *ptr);
};




void LockForceUnlock(void *vlock)
{
LOCK* lock = vlock;
}




LOCK func_lock;




void func()
{
ref = add_destructor(LockForceUnlock, mylock);
Lock(func_lock)
...
func2();   // May call longjmp.


Unlock(func_lock);
remove_destructor(ref);
}

使用这个系统,您可以做“完全异常处理,如 C + +”。但是它非常混乱,并且依赖于编写良好的代码。

既然你提到了嵌入式,我认为值得注意的是 非用例: 当你的编码标准禁止它。例如 MISRA (MISRA-C: 2004: Rule 20.7)和 JFS(AV Rule 20) : “不应使用 setjmp 宏和 longjmp 函数。”

setjmplongjmp在单元测试中非常有用。

假设我们要测试以下模块:

#include <stdlib.h>


int my_div(int x, int y)
{
if (y==0) exit(2);
return x/y;
}

通常,如果要测试的函数调用另一个函数,您可以声明一个存根函数供它调用,该存根函数将模拟实际函数对测试某些流所做的工作。但是,在这种情况下,函数调用 exit,而 exit不返回。存根需要以某种方式模仿这种行为。setjmplongjmp可以为你做到这一点。

为了测试这个函数,我们可以创建以下测试程序:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <setjmp.h>


// redefine assert to set a boolean flag
#ifdef assert
#undef assert
#endif
#define assert(x) (rslt = rslt && (x))


// the function to test
int my_div(int x, int y);


// main result return code used by redefined assert
static int rslt;


// variables controling stub functions
static int expected_code;
static int should_exit;
static jmp_buf jump_env;


// test suite main variables
static int done;
static int num_tests;
static int tests_passed;


//  utility function
void TestStart(char *name)
{
num_tests++;
rslt = 1;
printf("-- Testing %s ... ",name);
}


//  utility function
void TestEnd()
{
if (rslt) tests_passed++;
printf("%s\n", rslt ? "success" : "fail");
}


// stub function
void exit(int code)
{
if (!done)
{
assert(should_exit==1);
assert(expected_code==code);
longjmp(jump_env, 1);
}
else
{
_exit(code);
}
}


// test case
void test_normal()
{
int jmp_rval;
int r;


TestStart("test_normal");
should_exit = 0;
if (!(jmp_rval=setjmp(jump_env)))
{
r = my_div(12,3);
}


assert(jmp_rval==0);
assert(r==4);
TestEnd();
}


// test case
void test_div0()
{
int jmp_rval;
int r;


TestStart("test_div0");
should_exit = 1;
expected_code = 2;
if (!(jmp_rval=setjmp(jump_env)))
{
r = my_div(2,0);
}


assert(jmp_rval==1);
TestEnd();
}


int main()
{
num_tests = 0;
tests_passed = 0;
done = 0;
test_normal();
test_div0();
printf("Total tests passed: %d\n", tests_passed);
done = 1;
return !(tests_passed == num_tests);
}

在本例中,在输入函数进行测试之前使用 setjmp,然后在存根 exit中调用 longjmp直接返回到测试用例。

还要注意,重新定义的 exit有一个特殊的变量,它会检查您是否真的想要退出程序,并调用 _exit这样做。如果您不这样做,您的测试程序可能不会干净利落地退出。

我已经用 C 编写了一个 类 Java 异常处理机制 ,使用 setjmp()longjmp()和系统函数。它可以捕获自定义异常,也可以捕获像 SIGSEGV这样的信号。它具有异常处理块的无限嵌套,可以跨函数调用工作,并支持两种最常见的线程实现。它允许您定义具有链接时继承特性的异常类的树层次结构,并且 catch语句遍历该树以查看它是否需要捕获或传递。

下面是使用这种方法的代码样例:

try
{
*((int *)0) = 0;    /* may not be portable */
}
catch (SegmentationFault, e)
{
long f[] = { 'i', 'l', 'l', 'e', 'g', 'a', 'l' };
((void(*)())f)();   /* may not be portable */
}
finally
{
return(1 / strcmp("", ""));
}

下面是包含文件的一部分,它包含了很多逻辑:

#ifndef _EXCEPT_H
#define _EXCEPT_H


#include <stdlib.h>
#include <stdio.h>
#include <signal.h>
#include <setjmp.h>
#include "Lifo.h"
#include "List.h"


#define SETJMP(env)             sigsetjmp(env, 1)
#define LONGJMP(env, val)       siglongjmp(env, val)
#define JMP_BUF                 sigjmp_buf


typedef void (* Handler)(int);


typedef struct _Class *ClassRef;        /* exception class reference */
struct _Class
{
int         notRethrown;            /* always 1 (used by throw()) */
ClassRef    parent;                 /* parent class */
char *      name;                   /* this class name string */
int         signalNumber;           /* optional signal number */
};


typedef struct _Class Class[1];         /* exception class */


typedef enum _Scope                     /* exception handling scope */
{
OUTSIDE = -1,                       /* outside any 'try' */
INTERNAL,                           /* exception handling internal */
TRY,                                /* in 'try' (across routine calls) */
CATCH,                              /* in 'catch' (idem.) */
FINALLY                             /* in 'finally' (idem.) */
} Scope;


typedef enum _State                     /* exception handling state */
{
EMPTY,                              /* no exception occurred */
PENDING,                            /* exception occurred but not caught */
CAUGHT                              /* occurred exception caught */
} State;


typedef struct _Except                  /* exception handle */
{
int         notRethrown;            /* always 0 (used by throw()) */
State       state;                  /* current state of this handle */
JMP_BUF     throwBuf;               /* start-'catching' destination */
JMP_BUF     finalBuf;               /* perform-'finally' destination */
ClassRef    class;                  /* occurred exception class */
void *      pData;                  /* exception associated (user) data */
char *      file;                   /* exception file name */
int         line;                   /* exception line number */
int         ready;                  /* macro code control flow flag */
Scope       scope;                  /* exception handling scope */
int         first;                  /* flag if first try in function */
List *      checkList;              /* list used by 'catch' checking */
char*       tryFile;                /* source file name of 'try' */
int         tryLine;                /* source line number of 'try' */


ClassRef    (*getClass)(void);      /* method returning class reference */
char *      (*getMessage)(void);    /* method getting description */
void *      (*getData)(void);       /* method getting application data */
void        (*printTryTrace)(FILE*);/* method printing nested trace */
} Except;


typedef struct _Context                 /* exception context per thread */
{
Except *    pEx;                    /* current exception handle */
Lifo *      exStack;                /* exception handle stack */
char        message[1024];          /* used by ExceptGetMessage() */
Handler     sigAbrtHandler;         /* default SIGABRT handler */
Handler     sigFpeHandler;          /* default SIGFPE handler */
Handler     sigIllHandler;          /* default SIGILL handler */
Handler     sigSegvHandler;         /* default SIGSEGV handler */
Handler     sigBusHandler;          /* default SIGBUS handler */
} Context;


extern Context *        pC;
extern Class            Throwable;


#define except_class_declare(child, parent) extern Class child
#define except_class_define(child, parent)  Class child = { 1, parent, #child }


except_class_declare(Exception,           Throwable);
except_class_declare(OutOfMemoryError,    Exception);
except_class_declare(FailedAssertion,     Exception);
except_class_declare(RuntimeException,    Exception);
except_class_declare(AbnormalTermination, RuntimeException);  /* SIGABRT */
except_class_declare(ArithmeticException, RuntimeException);  /* SIGFPE */
except_class_declare(IllegalInstruction,  RuntimeException);  /* SIGILL */
except_class_declare(SegmentationFault,   RuntimeException);  /* SIGSEGV */
except_class_declare(BusError,            RuntimeException);  /* SIGBUS */




#ifdef  DEBUG


#define CHECKED                                                         \
static int checked


#define CHECK_BEGIN(pC, pChecked, file, line)                           \
ExceptCheckBegin(pC, pChecked, file, line)


#define CHECK(pC, pChecked, class, file, line)                          \
ExceptCheck(pC, pChecked, class, file, line)


#define CHECK_END                                                       \
!checked


#else   /* DEBUG */


#define CHECKED
#define CHECK_BEGIN(pC, pChecked, file, line)           1
#define CHECK(pC, pChecked, class, file, line)          1
#define CHECK_END                                       0


#endif  /* DEBUG */




#define except_thread_cleanup(id)       ExceptThreadCleanup(id)


#define try                                                             \
ExceptTry(pC, __FILE__, __LINE__);                                  \
while (1)                                                           \
{                                                                   \
Context *       pTmpC = ExceptGetContext(pC);                   \
Context *       pC = pTmpC;                                     \
CHECKED;                                                        \
\
if (CHECK_BEGIN(pC, &checked, __FILE__, __LINE__) &&            \
pC->pEx->ready && SETJMP(pC->pEx->throwBuf) == 0)           \
{                                                               \
pC->pEx->scope = TRY;                                       \
do                                                          \
{


#define catch(class, e)                                                 \
}                                                           \
while (0);                                                  \
}                                                               \
else if (CHECK(pC, &checked, class, __FILE__, __LINE__) &&      \
pC->pEx->ready && ExceptCatch(pC, class))              \
{                                                               \
Except *e = LifoPeek(pC->exStack, 1);                       \
pC->pEx->scope = CATCH;                                     \
do                                                          \
{


#define finally                                                         \
}                                                           \
while (0);                                                  \
}                                                               \
if (CHECK_END)                                                  \
continue;                                                   \
if (!pC->pEx->ready && SETJMP(pC->pEx->finalBuf) == 0)          \
pC->pEx->ready = 1;                                         \
else                                                            \
break;                                                      \
}                                                                   \
ExceptGetContext(pC)->pEx->scope = FINALLY;                         \
while (ExceptGetContext(pC)->pEx->ready > 0 || ExceptFinally(pC))   \
while (ExceptGetContext(pC)->pEx->ready-- > 0)


#define throw(pExceptOrClass, pData)                                    \
ExceptThrow(pC, (ClassRef)pExceptOrClass, pData, __FILE__, __LINE__)


#define return(x)                                                       \
{                                                                   \
if (ExceptGetScope(pC) != OUTSIDE)                              \
{                                                               \
void *      pData = malloc(sizeof(JMP_BUF));                \
ExceptGetContext(pC)->pEx->pData = pData;                   \
if (SETJMP(*(JMP_BUF *)pData) == 0)                         \
ExceptReturn(pC);                                       \
else                                                        \
free(pData);                                            \
}                                                               \
return x;                                                       \
}


#define pending                                                         \
(ExceptGetContext(pC)->pEx->state == PENDING)


extern Scope    ExceptGetScope(Context *pC);
extern Context *ExceptGetContext(Context *pC);
extern void     ExceptThreadCleanup(int threadId);
extern void     ExceptTry(Context *pC, char *file, int line);
extern void     ExceptThrow(Context *pC, void * pExceptOrClass,
void *pData, char *file, int line);
extern int      ExceptCatch(Context *pC, ClassRef class);
extern int      ExceptFinally(Context *pC);
extern void     ExceptReturn(Context *pC);
extern int      ExceptCheckBegin(Context *pC, int *pChecked,
char *file, int line);
extern int      ExceptCheck(Context *pC, int *pChecked, ClassRef class,
char *file, int line);




#endif  /* _EXCEPT_H */

还有一个 C 模块,其中包含用于信号处理和一些簿记的逻辑。

实施起来非常棘手,我可以告诉你,我差点就辞职了。我真的努力让它尽可能接近 Java; 我发现仅仅使用 C 就能做到这一点令人惊讶。

如果你有兴趣的话,叫我一声。

毫无疑问,setjmp/longjmp 最关键的用法是它充当“非本地 goto 跳转”。Goto 命令(在很少的情况下需要使用 Goto over for 和 while 循环)在同一范围内使用得最为安全。如果您使用 goto 来跨越作用域(或跨越自动分配) ,您很可能会损坏程序的堆栈。Setjmp/longjmp 通过将堆栈信息保存到要跳转到的位置来避免这种情况。然后,当您跳转时,它加载这个堆栈信息。如果没有这个特性,C 程序员很可能不得不求助于汇编编程来解决只有 setjmp/longjmp 才能解决的问题。谢天谢地它存在。C 库中的一切都非常重要。你需要的时候自然会知道。

除了错误处理之外,您可以做的另一件事是以一种聪明的方式在 C 中实现尾部递归计算。

这实际上是如何在 C 中实现延续,而不用以延续传递样式转换输入代码。