C 语言中 setjmp 和 longjmp 的实际用法

144

请问有人可以解释一下在嵌入式编程中,setjmp()longjmp()函数实际上可以被用于哪些场景吗?我知道它们是用于错误处理的,但我想知道一些具体的用例。


7
当然,http://thedailywtf.com/Articles/Longjmp--FOR-SPEED!!!.aspx。 - Daniel Fischer
1
除了已经给出的答案,这里还有一个:https://dev59.com/r2s05IYBdhLWcg3wR_63。您可以使用`longjmp()`来退出信号处理程序,特别是像“总线错误”这样的情况。通常情况下,此信号无法重新启动。嵌入式应用程序可能希望处理此情况以确保安全和稳健的操作。 - artless noise
2
关于BSD和Linux之间setjmp性能差异的问题,请参考"Timing setjmp, and the Joy of Standards"。该文章建议使用sigsetjmp - 0 _
8个回答

116

错误处理
如果一个深层的函数嵌套在许多其他函数中存在错误,而且只有在顶层函数中才有意义进行错误处理。

如果所有中间函数都必须正常返回并评估返回值或全局错误变量以确定进一步处理没有意义甚至会很糟糕,那将非常乏味和笨拙。

这是使用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

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

警告
当使用setjmp/longjmp时,请注意它们会对通常不被考虑的局部变量的有效性产生影响。
请参阅我的有关此主题的问题


3
由于setjmp准备,而longjmp执行跳出当前调用范围返回到setjmp范围,那么这如何支持协程的实现呢?我不明白如何继续执行longjmp跳出的例程。 - TheBlastOne
23
你需要将协程运行在不同的堆栈上,而不是像你的示例中所示在相同的堆栈上。由于routineAroutineB使用同一堆栈,因此它仅适用于非常基本的协程。如果在对routineB的第一次调用之后,routineA调用了一个深度嵌套的routineC,并且这个routineC作为协程运行了routineB,那么routineB甚至可能破坏routineC的返回堆栈(不仅仅是局部变量)。因此,在没有分配专用堆栈(通过在调用rountineB之后使用alloca())的情况下,如果将此示例用作教程,则会遇到严重问题。 - Tino
10
请在你的回答中提及,从调用栈 A 跳转到 B (jump down the callstack) 是未定义行为。 - MikeMB
2
在注释248)中,它写道:“例如,通过执行返回语句或因为另一个longjmp调用导致转移到嵌套调用集合中较早的函数中的setjmp调用。”因此,在函数外调用longjmp函数到调用栈更高的位置也会终止该函数,因此之后跳回该函数是未定义行为。 - MikeMB
4
确实是未定义的。您需要使每个函数在自己独立的堆栈上运行,以便优雅地切换上下文。 - Curious
显示剩余14条评论

25
理论上,您可以将它们用于错误处理,以便您可以跳出深度嵌套的调用链而无需在链中的每个函数中处理错误。
但是,像每个聪明的理论一样,当面对现实时,这个理论就会崩溃。您的中间函数将分配内存、获取锁定、打开文件并执行各种需要清理的不同操作。因此,在实际情况下,setjmp/longjmp通常是一个坏主意,除非您在环境方面具有完全控制权(某些嵌入式平台)。
根据我的经验,在大多数情况下,每当您认为使用setjmp/longjmp会起作用时,您的程序就足够清晰简单,使得调用链中的每个中间函数都可以进行错误处理;或者它太杂乱无法解决,您应该在遇到错误时进行exit。

6
请查看 libjpeg。与 C++ 一样,大多数 C 程序集合都需要一个指向结构体的指针来操作某个东西。将中间函数的内存分配存储在结构体中,而不是作为局部变量存储。这样可以允许 longjmp() 处理程序释放内存。此外,这种方法不会像所有 C++ 编译器在 20 年之后仍然生成的那样产生太多的异常表。 - artless noise
像所有聪明的理论一样,当遇到现实时,它就会崩溃。确实,临时分配等问题使得longjmp()变得棘手,因为您需要在调用堆栈中多次进行setjmp()(对于每个需要在退出之前执行某些清理操作的函数都需要进行一次,并且需要通过longjmp()返回到最初接收到的上下文以"重新引发异常")如果这些资源在setjmp()之后被修改,则必须将它们声明为“volatile”以防止longjmp()覆盖它们,情况会更糟。 - sevko

16
我使用setjmp()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就能做到这么远。

如果你感兴趣,请联系我。


1
我很惊讶在没有实际编译器支持自定义异常的情况下,这是可能的。但真正有趣的是信号如何转换为异常。 - Paul Stelian
我有一个问题:如果异常最终没有被捕获,那么main()函数会如何退出? - Paul Stelian
2
@PaulStelian 这里是你关于未捕获异常时 main() 函数如何退出的答案(链接:https://dev59.com/2LPma4cB1Zd3GeqPn0fI)。请给这个回答点赞 :-) - meaning-matters
1
@PaulStelian 啊,我现在明白你的意思了。我相信没有被捕获的运行时异常会再次引发,因此适用于通用(平台相关)答案。未捕获的自定义异常将被打印并忽略。请参见README中的“传播”部分。我已经将我的1999年4月代码发布到GitHub上(请参见编辑后答案中的链接)。看一下;这是一个难题。很高兴听听你的想法。 - meaning-matters
2
看了一下README,写得很不错。基本上它会传播到最外层的try块并报告错误,类似于JavaScript的异步函数。很好。稍后我会查看源代码本身。 - Paul Stelian
显示剩余2条评论

12

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

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

#include <stdlib.h>

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

通常情况下,如果要测试的函数调用了另一个函数,您可以声明一个存根函数供其调用,以模拟实际函数执行以测试某些流程。但在这种情况下,函数调用了不返回的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来执行退出。如果没有这样做,你的测试程序可能无法干净退出。

1
@milanHrabos 当测试正在运行时,done标志被设置为0。当调用exit(2)时,存根函数首先检查done是否为0,它是的。然后检查全局变量should_exit是否为1(真)和全局变量expected_code是否为2(真)。然后使用状态1调用longjmp。这将跳回到test_div0,其中从setjmp返回1。 - dbush

12
setjmplongjmp的结合就像是超级强大的goto。但必须极其小心谨慎使用,正如其他人所解释的那样,当你想快速回到setjmp开始的位置,而不必在18层函数中逐层返回错误消息时,longjmp非常有用。
然而,就像goto一样,longjmp更糟糕,你必须非常小心地使用它。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++一样进行完整的异常处理。但是这相当混乱,并且依赖于代码的编写质量。


当然,理论上你可以通过调用setjmp来保护每个初始化实现干净的异常处理,就像C++一样...值得一提的是,在线程中使用它是非标准的。 - Potatoswatter

10

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


5
毋庸置疑,setjmp/longjmp 最重要的用途就是作为“非局部goto跳转”。在同一作用域中,goto命令(以及极少数需要使用goto而不是for和while循环的情况)是最安全的使用方式。如果你使用goto跨越作用域(或者跨越自动分配),你很可能会破坏程序的堆栈。setjmp/longjmp避免了这种情况,它在想要跳转到的位置保存了堆栈信息。然后,当你跳转时,它加载这些堆栈信息。没有这个功能,C程序员很可能不得不转向汇编编程来解决只有setjmp/longjmp才能解决的问题。感谢它的存在。C库中的每一个东西都非常重要。你会知道你需要它们的时候。

3
C库中的每个部分都非常重要。但也有很多已被弃用或从一开始就不太好的东西,比如locales。 - qwr

1
除了错误处理,另一件事情是在 C 中以聪明的方式实现尾递归计算。这实际上就是在不将输入代码转换为 continuation passing style 的情况下,在 C 中实现 continuations 的方式。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接