Utilisation pratique de setjmp et longjmp en C

97

Quelqu'un peut-il m'expliquer où exactement setjmp()et les longjmp()fonctions peuvent être utilisées pratiquement dans la programmation embarquée? Je sais que ce sont pour la gestion des erreurs. Mais j'aimerais connaître quelques cas d'utilisation.

Pala
la source
Pour la gestion des erreurs comme dans toute autre programmation. Je ne vois pas la différence d'utilisation ???
Tony The Lion
Pour la vitesse? Oui. Parce que a) il fonctionne plus lentement qu'une boucle, et b) parce qu'il ne peut pas être optimisé facilement (comme la suppression d'un délai ou deux). Donc setjmp & longjmp règnent clairement!
TheBlastOne
Une autre réponse que celles données est ici stackoverflow.com/questions/7334595/ ... Vous pouvez utiliser longjmp()pour sortir d'un gestionnaire de signaux, en particulier des choses comme un BUS ERROR. Ce signal ne peut généralement pas redémarrer. Une application embarquée peut souhaiter gérer ce cas pour la sécurité et un fonctionnement robuste.
bruit naïf
Et concernant les différences de performances setjmpentre BSD et Linux, consultez «Timing setjmp, and the Joy of Standards» , qui suggère d'utiliser sigsetjmp.
Ioannis Filippidis

Réponses:

83

Gestion des erreurs
Supposons qu'il y ait une erreur au fond d'une fonction imbriquée dans de nombreuses autres fonctions et que la gestion des erreurs n'a de sens que dans la fonction de niveau supérieur.

Ce serait très fastidieux et gênant si toutes les fonctions intermédiaires devaient retourner normalement et évaluer les valeurs de retour ou une variable d'erreur globale pour déterminer qu'un traitement ultérieur n'a pas de sens ou même serait mauvais.

C'est une situation où setjmp / longjmp a du sens. Ces situations sont similaires à celles où l'exception dans d'autres langages (C ++, Java) a du sens.

Coroutines
Outre la gestion des erreurs, je peux également penser à une autre situation où vous avez besoin de setjmp / longjmp en C:

C'est le cas lorsque vous devez implémenter des coroutines .

Voici un petit exemple de démonstration. J'espère que cela satisfait la demande de Sivaprasad Palas pour un exemple de code et répond à la question de TheBlastOne comment setjmp / longjmp prend en charge l'implémentation de corroutines (autant que je vois, il ne repose sur aucun comportement non standard ou nouveau).

EDIT:
Il se peut que ce soit en fait un comportement indéfini pour faire un longjmp down the callstack (voir le commentaire de MikeMB; bien que je n'ai pas encore eu l'occasion de le vérifier).

#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;
}

La figure suivante montre le déroulement de l'exécution:
flux d'exécution

Note d'avertissement
Lorsque vous utilisez setjmp / longjmp, sachez qu'ils ont un effet sur la validité des variables locales souvent non prises en compte.
Cf. ma question sur ce sujet .

fromage blanc
la source
2
Puisque setjmp prépare et que longjmp exécute le saut hors de la portée d'appel actuelle vers la portée setjmp, comment cela prendrait-il en charge l'implémentation des coroutines? Je ne vois pas comment on pourrait continuer l’exécution de la routine qui a été longue.
TheBlastOne
2
@TheBlastOne Voir l'article Wikipédia . Vous pouvez continuer l'exécution si vous êtes setjmpavant vous longjmp. Ce n'est pas standard.
Potatoswatter
10
Les coroutines doivent s'exécuter sur des piles distinctes, pas sur les mêmes que celles illustrées dans votre exemple. Comme routineAet routineButiliser la même pile, cela ne fonctionne que pour les coroutines très primitives. Si routineAappelle un profondément imbriqué routineCaprès le premier appel à routineBet que cela routineCs'exécute routineBcomme coroutine, alors routineBpeut même détruire la pile de retour (pas seulement les variables locales) de routineC. Donc, sans allouer une pile exclusive ( alloca()après avoir appelé rountineB?), Vous aurez de sérieux problèmes avec cet exemple s'il est utilisé comme recette.
Tino
7
Veuillez mentionner, dans votre réponse, que sauter la pile d'appels (de A à B) est un comportement indéfini).
MikeMB
1
Et dans la note de bas de page 248), il lit: "Par exemple, en exécutant une instruction return ou parce qu'un autre appel longjmp a provoqué un transfert vers un appel setjmp dans une fonction plus tôt dans l'ensemble des appels imbriqués." Donc, appeler une fonction longjmp hors d'une fonction à un point plus haut dans la pile d'appels met également fin à cette fonction et, par conséquent, y retourner par la suite est UB.
MikeMB
18

La théorie est que vous pouvez les utiliser pour la gestion des erreurs afin de pouvoir sortir d'une chaîne d'appels profondément imbriquée sans avoir à gérer les erreurs de gestion dans chaque fonction de la chaîne.

Comme toute théorie intelligente, cela s'effondre en rencontrant la réalité. Vos fonctions intermédiaires alloueront de la mémoire, saisiront les verrous, ouvriront des fichiers et feront toutes sortes de choses qui nécessitent un nettoyage. Donc dans la pratique setjmp/ longjmpsont généralement une mauvaise idée sauf dans des circonstances très limitées où vous avez un contrôle total sur votre environnement (certaines plates-formes embarquées).

D'après mon expérience, dans la plupart des cas, chaque fois que vous pensez que l'utilisation de setjmp/ longjmpfonctionnerait, votre programme est suffisamment clair et simple pour que chaque appel de fonction intermédiaire dans la chaîne d'appels puisse gérer les erreurs, ou il est si compliqué et impossible à résoudre que vous devriez le faire exitlorsque vous rencontrer l'erreur.

Art
la source
3
Veuillez regarder libjpeg. Comme en C ++, la plupart des collections de routines C prennent un struct *pour opérer sur quelque chose en tant que collectif. Au lieu de stocker vos allocations de mémoire de fonctions intermédiaires en tant que locaux, elles peuvent être stockées dans la structure. Cela permet à un longjmp()gestionnaire de libérer de la mémoire. De plus, cela n'a pas tellement de tables d'exceptions foudroyées que tous les compilateurs C ++ génèrent encore 20 ans après les faits.
bruit naïf
Like every clever theory this falls apart when meeting reality.En effet, l'allocation temporaire et autres compliquent la longjmp()tâche, car vous devez ensuite setjmp()plusieurs fois dans la pile d'appels (une fois pour chaque fonction qui doit effectuer une sorte de nettoyage avant de se terminer, qui doit alors "re-lever l'exception" en fonction longjmp()du contexte qu'il avait initialement reçu). Cela devient encore pire si ces ressources sont modifiées après le setjmp(), car vous devez les déclarer volatilepour éviter que le ne les longjmp()écrase.
sevko
10

La combinaison de setjmpet longjmpest "super force goto". Utiliser avec un soin EXTRÊME. Cependant, comme d'autres l'ont expliqué, a longjmpest très utile pour sortir d'une situation d'erreur désagréable, lorsque vous le souhaitez get me back to the beginningrapidement, plutôt que de devoir renvoyer un message d'erreur pour 18 couches de fonctions.

Cependant, tout comme goto, mais pire, vous devez VRAIMENT faire attention à la façon dont vous l'utilisez. A longjmpvous ramènera simplement au début du code. Cela n'affectera pas tous les autres états qui peuvent avoir changé entre le setjmpet le retour au point de setjmpdépart. Ainsi, les allocations, verrous, structures de données à moitié initialisées, etc., sont toujours alloués, verrouillés et à moitié initialisés lorsque vous revenez à l'endroit où a setjmpété appelé. Cela signifie que vous devez vraiment vous soucier des endroits où vous faites cela, qu'il est VRAIMENT correct d'appeler longjmpsans causer PLUS de problèmes. Bien sûr, si la prochaine chose que vous faites est de "redémarrer" [après avoir stocké un message sur l'erreur, peut-être] - dans un système embarqué où vous avez découvert que le matériel est en mauvais état, par exemple, alors très bien.

J'ai également vu setjmp/ longjmputilisé pour fournir des mécanismes de filetage très basiques. Mais c'est un cas assez particulier - et certainement pas comment fonctionnent les threads "standard".

Edit: On pourrait bien sûr ajouter du code pour "s'occuper du nettoyage", de la même manière que C ++ stocke les points d'exception dans le code compilé et sait ensuite ce qui a donné une exception et ce qui doit être nettoyé. Cela impliquerait une sorte de table de pointeur de fonction et le stockage de "si nous sautons de dessous ici, appelons cette fonction, avec cet argument". Quelque chose comme ça:

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);
}

Avec ce système, vous pouvez effectuer une «gestion complète des exceptions comme C ++». Mais c'est assez compliqué et dépend du fait que le code est bien écrit.

Mats Petersson
la source
+1, bien sûr, vous pourriez en théorie implémenter une gestion propre des exceptions en appelant setjmpà guard chaque initialisation, à la C ++… et il convient de mentionner que son utilisation pour le threading n'est pas standard.
Potatoswatter
8

Puisque vous mentionnez embarqué, je pense qu'il vaut la peine de noter un cas de non-utilisation : lorsque votre norme de codage l'interdit. Par exemple MISRA (MISRA-C: 2004: règle 20.7) et JFS (règle 20 AV): "La macro setjmp et la fonction longjmp ne doivent pas être utilisées."

Clément J.
la source
8

setjmpet longjmppeut être très utile dans les tests unitaires.

Supposons que nous voulions tester le module suivant:

#include <stdlib.h>

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

Normalement, si la fonction à tester appelle une autre fonction, vous pouvez déclarer une fonction stub à appeler qui imitera ce que fait la fonction réelle pour tester certains flux. Dans ce cas cependant, la fonction appelle exitqui ne retourne pas. Le stub doit en quelque sorte émuler ce comportement. setjmpet longjmppeut le faire pour vous.

Pour tester cette fonction, nous pouvons créer le programme de test suivant:

#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);
}

Dans cet exemple, vous utilisez setjmpavant d'entrer la fonction à tester, puis dans le stubbed que exitvous appelez longjmppour revenir directement à votre cas de test.

Notez également que le redéfini exita une variable spéciale qu'il vérifie pour voir si vous voulez réellement quitter le programme et appelle _exitpour le faire. Si vous ne le faites pas, votre programme de test risque de ne pas se fermer correctement.

dbush
la source
6

J'ai écrit un mécanisme de gestion de type Java exception en C en utilisant setjmp(), longjmp()et les fonctions du système. Il détecte les exceptions personnalisées mais également des signaux comme SIGSEGV. Il propose une imbrication infinie des blocs de gestion des exceptions, qui fonctionne sur les appels de fonction et prend en charge les deux implémentations de thread les plus courantes. Il vous permet de définir une arborescence de classes d'exceptions qui présentent l'héritage au moment de la liaison, et lecatch instruction parcourt cette arborescence pour voir si elle doit intercepter ou transmettre.

Voici un exemple de l'apparence du code en utilisant ceci:

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("", ""));
}

Et voici une partie du fichier d'inclusion qui contient beaucoup de logique:

#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 */

Il existe également un module C qui contient la logique de gestion du signal et une certaine comptabilité.

C'était extrêmement difficile à mettre en œuvre, je peux vous le dire et j'ai presque arrêté. J'ai vraiment poussé à le rendre aussi proche que possible de Java; J'ai trouvé surprenant à quel point je suis arrivé avec seulement C.

Donnez-moi un cri si vous êtes intéressé.

le sens compte
la source
1
Je suis surpris que cela soit possible sans la prise en charge réelle du compilateur pour les exceptions personnalisées. Mais ce qui est vraiment intéressant, c'est la façon dont les signaux se convertissent en exceptions.
Paul Stelian
Je demanderai une chose: qu'en est-il des exceptions qui finissent par ne jamais se faire prendre? Comment main () se terminera-t-il?
Paul Stelian
1
@PaulStelian Et, voici votre réponse à la façon dont main()sortira sur une exception non interceptée. S'il vous plaît upvote cette réponse :-)
sens-questions
1
@PaulStelian Ah, je vois ce que tu veux dire maintenant. Les exceptions d'exécution qui ne sont pas détectées, je crois, ont été à nouveau soulevées afin que la réponse générale (dépendante de la plate-forme) s'applique. Les exceptions personnalisées non interceptées ont été imprimées et ignorées. Voir la Progagationsection dans le README J'ai publié mon code d'avril 1999 sur GitHub (voir le lien dans la réponse modifiée). Regarde; c'était difficile à casser. Ce serait bien d'entendre ce que vous pensez.
sens-questions
2
J'ai jeté un bref coup d'œil au README, plutôt sympa. Donc, fondamentalement, il se propage vers le bloc try le plus externe et est signalé, comme les fonctions asynchrones de JavaScript. Agréable. Je regarderai le code source lui-même plus tard.
Paul Stelian
1

Sans conteste, l'utilisation la plus cruciale de setjmp / longjmp est qu'il agit comme un "saut goto non local". La commande Goto (et dans de rares cas où vous devrez utiliser goto over for et while boucles) est la plus utilisée en toute sécurité dans la même portée. Si vous utilisez goto pour sauter à travers les étendues (ou à travers l'allocation automatique), vous endommagerez probablement la pile de votre programme. setjmp / longjmp évite cela en enregistrant les informations de la pile à l'emplacement auquel vous souhaitez accéder. Ensuite, lorsque vous sautez, il charge ces informations de pile. Sans cette fonctionnalité, les programmeurs C devraient très probablement se tourner vers la programmation d'assemblage pour résoudre des problèmes que seul setjmp / longjmp pourrait résoudre. Dieu merci, cela existe. Tout dans la bibliothèque C est extrêmement important. Vous saurez quand vous en aurez besoin.

AndreGraveler
la source
"Tout dans la bibliothèque C est extrêmement important." Il y a tout un tas de choses obsolètes et de choses qui n'ont jamais été bonnes, comme les locales.
qwr