Linux | Señales

Tabla de contenido

Prefacio

1. Conceptos básicos de señales.

1. Señales en la vida

2. Señales en Linux

2. Generación de señales

1. Introducción a la interfaz

2. Cómo se generan las señales

(1) Las señales se generan presionando teclas en el terminal

(2) Interfaz de llamada al sistema

un, matar 

cocer a fuego lento 

c、abortar

(3) Señales generadas por las condiciones del software.

a. La señal SIGPIPE es generada por la tubería.

B. Señal generada por la función de alarma.

(4) Señales generadas por anomalías del hardware

a. Señal generada por división por cero

B. Señales generadas por punteros salvajes.

3. Volcado de memoria

3. Preservación de la señal

1. Suplemento conceptual

2. Estructura del núcleo

3、sigset_t

4. Función de operación del conjunto de señales

(1) interfaces relacionadas con sigset_t

(2) máscara sigproc

(3) pendiente

5. Código de prueba

4. Procesamiento de señales

1. Estado del usuario y estado del kernel

2. Cuándo procesar señales

3. Todo el proceso de procesamiento de señales.


Prefacio

        Este capítulo presenta principalmente el contenido relacionado con las señales de Linux y presenta en detalle el ciclo de vida completo de las señales desde tres aspectos: generación de señales, almacenamiento de señales y procesamiento de señales.

1. Conceptos básicos de señales.

1. Señales en la vida

        En la vida diaria, hay varias señales, como nuestros semáforos, timbres de la escuela, timbres de teléfonos, etc. Entonces tengo las siguientes preguntas para presentar nuestro tema de hoy;

Pregunta 1:¿Cómo reconocemos estas señales?

        Puede haber muchas respuestas a esta pregunta, puede ser lo que nos enseñaron nuestras maestras de jardín de infantes, o puede ser lo que nos dijeron nuestros padres cuando éramos pequeños, etc.;

Pregunta 2:¿Podemos reconocer estas señales y procesarlas?

        ¿No es esto inevitable? Ahora que lo sabemos, por supuesto que también procesaremos estas señales. Desde el momento en que las conozcamos, alguien nos dirá cómo procesarlas, o tendremos nuestra propia comprensión subjetiva de estas señales y emitiremos nuestros propios juicios para dinos cómo procesar estas señales, completa la acción correspondiente;

Pregunta 3:Después de recibir ciertas señales, ¿las procesaremos inmediatamente?

        La respuesta es, por supuesto, no. Una vez que recibimos determinadas señales, no las procesaremos inmediatamente. Por ejemplo, cuando estamos jugando, de repente suena el teléfono y la otra parte nos dice que ha llegado la comida para llevar, y este puede ser el momento más importante para que juguemos. En este momento, podemos decirle al tipo de comida para llevar que lo deje en la puerta, es decir, no procesaremos esta señal de inmediato. En cuanto a cuándo procesar esta señal, depende de cuándo tengamos el momento adecuado. Este momento adecuado puede ser al final de nuestra juego, o es posible que hayamos olvidado este asunto después de jugar, por lo que la señal no se procesa;

2. Señales en Linux

        Las señales en nuestro Linux también están relacionadas con las señales en la vida. Respondemos las señales en Linux en forma de las tres preguntas anteriores respectivamente; el tema de recibir señales en la vida somos las personas, es decir, nosotros mismos y en el programa. la recepción El tema de la señal es el proceso;

1. ¿Cómo reconoce el proceso estas señales?

        Debe saber que el sistema operativo está escrito por programadores y, por supuesto, las señales las definen los programadores. Podemos usar kill -l para ver qué señales hay en Linux, como se muestra en la siguiente figura, donde los números del 1 al 31 son nuestros. señales ordinarias, y los números del 34 al 64 son señales en tiempo real y no se explicarán en este capítulo;

2. ¿El proceso manejará estas señales inmediatamente?

        Por supuesto, puede que no sea posible. Puede que haya cosas que deban procesarse con mayor prioridad que el procesamiento de esta señal, por lo que debemos procesarla en el momento adecuado. Como necesitamos procesarla más tarde, definitivamente necesitamos guarde esta señal, de lo contrario la señal se perderá, esta señal se guardará en la PCB de nuestro proceso, entonces, ¿cómo guardarla? Tenemos 31 señales y queremos guardarlas. No es difícil pensar que podemos usar mapas de bits para guardarlas, de modo que se necesitan al menos 31 bits para guardar estas señales. Podemos usar 1 para indicar la recepción de esta señal. , 0 Indica no recibido;

3. ¿Cuáles son las formas de procesar señales?

        Tres tipos, divisiones 默认方法 , 忽 Estrategia , 用户户义

2. Generación de señales

1. Introducción a la interfaz

        Antes de introducir formalmente la generación de señales, primero entendemos una llamada al sistema --- señal;

        La función principal de esta llamada al sistema es ejecutar acciones personalizadas específicas cuando llega cada señal, luego veamos los parámetros;

Parámetro 1: Número de señal (podemos verificar este parámetro a través de kill -l, o podemos consultar la señal a través del manual de hombre No. 7) Para este parámetro, podemos completar la macro en mayúsculas o completar directamente el número;

Parámetro dos:Este parámetro es un puntero de función, que registra una acción de función para la señal especificada. Este parámetro es la acción personalizada que se ejecutará;

Valor de retorno:Si la llamada tiene éxito, esta función devuelve la acción de señal original. Si falla, devuelve SIG_ERR y se establece el código de error;

2. Cómo se generan las señales

(1) Las señales se generan presionando teclas en el terminal

        Podemos usar el teclado para ingresar una combinación de teclas específica en el terminal para generar una señal y enviar una señal a nuestro proceso actual, como el siguiente programa;

#include <iostream>
#include <sys/types.h>
#include <unistd.h>
#include <signal.h>

using namespace std;

int main()
{
    while(true)
    {
        cout << "我正在运行..., pid: " <<  getpid() << endl;
        sleep(1);
    }
    return 0;
}

        Podemos encontrar que el programa es un bucle infinito, pero podemos finalizar el programa mediante la combinación de teclas Ctrl + C. De hecho, Ctrl + C envía la señal número 2 al proceso de primer plano actual, que es SIGINT (interrupción); presionamos La combinación de teclas ctrl + \ envía la señal No. 3 al proceso actual en primer plano para salir del proceso actual;

        Cambiemos el código nuevamente para que el efecto parezca más obvio, capturemos la señal número 2 para que complete la acción que especificamos;

#include <iostream>
#include <sys/types.h>
#include <unistd.h>
#include <signal.h>

using namespace std;

void handler(int signum)
{
    cout << "signum: " << signum << endl;
}

int main()
{
    // 注册2号信号的捕捉方法
    signal(2, handler);

    while(true)
    {
        cout << "我正在运行..., pid: " <<  getpid() << endl;
        sleep(1);
    }
    return 0;
}

        Lo compilamos y ejecutamos y encontramos los siguientes resultados;

        Cuando usamos ctrl + c para enviar la señal No. 2, dado que hemos personalizado la acción para la señal No. 2 de antemano, se nos imprimirá el valor de la señal actual cada vez que la enviemos, y cuando enviemos la señal No. 3. , el programa saldrá. , porque no capturamos la señal No. 3 y registramos una acción personalizada, por lo que la señal No. 3 completó la acción predeterminada y salió del programa; 

Resumen:¿Cómo entender las señales generadas por la entrada del teclado del terminal?

        Primero, causamos una interrupción a través de la entrada del teclado. Después de que nuestro sistema operativo recibe la entrada del teclado, analiza la combinación de teclas de entrada, luego busca en la lista de procesos para encontrar el proceso que se está ejecutando actualmente en primer plano. El sistema operativo escribe una señal específica en el PCB del proceso, es decir, la posición de bit específica en el mapa de bits que guarda la señal se establece en 1;

(2) Interfaz de llamada al sistema
un, matar 

        Esto es muy simple. Hemos aprendido un comando de línea de comando matar antes. De hecho, este comando envía una señal específica al proceso especificado, sigue siendo el programa anterior;

        De hecho, no solo podemos enviar señales al proceso a través de la línea de comando, sino también a través de llamadas al sistema.Hay una llamada al sistema kill con el mismo nombre, como se muestra a continuación;

        Usaremos esta llamada al sistema incluso de un vistazo. El primer parámetro es el pid del proceso, que es el proceso al que queremos enviar una señal. El segundo parámetro es el número de señal, si la llamada es exitosa, devuelve 0. , y si la llamada falla, devuelve -1. , el código de error está configurado; podemos encapsular una instrucción de línea de comando kill a través de esta llamada al sistema;

#include <iostream>
#include <cstdio>
#include <cstdlib>
#include <signal.h>
#include <sys/types.h>

// 我们kill使用格式:   kill pid signum
int main(int argc, char* args[])
{
    if(argc != 3)
    {
        std::cout << args[0] << " pid  signum" << std::endl;
        exit(1);
    }

    pid_t id = atoi(args[1]);
    int signum = atoi(args[2]);
    int n = kill(id, signum);
    if(n == - 1)
    {
        perror("kill");
        exit(2);
    }

    return 0;
}

cocer a fuego lento 

        También podemos usar la llamada al sistema rise para enviar señales. A diferencia de kill, rise solo envía señales al proceso actual y no puede enviar señales al proceso especificado. El uso de esta llamada al sistema es más simple, como se muestra a continuación;

        Esta llamada al sistema tiene solo un parámetro, que es el valor de la señal. El valor de retorno es el mismo que el valor de retorno de kill. Si tiene éxito, devuelve 0. Si falla, devuelve -1. Se establece el código de error. A continuación, escribimos un pequeño programa para hacer que el proceso actual se envíe a sí mismo un mensaje después de 5 segundos: Señal No. 3;

#include <iostream>
#include <sys/types.h>
#include <unistd.h>
#include <signal.h>

using namespace std;

void handler(int signum)
{
    cout << "signum: " << signum << endl;
}

int main()
{
    // 注册2号信号的捕捉方法
    signal(2, handler);
    int count = 0;
    while(true)
    {
        cout << "我正在运行..., pid: " <<  getpid() << endl;
        sleep(1);
        count++;
        if(count == 5)
        {
            raise(3);
        }
    }
    return 0;
}

        Seguimos presionando ctrl + c porque fue capturado y se realizó una acción personalizada, por lo que no salió, sin embargo, a los 5 segundos, debido a que el programa se envió la señal número 3, salió;

c、abortar

        A continuación esta función es similar a salir, enviando la señal N° 6 a nuestro proceso actual para hacer que nuestro proceso salga, este parámetro no tiene parámetros, y como siempre es exitoso no hay valor de retorno, podemos cambiar el proceso del código anterior, y después de 5 segundos llame a la función de cancelación, como se muestra a continuación;

#include <iostream>
#include <sys/types.h>
#include <unistd.h>
#include <signal.h>

using namespace std;

void handler(int signum)
{
    cout << "signum: " << signum << endl;
}

int main()
{
    // 注册2号信号的捕捉方法
    signal(2, handler);
    int count = 0;
    while(true)
    {
        cout << "我正在运行..., pid: " <<  getpid() << endl;
        sleep(1);
        count++;
        if(count == 5)
        {
            //raise(3);
            abort();
        }
    }
    return 0;
}

Resumen:Cómo entender las señales que generamos a través de llamadas al sistema

        Primero, el usuario llama a una llamada al sistema y ejecuta el código de llamada del sistema, luego el sistema operativo extrae los parámetros de la llamada al sistema, como el pid del proceso, la señal, etc., y luego el sistema operativo encuentra el bloque de control de PCB del proceso y escribe la señal correspondiente en su mapa de bits;

(3) Señales generadas por las condiciones del software.
a. La señal SIGPIPE es generada por la tubería.

        Anteriormente hicimos un pequeño experimento al aprender tuberías anónimas, conectando ambos lados de la tubería; si el descriptor del archivo de tubería del lado de escritura está cerrado, el lado de lectura leerá hasta el final del archivo y devolverá 0; si el lado de lectura El descriptor del archivo de tubería se cierra y el descriptor de tubería del lado de escritura se cerrará. ¡El proceso final finaliza directamente! ¿Cómo finaliza el proceso de escritura? De hecho nuestro SO envía una señal SIGPIPE al final de la escritura, si no lo crees podemos hacer el siguiente experimento;

Descripción del experimento: Capturamos la señal No. 13 (SIGPIPE) para que no salga del proceso, sino que imprima información. Dejamos que el proceso hijo sirva como final de lectura y el proceso padre como final de escritura. Después de 5 segundos, el niño El proceso cierra el archivo final de escritura, podemos verificar si el proceso principal realizará nuestra acción de captura personalizada;       

#include <iostream>
#include <cstring>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <signal.h>

using namespace std;

void handler(int signum)
{
    cout << "pid:" << getpid() << ", 收到了信号: " << signum << std::endl;
}

int main()
{
    // 注册SIGPIPE方法
    signal(SIGPIPE, handler);

    // 创建匿名管道
    int pipefd[2];
    pipe(pipefd);
    // 创建子进程
    int id = fork();
    if(id == 0)
    {
        // 子进程(读端)
        close(pipefd[1]); // 关闭写端
        int count = 0;
        char buf[1024];
        while(true)
        {
            ssize_t sz = read(pipefd[0], buf, sizeof(buf) - 1);
            buf[sz] = '\0';
            std::cout << "from father# " << buf << " , 我的pid: " << getpid() << std::endl;
            sleep(1);
            count++;
            if(count == 5)
            {
                // 关闭子进程读端
                std::cout << "子进程要关闭管道读端啦" << std::endl;
                close(pipefd[0]);
                sleep(3); // 子进程不马上退出
                break;
            }
        }
        std::cout << "子进程退出" << std::endl;
        exit(0);
    }

    // 父进程(写端)
    close(pipefd[0]); // 关闭读端
    const char* buf = "我是父进程,我在给你发信息";
    while (true)
    {
        std::cout << "写入中...." << std::endl;
        write(pipefd[1], buf, strlen(buf));
        sleep(1);
    }
    
    // 进程等待
    wait(nullptr);

    return 0;
}

        Descubrimos que cuando el lector está cerrado, cada vez que queremos escribir datos en la tubería, recibiremos nuestra señal No. 13; originalmente la señal No. 13 hará que el proceso principal salga y capturamos de forma personalizada la acción de la señal. No. 13, por lo que no hay salida, porque seguimos escribiendo datos en el archivo de canalización, por lo que continuaremos recibiendo la señal No. 13;

B. Señal generada por la función de alarma.

        Esta llamada al sistema es similar a un reloj de alarma. Enviará la señal No. 14 (SIGALRM) al proceso actual de vez en cuando. La llamada al sistema se declara de la siguiente manera;

        Esta llamada al sistema tiene solo un parámetro, que es el número de segundos. Después de unos segundos, se enviará la señal número 14 al proceso actual. La acción predeterminada es finalizar el proceso, el valor de retorno de esta función generalmente es 0. . Si el tiempo para usar la última alarma aún no ha expirado, llámelo nuevamente. Cuando se le llame, reinicie la hora de la alarma y devuelva el tiempo restante de la última configuración; simplemente podemos implementar un programa de tareas programadas a través de esta llamada al sistema, como se muestra abajo;

#include <iostream>
#include <vector>
#include <functional>
#include <cstring>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <signal.h>

using namespace std;

// 定义一个任务类型
using task_t = std::function<void()>;
// 定义一个任务数组
std::vector<task_t> tasks;

// 数据库任务
void mysqlTask()
{
    std::cout << "正在执行数据库任务" << std::endl;
}
// 刷新磁盘
void flushDiskTask()
{
    std::cout << "正在执行刷新磁盘任务" << std::endl;
}
// 加载任务进任务数组
void load()
{
    tasks.push_back(mysqlTask);
    tasks.push_back(flushDiskTask);
}

void handler(int signum)
{
    for(auto& t : tasks)
        t();
    sleep(1);
    // 设置下次执行任务时间
    alarm(3);
}

int main()
{
    load(); // 加载任务队列

    // 对14号信号方法进行录入
    signal(SIGALRM, handler);

    alarm(5);

    // 防止主进程退出
    while(true);

    return 0;
}

        Puedes encontrar que las tareas que asignamos se completarán cada 3 segundos, también podemos escribir muchos programas como este;

Resumen:¿Cómo entender las señales generadas por las condiciones del software?

        Primero, el sistema operativo identifica si se cumple o se activa una determinada condición de software. Si se cumple o se activa, encuentra el bloque de control de PCB del proceso correspondiente y establece la posición del bit de la señal específica en 1 en su mapa de bits interno;

(4) Señales generadas por anomalías del hardware
a. Señal generada por división por cero

        Para los estudiantes que son nuevos en programación, a menudo escuchamos que dividir por cero causará un error de división por cero, sin embargo, en realidad no sabemos qué es un error de división por cero y cómo se causa. Todos pensamos que el error de división por cero es causado por nuestro programa. El error de software causado por el código es en realidad un error de hardware. A continuación, usamos el código para demostrar el error de división por cero. error cero enviará la señal No. 8 (SIGFPE);

#include <iostream>
#include <vector>
#include <functional>
#include <cstring>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <signal.h>

using namespace std;

void handler(int signum)
{
    cout << "收到信号signum: " << signum << endl;
    sleep(1);
}

int main()
{
    signal(SIGFPE, handler);

    int a = 10;
    a /= 0;

    // 防止程序退出
    while(true);
    return 0;
}

        Captamos la señal N° 8 y cambiamos su comportamiento, pero descubrimos un fenómeno muy mágico, solo dividimos por cero una vez, pero seguimos recibiendo la señal N° 8, ¿por qué? Esto tiene que ver con el diseño del hardware;

        En primer lugar, lo que debemos entender es que nuestros cálculos son realizados por la CPU. Nuestro compilador traduce nuestro código al lenguaje de máquina. Nuestra CPU seguirá estúpidamente obteniendo instrucciones de la máquina para ejecutar. La operación real en nuestra CPU Hay dos tipos de registros en la CPU, uno es lo que nuestros programadores pueden usar como eax, ebx, etc., y el otro es el registro de estado. Cuando nuestra CPU encuentra que hay división por cero, establecerá una cierta posición de bit de nuestro registro de estado a 1. Indica que el resultado es anormal. En este momento, nuestro sistema operativo detectará el estado del registro después de que la CPU complete el cálculo y descubra que un bit específico se ha establecido en 1. Saber que hay un problema con el Como resultado del cálculo, encuentra la PCB del proceso actual y la almacena en la PCB. La posición de bit específica de la señal es 1 y la transmisión de la señal se completa; sin embargo, debido a la posición de bit específica del registro de estado en el hardware de nuestra CPU sigue siendo 1, la señal número 8 se enviará continuamente, por lo que ocurrirá el fenómeno anterior;

B. Señales generadas por punteros salvajes.

        Se cree que el error de puntero salvaje es un error clásico de todo programador de C/C++. Es un error que casi todos los programadores de C/C++ cometerán, pero ¿realmente entiendes el error de puntero salvaje? El error de puntero salvaje real también es un error de hardware. Después de que se reconoce nuestro puntero salvaje, el sistema operativo envía la señal No. 11 (SIGSEGV) al proceso actual para hacer que el proceso actual salga. A continuación, lo probamos mediante el siguiente código ;

#include <iostream>
#include <vector>
#include <functional>
#include <cstring>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <signal.h>

using namespace std;

void handler(int signum)
{
    cout << "收到信号signum: " << signum << endl;
    sleep(1);
}

int main()
{
    signal(SIGSEGV, handler);

    int* p = nullptr;
    *p = 100;

    // 防止程序退出
    while(true);
    return 0;
}

        Al igual que nuestro error de división por cero, recibimos constantemente la señal número 11. ¿Qué está pasando? Debes ser lo suficientemente inteligente como para adivinar un poco, esto también debe estar relacionado con nuestro hardware;

        Con el estudio previo del espacio de direcciones, todos sabemos que todas las direcciones que vemos actualmente y las direcciones que ve la CPU son direcciones virtuales, y necesitamos encontrar el espacio físico real a través del mapeo de tablas de páginas. use un hardware ---- MMU, completamos la conversión mutua entre la dirección virtual y la dirección física a través de la tabla de páginas + MMU. Como se muestra en el código anterior, cuando pasamos una dirección ilegal, nuestra MMU detecta que es una dirección ilegal y convierte esto Se registra el error. Cuando es necesario recuperar el resultado, el sistema operativo detecta una excepción de MMU, por lo que encontrará la PCB del proceso actual y establecerá la posición especificada por la PCB en 1 para completar la generación de la señal; y este proceso no hará que nuestro hardware destruya el registro incorrecto, por lo que la señal especificada se enviará continuamente al proceso actual;

Resumen de envío de señal:

        Podemos encontrar que para toda transmisión de señales, nuestro sistema operativo primero reconoce la señal y luego escribe la señal especificada en el bloque de control de PCB del proceso especificado;

3. Volcado de memoria

        Descubrimos que el comportamiento predeterminado de todas las señales que aprendimos anteriormente es salir, entonces, ¿hay alguna diferencia entre ellas? Consultamos la señal a través de la señal del comando man 7, como se muestra en la siguiente figura;

        Podemos encontrar la tabla anterior, donde señal se refiere a qué señal, valor se refiere al valor correspondiente a la señal, Acción se refiere a la acción predeterminada correspondiente y comentario se refiere a la descripción correspondiente a la señal; estamos acostumbrados a la Acción con cuidado. , Podemos encontrar las siguientes categorías de comportamiento;

Término: Salir del proceso actual

Núcleo: salga del proceso actual y genere un archivo de volcado de núcleo

ign: ignorar

Detener: suspender el proceso actual

Cont: continuar ejecutando el proceso actual

        Cuando ves este volcado de núcleo, ¿recuerdas algo? Este es un presagio dejado por nuestra explicación anterior sobre el proceso en espera. En ese momento, había una marca de volcado de núcleo. Si no está seguro, puede consultar el siguiente artículo;

Linux | Terminación de procesos y espera de procesos-Blog CSDN

        Ya debes haber entendido qué es esta señal de terminación. Es la señal por la cual el proceso actual sale debido a. Con respecto a este indicador de volcado de núcleo, primero entendemos qué es un volcado de núcleo;

Volcado de núcleo:Cuando ocurre alguna excepción en nuestro proceso, el sistema operativo vuelca los datos centrales del proceso actual en la memoria al disco;

archivo de volcado de núcleo:El llamado archivo de volcado de núcleo es el archivo transferido al disco durante el proceso de volcado de núcleo;

Nota: Para los servidores en la nube, la función de volcado de núcleo está desactivada de forma predeterminada. Necesitamos activar esta función; podemos verificar si el volcado de núcleo está desactivado por ulimit -a ;

        Podemos ver que el tamaño del archivo central es 0, lo que significa que nuestra función de volcado de núcleo está desactivada; configuramos el tamaño del archivo de volcado de núcleo mediante ulimit -c tamaño y abrimos el archivo después de configurarlo, como se muestra a continuación;

        A continuación usamos la señal número 3 para probar la función de volcado de núcleo. Primero, configuramos el tamaño del archivo de núcleo en 0 y luego ejecutamos el siguiente código;

#include <iostream>
#include <vector>
#include <functional>
#include <cstring>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <signal.h>

using namespace std;

int main()
{
    pid_t id = fork();
    if(id == 0)
    {
        // 子进程
        int a = 10;
        a /= 0; // OS会对子进程发送8号信号

        // 正常退出
        exit(0);
    }
    // 父进程
    int status = 0;
    waitpid(id, &status, 0);
    if(WIFEXITED(status))
    {
        // 正常退出
        cout << "exit code: " << WEXITSTATUS(status) << endl;
    }
    else
    {
        // 异常退出
        cout << "exit signal: " << (status & 0x7F) << ", core dump: " << ((status >> 7) & 1) << endl;
    }
    return 0;
}

        Descubrimos que la marca de volcado de núcleo es 0 y la señal de salida es de hecho SIGFPE --- señal número 8, lo cual está en línea con nuestras expectativas. Debido a que establecemos el tamaño del archivo de volcado de núcleo en 0, el archivo de volcado de núcleo no se generará; a continuación realizaremos un volcado de núcleo. Establezca el tamaño del archivo en 10240 y ejecútelo nuevamente para ver la respuesta;

        Esta vez nuestro indicador de volcado de núcleo cambió a 1 y generamos un archivo de volcado de núcleo. Este archivo lleva el nombre de core. + proceso pid; entonces, ¿para qué sirve este archivo de volcado de núcleo?​   

        Podemos usar el archivo de volcado del núcleo para depurar, agregamos -g a la opción de compilación g++ y luego compilamos nuevamente;

        Ingresamos core-file + nombre del archivo de volcado de núcleo en gdb para localizar rápidamente la línea de código donde cometimos el error; entonces, ¿por qué nuestro servidor en la nube desactiva la función de volcado de núcleo de forma predeterminada? No es difícil encontrar que nuestro archivo de volcado de núcleo es muy grande y los programas que se ejecutan en nuestro servidor generalmente no se detendrán. Incluso si el programa se bloquea, es posible que haya un reinicio automático del programa para reiniciar. Si nuestro programa continúa reiniciándose, y Los archivos de volcado de núcleo siempre se generan al mismo tiempo, al final nuestro disco pronto se llenará de archivos de volcado de núcleo;

3. Preservación de la señal

1. Suplemento conceptual

        Antes de aprender formalmente sobre la preservación de señales, primero aprendemos una serie de conceptos;

Entrega de señal:Ejecutar realmente la acción de procesamiento de señal;

Señal pendiente:El estado entre la generación y entrega de la señal;

Bloqueo de señal: Proporciona un determinado identificador de señal, indicando que incluso si la señal ha sido generada, no se permite su entrega. Solo cancelando el identificador de bloqueo se puede entregado y bloqueado. La señal de 'está pendiente;

Nota:Bloquear e ignorar son diferentes. El bloqueo de señal se refiere a una señal que no se puede entregar, mientras que ignorar es una acción predeterminada para procesar señales;

2. Estructura del núcleo

        Mencionamos anteriormente que la señal se guardará en un mapa de bits en la PCB. De hecho, además de guardar el mapa de bits de generación de señal, la PCB también guardará el mapa de bits de bloqueo y la matriz de punteros de función de la acción de procesamiento, como se muestra en la la siguiente figura;

        Cuando queremos procesar una señal, primero recorremos el mapa de bits pendiente y encontramos que se genera la señal SIGHUP, luego verificamos los bits correspondientes del mapa de bits del bloque y encontramos que no hay bloqueo, luego podemos encontrar el método de procesamiento correspondiente en la matriz del controlador y encontramos que es la acción de procesamiento predeterminada, realizamos el procesamiento predeterminado; lo mismo es cierto para el análisis de SIGINT. Primero, verificamos el mapa de bits pendiente y encontramos que se genera su señal. Luego miramos su mapa de bits de bloqueo y encontramos que la señal también está bloqueada, por lo que no se llama y continúa Recorre para encontrar el mapa de bits pendiente;

3、sigset_t

        Como se puede ver en la figura anterior, cada señal puede ser 0 o 1 para indicar si está bloqueada y si ocurre; por lo tanto, podemos usar un mapa de bits para representarla y el kernel nos proporciona un tipo de datos sigset_t. Usamos este tipo de datos para almacenar bloques y pendientes. sigset_t también se denomina conjunto de señales, y un conjunto de señales de bloqueo también se denomina palabra de máscara de señal; a>

4. Función de operación del conjunto de señales

(1) interfaces relacionadas con sigset_t

#incluir <signal.h>

int sigemptyset(sigset_t *set);   // Establece todas las posiciones de bits de la señal establecida en 0

int sigfillset(sigset_t *set);   // Establece todas las posiciones de bits de la señal establecida en 1

int sigaddset (sigset_t *set, int signo);   // Establece la posición del bit correspondiente de una señal en 1

int sigdelset(sigset_t *set, int signo);   // Establece la posición del bit correspondiente de una señal a 0

int sigismember (const sigset_t *set, int signo); // Comprobar si el bit correspondiente de una señal está establecido en 1

(2) máscara sigproc

        Esta función puede leer o cambiar el conjunto de señales de bloqueo;

int sigprocmask(int cómo, const sigset_t *set, sigset_t *oldset);

Parámetro 1:Este parámetro determina qué operaciones queremos hacer en el conjunto de señales de bloqueo. Existen principalmente las siguientes opciones;

SIG_BLOQUE Agregue la señal que queremos agregar a la palabra de máscara de señal al conjunto de señales del parámetro dos
SIG_UNBLOCK Coloque la señal que queremos eliminar de la palabra de máscara de señal en el conjunto de señales del parámetro dos.
SIG_SETMASK Establezca el conjunto de señales que queremos, parámetro 2, en la palabra de máscara de señal

Parámetro dos:Relacionado con el parámetro uno;

Parámetro tres:Devuelve la palabra de máscara de señal original (parámetro de salida);

Valor de retorno:Si la llamada a la función tiene éxito, devuelve 0, si falla, devuelve -1 y se establece el código de error;

(3) pendiente

        Esta llamada al sistema se utiliza principalmente para leer el conjunto de señales pendientes, se devuelve a través del conjunto de parámetros;

int sigpending(sigset_t *conjunto);

Parámetro 1:Devuelve el conjunto de señales pendientes (parámetro de salida);

Valor de retorno:Si la llamada a la función tiene éxito, devuelve 0, si falla, devuelve -1 y se establece el código de error;

5. Código de prueba

1. Quiero verificar que si bloqueo y capturo todos los semáforos, el proceso no puede salir;

#include <iostream>
#include <signal.h>
#include <unistd.h>

void handler(int signum)
{
    std::cout << "signum: " << signum << std::endl;
}

// 打印未决位图
void showSignal()
{
    sigset_t pending;
    sigemptyset(&pending);
    sigpending(&pending);
    for(int i = 1; i < 32; i++)
    {
        // 捕捉
        signal(i, handler);
        // 设置屏蔽字
        if(sigismember(&pending, i))
            std::cout << "1";
        else
            std::cout << "0";
    }
    std::cout << ", pid: " << getpid() << std::endl;
}

int main()
{
    // 内存级别设置信号屏蔽字
    sigset_t block;
    sigemptyset(&block);
    for(int i = 0; i < 32; i++)
    {
        sigaddset(&block, i);
    }
    // 将信号集设置进内核
    sigprocmask(SIG_SETMASK, &block, nullptr);

    // 死循环
    while (true)
    {
        showSignal();
        sleep(1);
    }
    
    return 0;
}

        ¡Es obvio que nuestra señal número 9 no se puede capturar ni bloquear! Esta es una señal del administrador para evitar que programas maliciosos impidan que se finalice el proceso;

        Entre todas las interfaces anteriores, parece que no hemos encontrado una interfaz para modificar el mapa de bits pendiente en el kernel. Puede que esté confundido. De hecho, no necesitamos modificar la interfaz de mapa de bits pendiente en absoluto, porque podemos enviar señales al proceso a través de interfaces como kill. ;

4. Procesamiento de señales

        Anteriormente aprendimos la función de captura de señal. Esta función puede cambiar la acción tomada cuando llega la señal, es decir, la acción de procesamiento de la señal, pero no hemos resuelto una pregunta de principio a fin: ¿cuándo se procesa la señal? Acabamos de mencionar un concepto muy vago antes, en el momento adecuado, entonces, ¿cuándo es el momento adecuado? Aquí exploramos este tema;

1. Estado del usuario y estado del kernel

        Antes de presentar, tenemos que agregar un conjunto de conceptos: cuando nuestro sistema operativo ejecuta código, en realidad tiene dos estados, a saber, modo de usuario y modo kernel; cuando el sistema operativo ejecuta código escrito por el usuario, generalmente se ejecuta en modo de usuario. Cuando el sistema operativo ejecuta código del kernel, generalmente se ejecuta en el estado del kernel, el estado del kernel tiene mayores permisos que el estado del usuario;

        Cuando presentamos el espacio de direcciones virtuales anteriormente, dijimos que en una máquina de 32 bits, 1-3G pertenece al espacio del usuario y 3-4G pertenece al espacio del kernel. Aquí hay otro conocimiento: nuestro código y datos del espacio de usuario. se pasan a través de la tabla de páginas. Para establecer una asignación al espacio físico real, esta tabla de páginas se denomina tabla de páginas a nivel de usuario. Cada proceso tiene su propio usuario y tabla de páginas; y nuestro espacio del kernel también se asigna a la memoria física a través de la tabla de páginas, solo Sin embargo, la tabla de páginas que utilizamos es una tabla de páginas a nivel de kernel, y todos los procesos comparten la tabla de páginas a nivel de kernel;

        De esta manera podemos ejecutar código de usuario y código de kernel en el mismo espacio de direcciones de proceso;

2. Cuándo procesar señales

        Con el conocimiento anterior presagiado, el siguiente contenido será mucho más fácil; ¿cuál es la esencia de nuestro procesamiento de señales? Básicamente, el proceso consiste en atravesar el mapa de bits pendiente y luego verificar si la palabra de máscara de señal correspondiente está configurada. De lo contrario, busque el método de procesamiento correspondiente en la matriz del controlador y realice una devolución de llamada; todas estas estructuras de datos están en el bloque de control de PCB ! Por lo tanto, procesamos señales en el kernel,¡Entonces procesamos señales en estado de kernel!

        ¡A continuación debemos estudiar cuándo alcanzaremos el estado del núcleo, es decir, cuándo podremos procesar la señal! En circunstancias normales, solo ingresamos al estado del kernel cuando usamos llamadas al sistema, interrupciones, etc.; y generalmente realizamos detección y procesamiento de señales antes de salir del estado del kernel;

        Algunos amigos pueden tener un problema: si todo mi código es un bucle infinito sin ninguna otra operación y no llamo al sistema, ¿no podré ingresar al estado del kernel? Obviamente, esto está mal. Debemos saber que la mayoría de nuestras computadoras actuales son sistemas operativos de tiempo compartido y generalmente usan la rotación de intervalos de tiempo para programar procesos. Una vez que queramos programar un proceso, inevitablemente entraremos en el estado del núcleo. Así que no ¡No te preocupes por este problema!

3. Todo el proceso de procesamiento de señales.

        En realidad, existen tres acciones para procesar señales. Las hemos mencionado antes, a saber, acción predeterminada y Ignorar y captura definida por el usuario, de hecho, de estos tres, los dos primeros sistemas operativos han proporcionado; como se muestra a continuación;

        Podemos completar directamente estos dos parámetros en el parámetro dos de la señal, entre ellos, nos centramos principalmente en explicar todos los procesos de procesamiento de señales del tercer método;

        Para los dos primeros, cuando usamos la señal para registrar un método, una vez que la señal llega y no está bloqueada, cuando ingresamos al estado del kernel debido a una interrupción, llamada al sistema, etc., después de ejecutar el código del kernel, estamos listos para volver al estado de usuario. Antes, se realizará la detección y procesamiento de señales. Cuando encontramos que la acción de procesamiento de señales es la acción predeterminada o ignorada, podemos realizarla directamente y luego regresar al estado de usuario;

        Para las acciones de procesamiento de captura definidas por el usuario, este proceso puede ser un poco complicado, como se muestra en la siguiente figura;

        ​ ​ ​ Puede que haya algunos amigos aquí que tengan algunas preguntas detalladas. Enumeraré algunas a continuación;

Pregunta 1:En los pasos tercero a cuarto, ¿no podemos manejar directamente el método de procesamiento establecido por el usuario en el estado del kernel? ¿No son los permisos del modo kernel mayores que los del modo usuario?

        Sí, el estado del kernel tiene mayores permisos que el estado del usuario, por lo que cualquier código que pueda ejecutar el estado del usuario también puede ser ejecutado por nuestro estado del kernel, pero ¿deberíamos confiar completamente en los métodos de procesamiento de señales escritos por los propios usuarios? Si el método de procesamiento implementado por el usuario tiene algunas solicitudes ilegales y nuestro estado del kernel tiene permisos de ejecución, ¿no destruiría esto el kernel? Por lo tanto, debemos volver al modo de usuario para ejecutar el método de procesamiento de señales escrito por el usuario;

Pregunta 2: Del cuarto al quinto paso, ¿por qué volvemos deliberadamente al estado del núcleo en lugar de regresar directamente al código ejecutado antes de la última vez que quedamos atrapados? el núcleo?

        La razón por la que volvemos al modo kernel es porque necesitamos volver al código que se ejecutó antes de caer en el kernel, y no podemos hacerlo en el modo usuario, por lo que primero debemos regresar al modo kernel y luego volver al modo usuario desde el modo kernel;

Pregunta 3:Cuando hemos terminado de procesar una señal y queremos volver al modo usuario, es decir, del quinto paso al primer paso, tenemos otra señal Viene ¿Qué hacer? Si llega una señal, ¿nuestro proceso cambia repetidamente entre el modo de usuario y el modo kernel para manejar la señal?

        Cuando terminamos de procesar una señal, si se traen otras señales, configuraremos temporalmente otras señales para que se bloqueen por adelantado y luego regresaremos al modo de usuario. Solo la próxima vez que ingresemos al modo kernel procesaremos las señales posteriores;

        Con respecto a la figura anterior, podemos usar el siguiente método para la memoria;

Supongo que te gusta

Origin blog.csdn.net/Nice_W/article/details/134475247
Recomendado
Clasificación