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

99

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

Пала
источник
Для обработки ошибок, как и в любом другом программировании. Не вижу разницы в использовании ???
Тони Лев,
3
И, конечно же, thedailywtf.com/Articles/Longjmp--FOR-SPEED!!!.aspx
Дэниел Фишер
Для скорости? Да. Потому что а) он работает медленнее, чем цикл, и б) потому, что его нельзя легко оптимизировать (например, удаление задержки или двух). Итак, setjmp и longjmp однозначно правят!
TheBlastOne 05
Другой ответ, кроме приведенных, находится здесь stackoverflow.com/questions/7334595/… Вы можете использовать, longjmp()чтобы выйти из обработчика сигналов, особенно таких вещей, как файл BUS ERROR. Этот сигнал обычно не может быть перезапущен. Встроенное приложение может захотеть обработать этот случай для безопасности и надежной работы.
бесхитростный шум
Что касается различий в производительности setjmpмежду BSD и Linux, см. «Timing setjmp и радость стандартов» , где предлагается использовать sigsetjmp.
Иоаннис Филиппидис

Ответы:

85

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

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

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

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

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

Вот небольшой демонстрационный пример. Я надеюсь, что он удовлетворит запрос Sivaprasad Palas о некотором примере кода и ответит на вопрос TheBlastOne, как setjmp / longjmp поддерживает реализацию сопрограмм (насколько я вижу, это не основано на каком-либо нестандартном или новом поведении).

РЕДАКТИРОВАТЬ:
Возможно, на самом деле это поведение undefined для выполнения 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 имейте в виду, что они влияют на достоверность локальных переменных, которые часто не рассматриваются.
Ср. мой вопрос по этой теме .

Творог
источник
2
Поскольку setjmp подготавливает, а longjmp выполняет переход из области текущего вызова обратно в область setjmp, как это поддержит реализацию сопрограмм? Я не понимаю, как можно было бы продолжать выполнение такой долгой рутины.
TheBlastOne
2
@TheBlastOne См. Статью в Википедии . Вы можете продолжить казнь, если вы setjmpперед вами longjmp. Это нестандартно.
Potatoswatter
11
Сопрограммы должны запускаться в отдельных стеках, а не в тех, которые показаны в вашем примере. Поскольку routineAи routineBиспользуется один и тот же стек, он работает только для очень примитивных сопрограмм. Если после первого вызова routineAвызывает глубоко вложенный объект, а он выполняется как сопрограмма, то может даже уничтожить стек возврата (не только локальные переменные) . Таким образом, без выделения эксклюзивного стека (через после вызова ?) У вас возникнут серьезные проблемы с этим примером, если он используется в качестве рецепта. routineCroutineBroutineCroutineBroutineBroutineCalloca()rountineB
Тино
7
Пожалуйста, укажите в своем ответе, что переход вниз по стеку вызовов (от A до B) является неопределенным поведением).
MikeMB
2
Это действительно не определено. Вам нужно будет заставить каждую функцию работать в своем собственном независимом стеке, чтобы изящно переключать контексты
Любопытно,
19

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

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

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

Изобразительное искусство
источник
3
Пожалуйста, посмотрите libjpeg. Как и в C ++, большинство наборов процедур C используют a struct *для работы с чем-то как с коллективом. Вместо того, чтобы хранить выделения памяти промежуточных функций как локальные, их можно сохранить в структуре. Это позволяет longjmp()обработчику освободить память. Кроме того, здесь не так много взорванных таблиц исключений, чтобы все компиляторы C ++ по-прежнему генерировали 20 лет спустя.
бесхитростный шум
Like every clever theory this falls apart when meeting reality.В самом деле, временное выделение и тому подобное longjmp()усложняют задачу, так как в этом случае вам придется setjmp()выполнять несколько операций в стеке вызовов (один раз для каждой функции, которая должна выполнить какую-либо очистку перед ее завершением, а затем необходимо «повторно вызвать исключение» в соответствии с longjmp()исходным контекстом). Еще хуже, если эти ресурсы изменяются после setjmp(), так как вы должны объявить их, volatileчтобы предотвратить longjmp()их уничтожение.
sevko
10

Сочетание setjmpи longjmpесть «суперсила goto». Используйте с КРАЙНЕЙ осторожностью. Однако, как объясняли другие, a longjmpочень полезен, чтобы выйти из неприятной ситуации с ошибкой, когда вы хотите get me back to the beginningбыстро, вместо того, чтобы возвращать сообщение об ошибке для 18 уровней функций.

Однако, точно так же goto, но хуже того, вы должны быть ДЕЙСТВИТЕЛЬНО осторожны при использовании этого. A 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 ++». Но это довольно беспорядочно и зависит от хорошо написанного кода.

Матс Петерссон
источник
+1, конечно, теоретически вы могли бы реализовать чистую обработку исключений, вызывая setjmpдля защиты каждой инициализации, а-ля C ++… и стоит упомянуть, что использование ее для потоковой передачи нестандартно.
Potatoswatter
8

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

Клемент Дж.
источник
8

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

dbush
источник
7

Я написал 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.

Скажите мне, если вам интересно.

значение имеет значение
источник
1
Я удивлен, что это возможно без реальной поддержки компилятором настраиваемых исключений. Но что действительно интересно, так это то, как сигналы преобразуются в исключения.
Пол Стелиан,
Я спрошу одну вещь: а как насчет исключений, которые никогда не перехватываются? Как выйти из main ()?
Пол Стелиан,
1
@PaulStelian И вот ваш ответ на вопрос, как main()выйти из неперехваченного исключения. Проголосуйте за этот ответ :-)
значение имеет значение
1
@PaulStelian А, теперь я понимаю, что вы имеете в виду. Исключения во время выполнения, которые не обнаруживаются, я считаю, были снова вызваны, поэтому применяется общий (зависящий от платформы) ответ. Необнаруженные пользовательские исключения были напечатаны и проигнорированы. См. ProgagationРаздел в README. Я разместил свой код от апреля 1999 г. на GitHub (см. Ссылку в отредактированном ответе). Взглянуть; это был крепкий орешек. Было бы неплохо услышать, что вы думаете.
значение имеет значение
2
Бегло взглянул на README, там довольно неплохо. Таким образом, в основном он распространяется на самый внешний блок try и сообщается, как и асинхронные функции JavaScript. Ницца. Позже я посмотрю сам исходный код.
Пол Стелиан
1

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

АндреГравелер
источник
1
«Все в библиотеке C чрезвычайно важно». Есть множество устаревших вещей и вещей, которые никогда не были хорошими, например, локализации.
qwr
0

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

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

Alinsoar
источник