有人能解释一下 setjmp()和 longjmp()函数在嵌入式编程中的具体应用吗?我知道这些是用于错误处理的。但我想知道一些用例。
setjmp()
longjmp()
错误处理 假设在嵌套在许多其他函数中的函数内部存在一个错误,错误处理只有在顶级函数中才有意义。
如果中间的所有函数都必须正常返回并计算返回值或全局错误变量,以确定进一步处理没有意义,甚至是不好的,那将是非常繁琐和尴尬的。
在这种情况下,setjmp/longjmp 是有意义的。 这些情况类似于其他语言(C + + ,Java)中的异常有意义的情况。
协奏曲 除了错误处理之外,我还可以想到在 C 语言中需要 setjmp/longjmp 的另一种情况:
当您需要实现 协同动作时,就是这种情况。
下面是一个小小的演示示例。 我希望它能满足 Sivaprasad Palas 对一些示例代码的要求,并回答了 TheBlastOne 关于 setjmp/longjmp 如何支持协议实现的问题(尽管我看到它没有基于任何非标准或新的行为)。
编辑: 它可能是实际上 是未定义的行为,做一个 longjmp 放下的调用堆栈(见 MikeMB 的评论; 虽然我还没有机会验证)。
longjmp
#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; }
下图显示了执行流程:
警告 在使用 setjmp/longjmp 时,应该意识到它们对局部变量的有效性有影响,而这些影响通常是不被考虑的。 参考我的 关于这个话题的问题。
其理论是,您可以使用它们进行错误处理,这样您就可以跳出深度嵌套的调用链,而无需处理链中每个函数中的处理错误。
就像每一个聪明的理论一样,当遇到现实的时候,这个理论就分崩离析了。您的中间函数将分配内存、抓取锁、打开文件并执行需要清理的各种不同操作。所以在实践中,setjmp/longjmp通常是一个坏主意,除非在非常有限的情况下,您可以完全控制您的环境(一些嵌入式平台)。
setjmp
根据我的经验,在大多数情况下,只要你认为使用 setjmp/longjmp可行,你的程序就足够清晰和简单,以至于调用链中的每个中间函数调用都可以执行错误处理,或者它是如此混乱和不可能修复,以至于当你遇到错误时,你应该执行 exit。
exit
setjmp和 longjmp的组合是“超强 goto”。小心使用。然而,正如其他人所解释的那样,当您想要快速地执行 get me back to the beginning而不是为18层函数逐层返回错误消息时,longjmp对于摆脱令人讨厌的错误情况非常有用。
goto
get me back to the beginning
然而,就像 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 函数。”
setjmp和 longjmp在单元测试中非常有用。
假设我们要测试以下模块:
#include <stdlib.h> int my_div(int x, int y) { if (y==0) exit(2); return x/y; }
通常,如果要测试的函数调用另一个函数,您可以声明一个存根函数供它调用,该存根函数将模拟实际函数对测试某些流所做的工作。但是,在这种情况下,函数调用 exit,而 exit不返回。存根需要以某种方式模仿这种行为。setjmp和 longjmp可以为你做到这一点。
为了测试这个函数,我们可以创建以下测试程序:
#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这样做。如果您不这样做,您的测试程序可能不会干净利落地退出。
_exit
我已经用 C 编写了一个 类 Java 异常处理机制 ,使用 setjmp()、 longjmp()和系统函数。它可以捕获自定义异常,也可以捕获像 SIGSEGV这样的信号。它具有异常处理块的无限嵌套,可以跨函数调用工作,并支持两种最常见的线程实现。它允许您定义具有链接时继承特性的异常类的树层次结构,并且 catch语句遍历该树以查看它是否需要捕获或传递。
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 中实现延续,而不用以延续传递样式转换输入代码。