Use setjmp y longjmp en lenguaje C para realizar captura de excepciones y corrutina


Este es el 017 original del hermano Dao.

I. Introducción

En la biblioteca estándar de C, hay dos funciones poderosas: setjmp y longjmp . Me pregunto si ustedes las han usado en el código. Le pregunté a varios colegas en el cuerpo, algunas personas no conocen estas dos funciones, algunas personas conocen esta función, pero nunca la han usado.

A juzgar por el alcance de los puntos de conocimiento, las funciones de estas dos funciones son relativamente simples y un código de muestra simple puede aclararlo. Sin embargo, necesitamos divergir y pensar desde este punto de conocimiento , en diferentes dimensiones, asociar y comparar este punto de conocimiento con otros conocimientos similares en este lenguaje de programación ; compararlo con conceptos similares en otros lenguajes de programación ; y luego pensar en dónde apunta este conocimiento. se puede usar y cómo lo usan otros.

Hoy, hablemos de estas dos funciones. Aunque no se puede usar en programas generales, en alguna ocasión en el futuro, cuando necesite lidiar con algunos flujos de programa más peculiares, tal vez puedan traerle resultados inesperados.

Por ejemplo: compararemos setjmp / longjmp con declaraciones goto en términos de función; comparar con el valor de retorno de funciones ; comparar con los escenarios de uso de corrutinas en el lenguaje . fork Python/Lua

Introducción a la sintaxis de dos funciones

1. Ejemplo mínimo

No tengamos sentido, solo mira este código de ejemplo más simple , no importa si no lo entiendes, estás familiarizado con él:

int main()
{
    // 一个缓冲区,用来暂存环境变量
    jmp_buf buf;
    printf("line1 \n");
    
    // 保存此刻的上下文信息
    int ret = setjmp(buf);
    printf("ret = %d \n", ret);
    
    // 检查返回值类型
    if (0 == ret)
    {
        // 返回值0:说明是正常的函数调用返回
        printf("line2 \n");
        
        // 主动跳转到 setjmp 那条语句处
        longjmp(buf, 1);
    }
    else
    {
        // 返回值非0:说明是从远程跳转过来的
        printf("line3 \n");
    }
    printf("line4 \n");
    return 0;
}

Resultados del:

La secuencia de ejecución es la siguiente (si no entiende, no entre en ella , mire hacia atrás después de leer la explicación a continuación):

2. Descripción de la función

Primero observe las firmas de estas dos funciones:

int setjmp(jmp_buf env);
void longjmp(jmp_buf env, int value);

Todos están declarados en el archivo de encabezado , Wikipedia explica lo siguiente: setjmp.h

setjmp: configura el búfer jmp_buf local y lo inicializa para el salto. Esta rutina guarda el entorno de llamada del programa en el búfer de entorno especificado por el argumento env para su uso posterior por longjmp. Si la devolución es de una invocación directa, setjmp devuelve 0. Si la devolución es de una llamada a longjmp, setjmp devuelve un valor distinto de cero。
longjmp : Restaura el contexto del entorno del búfer env que se guardó mediante la invocación de la rutina setjmp en la misma invocación del programa. La invocación de longjmp desde un manejador de señales anidado no está definida. El valor especificado por valor se pasa de longjmp a setjmp. Una vez que se completa longjmp, la ejecución del programa continúa como si la invocación correspondiente de setjmp acabara de regresar. Si el valor pasado a longjmp es 0, setjmp se comportará como si hubiera devuelto 1; de lo contrario, se comportará como si hubiera devuelto un valor。

Permítanme usar mi propio entendimiento para explicar el párrafo anterior en inglés:

función setjmp

  1. Función: Guarda diversa información de contexto al ejecutar esta función, principalmente el valor de algunos registros;
  2. Parámetros: el búfer utilizado para guardar la información de contexto, que equivale a tomar una instantánea de la información de contexto actual y guardarla;
  3. Valor de retorno: Hay dos tipos de valores de retorno. Si se llama directamente a la función setjmp, el valor de retorno es 0; si se llama a la función longjmp para saltar, el valor de retorno es distinto de cero; aquí se puede comparar con la función tenedor que crea el proceso.

función longjmp

  1. Función: Salta al contexto (instantánea) guardado en el búfer env de parámetros para ejecutar;
  2. Parámetros: El parámetro env especifica a qué contexto (instantánea) saltar para la ejecución, el valor se usa para proporcionar información de juicio de retorno a la función setjmp, es decir: cuando se llama a la función longjmp, este valor de parámetro se usará como valor de retorno de la función setjmp;
  3. Valor de retorno: sin valor de retorno. Porque cuando se llama a esta función, salta directamente al código en otros lugares para su ejecución y no volverá de nuevo.

Resumen: Estas dos funciones se utilizan juntas para realizar el salto del programa.

3. setjmp: guardar información de contexto

Sabemos que después de que el código C se compila en un archivo binario, se carga en la memoria durante la ejecución y la CPU saca cada instrucción al segmento de código para ejecutarlo. Hay muchos registros en la CPU para salvar el entorno de ejecución actual , tales como: registro de segmento de código CS, registro de desplazamiento de instrucción IP y, por supuesto, hay muchos otros registros A este entorno de ejecución lo llamamos contexto.

Cuando la CPU obtiene la siguiente instrucción de ejecución, la instrucción a ejecutar se puede obtener a través de los dos registros de CS e IP , como se muestra en la siguiente figura:

Agregue algunos puntos de conocimiento:

  1. En la figura anterior, el registro de segmento de código CS se considera como una dirección base, es decir: CS apunta a la dirección de inicio del segmento de código en la memoria, y el registro de IP representa el desplazamiento de la siguiente dirección de instrucción a ejecutar. desde esta dirección base. Por lo tanto, cada vez que obtiene una instrucción, solo necesita agregar los valores en estos dos registros para obtener la dirección de la instrucción;
  2. De hecho, en la plataforma x86, el registro de segmento de código CS no es una dirección base, sino un selector. Hay una tabla en algún lugar del sistema operativo, esta tabla almacena la dirección de inicio real del segmento de código y el registro CS solo almacena un valor de índice, este valor de índice apunta a una entrada de tabla en esta tabla, lo que implica el conocimiento relacionado de virtual memoria;
  3. Después de obtener una instrucción, el registro IP se mueve automáticamente hacia abajo al comienzo de la siguiente instrucción En cuanto a cuántos bytes se mueven, depende de cuántos bytes estén ocupados por la instrucción obtenida actualmente.

La CPU es un gran tonto. No tiene ni idea. Sea lo que sea que le dejamos hacer, hace lo que hace. Por ejemplo, recuperar instrucciones: siempre que establezcamos los registros CS e IP, la CPU utilizará los valores de estos dos registros para recuperar instrucciones. Si estos dos registros se establecen en un valor incorrecto, la CPU buscará instrucciones estúpidamente, pero se bloqueará durante la ejecución.

Simplemente podemos entender esta información de registro como información de contexto, y la CPU la ejecutará de acuerdo con la información de contexto. Por lo tanto, el lenguaje C preparó la función de biblioteca setjmp para que guardemos la información del contexto actual y la almacenemos temporalmente en un búfer.

¿Cuál es el propósito de la conservación? Para poder restaurar al lugar actual en el futuro para continuar con la ejecución.

Hay un ejemplo más simple: instantáneas en el servidor. ¿Cuál es el propósito de las instantáneas? Cuando el servidor tiene un error, ¡puede volver a una instantánea determinada !

4. longjmp: implementar salto

Hablando de saltos, el concepto que inmediatamente me vino a la mente es la declaración goto . Descubrí que muchos tutoriales tienen muchas opiniones sobre la declaración goto y creo que deberías intentar no usarla en tu código. Este punto de vista es un buen punto de partida: si se usa demasiado goto, afectará la comprensión del orden de ejecución del código.

Pero si observa el código del kernel de Linux, puede encontrar muchas declaraciones goto. Nuevamente: encuentre un equilibrio entre el mantenimiento del código y la eficiencia de ejecución .

Saltar cambia la secuencia de ejecución del programa. La instrucción goto sólo puede saltar dentro de la función, y no puede hacer nada si cruza la función.

Por lo tanto, el lenguaje C nos proporciona la función longjmp para implementar saltos remotos , que se puede ver en su nombre, lo que significa que puede saltar entre funciones.

Desde la perspectiva de la CPU, el llamado salto consiste en establecer varios registros en el contexto como una instantánea en un momento determinado. Obviamente, en la función setjmp anterior, la información de contexto (instantánea) en ese momento se ha almacenado en un búfer temporal en el área , y si desea saltar a ese lugar, entonces ejecutado le dice directamente a la CPU en la línea.

¿Cómo decirle a la CPU? Simplemente sobrescriba la información del registro en el búfer temporal sobre los registros utilizados en la CPU.

5. setjmp: tipo de retorno y valor de retorno

En algunos programas que requieren múltiples procesos, a menudo usamos la función fork para "incubar" un nuevo proceso del proceso actual , y el nuevo proceso se ejecuta desde la siguiente instrucción de la función fork .

Para el proceso principal , regresar después de llamar a la función fork también continúa ejecutando la siguiente declaración, entonces, ¿cómo distinguir entre el proceso principal y el nuevo proceso? La función fork proporciona un valor de retorno para que podamos distinguir:

La función fork devuelve 0: significa que se trata de un nuevo proceso; la
función fork devuelve un valor distinto de cero: significa el proceso principal original y el valor devuelto es el número de proceso del nuevo proceso.

De manera similar, la función setjmp también tiene diferentes tipos de retorno. Quizás no sea exacto expresar el tipo de retorno. Se puede entender así: Volviendo de la función setjmp, hay 2 escenarios en total :

  1. Cuando setjmp se llama activamente: se devuelve 0. El propósito de la llamada activa es guardar el contexto y crear una instantánea.
  2. Al saltar por longjmp: devuelve un valor distinto de cero, y el valor de retorno en este momento lo especifica el segundo parámetro de longjmp.

De acuerdo con los dos valores diferentes anteriores, podemos realizar diferentes procesamientos de rama. Al regresar por salto largo , se pueden devolver diferentes valores distintos de cero de acuerdo con la escena real . Para aquellos que tienen experiencia en programación en lenguajes de scripting como Python y Lua , ¿pensaron en la función yield / resume ? ¡Su rendimiento externo en parámetros y valores de retorno es el mismo!

Resumen: Hasta ahora, básicamente terminé el uso de las dos funciones setjmp / longjmp, no sé si lo he descrito con suficiente claridad. En este punto, mire el código de muestra al principio del artículo, debería quedar claro de un vistazo.

Tres, use setjmp / longjmp para lograr la captura de excepciones

Dado que la biblioteca C nos proporciona esta herramienta, debe haber ciertos escenarios de uso . La captura de excepciones se admite directamente a nivel gramatical en algunos lenguajes de alto nivel (Java / C ++), generalmente declaraciones try-catch, pero debe implementarlas usted mismo en lenguaje C.

Demostremos uno de los modelos de captura de excepciones más simples, el código tiene un total de 56 líneas:

#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <setjmp.h>

typedef int     BOOL;
#define TRUE    1
#define FALSE   0

// 枚举:错误代码
typedef enum _ErrorCode_ {
    ERR_OK = 100,         // 没有错误
    ERR_DIV_BY_ZERO = -1  // 除数为 0
} ErrorCode;

// 保存上下文的缓冲区
jmp_buf gExcptBuf;

// 可能发生异常的函数
typedef int (*pf)(int, int);
int my_div(int a, int b)
{
    if (0 == b)
    {
        // 发生异常,跳转到函数执行之前的位置
        // 第2个参数是异常代码
        longjmp(gExcptBuf, ERR_DIV_BY_ZERO);
    }
    // 没有异常,返回正确结果
    return a / b;
}

// 在这个函数中执行可能会出现异常的函数
int try(pf func, int a, int b)
{
    // 保存上下文,如果发生异常,将会跳入这里
    int ret = setjmp(gExcptBuf);
    if (0 == ret)
    {
        // 调用可能发生异常的哈数
        func(a, b);
        // 没有发生异常
        return ERR_OK;
    }
    else
    {
        // 发生了异常,ret 中是异常代码
        return ret;
    }
}

int main()
{
    int ret = try(my_div, 8, 0);     // 会发生异常
    // int ret = try(my_div, 8, 2);  // 不会发生异常
    if (ERR_OK == ret)
    {
        printf("try ok ! \n");
    }
    else
    {
        printf("try excepton. error = %d \n", ret);
    }
    
    return 0;
}

No es necesario explicar el código en detalle, solo mire los comentarios en el código para comprenderlo. Este código es solo indicativo y debe usarse en el código de producción con un paquete más completo.

Una cosa a tener en cuenta: setjmp / longjmp solo cambia el orden de ejecución del programa. Si algunos datos de la aplicación necesitan ser revertidos, necesitamos procesarlos manualmente.

Cuarto, use setjmp / longjmp para implementar la corrutina

1. ¿Qué es una corrutina?

En un programa C, si la secuencia que debe ejecutarse al mismo tiempo es generalmente implementada por subprocesos, entonces ¿qué es una corrutina ? La explicación de Wikipedia de la corrutina es:

Información más detallada en esta corrutina de página , páginas que describen específicamente corrutinas e hilos, generador de comparación, mecanismo de implementación de varios lenguajes.

Utilizamos productores y consumidores para comprender brevemente la diferencia entre corrutinas e hilos:

2. Productores y consumidores en hilos

  1. Productor y consumidor son dos secuencias de ejecución paralelas, por lo general se utilizan dos subprocesos para ejecutar;
  2. Cuando el productor produce los bienes, el consumidor está en un estado de espera (bloqueado). Una vez que se completa la producción, el semáforo informa a los consumidores que consuman los bienes;
  3. Cuando los consumidores consumen bienes, los productores están en un estado de espera (bloqueados). Una vez finalizado el consumo, el semáforo notifica al productor que continúe produciendo la mercancía.

3. Productores y consumidores en la co-rutina

  1. Los productores y los consumidores ejecutan en la misma secuencia de ejecución, la ejecución alterna saltando a la secuencia de ejecución;
  2. Una vez que el productor produce los bienes, cede la CPU y deja que el consumidor la ejecute;
  3. Después de que el consumidor consume el producto, abandona la CPU y deja que el productor lo ejecute;

4. Implementación de corrutina en lenguaje C

Aquí está el modelo más simple , el mecanismo de la corrutina se realiza a través de setjmp / longjmp, el propósito principal es comprender la secuencia de ejecución de la corrutina, sin resolver el problema de pasar parámetros y valores de retorno.

Si desea estudiar la implementación de corrutinas en lenguaje C, puede echar un vistazo al concepto de dispositivos Duff , donde las sentencias goto y switch se utilizan para implementar saltos de rama La sintaxis utilizada es extraña pero legal.

typedef int     BOOL;
#define TRUE    1
#define FALSE   0

// 用来存储主程和协程的上下文的数据结构
typedef struct _Context_ {
    jmp_buf mainBuf;
    jmp_buf coBuf;
} Context;

// 上下文全局变量
Context gCtx;

// 恢复
#define resume() \
    if (0 == setjmp(gCtx.mainBuf)) \
    { \
        longjmp(gCtx.coBuf, 1); \
    }

// 挂起
#define yield() \
    if (0 == setjmp(gCtx.coBuf)) \
    { \
        longjmp(gCtx.mainBuf, 1); \
    }

// 在协程中执行的函数
void coroutine_function(void *arg)
{
    while (TRUE)  // 死循环
    {
        printf("\n*** coroutine: working \n");
        // 模拟耗时操作
        for (int i = 0; i < 10; ++i)
        {
            fprintf(stderr, ".");
            usleep(1000 * 200);
        }
        printf("\n*** coroutine: suspend \n");
        
        // 让出 CPU
        yield();
    }
}

// 启动一个协程
// 参数1:func 在协程中执行的函数
// 参数2:func 需要的参数
typedef void (*pf)(void *);
BOOL start_coroutine(pf func, void *arg)
{
    // 保存主程的跳转点
    if (0 == setjmp(gCtx.mainBuf))
    {
        func(arg); // 调用函数
        return TRUE;
    }

    return FALSE;
}

int main()
{
    // 启动一个协程
    start_coroutine(coroutine_function, NULL);
    
    while (TRUE) // 死循环
    {
        printf("\n=== main: working \n");

        // 模拟耗时操作
        for (int i = 0; i < 10; ++i)
        {
            fprintf(stderr, ".");
            usleep(1000 * 200);
        }

        printf("\n=== main: suspend \n");
        
        // 放弃 CPU,让协程执行
        resume();
    }

    return 0;
}

La información de impresión es la siguiente:

Cinco, resumen

El objetivo de este artículo es presentar la sintaxis y los escenarios de uso de setjmp / longjmp. En algunos escenarios de demanda, puede lograr un efecto multiplicador con la mitad del esfuerzo.

Por supuesto, también puede usar su imaginación para lograr funciones más sofisticadas ejecutando saltos de secuencia, ¡todo es posible!


No presumas, no exageres, no exageres, ¡escribe cada artículo con cuidado!
¡Bienvenido a reenviar, compartir con amigos sobre tecnología, Columbia Road, para expresar mi más sincero agradecimiento! El lenguaje recomendado reenviado le ha ayudado a pensarlo:

Este artículo de resumen resumido por el hermano Dao fue escrito con mucho cuidado, lo que es muy útil para mi mejora técnica. ¡Buenas cosas para compartir!

Finalmente, les deseo: frente al código, no habrá errores; frente a la vida, ¡flores de primavera!


[Declaración original]

OF: Columbia Road (número público: El IOT de las cosas de la ciudad )
sé casi: Columbia Road
estación B: Share Columbia Road
Denver: Columbia Road share
CSDN: Columbia Road Share


¡Pondré diez años de experiencia práctica en el resumen de resultados de proyectos de desarrollo integrado !

Mantenga presionado el código QR en la imagen siguiente, siga + cuenta pública estrella , cada artículo tiene productos secos.


Reimpresión: bienvenido a reimprimir, pero sin el consentimiento del autor, esta declaración debe conservarse y el enlace original debe incluirse en el artículo.

Lectura recomendada

Puntero del lenguaje C, desde los principios subyacentes hasta habilidades sofisticadas, con imágenes y códigos para ayudarlo a explicar un
análisis detallado paso a paso, cómo usar C para implementar la programación orientada a objetos, el
principio de depuración de gdb original es tan simple
, esas cosas sobre cifrado, certificados,
profundamente en el lenguaje de secuencia de comandos LUA, le permiten comprender completamente el principio de depuración

Supongo que te gusta

Origin blog.csdn.net/u012296253/article/details/113543344
Recomendado
Clasificación