Uso práctico de setjmp y longjmp en C

98

¿Alguien puede explicarme dónde exactamente setjmp()y las longjmp()funciones se pueden usar prácticamente en la programación integrada? Sé que estos son para el manejo de errores. Pero me gustaría conocer algunos casos de uso.

Pala
fuente
Para el manejo de errores como en cualquier otra programación. No veo la diferencia de uso ???
Tony The Lion
3
Y, por supuesto, thedailywtf.com/Articles/Longjmp--FOR-SPEED!!!.aspx
Daniel Fischer
¿Por velocidad? Si. Porque a) funciona más lento que un bucle yb) porque no se puede optimizar fácilmente (como eliminar un retraso o dos). ¡Así que setjmp y longjmp claramente gobiernan!
TheBlastOne
Otra respuesta que las dadas está aquí stackoverflow.com/questions/7334595/… Puede usar longjmp()para salir de un manejador de señales, especialmente cosas como a BUS ERROR. Esta señal no suele reiniciarse. Es posible que una aplicación incorporada desee manejar este caso por motivos de seguridad y funcionamiento robusto.
Artless noise
Y con respecto a las diferencias de rendimiento setjmpentre BSD y Linux, consulte "Timing setjmp y el placer de los estándares" , que sugiere usar sigsetjmp.
Ioannis Filippidis

Respuestas:

84

Manejo de errores
Suponga que hay un error en el fondo de una función anidada en muchas otras funciones y el manejo de errores solo tiene sentido en la función de nivel superior.

Sería muy tedioso e incómodo si todas las funciones intermedias tuvieran que regresar normalmente y evaluar los valores devueltos o una variable de error global para determinar que el procesamiento posterior no tiene sentido o incluso sería malo.

Esa es una situación en la que setjmp / longjmp tiene sentido. Esas situaciones son similares a situaciones en las que la excepción en otros idiomas (C ++, Java) tiene sentido.

Coroutines
Además del manejo de errores, puedo pensar también en otra situación en la que necesita setjmp / longjmp en C:

Es el caso cuando necesita implementar corrutinas .

Aquí hay un pequeño ejemplo de demostración. Espero que satisfaga la solicitud de Sivaprasad Palas de un código de ejemplo y responda a la pregunta de TheBlastOne de cómo setjmp / longjmp admite la implementación de corroutines (por lo que veo, no se basa en ningún comportamiento nuevo o no estándar).

EDIT:
Podría ser que en realidad es un comportamiento indefinido para hacer un longjmp abajo la pila de llamadas (ver comentario de MikeMB, aunque todavía no he tenido oportunidad de verificar que).

#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 siguiente figura muestra el flujo de ejecución:
flujo de ejecución

Nota de advertencia
Cuando utilice setjmp / longjmp, tenga en cuenta que tienen un efecto sobre la validez de las variables locales que a menudo no se consideran.
Cf. mi pregunta sobre este tema .

Cuajada
fuente
2
Dado que setjmp se prepara, y longjmp ejecuta el salto fuera del alcance de la llamada actual al alcance de setjmp, ¿cómo apoyaría eso la implementación de corrutinas? No veo cómo se podría continuar con la ejecución de la rutina que hace mucho tiempo.
TheBlastOne
2
@TheBlastOne Consulte el artículo de Wikipedia . Puede continuar la ejecución si setjmpantes longjmp. Esto no es estándar.
Potatoswatter
10
Las corrutinas deben ejecutarse en pilas separadas, no en las mismas que se muestran en su ejemplo. Como routineAy routineBusa la misma pila, solo funciona para corrutinas muy primitivas. Si routineAllama a anidado profundamente routineCdespués de la primera llamada a routineBy este se routineCejecuta routineBcomo una rutina, entonces routineBpodría incluso destruir la pila de retorno (no solo las variables locales) de routineC. Entonces, sin asignar una pila exclusiva (¿ alloca()después de llamar rountineB?), Tendrá serios problemas con este ejemplo si se usa como receta.
Tino
7
En su respuesta, mencione que saltar por la pila de llamadas (de A a B) es un comportamiento indefinido).
MikeMB
1
Y en la nota al pie 248) dice: "Por ejemplo, al ejecutar una declaración de retorno o porque otra llamada longjmp ha provocado una transferencia a una invocación setjmp en una función anterior en el conjunto de llamadas anidadas". Entonces, llamar a una función longjmp desde una función a un punto más arriba de la pila de llamadas también termina esa función y, por lo tanto, volver a ella después es UB.
MikeMB
18

La teoría es que puede usarlos para el manejo de errores de modo que pueda saltar de una cadena de llamadas profundamente anidada sin necesidad de lidiar con errores de manejo en cada función de la cadena.

Como toda teoría inteligente, esto se desmorona cuando se encuentra con la realidad. Sus funciones intermedias asignarán memoria, tomarán bloqueos, abrirán archivos y harán todo tipo de cosas diferentes que requieran limpieza. Por lo tanto, en la práctica, setjmp/ longjmpsuelen ser una mala idea, excepto en circunstancias muy limitadas en las que tiene un control total sobre su entorno (algunas plataformas integradas).

En mi experiencia, en la mayoría de los casos, siempre que piense que usar setjmp/ longjmpfuncionaría, su programa es lo suficientemente claro y simple como para que cada llamada de función intermedia en la cadena de llamadas pueda manejar errores, o es tan complicado e imposible de arreglar que debería hacerlo exitcuando encontrar el error.

Arte
fuente
3
Por favor mire libjpeg. Como en C ++, la mayoría de las colecciones de rutinas de C necesitan un struct *para operar en algo como colectivo. En lugar de almacenar las asignaciones de memoria de sus funciones intermedias como locales, se pueden almacenar en la estructura. Esto permite que un longjmp()manejador libere la memoria. Además, esto no tiene tantas tablas de excepciones malditas que todos los compiladores de C ++ todavía generan 20 años después del hecho.
ruido sin arte
Like every clever theory this falls apart when meeting reality.De hecho, la asignación temporal y cosas por el estilo son longjmp()complicadas, ya que luego tienes que hacerlo setjmp()varias veces en la pila de llamadas (una vez para cada función que necesita realizar algún tipo de limpieza antes de salir, que luego necesita "volver a generar la excepción" según longjmp()el contexto que había recibido inicialmente). Se pone aún peor si esos recursos se modifican después del setjmp(), ya que debe declararlos volatilepara evitar que los longjmp()golpee.
sevko
10

La combinación de setjmpy longjmpes "superfuerza goto". Utilizar con EXTREMO cuidado. Sin embargo, como han explicado otros, a longjmpes muy útil para salir de una situación de error desagradable, cuando lo desea get me back to the beginning, rápidamente, en lugar de tener que enviar un mensaje de error para 18 capas de funciones.

Sin embargo, al igual que goto, pero peor, tienes que ser MUY cuidadoso con la forma en que usas esto. A longjmpsolo lo llevará de regreso al principio del código. No afectará a todos los demás estados que pueden haber cambiado entre setjmpy volver al punto de setjmppartida. Por lo tanto, las asignaciones, bloqueos, estructuras de datos medio inicializadas, etc., todavía están asignados, bloqueados y medio inicializados cuando regresa al lugar donde setjmpse llamó. Esto significa que tienes que preocuparte mucho por los lugares donde haces esto, que está REALMENTE bien llamar longjmpsin causar MÁS problemas. Por supuesto, si lo siguiente que haces es "reiniciar" [después de almacenar un mensaje sobre el error, tal vez], en un sistema integrado en el que has descubierto que el hardware está en mal estado, por ejemplo, entonces está bien.

También he visto setjmp/ longjmputilizado para proporcionar mecanismos de subprocesamiento muy básicos. Pero ese es un caso bastante especial, y definitivamente no es cómo funcionan los subprocesos "estándar".

Editar: Por supuesto, se podría agregar código para "lidiar con la limpieza", de la misma manera que C ++ almacena los puntos de excepción en el código compilado y luego sabe qué dio una excepción y qué necesita limpieza. Esto implicaría algún tipo de tabla de punteros de función y almacenar "si saltamos desde aquí, llame a esta función, con este argumento". Algo como esto:

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 este sistema, podría realizar un "manejo completo de excepciones como C ++". Pero es bastante complicado y depende de que el código esté bien escrito.

Mats Petersson
fuente
+1, por supuesto, en teoría, podría implementar un manejo limpio de excepciones llamando setjmppara proteger cada inicialización, a la C ++ ... y vale la pena mencionar que usarlo para subprocesos no es estándar.
Potatoswatter
8

Dado que menciona incrustado, creo que vale la pena señalar un caso de no uso : cuando su estándar de codificación lo prohíbe. Por ejemplo, MISRA (MISRA-C: 2004: Regla 20.7) y JFS (AV Regla 20): "La macro setjmp y la función longjmp no se utilizarán".

Clemente J.
fuente
8

setjmpy longjmppuede ser muy útil en pruebas unitarias.

Supongamos que queremos probar el siguiente módulo:

#include <stdlib.h>

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

Normalmente, si la función para probar llama a otra función, puede declarar una función auxiliar para que la llame que imitará lo que hace la función real para probar ciertos flujos. En este caso, sin embargo, la función llama a exitque no regresa. El stub necesita emular de alguna manera este comportamiento. setjmpy longjmppuedo hacer eso por ti.

Para probar esta función, podemos crear el siguiente programa de prueba:

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

En este ejemplo, usa setjmpantes de ingresar a la función para probar, luego en el stubbed exit, llama longjmppara regresar directamente a su caso de prueba.

También tenga en cuenta que el redefined exittiene una variable especial que verifica para ver si realmente desea salir del programa y llama _exitpara hacerlo. Si no lo hace, es posible que su programa de prueba no se cierre correctamente.

dbush
fuente
6

He escrito un mecanismo similar a Java manejo de excepciones en C usando setjmp(), longjmp()y las funciones del sistema. Detecta excepciones personalizadas pero también señales como SIGSEGV. Cuenta con anidación infinita de bloques de manejo de excepciones, que funciona en llamadas a funciones y admite las dos implementaciones de subprocesos más comunes. Le permite definir una jerarquía de árbol de clases de excepción que cuentan con herencia de tiempo de enlace, y la catchdeclaración recorre este árbol para ver si necesita atrapar o transmitir.

Aquí hay una muestra de cómo se ve el código usando esto:

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

Y aquí hay parte del archivo de inclusión que contiene mucha lógica:

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

También hay un módulo C que contiene la lógica para el manejo de señales y algo de contabilidad.

Fue extremadamente complicado de implementar, les puedo decir y casi lo dejo. Realmente presioné para hacerlo lo más cerca posible de Java; Me sorprendió lo lejos que llegué solo con C.

Dame un grito si estás interesado.

el significado importa
fuente
1
Me sorprende que esto sea posible sin el soporte real del compilador para las excepciones personalizadas. Pero lo realmente interesante es cómo las señales se convierten en excepciones.
Paul Stelian
Preguntaré una cosa: ¿qué pasa con las excepciones que terminan nunca siendo detectadas? ¿Cómo saldrá main ()?
Paul Stelian
1
@PaulStelian Y, aquí está su respuesta a cómo main()saldrá sin ser detectado. Vota esta respuesta :-)
significado importa
1
@PaulStelian Ah, veo lo que quieres decir ahora. Las excepciones en tiempo de ejecución que no se detectan creo que se volvieron a generar para que se aplique la respuesta general (dependiente de la plataforma). Se imprimieron e ignoraron las excepciones personalizadas no detectadas. Vea la Progagationsección en el README . Publiqué mi código de abril de 1999 en GitHub (vea el enlace en la respuesta editada). Echar un vistazo; era un hueso duro de roer. Sería bueno escuchar lo que piensas.
significado importa
2
Eché un vistazo breve al archivo README, uno bastante bueno allí. Básicamente, se propaga al bloque try más externo y se informa, similar a las funciones asíncronas de JavaScript. Agradable. Veré el código fuente en sí más tarde.
Paul Stelian
1

Sin lugar a dudas, el uso más importante de setjmp / longjmp es que actúa como un "salto goto no local". El comando Goto (y hay casos raros en los que necesitará usar goto over para y while bucles) es el más utilizado y seguro en el mismo ámbito. Si usa goto para saltar a través de ámbitos (o asignación automática), lo más probable es que corrompa la pila de su programa. setjmp / longjmp evita esto al guardar la información de la pila en la ubicación a la que desea saltar. Luego, cuando saltas, carga esta información de pila. Sin esta característica, los programadores de C probablemente tendrían que recurrir a la programación en ensamblador para resolver problemas que solo setjmp / longjmp podría resolver. Gracias a Dios existe. Todo en la biblioteca de C es extremadamente importante. Sabrá cuando lo necesite.

AndreGraveler
fuente
"Todo lo que hay en la biblioteca C es extremadamente importante". Hay un montón de cosas obsoletas y cosas que nunca fueron buenas, como las configuraciones regionales.
qwr