Практическое использование setjmp и longjmp в C

Может кто-нибудь объяснить мне, где именно setjmp() а также longjmp() функции можно использовать практически во встроенном программировании? Я знаю, что это для обработки ошибок. Но я хотел бы знать некоторые варианты использования.

7 ответов

Решение

Обработка ошибок
Предположим, что есть ошибка глубоко внутри функции, вложенной во многие другие функции, и обработка ошибок имеет смысл только в функции верхнего уровня.

Было бы очень утомительно и неловко, если бы все промежуточные функции возвращались нормально и оценивали возвращаемые значения или глобальную переменную ошибки, чтобы определить, что дальнейшая обработка не имеет смысла или даже будет плохой.

Это ситуация, в которой setjmp/longjmp имеет смысл. Эти ситуации аналогичны ситуации, в которой имеет место исключение в других языках (C++, Java).

Сопрограммы
Помимо обработки ошибок, я могу подумать и о другой ситуации, когда вам нужен setjmp/longjmp в C:

Это тот случай, когда нужно реализовать сопрограммы.

Вот небольшой демонстрационный пример. Я надеюсь, что он удовлетворит запрос 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;
}

На следующем рисунке показан ход выполнения:
поток исполнения

Предупреждение примечание
При использовании setjmp/longjmp следует помнить, что они влияют на достоверность локальных переменных, которые часто не учитываются.
Ср мой вопрос по этой теме.

Теория заключается в том, что вы можете использовать их для обработки ошибок, чтобы вы могли выпрыгнуть из глубоко вложенной цепочки вызовов, не сталкиваясь с обработкой ошибок в каждой функции в цепочке.

Как и любая умная теория, она разваливается при встрече с реальностью. Ваши промежуточные функции будут распределять память, захватывать блокировки, открывать файлы и выполнять различные действия, требующие очистки. Так на практике setjmp/longjmp Обычно это плохая идея, за исключением очень ограниченных обстоятельств, когда у вас есть полный контроль над вашей средой (некоторые встроенные платформы).

По моему опыту, в большинстве случаев, когда вы думаете, что с помощью setjmp/longjmp сработает, ваша программа достаточно ясна и проста, так что каждый промежуточный вызов функции в цепочке вызовов может выполнять обработку ошибок, или это настолько грязно и невозможно исправить, что вы должны сделать exit когда вы сталкиваетесь с ошибкой.

Я написал Java-подобный механизм обработки исключений в C, используя 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". Используйте с особой осторожностью. Однако, как объяснили другие, longjmp очень полезно выйти из неприятной ситуации с ошибками, когда вы хотите get me back to the beginning быстро, вместо того, чтобы выводить сообщение об ошибке для 18 уровней функций.

Тем не менее, так же, как 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++". Но это довольно грязно, и полагается на хорошо написанный код.

setjmp а также 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 сделать это. Если вы этого не сделаете, ваша тестовая программа может завершиться некорректно.

Поскольку вы упоминаете встроенный, я думаю, что стоит отметить случай неиспользования: когда ваш стандарт кодирования запрещает это. Например, MISRA (MISRA-C:2004: правило 20.7) и JFS (AV-правило 20): "Макрос setjmp и функция longjmp не должны использоваться".

Несомненно, наиболее важным применением setjmp/longjmp является то, что он выполняет "нелокальный переход". Команда Goto (и есть редкие случаи, когда вам нужно будет использовать циклы goto over for и while) наиболее безопасно и безопасно используется в той же области. Если вы используете goto для перехода между областями видимости (или автоматическим распределением), вы, скорее всего, повредите стек своей программы. setjmp/longjmp позволяет избежать этого, сохраняя информацию стека в том месте, куда вы хотите перейти. Затем, когда вы прыгаете, он загружает эту информацию о стеке. Без этой функции программистам на C, скорее всего, пришлось бы обратиться к программированию на ассемблере для решения проблем, которые мог решить только setjmp/longjmp. Слава богу, есть. Все в библиотеке C чрезвычайно важно. Вы будете знать, когда вам это нужно.

Помимо обработки ошибок, еще одна вещь, которую вы можете сделать и о которой ранее не упоминалось, - это разумно реализовать хвостовое рекурсивное вычисление в C.

Именно так реализованы продолжения в C без преобразования входного кода в стиль передачи продолжения.

setjmp() и longjmp() полезны для обработки ошибок и прерываний, встречающихся в низкоуровневой подпрограмме программы.

Другие вопросы по тегам