Uso pratico di setjmp e longjmp in C


98

Qualcuno può spiegarmi dove esattamente setjmp()e le longjmp()funzioni possono essere utilizzate praticamente nella programmazione embedded? So che servono per la gestione degli errori. Ma mi piacerebbe conoscere alcuni casi d'uso.


Per la gestione degli errori come in qualsiasi altra programmazione. Non vedo la differenza nell'utilizzo ???
Tony The Lion


Per velocità? Sì. Perché a) funziona più lentamente di un ciclo eb) perché non può essere ottimizzato facilmente (come l'eliminazione di uno o due ritardi). Quindi setjmp e longjmp dominano chiaramente!
TheBlastOne

Un'altra risposta rispetto a quelle fornite è qui stackoverflow.com/questions/7334595/… Puoi usarla longjmp()per uscire da un gestore di segnali, specialmente cose come un file BUS ERROR. Questo segnale di solito non può riavviarsi. Un'applicazione incorporata potrebbe voler gestire questo caso per un funzionamento sicuro e robusto.
rumore senza arte

E per quanto riguarda le differenze di prestazioni setjmptra BSD e Linux, vedere "Timing setjmp, and the Joy of Standards" , che suggerisce l'uso sigsetjmp.
Ioannis Filippidis

Risposte:


84

Gestione degli errori
Supponiamo che ci sia un errore in profondità in una funzione annidata in molte altre funzioni e che la gestione degli errori abbia senso solo nella funzione di primo livello.

Sarebbe molto noioso e imbarazzante se tutte le funzioni intermedie dovessero tornare normalmente e valutare i valori di ritorno o una variabile di errore globale per determinare che un'ulteriore elaborazione non ha senso o addirittura sarebbe dannosa.

Questa è una situazione in cui setjmp / longjmp ha senso. Queste situazioni sono simili alla situazione in cui l'eccezione in altre lingue (C ++, Java) ha senso.

Coroutines
Oltre alla gestione degli errori, posso pensare anche a un'altra situazione in cui hai bisogno di setjmp / longjmp in C:

È il caso in cui è necessario implementare le coroutine .

Ecco un piccolo esempio dimostrativo. Spero che soddisfi la richiesta di Sivaprasad Palas per un codice di esempio e risponda alla domanda di TheBlastOne su come setjmp / longjmp supporta l'implementazione delle corroutine (per quanto vedo non si basa su alcun comportamento nuovo o non standard).

EDIT:
Potrebbe essere che in realtà è un comportamento indefinito di fare un longjmp lungo lo stack di chiamate (si veda il commento di MikeMB, anche se non ho ancora avuto occasione di verificare che).

#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 figura seguente mostra il flusso di esecuzione:
flusso di esecuzione

Nota di avvertenza
Quando si utilizzano setjmp / longjmp, tenere presente che hanno un effetto sulla validità delle variabili locali spesso non considerate.
Cfr. la mia domanda su questo argomento .


2
Poiché setjmp si prepara e longjmp esegue il salto dall'ambito della chiamata corrente all'ambito setjmp, come supporterebbe l'implementazione delle coroutine? Non vedo come si possa continuare l'esecuzione della routine così a lungo.
TheBlastOne

2
@TheBlastOne Vedi l'articolo di Wikipedia . Puoi continuare l'esecuzione se setjmpprima di te longjmp. Questo non è standard.
Potatoswatter

10
Le coroutine devono essere eseguite su stack separati, non sullo stesso come mostrato nel tuo esempio. Poiché routineAe routineBusa lo stesso stack, funziona solo per coroutine molto primitive. Se routineAchiama un profondamente annidato routineCdopo la prima chiamata a routineBe questo routineCviene eseguito routineBcome coroutine, routineBpotrebbe persino distruggere lo stack di ritorno (non solo le variabili locali) di routineC. Quindi senza allocare uno stack esclusivo (attraverso alloca()dopo aver chiamato rountineB?) Ti troverai in guai seri con questo esempio se usato come ricetta.
Tino

7
Per favore, menziona, nella tua risposta, che saltare giù dallo stack di chiamate (da A a B) è un comportamento indefinito).
MikeMB

1
E nella nota a piè di pagina 248) si legge: "Ad esempio, eseguendo un'istruzione return o perché un'altra chiamata longjmp ha causato un trasferimento a una chiamata setjmp in una funzione precedente nel set di chiamate annidate." Quindi chiamare una funzione longjmp da una funzione a un punto più in alto nello stack di chiamate termina anche quella funzione e quindi tornare indietro in seguito è UB.
MikeMB

18

La teoria è che puoi usarli per la gestione degli errori in modo da poter saltare fuori da una catena di chiamate profondamente annidata senza dover gestire gli errori di gestione in ogni funzione della catena.

Come ogni teoria intelligente, questa va in pezzi quando si incontra la realtà. Le tue funzioni intermedie allocheranno memoria, acquisiranno blocchi, apriranno file e faranno tutti i tipi di cose diverse che richiedono la pulizia. Quindi in pratica setjmp/ longjmpsono di solito una cattiva idea tranne in circostanze molto limitate in cui hai il controllo totale sul tuo ambiente (alcune piattaforme incorporate).

Nella mia esperienza nella maggior parte dei casi ogni volta che si pensa che l'uso di setjmp/ longjmpavrebbe funzionato, il programma è abbastanza chiaro e semplice che ogni chiamata di funzione intermedia nella catena di chiamate può gestire gli errori, oppure è così disordinato e impossibile da risolvere che dovresti farlo exitquando lo fai riscontrare l'errore.


3
Per favore guarda libjpeg. Come in C ++, la maggior parte delle raccolte di routine C richiede a struct *per operare su qualcosa come un collettivo. Invece di archiviare le allocazioni di memoria delle funzioni intermedie come locali, è possibile archiviarle nella struttura. Ciò consente a un longjmp()gestore di liberare la memoria. Inoltre, questo non ha così tante dannate tabelle di eccezioni che tutti i compilatori C ++ generano ancora 20 anni dopo il fatto.
rumore senza arte

Like every clever theory this falls apart when meeting reality.In effetti, l'allocazione temporanea e simili rendono longjmp()complicato, dal momento che devi quindi setjmp()più volte nello stack di chiamate (una volta per ogni funzione che deve eseguire una sorta di pulizia prima che esca, che quindi deve "rilanciare l'eccezione" in base longjmp()al contesto che aveva inizialmente ricevuto). Peggio ancora se quelle risorse vengono modificate dopo il setjmp(), dal momento che devi dichiararle volatileper evitare che le longjmp()distruggano.
sevko

10

La combinazione di setjmped longjmpè "super forza goto". Utilizzare con cura ESTREMA. Tuttavia, come altri hanno spiegato, a longjmpè molto utile per uscire da una brutta situazione di errore, quando lo si desidera get me back to the beginningrapidamente, piuttosto che dover ritirare un messaggio di errore per 18 livelli di funzioni.

Tuttavia, proprio come goto, ma peggio, devi stare DAVVERO attento a come lo usi. A longjmpti riporterà solo all'inizio del codice. Non influenzerà tutti gli altri stati che potrebbero essere cambiati tra il setjmpe il ritorno al punto di setjmppartenza. Quindi allocazioni, blocchi, strutture dati parzialmente inizializzate, ecc., Vengono ancora allocate, bloccate e parzialmente inizializzate quando torni al punto in cui è setjmpstato chiamato. Ciò significa che devi veramente preoccuparti dei luoghi in cui lo fai, che è DAVVERO ok chiamare longjmpsenza causare PIÙ problemi. Ovviamente, se la prossima cosa che fai è "riavviare" [dopo aver memorizzato un messaggio sull'errore, forse] - in un sistema embedded in cui hai scoperto che l'hardware è in uno stato difettoso, per esempio, allora va bene.

Ho anche visto setjmp/ longjmpusato per fornire meccanismi di threading molto semplici. Ma questo è un caso piuttosto speciale - e sicuramente non come funzionano i thread "standard".

Modifica: si potrebbe ovviamente aggiungere codice per "occuparsi della pulizia", ​​nello stesso modo in cui C ++ memorizza i punti di eccezione nel codice compilato e quindi sa cosa ha dato un'eccezione e cosa deve essere ripulito. Ciò comporterebbe una sorta di tabella di puntatori di funzione e la memorizzazione di "se saltiamo fuori da qui, chiama questa funzione, con questo argomento". Qualcosa come questo:

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

Con questo sistema è possibile eseguire "la gestione completa delle eccezioni come il C ++". Ma è piuttosto disordinato e si basa sul codice ben scritto.


+1, ovviamente potresti in teoria implementare una gestione pulita delle eccezioni chiamando setjmpper proteggere ogni inizializzazione, come C ++ ... e vale la pena ricordare che usarlo per il threading non è standard.
Potatoswatter

8

Dato che hai menzionato l'incorporato, penso che valga la pena notare un caso di non utilizzo : quando il tuo standard di codifica lo proibisce. Ad esempio MISRA (MISRA-C: 2004: regola 20.7) e JFS (regola AV 20): "La macro setjmp e la funzione longjmp non devono essere utilizzate."


8

setjmpe longjmppuò essere molto utile nei test unitari.

Supponiamo di voler testare il seguente modulo:

#include <stdlib.h>

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

Normalmente, se la funzione da testare chiama un'altra funzione, è possibile dichiarare una funzione stub da chiamare che imiterà ciò che la funzione effettiva fa per testare determinati flussi. In questo caso, tuttavia, la funzione chiama exitche non restituisce. Lo stub deve in qualche modo emulare questo comportamento. setjmpe longjmppuò farlo per te.

Per testare questa funzione, possiamo creare il seguente programma di test:

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

In questo esempio, si utilizza setjmpprima di entrare nella funzione da testare, quindi nello stubbed exitsi chiama longjmpper tornare direttamente al proprio caso di test.

Notare anche che il ridefinito exitha una variabile speciale che controlla per vedere se si vuole effettivamente uscire dal programma e chiama _exitper farlo. Se non lo fai, il tuo programma di test potrebbe non chiudersi correttamente.


6

Ho scritto un Java-like eccezione meccanismo di gestione in C usando setjmp(), longjmp()e funzioni del sistema. Rileva eccezioni personalizzate ma segnala anche come SIGSEGV. Presenta l'annidamento infinito dei blocchi di gestione delle eccezioni, che funziona attraverso le chiamate di funzione e supporta le due implementazioni di threading più comuni. Consente di definire una gerarchia ad albero di classi di eccezioni che caratterizzano l'ereditarietà del tempo di collegamento e l' catchistruzione percorre questo albero per vedere se è necessario catturarlo o trasmetterlo.

Ecco un esempio di come appare il codice usando questo:

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

Ed ecco una parte del file include che contiene molta logica:

#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'è anche un modulo C che contiene la logica per la gestione del segnale e un po 'di contabilità.

È stato estremamente complicato da implementare, posso dirtelo e ho quasi smesso. Ho davvero spinto per renderlo il più vicino possibile a Java; Ho trovato sorprendente fino a che punto sono arrivato con solo C.

Fammi un grido se sei interessato.


1
Sono sorpreso che ciò sia possibile senza il supporto effettivo del compilatore per le eccezioni personalizzate. Ma ciò che è veramente interessante è come i segnali si convertono in eccezioni.
Paul Stelian

Chiederò una cosa: che dire delle eccezioni che finiscono per non essere mai scoperte? Come uscirà main ()?
Paul Stelian

1
@PaulStelian E, ecco la tua risposta a come main()uscirà con un'eccezione non catturata. Si prega di votare questa risposta :-)
significato-importa

1
@PaulStelian Ah, capisco cosa intendi ora. Le eccezioni di runtime che non vengono rilevate credo siano state sollevate di nuovo in modo che si applichi la risposta generale (dipendente dalla piattaforma). Le eccezioni personalizzate non rilevate sono state stampate e ignorate. Vedere la Progagationsezione nel README Ho pubblicato il mio codice dell'aprile 1999 su GitHub (vedere il collegamento nella risposta modificata). Dare un'occhiata; era un osso duro da rompere. Sarebbe bello sentire cosa ne pensi.
significato conta

2
Ho dato una breve occhiata al README, molto carino lì. Quindi fondamentalmente si propaga al blocco try più esterno e viene segnalato, in modo simile alle funzioni asincrone di JavaScript. Bello. Guarderò più tardi il codice sorgente stesso.
Paul Stelian

1

Senza dubbio, l'uso più cruciale di setjmp / longjmp è che agisce come un "salto goto non locale". Il comando Goto (e in rari casi in cui sarà necessario utilizzare goto over for e while loop) è più utilizzato in modo sicuro nello stesso ambito. Se usi goto per saltare attraverso gli ambiti (o attraverso l'allocazione automatica), molto probabilmente corromperai lo stack del tuo programma. setjmp / longjmp lo evita salvando le informazioni sullo stack nella posizione in cui vuoi saltare. Quindi, quando salti, carica queste informazioni sullo stack. Senza questa funzionalità, i programmatori C molto probabilmente avrebbero dovuto rivolgersi alla programmazione in assembly per risolvere problemi che solo setjmp / longjmp potevano risolvere. Grazie a Dio esiste. Tutto nella libreria C è estremamente importante. Saprai quando ne avrai bisogno.


"Tutto nella libreria C è estremamente importante." C'è un sacco di cose deprecate e cose che non sono mai state buone, come le impostazioni locali.
qwr
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.