请问有人可以解释一下在嵌入式编程中,setjmp()
和longjmp()
函数实际上可以被用于哪些场景吗?我知道它们是用于错误处理的,但我想知道一些具体的用例。
错误处理
如果一个深层的函数嵌套在许多其他函数中存在错误,而且只有在顶层函数中才有意义进行错误处理。
如果所有中间函数都必须正常返回并评估返回值或全局错误变量以确定进一步处理没有意义甚至会很糟糕,那将非常乏味和笨拙。
这是使用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("- 12 : (A1)\n");
r = setjmp(bufferA);
if (r == 0) routineB();
printf("- 17 : (A2) r=%d\n",r);
r = setjmp(bufferA);
if (r == 0) longjmp(bufferB, 20001);
printf("- 22 : (A3) r=%d\n",r);
r = setjmp(bufferA);
if (r == 0) longjmp(bufferB, 20002);
printf("- 27 : (A4) r=%d\n",r);
}
void routineB()
{
int r;
printf("- 34 : (B1)\n");
r = setjmp(bufferB);
if (r == 0) longjmp(bufferA, 10001);
printf("- 39 : (B2) r=%d\n", r);
r = setjmp(bufferB);
if (r == 0) longjmp(bufferA, 10002);
printf("- 44 : (B3) r=%d\n", r);
r = setjmp(bufferB);
if (r == 0) longjmp(bufferA, 10003);
}
int main(int argc, char **argv)
{
routineA();
return 0;
}
输出
- 12 : (A1)
- 34 : (B1)
- 17 : (A2) r=10001
- 39 : (B2) r=20001
- 22 : (A3) r=10002
- 44 : (B3) r=20002
- 27 : (A4) r=10003
以下图表显示执行流程:
警告
当使用setjmp/longjmp时,请注意它们会对通常不被考虑的局部变量的有效性产生影响。
请参阅我的有关此主题的问题。
routineA
和routineB
使用同一堆栈,因此它仅适用于非常基本的协程。如果在对routineB
的第一次调用之后,routineA
调用了一个深度嵌套的routineC
,并且这个routineC
作为协程运行了routineB
,那么routineB
甚至可能破坏routineC
的返回堆栈(不仅仅是局部变量)。因此,在没有分配专用堆栈(通过在调用rountineB
之后使用alloca()
)的情况下,如果将此示例用作教程,则会遇到严重问题。 - Tinolibjpeg
。与 C++ 一样,大多数 C 程序集合都需要一个指向结构体的指针来操作某个东西。将中间函数的内存分配存储在结构体中,而不是作为局部变量存储。这样可以允许 longjmp()
处理程序释放内存。此外,这种方法不会像所有 C++ 编译器在 20 年之后仍然生成的那样产生太多的异常表。 - artless noiselongjmp()
变得棘手,因为您需要在调用堆栈中多次进行setjmp()
(对于每个需要在退出之前执行某些清理操作的函数都需要进行一次,并且需要通过longjmp()
返回到最初接收到的上下文以"重新引发异常")如果这些资源在setjmp()
之后被修改,则必须将它们声明为“volatile”以防止longjmp()
覆盖它们,情况会更糟。 - sevkosetjmp()
、longjmp()
和系统函数,在C语言中编写了一个类似Java的异常处理机制,它能够捕获自定义异常以及像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("", ""));
}
以下是包含大量逻辑的 include 文件的一部分:
#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就能做到这么远。
如果你感兴趣,请联系我。
main()
函数如何退出的答案(链接:https://dev59.com/2LPma4cB1Zd3GeqPn0fI)。请给这个回答点赞 :-) - meaning-matterssetjmp
和longjmp
在单元测试中非常有用。
假设我们想要测试以下模块:
#include <stdlib.h>
int my_div(int x, int y)
{
if (y==0) exit(2);
return x/y;
}
通常情况下,如果要测试的函数调用了另一个函数,您可以声明一个存根函数供其调用,以模拟实际函数执行以测试某些流程。但在这种情况下,函数调用了不返回的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
来执行退出。如果没有这样做,你的测试程序可能无法干净退出。done
标志被设置为0。当调用exit(2)
时,存根函数首先检查done
是否为0,它是的。然后检查全局变量should_exit
是否为1(真)和全局变量expected_code
是否为2(真)。然后使用状态1调用longjmp
。这将跳回到test_div0
,其中从setjmp
返回1。 - dbushsetjmp
和longjmp
的结合就像是超级强大的goto
。但必须极其小心谨慎使用,正如其他人所解释的那样,当你想快速回到setjmp
开始的位置,而不必在18层函数中逐层返回错误消息时,longjmp
非常有用。goto
一样,longjmp
更糟糕,你必须非常小心地使用它。longjmp
只会让你回到代码的开始处,不会影响在setjmp
和回到setjmp
开始的位置之间发生的所有其他状态。因此,当你回到setjmp
被调用的位置时,分配、锁定、半初始化的数据结构等仍然保持原样。这意味着,在使用longjmp
时,你必须真正关心你这样做的地方,确保它不会引起更多的问题。当然,如果接下来要做的事情是“重新启动”(可能在存储了关于错误的消息之后),比如在嵌入式系统中发现硬件处于错误状态,那就没问题了。setjmp
/longjmp
提供非常基本的线程机制。但那是相当特殊的情况,绝对不是“标准”线程的工作方式。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++一样进行完整的异常处理。但是这相当混乱,并且依赖于代码的编写质量。
setjmp
来保护每个初始化实现干净的异常处理,就像C++一样...值得一提的是,在线程中使用它是非标准的。 - Potatoswatter既然你提到嵌入式,我认为值得注意的是一个非使用情况:当你的编码标准禁止时。例如MISRA(MISRA-C:2004:Rule 20.7)和JFS(AV规则20):“不得使用setjmp宏和longjmp函数。”
setjmp
性能差异的问题,请参考"Timing setjmp, and the Joy of Standards"。该文章建议使用sigsetjmp
。 - 0 _