Uso prático de setjmp e longjmp em C

97

Alguém pode me explicar onde exatamente setjmp()e as longjmp()funções podem ser usadas praticamente na programação embarcada? Eu sei que são para tratamento de erros. Mas gostaria de conhecer alguns casos de uso.

Pala
fonte
Para tratamento de erros como em qualquer outra programação. Não vejo diferença no uso ???
Tony The Lion
Para velocidade? Sim. Porque a) ele é executado mais lentamente que um loop eb) porque não pode ser otimizado facilmente (como excluir um atraso ou dois). Então setjmp e longjmp governam claramente!
TheBlastOne
Outra resposta além das fornecidas está aqui stackoverflow.com/questions/7334595/… Você pode usar longjmp()para sair de um manipulador de sinal, especialmente coisas como a BUS ERROR. Este sinal geralmente não pode reiniciar. Um aplicativo embutido pode desejar lidar com este caso para segurança e operação robusta.
ruído natural de
E com relação às diferenças de desempenho setjmpentre o BSD e o Linux, consulte "Timing setjmp e a alegria dos padrões" , que sugere o uso sigsetjmp.
Ioannis Filippidis

Respostas:

82

Tratamento de erros
Suponha que haja um erro profundo em uma função aninhada em muitas outras funções e o tratamento de erros faça sentido apenas na função de nível superior.

Seria muito tedioso e estranho se todas as funções intermediárias tivessem que retornar normalmente e avaliar os valores de retorno ou uma variável de erro global para determinar que o processamento posterior não faz sentido ou mesmo seria ruim.

Essa é uma situação em que setjmp / longjmp faz sentido. Essas situações são semelhantes às situações em que a exceção em outras línguas (C ++, Java) faz sentido.

Coroutines
Além de tratamento de erros, eu posso pensar também de uma outra situação onde você precisa setjmp / longjmp em C:

É o caso quando você precisa implementar corrotinas .

Aqui está um pequeno exemplo de demonstração. Espero que satisfaça o pedido de Sivaprasad Palas por algum código de exemplo e responda à pergunta de TheBlastOne sobre como setjmp / longjmp suporta a implementação de corrotinas (tanto quanto vejo que não se baseia em nenhum comportamento novo ou não padrão).

EDITAR:
Pode ser que na verdade seja um comportamento indefinido fazer um longjmp down the callstack (veja o comentário de MikeMB; embora eu ainda não tenha tido oportunidade de verificar isso).

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

A figura a seguir mostra o fluxo de execução:
fluxo de execução

Nota de advertência
Ao usar setjmp / longjmp, esteja ciente de que eles têm um efeito sobre a validade de variáveis ​​locais frequentemente não consideradas.
Cf. minha pergunta sobre este tópico .

Coalhada
fonte
2
Visto que setjmp prepara e longjmp executa o salto do escopo da chamada atual de volta para o escopo setjmp, como isso daria suporte à implementação de corrotinas? Não vejo como poderíamos continuar a execução da rotina que havia tanto tempo.
TheBlastOne
2
@TheBlastOne Consulte o artigo da Wikipedia . Você pode continuar a execução se você setjmpantes de você longjmp. Isso não é padrão.
Potatoswatter
9
As corrotinas precisam ser executadas em pilhas separadas, não da mesma forma que você mostra em seu exemplo. Como routineAe routineBusa a mesma pilha, só funciona para corrotinas muito primitivas. Se routineAchama um aninhado profundamente routineCapós a primeira chamada para routineBe isso routineCé executado routineBcomo co-rotina, então routineBpode até destruir a pilha de retorno (não apenas as variáveis ​​locais) de routineC. Portanto, sem alocar uma pilha exclusiva (por meio de alloca()após a chamada rountineB?), Você terá sérios problemas com este exemplo se usado como uma receita.
Tino
6
Mencione, em sua resposta, que saltar para baixo na pilha de chamadas (de A para B) é um comportamento indefinido).
MikeMB
1
E na nota de rodapé 248) está escrito: "Por exemplo, executando uma instrução de retorno ou porque outra chamada longjmp causou uma transferência para uma chamada setjmp em uma função anterior no conjunto de chamadas aninhadas." Portanto, chamar uma função longjmp de uma função para um ponto mais acima na pilha de chamadas também encerra essa função e, portanto, voltar para ela depois é UB.
MikeMB
18

A teoria é que você pode usá-los para tratamento de erros, de modo que possa pular de uma cadeia de chamadas profundamente aninhada sem precisar lidar com erros de tratamento em todas as funções da cadeia.

Como toda teoria inteligente, isso desmorona ao encontrar a realidade. Suas funções intermediárias alocarão memória, agarrarão bloqueios, abrirão arquivos e farão todos os tipos de coisas que requerem limpeza. Assim, na prática setjmp/ longjmpsão geralmente uma má idéia, exceto em circunstâncias muito limitadas onde você tem total controle sobre seu ambiente (algumas plataformas embarcadas).

Na minha experiência, na maioria dos casos, sempre que você pensa que usar setjmp/ longjmpfuncionaria, seu programa é claro e simples o suficiente para que cada chamada de função intermediária na cadeia de chamadas possa lidar com erros, ou é tão confuso e impossível de consertar que você deve fazer exitquando você encontrar o erro.

Arte
fonte
3
Por favor, olhe libjpeg. Como em C ++, a maioria das coleções de rotinas C leva a struct *para operar em algo como um coletivo. Em vez de armazenar suas alocações de memória de funções intermediárias como locais, elas podem ser armazenadas na estrutura. Isso permite que um longjmp()manipulador libere a memória. Além disso, isso não tem tantas tabelas de exceções explosivas que todos os compiladores C ++ ainda geram 20 anos após o fato.
ruído natural de
Like every clever theory this falls apart when meeting reality.Na verdade, a alocação temporária e coisas do tipo tornam longjmp()-se complicadas, já que você precisa setjmp()várias vezes na pilha de chamadas (uma para cada função que precisa realizar algum tipo de limpeza antes de sair, o que precisa "gerar novamente a exceção" por longjmp()ing ao contexto que inicialmente tinha recebido). Fica ainda pior se esses recursos forem modificados após o setjmp(), já que você deve declará-los volatilepara evitar que o longjmp()destrua.
sevko
10

A combinação de setjmpe longjmpé "super força goto". Use com EXTREMO cuidado. No entanto, como outros explicaram, a longjmpé muito útil para sair de uma situação de erro desagradável, quando você deseja get me back to the beginningrapidamente, em vez de ter que retornar uma mensagem de erro para 18 camadas de funções.

No entanto, assim como goto, mas pior, você tem que ter MUITO cuidado ao usar isso. A longjmpapenas o levará de volta ao início do código. Não afetará todos os outros estados que podem ter mudado entre setjmpe voltar ao ponto de setjmppartida. Portanto, alocações, bloqueios, estruturas de dados semi-inicializadas, etc, ainda estão alocados, bloqueados e semi-inicializados quando você volta para onde setjmpfoi chamado. Isso significa que você realmente tem que se preocupar com os lugares onde faz isso, pois é REALMENTE ok ligar longjmpsem causar MAIS problemas. Claro, se a próxima coisa que você fizer for "reiniciar" [depois de armazenar uma mensagem sobre o erro, talvez] - em um sistema embarcado onde você descobriu que o hardware está em um estado ruim, por exemplo, tudo bem.

Eu também vi setjmp/ longjmpusei para fornecer mecanismos de threading muito básicos. Mas esse é um caso muito especial - e definitivamente não é como os threads "padrão" funcionam.

Edit: É claro que alguém poderia adicionar código para "lidar com a limpeza", da mesma forma que C ++ armazena os pontos de exceção no código compilado e então sabe o que deu uma exceção e o que precisa ser limpo. Isso envolveria algum tipo de tabela de ponteiro de função e armazenamento "se pularmos a partir daqui, chame essa função, com este argumento". Algo assim:

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

Com este sistema, você poderia fazer "tratamento completo de exceções como C ++". Mas é bastante confuso e depende do código ser bem escrito.

Mats Petersson
fonte
+1, é claro que você poderia, em teoria, implementar um tratamento de exceção limpo chamando setjmppara proteger cada inicialização, a la C ++ ... e vale a pena mencionar que usá-lo para threading não é padrão.
Potatoswatter
8

Já que você mencionou incorporado, acho que vale a pena observar um caso de não uso : quando seu padrão de codificação o proíbe. Por exemplo, MISRA (MISRA-C: 2004: Regra 20.7) e JFS (AV Regra 20): "A macro setjmp e a função longjmp não devem ser usadas."

Clement J.
fonte
8

setjmpe longjmppode ser muito útil em testes de unidade.

Suponha que desejamos testar o seguinte módulo:

#include <stdlib.h>

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

Normalmente, se a função a ser testada chama outra função, você pode declarar uma função stub para ela chamar que imitará o que a função real faz para testar determinados fluxos. Neste caso, entretanto, a função chama exitque não retorna. O stub precisa de alguma forma emular esse comportamento. setjmpe longjmppode fazer isso por você.

Para testar essa função, podemos criar o seguinte programa de teste:

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

Neste exemplo, você usa setjmpantes de inserir a função para testar e, em seguida, no esboço, exitvocê chama longjmppara retornar diretamente ao seu caso de teste.

Observe também que o redefinido exittem uma variável especial que verifica se você realmente deseja sair do programa e faz chamadas _exitpara fazê-lo. Se você não fizer isso, seu programa de teste pode não fechar corretamente.

dbush
fonte
6

Eu escrevi um Java-like exceção mecanismo de manipulação em C usando setjmp(), longjmp()e as funções do sistema. Ele captura exceções personalizadas, mas também sinaliza como SIGSEGV. Ele apresenta aninhamento infinito de blocos de tratamento de exceção, que funcionam através de chamadas de função e oferece suporte às duas implementações de threading mais comuns. Ele permite que você defina uma hierarquia em árvore de classes de exceção que apresentam herança de tempo de link, e a catchinstrução percorre esta árvore para ver se ela precisa ser capturada ou passada adiante.

Aqui está um exemplo da aparência do código usando isto:

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

E aqui está parte do arquivo de inclusão que contém muita 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 */

Há também um módulo C que contém a lógica para tratamento de sinais e alguns registros contábeis.

Foi extremamente complicado de implementar, posso dizer que quase desisti. Eu realmente empurrei para torná-lo o mais próximo possível do Java; Achei surpreendente o quão longe cheguei com apenas C.

Me dê um grito se você estiver interessado.

o significado importa
fonte
1
Estou surpreso que isso seja possível sem o suporte real do compilador para as exceções personalizadas. Mas o que é realmente interessante é como os sinais se convertem em exceções.
Paul Stelian
Vou perguntar uma coisa: e as exceções que acabam nunca sendo detectadas? Como o main () sairá?
Paul Stelian
1
@PaulStelian E, aqui está sua resposta sobre como main()sairá com exceção não capturada. Vote a favor desta resposta :-)
que importa é
1
@PaulStelian Ah, entendo o que você quer dizer agora. As exceções de tempo de execução que não foram detectadas, acredito, foram levantadas novamente para que a resposta geral (dependente da plataforma) se aplique. As exceções personalizadas não detectadas foram impressas e ignoradas. Veja a Progagationseção no README que postei meu código de abril de 1999 no GitHub (veja o link na resposta editada). Dar uma olhada; era um osso duro de roer. Seria bom ouvir o que você pensa.
significado importa
2
Dei uma rápida olhada no README, muito legal lá. Basicamente, ele se propaga para o bloco try mais externo e é relatado, semelhante às funções assíncronas do JavaScript. Agradável. Vou examinar o próprio código-fonte mais tarde.
Paul Stelian
0

Sem dúvida, o uso mais crucial de setjmp / longjmp é que ele atua como um "salto goto não local". O comando Goto (e há casos raros em que você precisará usar goto sobre os loops for e while) é mais usado com segurança no mesmo escopo. Se você usar goto para saltar entre escopos (ou através da alocação automática), provavelmente irá corromper a pilha do seu programa. setjmp / longjmp evita isso salvando as informações da pilha no local para onde deseja ir. Então, quando você pula, ele carrega as informações da pilha. Sem esse recurso, os programadores C provavelmente teriam que recorrer à programação em assembly para resolver problemas que apenas setjmp / longjmp poderia resolver. Graças a Deus isso existe. Tudo na biblioteca C é extremamente importante. Você saberá quando precisar.

AndreGraveler
fonte
"Tudo na biblioteca C é extremamente importante." Há um monte de coisas obsoletas e coisas que nunca foram boas, como locais.
qwr