Практичне використання setjmp та longjmp в C


98

Хто-небудь може пояснити мені, де саме setjmp()та longjmp()функції можуть бути використані практично у вбудованому програмуванні? Я знаю, що вони призначені для обробки помилок. Але я хотів би знати деякі випадки використання.


Для обробки помилок, як і в будь-якому іншому програмуванні. Я не бачу різниці у використанні ???
Tony The Lion


Для швидкості? Так. Оскільки а) він працює повільніше, ніж цикл, і б) тому, що його неможливо легко оптимізувати (наприклад, видалення затримки або двох). Тож setjmp & longjmp чітко керують!
TheBlastOne

Інша відповідь, ніж подана тут, stackoverflow.com/questions/7334595/ ... Ви можете використовувати, longjmp()щоб вийти з обробника сигналу, особливо такі, як BUS ERROR. Зазвичай цей сигнал не можна перезапустити. Вбудована програма, можливо, захоче розглянути цю справу для безпеки та надійної роботи.
бездумний шум

А щодо різниці в продуктивності setjmpміж BSD та Linux див. "Час setjmp і радість стандартів" , де пропонується використовувати sigsetjmp.
Іоанніс Філіппідіс

Відповіді:


84

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

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

Це ситуація, коли setjmp / longjmp має сенс. Ці ситуації схожі на ситуацію, коли виняток в інших мовах (C ++, Java) має сенс.

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

Це той випадок, коли вам потрібно впровадити спільні програми .

Ось невеликий демонстраційний приклад. Я сподіваюся, що це задовольняє запит Сівапрасада Паласа щодо якогось прикладу коду і відповідає на питання TheBlastOne, як setjmp / longjmp підтримує реалізацію корутинів (наскільки я бачу, це не базується на будь-якій нестандартній чи новій поведінці).

EDIT:
Це може бути , що на насправді це невизначене поведінку , щоб зробити 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

10
Програми повинні працювати на окремих стеках, а не на тих самих, як показано у вашому прикладі. Як routineAі routineBвикористовуючи той самий стек, він працює лише для дуже примітивних програм. Якщо routineAвиклики глибоко вкладеного routineCпісля першого виклику до, routineBі це routineCвиконується routineBяк спільна програма, тоді routineBможе навіть знищити стек повернення (не тільки локальні змінні) з routineC. Тож, не виділяючи ексклюзивний стек (через alloca()після виклику rountineB?), Ви отримаєте серйозні проблеми з цим прикладом, якщо він використовується як рецепт.
Тіно

7
Будь ласка, зауважте, у своїй відповіді, що стрибок вниз з телефонної лінії (від А до Б) є невизначеною поведінкою).
MikeMB

1
І у виносці 248) це говорить: "Наприклад, виконуючи оператор return або тому, що інший виклик longjmp спричинив перехід до виклику setjmp у функції раніше в наборі вкладених викликів." Отже, виклик longjmp-функції з функції до точки, що знаходиться далі вгору, за допомогою виклику також припиняє цю функцію і, отже, перехід назад до неї - це UB.
MikeMB

18

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

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

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


3
Будь ласка, подивіться libjpeg. Як і в C ++, більшість колекцій підпрограм C вимагають 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це зробити. Якщо ви цього не зробите, програма тесту може не завершити роботу належним чином.


6

Я написав 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("", ""));
}

І ось частина файлу 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
Я здивований, що це можливо без фактичної підтримки компілятора спеціальних винятків. Але насправді цікаво те, як сигнали перетворюються на винятки.
Пол

Я запитаю одне: як щодо винятків, які в підсумку ніколи не потрапляють? Як main () вийде?
Пол

1
@PaulStelian І, ось ваша відповідь на те, як main()буде виходити при невпійманому звільненні. Будь ласка, проголосуйте цю відповідь :-)
значення має значення

1
@PaulStelian Ах, я розумію, що ти маєш на увазі зараз. Я вважаю, що винятки під час виконання, які не вловлюються, були підняті ще раз, щоб застосувати загальну (залежно від платформи) відповідь. Не впіймані спеціальні винятки друкувались та ігнорувались Дивіться Progagationрозділ README, який я опублікував у квітні 1999 року на GitHub (див. Посилання у відредагованій відповіді). Подивитися; це був твердий горіх. Було б непогано почути, що ти думаєш.
значення має значення

2
Коротко подивився README, там досить гарно. Отже, в основному він поширюється до самого зовнішнього блоку try і повідомляється, подібний до асинхронних функцій JavaScript. Приємно. Пізніше я розгляну сам код.
Пол

1

Руки вниз, найважливіше використання setjmp / longjmp полягає в тому, що він діє як "нелокальний перехід до переходу". Команда Goto (і там рідкісні випадки, коли вам потрібно буде використовувати goto over для циклів and while) найбільш безпечно використовується в тому ж обсязі. Якщо ви використовуєте goto для переходу між областями (або автоматичним розподілом), ви, швидше за все, пошкодите стек вашої програми. setjmp / longjmp уникає цього, зберігаючи інформацію про стек у місці, куди потрібно перейти. Потім, коли ви стрибаєте, він завантажує цю інформацію про стек. Без цієї функції програмістам C, швидше за все, довелося б звернутися до програмування збірки для вирішення проблем, які міг вирішити лише setjmp / longjmp. Слава Богу, воно існує. Все в бібліотеці C надзвичайно важливо. Ви дізнаєтесь, коли це вам буде потрібно.


"Усе в бібліотеці C надзвичайно важливо". Існує ціла купа застарілих речей і таких речей, які ніколи не були хорошими, як-от локалі.
qwr
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.