Desarrollo multiproceso de Linux

Directorio de desarrollo multiproceso de Linux

proceso

procedimientos y procesos

¿Qué es el programa?

Un programa es un archivo que contiene un conjunto de información que describe cómo crear un proceso en tiempo de ejecución.

archivo de información

  • Identificación de formato binario : cada archivo de programa contiene metainformación que describe el formato del archivo ejecutable. El kernel utiliza esta información para interpretar otra información en el archivo. (Formato de conexión ejecutable ELE)
  • Instrucciones en lenguaje de máquina : algoritmos de programas de codificación
  • Dirección de entrada del programa : identifica la ubicación de la instrucción inicial cuando el programa comienza a ejecutarse.
  • Datos : el archivo de programa contiene los valores iniciales de las variables y los valores literales utilizados por el programa (como cadenas).
  • Tabla de símbolos y tabla de reubicación : describe las ubicaciones y nombres de funciones y variables en el programa. Estas tablas sirven para múltiples propósitos, incluida la depuración y la resolución de símbolos en tiempo de ejecución (enlaces dinámicos).
  • Bibliotecas compartidas e información de enlaces dinámicos : algunos campos contenidos en el archivo del programa enumeran las bibliotecas compartidas que deben usarse cuando el programa se está ejecutando, así como el nombre de la ruta del enlazador dinámico que carga las bibliotecas compartidas.
  • Otra información : los archivos de programa también contienen mucha otra información que describe cómo crear el proceso.

¿Qué es el proceso?

Un proceso es una instancia de un programa en ejecución . Es una actividad en ejecución de un programa con ciertas funciones independientes en un determinado conjunto de datos. Es la unidad básica de ejecución dinámica del sistema operativo . En los sistemas operativos tradicionales, los procesos son tanto el unidad básica de ejecución y también es la unidad básica de distribución. Un proceso es la unidad más pequeña de asignación de recursos del sistema y puede tener varios subprocesos.

La relación entre el proceso y el kernel
Un programa se puede utilizar para crear múltiples procesos. Un proceso es una entidad abstracta definida por el kernel, y varios recursos del sistema utilizados para ejecutar el programa se asignan a esta entidad.
Desde la perspectiva del kernel, el proceso consta del espacio de memoria del usuario y una serie de estructuras de datos del kernel. El espacio de memoria del usuario contiene el código del programa y las variables utilizadas por el código, mientras que la estructura de datos del kernel se utiliza para mantener la información del estado del proceso.
La información registrada en la estructura de datos del kernel incluye muchos números de identificación (ID) relacionados con el proceso, tablas de memoria virtual, tablas de descriptores de archivos abiertos, información sobre la transmisión y el procesamiento de señales, el uso y las limitaciones de los recursos del proceso, el directorio de trabajo actual y una gran cantidad de de otra información.


Programación monocanal y multicanal

¿Qué es la programación única?

La programación única significa que solo se permite ejecutar un programa en la memoria de la computadora durante un período de tiempo. Una vez que un programa comienza a ejecutarse, otros programas no pueden ejecutarse hasta que se complete la ejecución o se produzca un error.

¿Qué es la multiprogramación?

Multiprogramación significa que dentro de un período de tiempo, la computadora puede cargar y ejecutar múltiples programas al mismo tiempo , de modo que varios programas se ejecuten intercalados entre sí bajo el control del programa de administración y estén en el mismo estado de principio a fin en el sistema informático Los programas comparten recursos del sistema informático, mejorando así los recursos del sistema y la utilización de la CPU.

Programa de procesamiento de CPU
Para un sistema de una sola CPU, los programas que se ejecutan al mismo tiempo son solo un concepto macro. Aunque todos han comenzado a ejecutarse, desde una perspectiva micro, solo hay un programa ejecutándose en la CPU en cualquier momento . En el modelo de multiprogramación, varios procesos se turnan para utilizar la CPU. Las CPU comunes de hoy están en el nivel de nanosegundos y pueden ejecutar aproximadamente mil millones de instrucciones por segundo. Dado que la velocidad de reacción del ojo humano es del orden de milisegundos, parecen funcionar simultáneamente.


porción de tiempo

¿Qué es un intervalo de tiempo?

Un intervalo de tiempo es un período microscópico de tiempo de CPU asignado por el sistema operativo a cada proceso en ejecución .
Cuando sólo se considera una CPU, estos procesos "parecen" estar ejecutándose al mismo tiempo, pero en realidad se están ejecutando alternativamente. Dado que el intervalo de tiempo suele ser muy corto (generalmente de unos pocos ms a decenas de ms en Linux), los usuarios no lo sentirán.

El papel de los intervalos de tiempo en el kernel
Los intervalos de tiempo son asignados a cada proceso por el programador del kernel del sistema operativo . Primero, el kernel asignará intervalos de tiempo iniciales iguales a cada proceso, y luego cada proceso ejecutará el tiempo correspondiente por turno. Cuando todos los procesos estén en un estado en el que el intervalo de tiempo se agote, el kernel volverá a calcular y asignará tiempo a cada proceso. .Película, etcétera.


Paralelismo y concurrencia

¿Qué es el paralelismo?

Paralelo significa que se ejecutan varias instrucciones simultáneamente en varios procesadores al mismo tiempo.

Insertar descripción de la imagen aquí

¿Qué es la concurrencia?

La concurrencia significa que solo se puede ejecutar una instrucción al mismo tiempo, pero varias instrucciones de proceso se ejecutan en rotación rápida, lo que tiene el efecto de que varios procesos se ejecuten al mismo tiempo desde una perspectiva macro, pero desde una perspectiva micro, no lo son. ejecutado al mismo tiempo . El tiempo se divide en varios segmentos, lo que permite que múltiples procesos se ejecuten alternativamente rápidamente.

Insertar descripción de la imagen aquí

entender ejemplos

El paralelismo son dos colas usando dos máquinas de café al mismo tiempo:
Insertar descripción de la imagen aquí

La concurrencia son dos colas que utilizan una máquina de café alternativamente:
Insertar descripción de la imagen aquí


Bloque de control de procesos (PCB)

Para gestionar procesos, el kernel debe tener una descripción clara de lo que hace cada proceso. El kernel asigna un bloque de control de proceso PCB (Bloque de control de procesamiento) a cada proceso para mantener la información relacionada con el proceso. El bloque de control de proceso del kernel de Linux es una task_structestructura.

task_structLa definición de la estructura se puede encontrar en los siguientes archivos :

/usr/src/linux-headers-xxx/include/linux/sched.h

Insertar descripción de la imagen aquí

Lo que necesitas saber sobre task_structel contenido de la estructura.

  • Identificación del proceso: cada proceso en el sistema tiene una identificación única, representada por el tipo pid t, que en realidad es un número entero no negativo.
  • Estado del proceso: listo, en ejecución, suspendido, detenido, etc.
  • Algunos registros de CPU que deben guardarse y restaurarse al cambiar de proceso
  • Información que describe el espacio de direcciones virtuales.
  • Información que describe el terminal de control.
  • Directorio de trabajo actual
  • máscara
  • Tabla de descriptores de archivos, que contiene muchos punteros a estructuras de archivos.
  • Información relacionada con señales.
  • ID de usuario e ID de grupo
  • Sesiones y grupos de procesos.
  • El límite superior de recursos que un proceso puede utilizar (límite de recursos)

Comando para ver el límite superior de recursos

ulimit -a

Insertar descripción de la imagen aquí


Transición del estado del proceso

estado del proceso

El estado del proceso refleja cambios en la ejecución del proceso . Estos estados cambian a medida que se ejecuta el proceso y cambian las condiciones externas.

En el modelo de tres estados, el estado del proceso se divide en tres estados básicos:

  • Estado de ejecución : el proceso ocupa el procesador y se está ejecutando.
  • Estado listo : el proceso está listo para ejecutarse y está esperando que el sistema asigne un procesador para ejecutarse. Una vez que a un proceso se le han asignado todos los recursos necesarios excepto la CPU, se puede ejecutar tan pronto como obtenga otra CPU. Puede haber varios procesos en estado listo en un sistema y, por lo general, están en cola, llamada cola lista.
  • Estado de bloqueo : También conocido como estado de espera o estado de suspensión, significa que el proceso no tiene condiciones de ejecución y está esperando que se complete un evento.

Insertar descripción de la imagen aquí

En el modelo de cinco estados, el estado de proceso agrega un nuevo estado y un estado de terminación a los tres estados básicos.

  • Nuevo estado : el estado en el que el proceso acaba de crearse y aún no ha ingresado a la cola lista.
  • Estado de terminación : el estado en el que un proceso completa una tarea y alcanza el punto final normal, o finaliza anormalmente debido a un error insuperable, o es finalizado por el sistema operativo y un proceso con derechos de terminación. El proceso que ingresa al estado terminado ya no se ejecutará, pero aún permanecerá en el sistema operativo esperando las consecuencias. Una vez que otros procesos hayan completado la extracción de información sobre el proceso terminado, el sistema operativo eliminará el proceso.

Insertar descripción de la imagen aquí

Comandos relacionados con el proceso

Ver progreso

a- Muestra todos los procesos en el terminal, incluidos los procesos de otros usuarios
u- Muestra detalles de los procesos
x- Muestra procesos que no tienen un terminal de control
j- Muestra información relacionada con el control del trabajo

ps aux

Insertar descripción de la imagen aquí

ps ajx

Insertar descripción de la imagen aquí

Significado del parámetro STAT
Insertar descripción de la imagen aquí


Visualización en tiempo real de la dinámica del proceso.

top

Puede agregar al usar topel comando -d npara especificar el intervalo de tiempo para actualizar la información de visualización.

top -d 5

Insertar descripción de la imagen aquí

Actualizar clasificación.
Después de ingresar a la interfaz superior, escriba las siguientes teclas del teclado para mostrar de acuerdo con las reglas.

  • M: Ordenar por uso de memoria
  • P: Ordenar por ocupación de CPU
  • T: Ordenar por tiempo de ejecución del proceso
  • U: Filtrar procesos según el nombre de usuario
  • K: Ingrese el PID especificado para finalizar el proceso
  • Q:Salir de la interfaz superior

proceso de matanza

Sintaxis :kill 信号选项 pid

enumerar todas las señales

kill -l

Insertar descripción de la imagen aquí

# 强制终止进程的命令
kill -9 pid #kill -SIGKILL pid

# 建议先向进程发送一个终止请求,允许进程执行清理工作并正常退出(15是kill默认信号)。别直接一来就9,除非出现严重问题。
kill -15 pid #kill -SIGTERM pid

Matar proceso por nombre de proceso

killall name

ID de proceso y funciones relacionadas

número de proceso

Cada proceso se identifica mediante un número de proceso (PID)pid_t , cuyo tipo es (entero). El rango del número de proceso: 0~32767. El ID del proceso es único pero se puede reutilizar. Cuando un proceso finaliza, su ID de proceso se puede utilizar nuevamente.

Número de proceso principal

Cualquier proceso (excepto el proceso de inicio) es creado por otro proceso, que se denomina proceso principal del proceso creado y el número de proceso correspondiente se denomina ID del proceso principal (PPID) .

número de grupo de proceso

Un grupo de procesos es una colección de uno o más procesos. Están relacionados entre sí. El grupo de procesos puede recibir varias señales desde un mismo terminal. El proceso asociado tiene un número de grupo de procesos (PGID) . De forma predeterminada, el número de proceso actual se utiliza como número de grupo de proceso actual.

funciones relacionadas

pid_t getpid(void);

  • Función : Se utiliza para obtener el número de proceso del proceso que llama.
  • Valor de retorno : si tiene éxito, el proceso hijo devuelve 0 y el proceso padre devuelve el ID del proceso hijo y el número de proceso del proceso que llama; si falla, devuelve -1 y establece errno.

pid_t getppid(void);

  • Función : se utiliza para obtener el número de proceso principal del proceso que llama.
  • Valor de retorno : si tiene éxito, el proceso hijo devuelve 0, el número de proceso principal del proceso que llama; si falla, devuelve -1 y establece errno.

pid_t getpgid(void);

  • Función : se utiliza para obtener el número de grupo de proceso del proceso que llama.
  • Valor de retorno : si tiene éxito, el número del grupo de procesos se devuelve al proceso hijo; si falla, se devuelve -1 y se establece errno.

Creación de procesos

El sistema permite que un proceso cree un nuevo proceso, y el nuevo proceso es un proceso hijo. El proceso hijo también puede crear un nuevo proceso hijo para formar un modelo de estructura de árbol de procesos.

función de horquilla

pid_t fork(void);

  • Función : Se utiliza para crear un nuevo proceso (proceso hijo)
  • Valor de retorno : si tiene éxito, se devuelve 0 en el proceso secundario y el ID del proceso secundario se devuelve en el proceso principal; si falla, se devuelve -1 y se establece errno.

Dos razones principales del fracaso

  1. El número de procesos en el sistema actual ha alcanzado el límite superior especificado por el sistema. En este momento, el valor de errno se establece en EAGAIN.
  2. El sistema no tiene memoria suficiente y el valor de errno está establecido en ENOMEM.

ejemplo de código

#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>

int main()
{
    
    

    // 创建子进程
    pid_t pid = fork();

    // 判断是父进程还是子进程
    if (pid > 0)
    {
    
    
        printf("pid : %d\n", pid);
        // 如果大于0,返回的是创建的子进程的进程号,当前是父进程
        printf("i am parent process, pid : %d, ppid : %d\n", getpid(), getppid());
    }
    else if (pid == 0)
    {
    
    
        // 当前是子进程
        printf("i am child process, pid : %d, ppid : %d\n", getpid(), getppid());
    }

    for (int i = 0; i < 5; i++)
    {
    
    
        printf("i : %d, pid : %d\n", i, getpid());
        sleep(2);
    }

    return 0;
}

Insertar descripción de la imagen aquí

PD: Permítanme plantear una pregunta controvertida aquí. La pregunta se originó a partir de la pregunta de prueba escrita de Qiniuyun.
Algunas personas todavía están discutiendo la respuesta a la pregunta en 2018. Invitamos a todos los grandes a venir y discutirla.

El siguiente programa genera () "-"

int main(void) {
     
     
    int i;
    for (i = 0; i < 2; i++) {
     
     
        fork();
        printf("-");
    }
    return 0; 
} 

A. 2 B. 4 C. 6 D. 8

Se imprimen un total de 8 impresiones después de compilar y ejecutar GCC en el entorno Linux.
Insertar descripción de la imagen aquí

Cambiar printf("-");e printf("-\n");imprimir 6 en el mismo entorno.
Insertar descripción de la imagen aquí


Situación del espacio de direcciones virtuales de procesos padre e hijo.

Diferencias entre la ejecución de procesos padre e hijo

  1. Cuando se ejecuta el proceso padre fork(), se crea un proceso hijo
  2. Una vez creado el proceso hijo, fork()el PID del proceso hijo se devolverá al proceso padre y se devolverá 0 al proceso hijo.
  3. El sistema generará un nuevo espacio de direcciones virtuales para los recursos copiados del proceso principal para que lo utilice el proceso secundario.
  4. El proceso padre ejecuta un juicio condicional y el proceso hijo ejecuta un juicio condicional (el proceso hijo solo ejecuta fork()el código después)
  5. Al realizar un bucle y sleep()reflejar explícitamente el manejo alternativo de los procesos padre e hijo por parte de la CPU
    Por favor agregue la descripción de la imagen.

Espacio de direcciones virtuales del proceso padre-hijo

Amplíe el paso 3 de la sección anterior.
Después de llamar fork(), los datos del área de usuario del proceso hijo son los mismos que los del proceso padre pero fork()el valor de retorno es diferente. Los datos del área central también se copiarán pero el pid es diferente.
Insertar descripción de la imagen aquí

ejemplo de código

#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>

int main()
{
    
    
    int num = 10;
    // 输出num原定义值
    printf("original num: %d\n", num);

    // 输出num原地址
    printf("Address of original num: %p\n", &num);

    // 创建子进程
    pid_t pid = fork();

    // 判断是父进程还是子进程
    if (pid > 0)
    {
    
    
        // printf("pid : %d\n", pid);
        //  如果大于0,返回的是创建的子进程的进程号,当前是父进程
        printf("i am parent process, pid : %d, ppid : %d\n", getpid(), getppid());

        printf("parent num: %d\n", num);
        num += 10;
        printf("parent num += 10: %d\n", num);

        // 输出父进程中num的地址
        printf("Address of num in parent precess: %p\n", &num);
    }
    else if (pid == 0)
    {
    
    
        // 当前是子进程
        printf("i am child process, pid : %d, ppid : %d\n", getpid(), getppid());
        printf("child num: %d\n", num);
        num += 100;
        printf("child num += 100: %d\n", num);

        // 输出子进程中num的地址
        printf("Address of num in child precess: %p\n", &num);
    }

    // for (int i = 0; i < 5; i++)
    // {
    
    
    //     printf("i : %d, pid : %d\n", i, getpid());
    //     sleep(2);
    // }

    return 0;
}

Insertar descripción de la imagen aquí
El resultado final de la impresión muestra que los dos procesos de dirección son iguales, esto se debe a que lo que entendemos es la dirección de memoria virtual. La dirección de memoria virtual es compartida por cada proceso, pero la dirección de memoria física asignada por mmu es diferente y se copiará. mediante operaciones de escritura de datos a la nueva dirección de memoria física.
Desde la perspectiva del sistema operativo, cada proceso tiene su propia tabla de páginas. Cuando el proceso padre bifurca un nuevo proceso hijo, el proceso hijo copia la tabla de páginas del proceso padre y los procesos padre e hijo cambian el estado de la tabla de páginas a protegido contra escritura. Cuando se produce una operación de escritura en el proceso principal o en el proceso secundario, se producirá una excepción de falla de página y la función de manejo de excepciones de falla de página asignará una nueva dirección física al proceso secundario. Dado que diferentes procesos tienen diferentes tablas de páginas, las direcciones físicas correspondientes al acceso a la misma dirección lógica son diferentes .


Tecnología de copia en escritura

Cuando un proceso o hilo quiere modificar datos compartidos, primero crea una copia de los datos y luego la modifica.
Los datos originales permanecen sin cambios y otros procesos o subprocesos aún pueden leer copias de los datos originales. Esta estrategia evita condiciones de carrera cuando varios procesos modifican datos al mismo tiempo, mejorando así el rendimiento de la concurrencia.

La fork()implementación anterior es compartir al leer y copiar al escribir . Cuando se lee el recurso, el kernel no copia el espacio de direcciones de todo el proceso en este momento, pero permite que los procesos padre e hijo compartan el mismo espacio de direcciones. Solo cuando sea necesario escribir, se copiará al nuevo espacio de direcciones, de modo que cada proceso tenga su propio espacio de direcciones.

La clase de la biblioteca de plantillas estándar STL stringes una clase con tecnología de copia en escritura.
Por lo general string, debe haber un miembro privado en la clase, que es una char*dirección de memoria asignada desde el montón registrada por el usuario, que asigna memoria durante la construcción y la libera durante la destrucción.
Debido a que la memoria se asigna desde el montón, stringla clase tiene mucho cuidado al mantener esta memoria. stringCuando la clase devuelve esta dirección de memoria, solo devuelve const char*, que es de solo lectura. Si es necesario escribirlo, solo se puede stringhacer a través del método proporcionado Reescritura de datos.

Aviso:

  1. Después de la bifurcación, los procesos padre e hijo comparten archivos. El proceso hijo resultante tiene el mismo descriptor de archivo que el proceso padre y apunta a la misma tabla de archivos. El recuento de referencias aumenta y el puntero de desplazamiento del archivo se comparte.
  2. Los diferentes compiladores de gcc tienen diferentes estrategias de manejo de la memoria compartida. En algunos entornos, puede ser una copia profunda directa, mientras que en otros entornos puede ser contenido compartido.

Relación de proceso padre-hijo y depuración multiproceso GDB

Relación del proceso padre-hijo

la diferencia

  • fork()El valor de retorno de
    • En el proceso padre: >0 devuelve el ID del proceso hijo
    • En proceso hijo: =0
  • Algunos datos en la PCB
    • La identificación del proceso actual: pid
    • La identificación del proceso padre del proceso actual: ppid
    • recogida de señales

Común
Cuando el proceso hijo acaba de crearse y no ha realizado ninguna operación de escritura de datos, los procesos padre e hijo comparten los siguientes objetos.

  • Datos del área de usuario
  • tabla de descriptores de archivos

¿Las variables se comparten entre los procesos padre e hijo?
Se comparten al principio, pero una vez que se modifican los datos, no se pueden compartir. Comparte al leer, copia al escribir.


Depuración multiproceso de GDB

Generalmente no hay trabajos y más entrevistas.

Cuando se utiliza GDB para la depuración, GDB solo puede rastrear un proceso de forma predeterminada. Puede fork()configurar la herramienta de depuración de GDB para rastrear el proceso principal o rastrear el proceso secundario a través de instrucciones antes de llamar. El proceso principal se rastrea de forma predeterminada.

ejemplo de código

#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>

int main(){
    
    
    printf("begin\n");
    if (fork()>0)
    {
    
    
        printf("我是父进程: pid = %d, ppid = %d\n", getpid(), getppid());
        int i;
        for(i = 0; i< 10; i++){
    
    
            printf("i = %d\n", i);
            sleep(1);
        }
    }else{
    
    
        printf("我是子进程: pid = %d, ppid = %d\n", getpid(), getppid());
        int j;
        for(j = 0; j< 10; j++){
    
    
            printf("j = %d\n", j);
            sleep(1);
        }
    }
    return 0;
}

Realizar la depuración de GDB

# 查看源代码
l
# 设置父进程打印断点
b 9
# 设置子进程打印断点
b 16
# 运行
r

Insertar descripción de la imagen aquí


Establecer proceso de depuración hijo o padre

Establecer el proceso principal de depuración (predeterminado)

set follow-fork-mode parent

Insertar descripción de la imagen aquí

Configurar el subproceso de depuración

set follow-fork-mode child

Insertar descripción de la imagen aquí
Realizar la depuración de GDB
Insertar descripción de la imagen aquí


Establecer el modo de depuración

Depurar el proceso actual, otros procesos continúan ejecutándose (predeterminado)

set detach-on-fork on

Insertar descripción de la imagen aquí

Depurando el proceso actual, GDB suspende otros procesos

set detach-on-fork off

Insertar descripción de la imagen aquí
Realizar la depuración de GDB
Insertar descripción de la imagen aquí


Ver el proceso de depuración

info inferiors

Insertar descripción de la imagen aquí


Cambiar el proceso actualmente depurado

inferiors id

Insertar descripción de la imagen aquí
Insertar descripción de la imagen aquí


Sacar un proceso de la depuración de GDB

detach inferiors id

Insertar descripción de la imagen aquí


familia de funciones ejecutivas

¿Qué es la familia de funciones ejecutivas?

Ejecute un archivo ejecutable dentro del proceso de llamada.

La función de la familia de funciones exec es encontrar el archivo ejecutable según el nombre de archivo especificado y usarlo para reemplazar el contenido del proceso de llamada.
Las funciones de la familia de funciones exec no regresarán después de una ejecución exitosa, porque las entidades del proceso de llamada, incluidos segmentos de código, segmentos de datos y pilas, han sido reemplazadas por contenido nuevo, dejando solo información superficial, como los ID de proceso tal como están. ; Sólo si la llamada falla, se devolverá -1 y la ejecución continuará desde el punto de llamada del programa original.
Llamar a la familia de funciones ejecutivas no crea un nuevo proceso, solo reemplaza los datos en el área de usuario.

familia de funciones ejecutivas

#include <unistd.h>
extern char **environ;

int execl(const char *path, const char *arg, ...);
int execlp(const char *file, const char *arg, ...);
int execle(const char *path, const char *arg,..., char * const envp[]);
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int execvpe(const char *file, char *const argv[],char *const envp[]);

distinción de significado

  • Banda l: cada parámetro de línea de comando del programa que llama debe escribirse por separado en forma de lista y terminar con un puntero nulo.
  • Banda p: si el parámetro de función filecontiene, /se considera como el nombre de la ruta; de lo contrario, el archivo ejecutable se busca en el directorio especificado por la variable de entorno PATH.
  • Banda v: Primero debe construir una matriz de punteros de parámetros de línea de comando y luego usar la dirección de la matriz como parámetro del programa de llamada.
  • Banda e: primero debe construir una matriz de punteros de cadena de entorno y luego pasar la dirección de la matriz a la función para usar las nuevas variables de entorno para reemplazar las variables de entorno del proceso de llamada.

Parámetros
path : El nombre de la ruta del archivo ejecutable
file: Busque el archivo ejecutable en el directorio especificado por la variable de entorno PATH
arg: Los parámetros de la línea de comando del programa ejecutable. El primer parámetro es el nombre del archivo ejecutable (es inútil, generalmente escriba esto), a partir del segundo parámetro está la lista de parámetros requeridos por el programa y debe terminar con NULL
argv[]: matriz de punteros de parámetros de línea de comando
envp[]: matriz de punteros de cadena de entorno

El valor de retorno no se devolverá después de
una ejecución exitosa, porque las entidades del proceso de llamada, incluidos los segmentos de código, los segmentos de datos y las pilas, han sido reemplazadas por contenido nuevo, dejando solo cierta información superficial, como la ID del proceso, que permanece intacta; solo el la llamada falla Solo entonces devolverá -1 y configurará erron para continuar la ejecución desde el punto de llamada del programa original.

Ejemplo de código
hola.c

#include <stdio.h>

int main(){
    
    
    printf("helloworld!\n");
    return 0;
}

Compilado en el archivo ejecutable hola

excl.c

#include <sys/types.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main()
{
    
    
    // 创建一个子进程,在子进程中执行exec函数族中的函数
    pid_t pid = fork();
    if (pid > 0)
    {
    
    
        // 父进程
        printf("i am parent process, pid : %d\n", getpid());
        sleep(1);
    }
    else if (pid == 0)
    {
    
    
        // 子进程
        // 建议绝对路径
        execl("/home/zxz/open_file_excise/exec_process/hello", "hello", NULL);
        printf("i am child process, pid : %d\n", getpid());
    }
    for (int i = 0; i < 3; i++)
    {
    
    
        printf("i = %d, pid = %d\n", i, getpid());
    }
    return 0;
}

Insertar descripción de la imagen aquí


control de procesos

Salidas de proceso

Insertar descripción de la imagen aquí
status: Es una información de estado cuando sale el proceso. Se puede obtener cuando el proceso padre recicla los recursos del proceso hijo.

ejemplo de código

  1. Funciones de biblioteca C estándarexit()
#include <sys/types.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main()
{
    
    
    printf("hello\n");
    printf("world");

    exit(0);
    // exit(0) 等价于 return 0,于是不再执行以下代码
    printf("byebye");

    return 0;
}

Insertar descripción de la imagen aquí

  1. Funciones estándar de la biblioteca del sistema Linux_exit()
#include <sys/types.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main()
{
    
    
    printf("hello\n");
    printf("world");

    _exit(0);
    // exit(0) 等价于 return 0,于是不再执行以下代码
    printf("byebye");
    return 0;
}

Insertar descripción de la imagen aquí
Aviso: El búfer de E/S se actualizará antes exit()de llamar _exit(). Desde cuando se emite std::endlo \n, el búfer se actualizará, por lo que los datos se mostrarán en la pantalla inmediatamente. Sin embargo, llamar directamente _exit()no actualizará el búfer de E/S, por lo que cuando std::endlo \nLos datos no se mostrarán en la pantalla cuando no se emitan.

Proceso huérfano

El proceso principal ha finalizado, pero el proceso secundario aún se está ejecutando (no finalizado), lo que se denomina proceso huérfano.

ejemplo de código

#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>

int main()
{
    
    
    // 创建子进程
    pid_t pid = fork();

    // 判断是父进程还是子进程
    if (pid > 0)
    {
    
    
        //  如果大于0,返回的是创建的子进程的进程号,当前是父进程
        printf("i am parent process, pid : %d, ppid : %d\n", getpid(), getppid());
    }
    else if (pid == 0)
    {
    
    
        sleep(1);
        // 当前是子进程
        printf("i am child process, pid : %d, ppid : %d\n", getpid(), getppid());
    }

    for (int i = 0; i < 5; i++)
    {
    
    
        printf("i : %d, pid : %d\n", i, getpid());
    }

    return 0;
}

Insertar descripción de la imagen aquí
No hay ningún daño en el proceso huérfano. El proceso padre (el proceso de inicio del kernel) que ha adoptado el proceso huérfano recorrerá los wait()procesos secundarios que han salido y eventualmente procesará el proceso secundario hasta que finalice su ciclo de vida.

proceso zombie

Una vez finalizado cada proceso, liberará los datos del área del usuario en su propio espacio de direcciones. La PCB en el área del kernel no puede liberarse por sí sola y debe ser liberada por el proceso principal. Cuando finaliza el proceso, el proceso principal aún no se ha reciclado y los recursos residuales (PCB) del proceso secundario se almacenan en el kernel y se convierten en un proceso zombie (Proceso Zombie).

ejemplo de código

#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>

int main()
{
    
    
    // 创建子进程
    pid_t pid = fork();

    // 判断是父进程还是子进程
    if (pid > 0)
    {
    
    
        while (1)
        {
    
    
            //  如果大于0,返回的是创建的子进程的进程号,当前是父进程
            printf("i am parent process, pid : %d, ppid : %d\n", getpid(), getppid());
            sleep(1);
        }
        
    }
    else if (pid == 0)
    {
    
    
        // 当前是子进程
        printf("i am child process, pid : %d, ppid : %d\n", getpid(), getppid());
    }

    for (int i = 0; i < 5; i++)
    {
    
    
        printf("i : %d, pid : %d\n", i, getpid());
    }

    return 0;
}

Después de ejecutar el proceso padre, el proceso hijo todavía se está ejecutando sin liberar los recursos del kernel (bucle infinito).
Insertar descripción de la imagen aquí
Cree una nueva entrada de terminal ps auxpara ver el proceso zombie.
Insertar descripción de la imagen aquí
Uso general de depuración Ctrl + C. Mate el proceso zombie ( kill -9no se puede matar).
Insertar descripción de la imagen aquí

PD: Si el proceso principal no llama wait()o waitpid(), entonces la información retenida no se liberará y su número de proceso siempre estará ocupado, pero el número de proceso que el sistema puede usar es limitado.Si se genera una gran cantidad de procesos zombies, Lo hará Si no hay un número de proceso disponible y el sistema no puede generar nuevos procesos, este es el daño de los procesos zombies y debe evitarse.


Reciclaje de procesos

Cuando cada proceso sale, el kernel libera todos los recursos del proceso, incluidos los archivos abiertos, la memoria ocupada, etc. Sin embargo, aún se conserva cierta información, que se refiere principalmente a la información de la PCB del bloque de control de proceso (incluido el número de proceso, el estado de salida, el tiempo de ejecución, etc.). El proceso principal puede obtener su estado de salida y limpiar el proceso por completo
llamando wait()a o .waitpid()

función de espera

pid_t wait(int *wstatus);

  • Función : Espere a que finalice cualquier proceso hijo. Si finaliza algún proceso hijo, esta función reciclará los recursos del proceso hijo.
  • Parámetros : wstatusInformación de estado al salir. Se pasa una dirección de tipo int y se pasan parámetros.
  • Valor de retorno : si tiene éxito, devuelve la identificación del proceso secundario reciclado; si falla, devuelve -1 (todos los procesos secundarios finalizan y la llamada a la función falla)

El proceso de llamada wait()se suspenderá (bloqueará) hasta que uno de sus procesos secundarios salga o reciba una señal que no se puede ignorar antes de ser despertado (equivalente a continuar con la ejecución). Si no hay procesos secundarios o todos los procesos secundarios han finalizado, se devolverá -1 inmediatamente.

ejemplo de código

#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
#include <sys/wait.h>

int main()
{
    
    
    // 有一个父进程,创建5个子进程(兄弟)
    pid_t pid;
    for (int i = 0; i < 5; i++)
    {
    
    
        pid = fork();
        if (pid == 0)
        {
    
    
            break;
        }
    }
    if (pid > 0)
    {
    
    
        // 父进程
        while (1)
        {
    
    
            printf("parent, pid = %d\n", getpid());

            int ret = wait(NULL);
            if (ret == -1)
            {
    
    
                break;
            }

            printf("child die, pid = %d\n", ret);

            sleep(1);
        }
    }
    else if (pid == 0)
    {
    
    
        // 子进程
        while (1)
        {
    
    
            printf("child, pid = %d\n", getpid());
            sleep(1);
        }
    }

    return 0;
}

Después de ejecutar el código, use ps aux para verificar que los 5 procesos secundarios creados estén todos bloqueados y
Insertar descripción de la imagen aquí
seleccione el primer proceso secundario para eliminar.

kill -9 84790

Insertar descripción de la imagen aquí

Salir de funciones macro relacionadas con información

abandonar

  • WIFEXITED(status): No-0, el proceso sale normalmente
  • WEXITSTATUS (status): Si la macro anterior es verdadera, obtenga el estado de salida del proceso ( exit()parámetros)

terminación

  • WIFSIGNALED(status): No-0, el proceso finaliza de forma anormal
  • WTERMSIG(status): Si la macro anterior es verdadera, obtenga el número de señal que provocó la finalización del proceso.

pausa

  • IFSTOPPED (status): No-0, el proceso está en estado suspendido
  • WSTOPSIG(status): Si la macro anterior es verdadera, obtenga el número de la señal que provocó la pausa del proceso.
  • WIFCONTINUED (status): No-0, el proceso ha seguido ejecutándose después de haber sido suspendido.

ejemplo de código

#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
#include <sys/wait.h>
#include <stdlib.h>

int main()
{
    
    
    // 有一个父进程,创建5个子进程(兄弟)
    pid_t pid;
    for (int i = 0; i < 5; i++)
    {
    
    
        pid = fork();
        if (pid == 0)
        {
    
    
            break;
        }
    }
    if (pid > 0)
    {
    
    
        // 父进程
        while (1)
        {
    
    
            printf("parent, pid = %d\n", getpid());

            // int ret = wait(NULL);
            int st;
            int ret = wait(&st);
            
            if (ret == -1)
            {
    
    
                break;
            }
            if (WIFEXITED(st))
            {
    
    
                // 是不是正常退出
                printf("退出的状态码:%d\n", WEXITSTATUS(st));
            }
            if (WIFSIGNALED(st))
            {
    
    
                // 是不是异常终止
                printf("被哪个信号干掉了:%d\n", WTERMSIG(st));
            }
            printf("child die, pid = %d\n", ret);
            sleep(1);
        }
    }
    else if (pid == 0)
    {
    
    
        // 子进程
        while (1)
        {
    
    
            printf("child, pid = %d\n", getpid());
            sleep(1);
        }
        exit(0);
    }

    return 0;
}

Matar proceso hijo usando señal
Insertar descripción de la imagen aquí
Insertar descripción de la imagen aquí

función de espera

pid_t waitpid(pid_t pid, int *wstatus, int options);

  • Función : Recicle un proceso hijo con un número de proceso específico y puede configurar si desea bloquearlo.
  • Parámetros :
    • pid:
      • pid > 0: pid de un proceso hijo
      • pid = 0: Recicla cualquier proceso hijo del grupo de procesos actual.
      • pid = -1: Reciclar todos los procesos secundarios, equivalente a esperar()
      • pid < -1: El valor absoluto del ID de grupo de un determinado grupo de procesos, reciclando los procesos secundarios en el grupo de procesos especificado.
    • wstatus: Información de estado al salir, pasar una dirección de tipo int, pasar parámetros
    • options: Establecer bloqueo o no bloqueo
      • 0:bloquear
      • WNOHANG: sin bloqueo
  • Valor de retorno :
    • >0:devuelve la identificación del proceso hijo
    • =0: options= WNOHANG, indica que todavía hay procesos secundarios vivos
    • = -1:Error o no hay ningún proceso hijo

ejemplo de código

#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
#include <sys/wait.h>
#include <stdlib.h>

int main()
{
    
    
    // 有一个父进程,创建5个子进程(兄弟)
    pid_t pid;
    for (int i = 0; i < 5; i++)
    {
    
    
        pid = fork();
        if (pid == 0)
        {
    
    
            break;
        }
    }
    if (pid > 0)
    {
    
    
        // 父进程
        while (1)
        {
    
    
            printf("parent, pid = %d\n", getpid());
            sleep(1);

            // int ret = wait(NULL);
            int st;
            // int ret = waitpid(-1, &st, 0);
            // 非阻塞,父进程不用挂起还可以执行
            int ret = waitpid(-1, &st, WNOHANG);
            if (ret == -1)
            {
    
    
                break;
            }
            if (ret == 0)
            {
    
    
                // 说明还有子进程存在
                continue;
            }
            else if (ret > 0)
            {
    
    
                if (WIFEXITED(st))
                {
    
    
                    // 是不是正常退出
                    printf("退出的状态码:%d\n", WEXITSTATUS(st));
                }
                if (WIFSIGNALED(st))
                {
    
    
                    // 是不是异常终止
                    printf("被哪个信号干掉了:%d\n", WTERMSIG(st));
                }
            }

            printf("child die, pid = %d\n", ret);
                }
    }
    else if (pid == 0)
    {
    
    
        // 子进程
        while (1)
        {
    
    
            printf("child, pid = %d\n", getpid());
            sleep(1);
        }
        exit(0);
    }

    return 0;
}

Insertar descripción de la imagen aquí
Matar proceso hijo usando señal
Insertar descripción de la imagen aquí
Insertar descripción de la imagen aquí


comunicación entre procesos

¿Qué es la comunicación entre procesos?

La comunicación entre procesos (IPC) se refiere a un mecanismo para el intercambio de datos y de información entre diferentes procesos de ejecución .
Un proceso es una unidad de asignación de recursos independiente. Los recursos entre diferentes procesos (los procesos mencionados aquí generalmente se refieren a procesos de usuario) son independientes y no están relacionados. Un proceso no puede acceder directamente a los recursos de otro proceso. Sin embargo, los procesos no están aislados: diferentes procesos necesitan interactuar con la información y transferir el estado, por lo que se requiere comunicación entre procesos.

Propósito de la comunicación entre procesos

  • Transferencia de datos : un proceso necesita enviar sus datos a otro proceso.
  • Evento de notificación : un proceso necesita enviar un mensaje a otro proceso o a un grupo de procesos para notificarles que ha ocurrido un determinado evento (como notificar al proceso principal cuando finaliza el proceso).
  • Compartir recursos : compartir los mismos recursos entre múltiples procesos. Para hacer esto, el núcleo necesita proporcionar mecanismos de sincronización y exclusión mutua.
  • Control de procesos : Algunos procesos esperan controlar completamente la ejecución de otro proceso (como el proceso de depuración), en este momento el proceso de control espera interceptar todas las trampas y excepciones de otro proceso y poder conocer sus cambios de estado a tiempo.

Método de comunicación entre procesos de Linux

Insertar descripción de la imagen aquí

tubería anónima

Las canalizaciones también se denominan canalizaciones sin nombre (anónimas). Son la forma más antigua de IPC (comunicación entre procesos) en los sistemas UNIX. Todos los sistemas UNIX admiten este mecanismo de comunicación. Una tubería anónima es una tubería de comunicación unidireccional que permite que un proceso escriba datos en un extremo de la tubería y otro proceso lea datos desde el otro extremo de la tubería .

El ejemplo de código
cuenta el número de archivos en un directorio. Para ejecutar este comando, el shell crea dos procesos para ejecutar lsywc

ls | wc -l

Insertar descripción de la imagen aquí

Características de las tuberías.

  • La canalización es en realidad un búfer mantenido en la memoria del kernel . La capacidad de almacenamiento de este búfer es limitada y es posible que diferentes sistemas operativos no necesariamente tengan el mismo tamaño.
  • Las canalizaciones tienen las características de los archivos: operaciones de lectura y operaciones de escritura. Las canalizaciones anónimas no tienen entidades de archivo. Las canalizaciones con nombre tienen entidades de archivo, pero no almacenan datos. Puede operar la canalización de la misma manera que un archivo.
  • Una tubería es un flujo de bytes. Cuando se utiliza una tubería, no existe el concepto de mensajes o límites de mensajes. El proceso de lectura de datos de la tubería puede leer bloques de datos de cualquier tamaño, independientemente del tamaño del bloque de datos escrito por la escritura. proceso a la tubería. ¿Cuántos
  • Los datos que pasan a través de la tubería son secuenciales. El orden de los bytes leídos de la tubería es exactamente el mismo que el orden en que se escribieron en la tubería.
  • La dirección de transmisión de datos en la tubería es unidireccional , un extremo se usa para escribir y el otro extremo se usa para leer.La tubería es semidúplex.
  • lseek()La lectura de datos de la tubería es una operación que se realiza una sola vez. Una vez que se leen los datos, se descartan de la tubería, lo que libera espacio para escribir más datos. No puede usarlo para acceder aleatoriamente a los datos en la tubería.
  • Las canalizaciones anónimas solo se pueden utilizar entre procesos con un ancestro común (un proceso padre y un proceso hijo, o dos procesos hermanos, que están relacionados)

Insertar descripción de la imagen aquí


Por qué se pueden utilizar tuberías para la comunicación entre procesos

O que las canalizaciones anónimas sólo se pueden utilizar entre procesos con un ancestro común.
De hecho, una canalización es similar a un archivo, y la lectura y escritura de datos por parte del extremo de lectura y escritura de la canalización es similar a la lectura y escritura de un archivo.
Insertar descripción de la imagen aquí


Estructura de datos de tubería

La capa inferior es una cola lineal, la lógica es similar a la cola circular y el orden de lectura y escritura es el mismo.


Los procesos padre e hijo se comunican a través de tuberías anónimas

Crear una tubería anónima

int pipe(int pipefd[2]);

  • Función : crear una tubería anónima para la comunicación entre procesos
  • Parámetros :: int pipefd[2]Esta matriz es un parámetro saliente
    • pipefd[0]: Corresponde al extremo de lectura de la tubería
    • pipefd[1]: Corresponde al final de escritura del pipeline
  • Valor de retorno : 0 si tiene éxito; -1 si falla

Aviso: Las canalizaciones anónimas solo se pueden utilizar para la comunicación entre procesos con relaciones (procesos padre-hijo, procesos hermanos). Si no hay información en el pipeline, la operación de lectura se bloquea. Si el pipeline está lleno de información, la operación de escritura se bloquea. La lectura y la escritura no se pueden realizar al mismo tiempo, de lo contrario permanecerá bloqueada. sleep()No se puede colocar read()debajo, de lo contrario provocará la autoescritura y la autolectura ( sleep()también se producirán comentarios).

Ejemplo de código:
proceso padre-hijo que sigue leyendo y escribiendo

// 子进程发送数据给父进程,父进程读取到数据输出
#include <unistd.h>
#include <sys/types.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main()
{
    
    

    // 在fork之前创建管道
    int pipefd[2];
    int ret = pipe(pipefd);
    if (ret == -1)
    {
    
    
        perror("pipe");
        exit(0);
    }

    // 创建子进程
    pid_t pid = fork();
    if (pid > 0)
    {
    
    
        // 父进程
        printf("i am parent processe, pid = %d\n", getpid());

        char buf[1024] = {
    
    0};
        while (1)
        {
    
    
            // 从管道中读取数据
            int len = read(pipefd[0], buf, sizeof(buf));
            printf("parent recv : %s, pid : %d\n", buf, getpid());

            // 向管道中写入数据
            char *str = "hello,i am parent";
            write(pipefd[1], str, strlen(str));
            sleep(1);
        }
    }
    else if (pid == 0)
    {
    
    
        // 子进程
        printf("i am child process, pid = %d\n", getpid());

        char buf[1024] = {
    
    0};
        while (1)
        {
    
    
            // 向管道中写入数据
            char *str = "hello, i am child";
            write(pipefd[1], str, strlen(str));
            sleep(1);

            // 从管道中读取数据
            int len = read(pipefd[0], buf, sizeof(buf));
            printf("child pecv: %s, pid = %d\n", buf, getpid());
        }
    }

    return 0;
}

Insertar descripción de la imagen aquí


Tamaño del buffer de tubería

Ver tamaño

ulimit -a

Insertar descripción de la imagen aquí

Utilice la función para obtener
long fpathconf(int fd, int name);

  • Función : Se utiliza para consultar atributos relacionados con el descriptor de archivo.
  • Parámetros :
    • fd: El descriptor de archivo que se va a consultar.
    • name: Indica el atributo a consultar (constante con el prefijo _PC_). Los siguientes son atributos de uso común:
      • _PC_LINK_MAX: El número máximo de enlaces a un archivo.
      • _PC_MAX_CANON: El número máximo de bytes en la cola de entrada de especificación.
      • _PC_MAX_INPUT: El número máximo de bytes en la cola de entrada.
      • _PC_NAME_MAX: Longitud máxima del nombre del archivo
      • _PC_PATH_MAX: La longitud máxima de la ruta del archivo.
      • _PC_PIPE_BUF: El tamaño máximo del buffer de tubería.
      • _PC_CHOWN_RESTRICTED: Indica si se permite cambiar el propietario del archivo
  • Valor de retorno : si tiene éxito, devuelve el valor del atributo consultado ( longtipo); si falla, devuelve -1 y establece errno

ejemplo de código

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

int main(){
    
    
    int pipefd[2];
    int ret = pipe(pipefd);

    // 获取管道的大小
    long size = fpathconf(pipefd[0],  _PC_PIPE_BUF);
    printf("pipe size : %ld\n", size);

    return 0; 
}

Insertar descripción de la imagen aquí

Modificar tamaño

ulimit -p

Tres situaciones de comunicación por tubería anónima.

Insertar descripción de la imagen aquí


Problemas encontrados en la segunda situación y soluciones.

Pregunta
Continúe el código del capítulo anterior. Si se comenta, sleep()se transformará directamente en la primera situación de autoescritura y autolectura:
Insertar descripción de la imagen aquí
Insertar descripción de la imagen aquí

Solución:
reescribe el código en el tercer caso.

ejemplo de código

// 子进程发送数据给父进程,父进程读取到数据输出
#include <unistd.h>
#include <sys/types.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main()
{
    
    

    // 在fork之前创建管道
    int pipefd[2];
    int ret = pipe(pipefd);
    if (ret == -1)
    {
    
    
        perror("pipe");
        exit(0);
    }

    // 创建子进程
    pid_t pid = fork();
    if (pid > 0)
    {
    
    
        // 父进程
        printf("i am parent processe, pid = %d\n", getpid());

        // 关闭写端
        close(pipefd[1]);

        char buf[1024] = {
    
    0};
        while (1)
        {
    
    
            // 从管道中读取数据
            int len = read(pipefd[0], buf, sizeof(buf));
            printf("parent recv : %s, pid : %d\n", buf, getpid());
        }
    }
    else if (pid == 0)
    {
    
    
        // 子进程
        printf("i am child process, pid = %d\n", getpid());

        // 关闭读端
        close(pipefd[0]);

        char buf[1024] = {
    
    0};
        while (1)
        {
    
    
            // 向管道中写入数据
            char *str = "hello, i am child\n";
            write(pipefd[1], str, strlen(str));
        }
    }

    return 0;
}

Insertar descripción de la imagen aquí


Caso de comunicación de tubería anónima

Implementar ps aux | grep xxxla comunicación entre procesos entre padres e hijos.

  • Proceso hijo: ps aux, una vez finalizado el proceso hijo, envía los datos al proceso padre
  • Proceso principal: obtener datos, filtrar
    pipe(), execlp()( dup2()el proceso secundario redirige la salida estándar al extremo de escritura de la tubería)
#include <unistd.h>
#include <sys/types.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <wait.h>

int main(){
    
    
    // 创建一个管道
    int fd[2];
    int ret = pipe(fd);

    if (ret == -1)
    {
    
    
        perror("pipe");
        exit(0);
    }
    
    // 创建子进程
    pid_t pid = fork();

    if (pid > 0)
    {
    
    
        // 父进程

        // 关闭写端
        close(fd[1]);
        // 从管道中读取
        char buf[4096] = {
    
    0};
        int len = -1;
        // size-1 表示减去字符串结束符 "\0"
        while ((len = read(fd[0], buf, sizeof(buf)-1)) > 0)
        {
    
    
            // 过滤数据输出
            printf("%s", buf);
            memset(buf, 0, 4096);
        }

        // 回收子进程资源
        wait(NULL);

    }else if(pid == 0)
    {
    
    
        // 子进程

        // 关闭读端
        close(fd[0]);

        // 文件描述符的重定向 stdout_fileno -> fd[1]
        dup2(fd[1], STDOUT_FILENO);

        // 执行 ps aux
        int flag = execlp("ps", "ps", "aux", NULL);
        if (flag == -1)
        {
    
    
            perror("execlp");
            exit(0);
        }
    }else
    {
    
    
        perror("fork");
        exit(0);
    }

    return 0;
}

PD: La función grep aún no se ha implementado y se actualizará más adelante. En realidad, 4096 no es el límite de memoria de la canalización, pero los datos que excedan 4096 se subcontratarán para garantizar operaciones atómicas.
Si desea enviar el tamaño real deseado (< 4096) al terminal, simplemente déjelo len = read(fd[0], buf, sizeof(buf)-1)en un bucle.

Características de lectura y escritura de tuberías y configuración de tuberías sin bloqueo

Funciones de lectura y escritura de tuberías

Al utilizar canalizaciones (incluidas las anónimas y las nombradas), debe prestar atención a las siguientes situaciones especiales (suponiendo que todas bloqueen las operaciones de E/S)

  • Todos los descriptores de archivos que apuntan al extremo de escritura de la tubería están cerrados (el recuento de referencia del extremo de escritura de la tubería es 0). Si un proceso lee datos desde el extremo de lectura de la tubería, los datos restantes en la tubería se leer, leer de nuevo devolverá 0, por lo que le gusta leer hasta el final del archivo
  • Si hay un descriptor de archivo que apunta al extremo de escritura de la tubería que no está cerrado (el recuento de referencias del extremo de escritura de la tubería es mayor que 0) y el proceso que sostiene el extremo de escritura de la tubería no escribe datos en la tubería. Y en este momento hay un proceso de lectura de datos de la tubería, luego la tubería Después de leer los datos restantes en la tubería, la lectura se bloqueará nuevamente. Los datos no se leerán ni devolverán hasta que haya datos en la tubería que puedan ser leido.
  • Si todos los descriptores de archivo que apuntan al extremo de lectura de la tubería están cerrados (el recuento de referencias del extremo de lectura de la tubería es mayor que 0) y un proceso escribe datos en la tubería en este momento, el proceso recibirá una señal SIGPIPE, lo que generalmente hace que el proceso finalice de manera anormal.
  • Si hay un descriptor de archivo que apunta al extremo de lectura de la tubería que no está cerrado (el recuento de referencia del extremo de lectura de la tubería es mayor que 0) y el proceso que sostiene el extremo de lectura de la tubería no lee datos de la tubería, y hay un proceso de escritura de datos en la tubería, luego en la tubería Cuando esté lleno, la escritura nuevamente se bloqueará hasta que haya una posición vacía en la tubería antes de que se puedan escribir y devolver los datos.

Resumir:

  • Leer canalización:
    • Hay datos en la tubería: leer devuelve el número de bytes realmente leídos.
    • No hay datos en proceso:
      • El final de escritura está todo cerrado y la lectura devuelve 0 (equivalente a leer hasta el final del archivo)
      • El extremo de escritura no está completamente cerrado y la lectura está bloqueada y en espera.
  • Escribir tubería:
    • Todos los lectores de tuberías están cerrados y el proceso finaliza de forma anormal (el proceso recibe la señal SIGPIPE)
    • No todos los lectores de tuberías están cerrados:
      • La tubería está llena, escribe bloques.
      • La tubería no está llena, escribe los datos y devuelve el número real de bytes escritos.

Tubería configurada sin bloqueo

ejemplo de código

// 子进程发送数据给父进程,父进程读取到数据输出
#include <unistd.h>
#include <sys/types.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
/*
    设置管道非阻塞
    fcntl
*/ 

int main()
{
    
    

    // 在fork之前创建管道
    int pipefd[2];
    int ret = pipe(pipefd);
    if (ret == -1)
    {
    
    
        perror("pipe");
        exit(0);
    }

    // 创建子进程
    pid_t pid = fork();
    if (pid > 0)
    {
    
    
        // 父进程
        printf("i am parent processe, pid = %d \n", getpid());

        // 关闭写端
        close(pipefd[1]);

        char buf[1024] = {
    
    0};

        // 获取原来的flag
        int flags = fcntl(pipefd[0], F_GETFL); 

        // 修改flag的值
        flags |= O_NONBLOCK;
        
        // 设置新的flag
        fcntl(pipefd[0], F_SETFL,  flags);
        
        while (1)
        {
    
    
            // 从管道中读取数据
            int len = read(pipefd[0], buf, sizeof(buf));
            printf("len=%d \n", len);
            printf("parent recv : %s , pid : %d \n", buf, getpid());
            memset(buf, 0, 1024);
            sleep(1);
        }
    }
    else if (pid == 0)
    {
    
    
        // 子进程
        printf("i am child process, pid = %d \n", getpid());

        // 关闭读端
        close(pipefd[0]);

        char buf[1024] = {
    
    0};
        while (1)
        {
    
    
            // 向管道中写入数据
            char *str = "hello, i am child \n";
            write(pipefd[1], str, strlen(str));
            sleep(3);
        }
    }

    return 0;
}

Insertar descripción de la imagen aquí


canal famoso

Una canalización con nombre (FIFO), también conocida como archivo FIFO, proporciona un nombre de ruta asociado. Siempre que se pueda acceder a la ruta, pueden comunicarse entre sí a través de FIFO .
Una canalización con nombre se abre de la misma manera que un archivo normal. Una vez abierto, se pueden utilizar en él las mismas funciones del sistema de E/S que las utilizadas para operar canalizaciones anónimas y otros archivos.

La diferencia entre canalizaciones con nombre y canalizaciones anónimas

  • FIFO existe como un archivo especial en el sistema de archivos, pero el contenido del FIFO se almacena en la memoria.
  • Después de que finaliza el proceso que utiliza FIFO, el archivo EIFO continúa guardándose en el sistema de archivos para su uso posterior.
  • Los FIFO tienen nombres y los procesos no relacionados pueden comunicarse abriendo canalizaciones con nombre.

Uso de pipas famosas.

Creación de comandos
Una vez que se ha creado un FIFO usando mkfifo, open()se puede abrir usando Las funciones comunes de E/S de archivos se pueden usar para FIFO. Tales como: close(), read(), write(), unlink()etc.

mkfifo filename

creación de funciones
int mkfifo(const char *pathname, mode t mode);

  • Función : Crear archivo FIFO
  • Parámetros :
    • pathname: ruta al nombre de la tubería
    • mode: Permisos de archivos (igual que open()desde mode)

ejemplo de código

escribir.c

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
//  向管道中写数据

int main(){
    
    

    // 1. 判断文件是否存在
    int flag = access("fifo_test", F_OK);
    if (flag == -1)
    {
    
    
        printf("管道不存在!请创建管道\n");

        // 1.1 不存在则创建管道文件
        int ret = mkfifo("fifo_test", 0664);
        
        if (ret == -1)
        {
    
    
            perror("mkfifo");
            exit(0);
        }
    }

    // 2. 以只写方式打开管道
    int fifo_fd = open("fifo_test", O_WRONLY);
    if (fifo_fd == -1)
    {
    
    
        perror("open");
        exit(0);
    }

    // 3. 写数据
    for (int i = 0; i < 100; i++)
    {
    
    
        char buf[1024];
        sprintf(buf,"hello,%d\n", i);
        printf("write data:%s\n", buf);
        write(fifo_fd, buf, strlen(buf));
        sleep(1);
    }

    // 4. 关闭FIFO文件描述符
    close(fifo_fd);

    return 0;
}

leer.c

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
//  从管道中读数据

int main(){
    
    
    // 1. 打开管道文件
    int fifo_fd = open("fifo_test", O_RDONLY);
    if(fifo_fd == -1){
    
    
        perror("open");
        exit(0);
    }

    // 2. 读取数据
    while (1)
    {
    
    
        char buf[1024] = {
    
    0};
        int len = read(fifo_fd, buf, sizeof(buf));
        if (len == 0)
        {
    
    
            printf("写端已断开连接...\n");
            break;
        }
        printf("recv buf:%s\n", buf);
    }
    
    // 3. 关闭文件描述符
    close(fifo_fd);

    return 0;
}

dos procesos en ejecución
Insertar descripción de la imagen aquí
Insertar descripción de la imagen aquí

Aviso:

  • Un proceso que abre una tubería para solo lectura se bloquea hasta que otro proceso abre la tubería para solo escritura.
  • Un proceso que abre una tubería para escribir solo se bloquea hasta que otro proceso abre la tubería para solo lectura.

Canales famosos implementan una versión simple de la función de chat

marco básico

Insertar descripción de la imagen aquí

Escribir, leer y chatear

Ejemplo de código
chatA.c

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>

int main(){
    
    
    // 1.判断有名管道是否存在
    int ret = access("fifo1", F_OK);
    if (ret == -1)
    {
    
    
        // 文件不存在
        printf("管道文件不存在,创建对应有名管道\n");
        ret = mkfifo("fifo1", 0664);
        if (ret == -1)
        {
    
    
            perror("mkfifo");
            exit(0);
        }
    }

    // 2.以只写的方式打开管道fifo1
    int w_fd = open("fifo1", O_WRONLY);
    if (w_fd == -1)
    {
    
    
        perror("open");
        exit(0);
    }
    printf("管道fifo1打开成功,等待写入...\n");

    // 3.以只读的方式打开管道fifo2
    int r_fd = open("fifo2", O_RDONLY);
    if (r_fd == -1)
    {
    
    
        perror("open");
        exit(0);
    }
    printf("管道fifo2打开成功,等待读取...\n");

    char buf[128];
    // 4.循环地写读数据
    while(1)
    {
    
    
        memset(buf, 0, 128);
        // 4.1.获取标准输入的数据
        fgets(buf, 128, stdin);
        // 4.2.写数据
        ret = write(w_fd, buf, strlen(buf));
        if (ret == -1)
        {
    
    
            perror("write");
            exit(0);
        }
        
        // 4.3.读数据
        memset(buf, 0, 128);
        ret = read(r_fd, buf, 128);
        if (ret == -1)
        {
    
    
            if (ret <= 0 )
            {
    
    
                perror("read");
                break;
            }
        }
        printf("chatB: %s\n",buf);
    }

    // 5.关闭读写文件描述符
        close(r_fd);
        close(w_fd);

    return 0;
}

gatoB.c

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>

int main(){
    
    
    // 1.判断有名管道是否存在
    int ret = access("fifo2", F_OK);
    if (ret == -1)
    {
    
    
        // 文件不存在
        printf("管道文件不存在,创建对应有名管道\n");
        ret = mkfifo("fifo2", 0664);
        if (ret == -1)
        {
    
    
            perror("mkfifo");
            exit(0);
        }
        
    }

    // 2.以只读的方式打开管道fifo1
    int r_fd = open("fifo1", O_RDONLY);
    if (r_fd == -1)
    {
    
    
        perror("open");
        exit(0);
    }
    printf("管道fifo1打开成功,等待读取...\n");

    // 3.以只写的方式打开管道fifo2
    int w_fd = open("fifo2", O_WRONLY);
    if (w_fd == -1)
    {
    
    
        perror("open");
        exit(0);
    }
    printf("管道fifo2打开成功,等待写入...\n");

    char buf[128];

    // 4.循环地读写数据
    while(1)
    {
    
    
        // 4.1.读数据
        memset(buf, 0, 128);
        ret = read(r_fd, buf, 128);
        if (ret == -1)
        {
    
    
            if (ret <= 0 )
            {
    
    
                perror("read");
                break;
            } 
        }
        printf("chatA: %s\n",buf);

        memset(buf, 0, 128);

        // 4.1.获取标准输入的数据
        fgets(buf, 128, stdin);

        // 4.2.写数据
        ret = write(w_fd, buf, strlen(buf));
        if (ret == -1)
        {
    
    
            perror("write");
            exit(0);
        }
    }

    // 5.关闭读写文件描述符
    close(w_fd);
    close(r_fd);
    
    return 0;
}

Insertar descripción de la imagen aquí
Insertar descripción de la imagen aquí


mapa de memoria

¿Qué es el mapeo de memoria?

La E/S asignada en memoria asigna los datos del archivo del disco a la memoria y los usuarios pueden modificar el archivo del disco modificando la memoria .

Insertar descripción de la imagen aquí


Llamadas al sistema relacionadas con el mapeo de memoria

void *mmap(void *addr, size t length, int prot, int flags, int fd, off_t offset);

  • Función : asigna los datos de un archivo o dispositivo a la memoria

  • Parámetros :

    • addr: NULL, especificado por el kernel
    • length: La longitud de los datos que se asignarán. Este valor no puede ser 0. Se recomienda utilizar la longitud del archivo para obtener la longitud del archivo: stat(),lseek()
    • prot: Permiso de operación para el área de mapeo de memoria aplicada. Para operar la memoria mapeada, debe tener permiso de lectura (comúnmente usado: PROT_READ, PROT_READ | PROT_WRITE)
      • PROT_EXEC: Permisos ejecutables
      • PROT_READ:Permiso de lectura
      • PROT_WRITE:permiso de escritura
      • PROT_NONE:Permiso denegado
    • flags:
      • MAP_SHARED: Los datos en el área de mapeo se sincronizarán automáticamente con el archivo del disco. Esta opción debe configurarse para la comunicación entre procesos.
      • MAP_PRIVATE: No sincronizado, los datos en el área de mapeo de memoria han cambiado, el archivo original no se modificará y se volverá a crear un archivo nuevo (copia en escritura)
    • fd: El descriptor de archivo del archivo que necesita ser mapeado
      • Se obtiene mediante apertura, que es un archivo de disco.
      • Aviso: El tamaño del archivo no puede ser 0 y los permisos especificados por open no pueden entrar en protconflicto con
        prot: PROT_READcorresponde a open: solo lectura/lectura-escritura
        prot: PROT_READ | PROT_WRITEcorresponde a open: lectura-escritura
    • offset: Desplazamiento, generalmente no utilizado. Lo que se debe especificar es un múltiplo entero de 4096, 0 significa que no hay compensación.
  • Valor de retorno : si tiene éxito, devuelve la primera dirección de la memoria creada; si falla, devuelve MAP_FAILED( (void*) -1 ) y establece errno


int munmap(void *addr, size t length);

  • Función : Liberar mapeo de memoria
  • Parámetros :
    • addr: La primera dirección de la memoria que se liberará.
    • length: La longitud de los datos que se liberarán (tamaño de la memoria), este valor no puede ser 0. Se recomienda utilizar la longitud del archivo para obtener la longitud del archivo: stat(),lseek()
  • Valor de retorno : Devuelve 0 si tiene éxito; devuelve -1 si falla y establece errno

Uso del mapeo de memoria para la comunicación entre procesos

Principio de implementación

  • Comunicación relacional entre procesos (proceso padre-hijo)
    • Cuando no hay ningún proceso hijo, primero cree un área de mapeo de memoria a través del único proceso padre.
    • Después de tener el área de mapeo de memoria, cree un proceso hijo.
    • Se comparte el área de mapeo de memoria creada por los procesos padre e hijo.
  • Comunicación entre procesos sin relaciones.
    • Prepare un archivo de disco con un tamaño distinto de 0
    • 进程1: Cree un área de mapeo de memoria a través de un archivo de disco y obtenga un puntero para operar esta memoria.
    • 进程2: Cree un área de mapeo de memoria a través de un archivo de disco y obtenga un puntero para operar esta memoria.
    • Utilice la comunicación del área asignada en memoria

Aviso: La comunicación del área asignada en memoria no es bloqueante.

Ejemplo de código
Comunicación relacional entre procesos

#include <stdio.h>
#include <wait.h>
#include <sys/mman.h>
#include <sys/types.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>

int main()
{
    
    
    // 1.打开一个文件
    int fd = open("test.txt", O_RDWR);
    int size = lseek(fd, 0, SEEK_END); // 获取文件大小

    // 2.创建内存映射
    void *ptr = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
    if (ptr == MAP_FAILED)
    {
    
    
        perror("mmap");
        exit(0);
    }

    // 3.创建子进程
    pid_t pid = fork();
    if (pid > 0)
    {
    
    
        wait(NULL);
        // 父进程
        char buf[64];
        strcpy(buf, (char *)ptr);
        printf("read data : %s\n", buf);
    }
    else if (pid == 0)
    {
    
    
        // 子进程
        strcpy((char *)ptr, "nihao, this's son!!!");
    }
    else
    {
    
    
        perror("fork");
        exit(0);
    }

    // 4.释放内存映射区
    munmap(ptr, size);

    return 0;
}

Insertar descripción de la imagen aquí


Problemas relacionados con el mapeo de memoria

1. Si realiza una operación ++ ( ) mmap()en el valor de retorno de , ¿será exitosa? ptrptr++munmap()
Respuesta : Puede realizar una operación ++, pero no se puede liberar con éxito. Se debe pasar la primera dirección.

2. ¿Qué pasará si se especifica open()cuándo ? O RDONLYmmap()protPROT_READ | PROT_WRITE
Respuesta : Se devolverá un error MAP_FAILED. Se recomienda queopen() los permisos y protlos permisos sean consistentes.

3. ¿Qué sucede si el desplazamiento del archivo es 1000?
Respuesta : El desplazamiento debe ser un múltiplo entero de 4096, devuelveMAP_FAILED

4. ¿ mmap()Bajo qué circunstancias fallará la llamada?
Respuesta : ① length= 0; ② protSolo se especifica permiso de escritura; ③ Se produce la segunda pregunta anterior.

5. ¿Es posible open()crear O_CREATun área de mapeo con un archivo nuevo?
Respuesta : Sí, pero el tamaño del archivo creado no es 0 (se puede expandir usando lseek()y ).truncate()

6. mmap()Después de cerrar el descriptor del archivo, mmap()¿tendrá algún impacto en el mapeo?
Respuesta : Sin impacto, el área de mapeo aún existe, solo que el área de mapeo fdse cerró.

ptr7. ¿Qué pasará si ocurre una operación fuera de límites?
Respuesta : Una operación fuera de límites opera en memoria ilegal y generará una falla de segmentación.


Implementar la función de copia de archivos mediante mapeo de memoria

Pasos de pensamiento

  1. Mapa de memoria del archivo original.
  2. Crear un nuevo archivo (ampliar el archivo)
  3. Asigna los datos del nuevo archivo a la memoria.
  4. Copie los datos de la memoria del primer archivo a la nueva memoria del archivo mediante copia de memoria
  5. Liberar recursos

ejemplo de código

#include <stdio.h>
#include <wait.h>
#include <sys/mman.h>
#include <sys/types.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>


int main(){
    
    
    // 1.对原始的文件进行内存映射
    int fd = open("english.txt", O_RDWR);
    if (fd == -1)
    {
    
    
        perror("open");
        exit(0);
    }

    // 获取源文件大小
    int len = lseek(fd, 0, SEEK_END);

    // 2.创建一个新文件(拓展该文件)
    int fd1 = open("cpy.txt", O_RDWR | O_CREAT, 0644);
    if (fd1 == -1)
    {
    
    
        perror("open");
        exit(0);
    }

    // 对新建文件进行拓展
    truncate("cpy.txt", len);
    write(fd1, " ", 1);

    // 3.把新文件的数据映射到内存中
    void *ptr = mmap(NULL, len, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); 
    void *ptr1 = mmap(NULL, len, PROT_READ | PROT_WRITE, MAP_SHARED, fd1, 0);   
    
    if(ptr == MAP_FAILED)
    {
    
    
        perror("mmap");
        exit(0);
    }
    if(ptr1 == MAP_FAILED)
    {
    
    
        perror("mmap");
        exit(0);
    }

    //内存拷贝
    memcpy(ptr1, ptr, len);

    // 释放资源(谁先打开先释放)
    munmap(ptr1, len);
    munmap(ptr, len);

    // 关闭文件描述符
    close(fd1);
    close(fd);

    return 0;
}

Aviso: En términos generales, es inconveniente copiar archivos que son demasiado grandes para evitar una memoria insuficiente.

Mapeo anónimo

El proceso de entidad de archivo no requiere una asignación de memoria .
Puede realizar un mapeo de procesos relacionados (proceso padre-hijo).

ejemplo de código

#define _DEFAULT_SOURCE // MAP_ANONYMOUS

#include <stdio.h>
#include <wait.h>
#include <sys/mman.h>
#include <sys/types.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>

#define BUF_LEN 4096

int main()
{
    
    

    // 1.创建匿名映射区
    void *ptr = mmap(NULL, BUF_LEN, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1, 0);
    if (ptr == MAP_FAILED)
    {
    
    
        perror("mmap");
        exit(0);
    }

    // 2.父子间通信
    pid_t pid = fork();

    if (pid > 0)
    {
    
    
        // 父进程
        strcpy((char *)ptr, "hello, world");
        wait(NULL);
    }
    else if (pid == 0)
    {
    
    
        // 子进程
        sleep(1);
        printf("%s\n", (char *)ptr);
    }

    // 3.释放内存映射区
    int ret = munmap(ptr, BUF_LEN);
    if (ret == -1)
    {
    
    
        perror("munmap");
        exit(0);
    }

    return 0;
}

Insertar descripción de la imagen aquí


Señal

¿Qué es la señal?

Las señales son comunicaciones entre procesos de Linux, que son mecanismos de notificación para los procesos cuando ocurren eventos, a veces también llamadas interrupciones de software .
Las señales son una simulación del mecanismo de interrupción a nivel de software y un método de comunicación asincrónica.
Las señales pueden hacer que un proceso en ejecución sea interrumpido por otro proceso asincrónico en ejecución y, en su lugar, manejar un evento inesperado.

Muchas señales enviadas al proceso generalmente se originan en el núcleo . Los diversos eventos que hacen que el núcleo genere señales para los procesos son los siguientes:

  • Para un proceso en primer plano, el usuario puede enviarle una señal ingresando caracteres de terminal especiales. Por ejemplo, escribir ctrl+C generalmente envía una señal Ctrl+Cde interrupción al proceso.

  • Se produce una excepción de hardware, es decir, el hardware detecta una condición de error y notifica al kernel, que luego envía la señal correspondiente al proceso relevante. Por ejemplo, ejecutar una instrucción anormal en lenguaje de máquina, como dividir por 0 o hacer referencia a un área de memoria inaccesible.

  • Cambios de estado del sistema. Por ejemplo, alarm()la expiración del temporizador provocará SIGALRMuna señal, el tiempo de CPU de la ejecución del proceso excede el límite o un proceso hijo del proceso sale.

  • Ejecute el comando Kill o llamekill()


Dos propósitos del uso de señales.

  • Hacer saber a un proceso que ha ocurrido algo específico

  • Obliga a un proceso a ejecutar un controlador de señales en su propio código.


Características de la señal

  • Simple

  • No puedo transportar mucha información.

  • Enviar solo cuando se cumplan ciertas condiciones

  • La prioridad es mayor


Ver una lista de señales definidas por el sistema

kill -l

PD: Las primeras 31 señales son señales regulares y el resto son señales en tiempo real.


Lista de señales de Linux

número de serie Nombre de la señal evento correspondiente Acción por defecto
1 Suspiro Todos los procesos iniciados por el shell cuando el usuario sale del shell recibirán esta señal Terminar el proceso
2 FIRMA
Cuando el usuario presiona Ctrl+Cla combinación de teclas, el terminal de usuario envía esta señal al programa en ejecución iniciado por el terminal. Terminar el proceso
3 SIGUE
Esta señal se genera cuando el usuario presiona Ctrl+\la combinación de teclas y el terminal de usuario envía algunas señales al programa en ejecución iniciado por el terminal. Terminar el proceso
4 SIGILO La CPU detecta que un proceso ha ejecutado una instrucción ilegal Finalice el proceso y genere el archivo principal.
5 TRAMPA DE SEÑAL Esta señal es generada por instrucciones de punto de interrupción u otras instrucciones de trampa. Finalice el proceso y genere el archivo principal.
6 SIGABRT abort()Esta señal se genera al llamar. Finalice el proceso y genere el archivo principal.
7 SIGBUS Acceso ilegal a la dirección de la memoria, incluido el error de alineación de la memoria Finalice el proceso y genere el archivo principal.
8 SIGFPE Emitido cuando ocurre un error aritmético fatal. Incluyendo no solo errores de operación de punto flotante, sino también todos los errores de algoritmo, como desbordamiento y división por cero. Finalice el proceso y genere el archivo principal.
9 SIGKILL
Terminar el proceso incondicionalmente. Esta señal no se puede ignorar, manejar ni bloquear. Finalice el proceso, lo que puede matar cualquier proceso normal (los procesos anormales como los procesos zombies no se cuentan)
10 1 Señales definidas por el usuario. Es decir, el programador puede definir y utilizar la señal en el programa. Terminar el proceso
11 SIGSEGV
Indica que el proceso realizó un acceso a memoria no válido (fallo de segmentación) Finalice el proceso y genere el archivo principal.
12 SIGUSR2 Otra señal definida por el usuario que los programadores pueden definir y usar en el programa. Terminar el proceso
13 SIGPIPE
Una tubería rota escribe datos en una tubería sin un final de lectura Terminar el proceso
14 SIGALRM El temporizador se agota y el tiempo de espera alarm()lo establece la llamada al sistema. Terminar el proceso
15 PLAZO OBJETIVO Señal de fin de programa. A diferencia de SIGKILL, esta señal se puede bloquear y finalizar. Generalmente se usa para indicar que el programa sale normalmente. Esta señal se genera de forma predeterminada al ejecutar el comando de shell Kill. Terminar el proceso
dieciséis SIGSTKFLT Las señales que aparecieron en versiones anteriores de Linux siguen siendo compatibles con versiones anteriores. Terminar el proceso
17 SEÑAL
Cuando finalice el proceso hijo, el proceso padre recibirá esta señal. ignora esta señal
18 SEÑAL
Si el proceso se detiene, mantenlo ejecutándose. continuar/ignorar
19 SIGSTOP
Detener la ejecución del proceso. Las señales no se pueden ignorar, manejar ni bloquear Para finalizar el proceso
20 SIGTSTP Detenga la ejecución del proceso interactivo del terminal. Esta señal se emite cuando se presiona la combinación de teclas <ctrl+z> pausar el proceso
21 SIGTTIN El proceso en segundo plano lee la consola del terminal pausar el proceso
22 SIGTTOU Esta señal es similar a SIGTTIN y ocurre cuando el proceso en segundo plano quiere enviar datos al terminal. pausar el proceso
23 SIGURG Cuando hay datos urgentes en el socket, se envían algunas señales al proceso actualmente en ejecución para informar la llegada de datos urgentes. Si llegan datos fuera de banda de la red ignora esta señal
24 SIGXCPU Cuando el tiempo de ejecución del proceso excede el tiempo de CPU asignado al proceso, el sistema genera esta señal y la envía al proceso. Terminar el proceso
25 SIGXFSZ Se superó la configuración de longitud máxima de archivo Terminar el proceso
26 SIGVTALRM Esta señal se genera cuando el temporizador virtual expira. Similar a SIGALRM, pero esta señal solo cuenta el tiempo de CPU ocupado por el proceso Terminar el proceso
27 PROFESOR SGI Similar a SIGVTALRM, no solo incluye el tiempo de CPU ocupado por el proceso sino que también incluye el tiempo de ejecución de las llamadas al sistema. Terminar el proceso
28 CABRESTANTE Emitido cuando la ventana cambia de tamaño. ignora esta señal
29 SIGIO Esta señal indica al proceso que se ha emitido un evento IO asíncrono. ignora esta señal
30 SINGAPUR Cerrar ignora esta señal
31 SIGSYS Llamada al sistema no válida Finalice el proceso y genere el archivo principal.
34
~
64
SIGRTMIN
~
SIGRTMAX
Señales LINUX en tiempo real, no tienen significado fijo (pueden ser personalizadas por el usuario) Terminar el proceso

PD: Es necesario dominar las señales marcadas en rojo . Las señales SIGKILL y SIGSTOP no se pueden captar, bloquear ni ignorar, y solo se pueden realizar acciones predeterminadas .


5 acciones de procesamiento predeterminadas para señales

Ver detalles de la señal

man 7 signal

5 acciones de procesamiento predeterminadas para señales

  • Plazo : dar por terminado el proceso
  • Ign : El proceso actual ignora esta señal.
  • Núcleo : finaliza el proceso y genera un archivo central
  • Detener : suspender el proceso actual
  • Cont : Continuar con la ejecución del proceso actualmente suspendido

PD:
El archivo principal guarda información sobre la salida anormal del proceso.

#include <stdio.h>
#include <string.h>
int main() (
	// 没有指向合法内存
	char * buf;
	strcpy(buf,"hello");
	return 0;

Error de segmentación al compilar y generar archivos de destino

gcc core.c
./a.out

Insertar descripción de la imagen aquí
Generar archivo principal

ulimit -a
ulimit -c 1024
./a.out

Insertar descripción de la imagen aquí
Insertar descripción de la imagen aquí
Insertar descripción de la imagen aquí
Depuración de archivos principales

gdb a.out
# 进入GDB调试界面
(gdb) core-file core

Insertar descripción de la imagen aquí
Insertar descripción de la imagen aquí

Varios estados de señales.

  • producir
  • pendiente
  • entrega

matar, aumentar, abortar funciones

int kill(pid t pid, int sig);

  • 作用:给任何的进程或者进程组 pid,发送任何的信号 sig

  • 参数

    • pid
      • > 0:将信号发送给指定的进程
      • = 0:将信号发送给当前的进程组
      • = -1:将信号发送给每一个有权限接收这个信号的进程
      • <-1:这个pid=某个进程组的ID取反 (如:-12345)
    • sig : 需要发送的信号的编号或者是宏值,0表示不发送任何信号
  • 返回值:如果成功返回0;如果失败返回-1,并设置errno


int raise(int sig);

  • 作用:给当前进程发送信号

  • 参数sig : 需要发送的信号的编号或者是宏值,0表示不发送任何信号

  • 返回值:如果成功返回0;如果失败返回-1,并设置errno


void abort(void);

  • 作用:发送SIGABRT信号给当前的进程,杀死当前进程

代码示例

#include <stdio.h>
#include <sys/types.h>
#include <signal.h>
#include <unistd.h>
int main(){
    
    

    pid_t pid = fork();

    if (pid == 0)
    {
    
    
        //子进程
        for (int i = 0; i < 5; i++)
        {
    
    
            printf("child process\n");
            sleep(1);
        }
    }else if (pid > 0)
        {
    
    
            // 父进程
            printf("parent process\n");
            sleep(2);
            printf("kill child process now\n");
            kill(pid, SIGINT);
        }
    

    return 0;
}

Insertar descripción de la imagen aquí


alarm 函数

unsigned int alarm(unsigned int seconds);

  • 作用:设置定时器(闹钟) 。函数调用,开始倒计时,当倒计时为8的时候函数会给当前的进程发送一个信号: SIGALARM
  • 参数seconds:倒计时的时长,单位: 秒。如果参数为0,定时器无效(不进行倒计时,不发信号),如取消一个定时器:alam(0)
  • 返回值:如果之前没有计时器,返回0;如果之前有计时器返回之前计时器剩余的时间

SIGALARM : 默认终止当前的进程,每一个进程都有且只有唯一的一个定时器。
例子

alarm(10); //-> 返回0
//过了1秒
alarm(5); //-> 返回9

alam(100); //该函数不阻塞

代码示例

#include <stdio.h>
#include <unistd.h>
int main() {
    
    
    int seconds = alarm(5);
    printf("seconds = %d\n", seconds); // 0

    sleep(2);
    seconds = alarm(2); //不阻塞
    printf("seconds = %d\n", seconds);// 3
    //while(1){}第一个例子

	alarm(0);
    
    // 1s电脑能数多少数//第二个例子
    alarm(1);
    int i = 0;
    while (1)
    {
    
    
        printf("%i\n", i++);
    }

    return 0;
}

Insertar descripción de la imagen aquí
Insertar descripción de la imagen aquí
Insertar descripción de la imagen aquí

ps:其实1s内计算机真实计数远远大于 2.txt 中的数。实际的时间 = 内核时间 + 用户时间 + 消耗的时间。进行文件IO操作的时候比较浪费时间。定时器与进程的状态无关(自然定时法),无论进程处于什么状态,alarm() 都会计时。


setitimer 定时器函数

int setitimer(int which, const struct itimerval *new_ value, struct itimerval *old value);

  • 作用:设置定时器(闹钟)。可以替代alarm() ,精度微秒:us,可以实现周期性定时
  • 参数
    • which:定时器以什么时间计时
      • ITIMER_REAL:真实时间,时间到达,发送 SIGALRM(常用)
      • ITIMER_VIRTUAL:用户时间,时间到达,发送 SIGVTALRM
      • ITIMER_PROF:以该进程在用户态和内核态下所消耗的时间来计算,时间到达,发送
    • new_value:设置定时器的属性
    • old_value:记录上一次定时器的时间参数,一般不用,指定NULL
  • 返回值:如果成功返回0;如果失败返回-1,并设置errno
// 定时器的结构体
struct itimerval {
    
    
	// 每个阶段的时间,间隔时间
	struct timeval it interval;
	
	// 延迟多长时间执行定时器
	struct timeval it_value;
};

// 时间的结构体
struct timeval {
    
    
	// 秒数
	time_ttv_sec;
	
	// 微秒
	suseconds t tv_usec;
};

代码示例
过3秒以后,每隔2秒钟定时一次

#include <sys/time.h>
#include <stdio.h>
#include <stdlib.h>

int main(){
    
    

    struct itimerval new_value;

    // 设置间隔的时间
    new_value.it_interval.tv_sec = 2;
    new_value.it_interval.tv_usec = 0;

    // 设置延迟的时间,3秒后开始第一次定时
    new_value.it_value.tv_sec = 3;
    new_value.it_value .tv_usec = 0;
    int ret =setitimer(ITIMER_REAL, &new_value, NULL);// 非阻塞
    printf("定时器开始了...\n");
    if(ret == -1)
    {
    
    
        perror("setitimer");
        exit(0);    
    }

    getchar();
    return 0;
}

Insertar descripción de la imagen aquí


signal 信号捕捉函数

sighandler_t signal(int signum, sighandler_t handler);

  • 作用:设置某个信号的捕捉行为
  • 参数
    • signum:要捕捉的信号
    • handler:捕捉到信号要如何处理
      • SIG_IGN:忽略信号
      • SIG_DFL:使用信号默认的行为
      • 回调函数:这个函数是内核调用,程序员只负责写,捕捉到信号后如何去处理信号
  • 返回值:如果成功返回上一次注册的信号处理函数的地址,第一次调用返回NULL;如果失败返回SIG_ERR,设置errno

ps:SIGKILL、SIGSTOP不能被捕捉,不能被忽略。
回调函数需要程序员实现,提前准备好的,函数的类型根据实际需求,看函数指针的定义,不是程序员调用,而是当信号产生,由内核调用。函数指针是实现回调的函数实现之后,将函数名放到函数指针的位置就可以了。

代码示例

#include <sys/time.h>
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>

void myalarm(int num)
{
    
    
    printf("捕捉到了信号的编号是: %d\n", num);
    printf("xxxxxxx\n");
}

int main(){
    
    

    // 注册信号捕捉
    // signal(SIGALRM,SIG_IGN);
    // signa1(SIGALRM,SIG_DFL);
    // void (*sighandler_t)(int); 函数指针,int 类型的参数表示信号捕捉到的值

    __sighandler_t flag = signal(SIGALRM, myalarm);
    if (flag == SIG_ERR)
    {
    
    
        perror("signal");
        exit(0);
    }
    

    struct itimerval new_value;

    // 设置间隔的时间
    new_value.it_interval.tv_sec = 2;
    new_value.it_interval.tv_usec = 0;

    // 设置延迟的时间
    new_value.it_value.tv_sec = 3;
    new_value.it_value .tv_usec = 0;
    int ret =setitimer(ITIMER_REAL, &new_value, NULL);// 非阻塞
    printf("定时器开始了...\n");
    if(ret == -1)
    {
    
    
        perror("setitimer");
        exit(0);    
    }

    getchar();
    return 0;
}

Insertar descripción de la imagen aquí

信号集及相关函数

什么是信号集

许多信号相关的系统调用都需要能表示一组不同的信号,多个信号可使用一个称之为信号集的数据结构来表示,其系统数据类型为 sigset_t
在 PCB 中有两个非常重要的信号集。一个称之为“阻塞信号集”,另一个称之为“未决信号集”。这两个信号集都是内核使用位图机制来实现的。但操作系统不允许我们直接对这两个信号集进行位操作。而需自定义另外一个集合,借助信号集操作函数来对 PCB 中的这两个信号集进行修改


阻塞信号集和未决信号集

信号的“未决”是一种状态,指的是从信号的产生到信号被处理前的这一段时间

信号的“阻塞”是一个开关动作,指的是阻止信号被处理,但不是阻止信号产生
信号的阻塞就是让系统暂时保留信号留待以后发送。由于另外有办法让系统忽略信号所以一般情况下信号的阻塞只是暂时的,只是为了防止信号打断敏感的操作。

工作原理
Insertar descripción de la imagen aquí

  1. 用户通过键盘ctrl + C,产生2号信号SIGINT (信号被创建)

  2. 信号产生但是没有被处理 (未决)

    • 在内核中将所有的没有被处理的信号存储在一个集合中 (未决信号集)
    • SIGINT信号状态被存储在第二个标志位上
      • 这个标志位的值为0,说明信号不是未决状态
      • 这个标志位的值为1,说明信号处于未决状态
  3. 这个未决状态的信号,需要被处理,处理之前需要和另一个信号集(阻塞信号集),进行比较

    • 阻塞信号集默认不阻塞任何的信号
    • 如果想要阻塞某些信号需要用户调用系统的API
  4. 在处理的时候和阻塞信号集中的标志位进行查询,看是不是对该信号设置阻塞了

    • 如果没有阻塞,这个信号就被处理
    • 如果阻塞了,这个信号就继续处于未决状态,直到阻塞解除,这个信号就被处理

信号集相关函数

int sigemptyset(sigset_t *set);

  • 作用:清空信号集中的数据,将信号集中的所有的标志位置为0

  • 参数set:传出参数,需要操作的信号集

  • 返回值:成功返回0,失败返回-1


int sigfillset(sigset t *set);int sigaddset(sigset_t *set, int signum);

  • 作用:将信号集中所有的标志位设为1

  • 参数

    • set:传出参数,需要操作的信号集
    • signum: 需要设置阻塞的那个信号
  • 返回值:成功返回0,失败返回-1


int sigaddset(sigset_t *set, int signum);

  • 作用:设置信号集中的某一个信号对应的标志位为1,表示阻塞这个信号

  • 参数

    • set:传出参数,需要操作的信号集
    • signum: 需要设置阻塞的那个信号
  • 返回值:成功返回0;失败返回-1


int sigdelset(sigset t *set, int signum);

  • 作用:设置信号集中的某一个信号对应的标志位为0,表示不阻塞这个信号

  • 参数set:传出参数,需要操作的信号集

  • 返回值:成功返回0;失败返回-1


int sigismember(const sigset_t *set, int signum);

  • 作用:判断某个信号是否阻塞

  • 参数

    • set:需要操作的信号集
    • signum: 需要判断的那个信号
  • 返回值:如果成功返回1表示 signum被阻塞,返回0表示signum不阻塞;如果失败返回-1


代码示例

#define _DEFAULT_SOURCE
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>

int main(){
    
    

    // 创建一个信号集
    sigset_t set;

    // 清空信号集的内容
    sigemptyset(&set);

    // 判断 SIGINT 是否在信号集 set 里
    int ret = sigismember(&set, SIGINT);
    if (ret == 0)
    {
    
    
        printf("SIGINT 不阻塞\n");
    }else if (ret == 1)
    {
    
    
        printf("SIGINT 阻塞\n");
    }
    
    // 添加几个信号到信号集中
    sigaddset(&set, SIGINT);
    sigaddset(&set, SIGQUIT);

    // 判断SIGINT是否在信号集中
    ret = sigismember(&set, SIGINT);
    if(ret == 0) {
    
    
        printf("SIGINT 不阻塞\n");
    } else if(ret == 1) {
    
    
        printf("SIGINT 阻塞\n");
    }

    // 判断SIGQUIT是否在信号集中
    int jug = sigismember(&set, SIGQUIT);
    if(jug == 0){
    
    
        printf("SIGQUIT 不阻塞\n");
    } else if(ret == 1){
    
    
        printf("SIGQUIT 阻塞\n");
    }

    // 从信号集中删除一个信号
    sigdelset(&set, SIGQUIT);

    // 判断SIGQUIT是否在信号集中
    jug = sigismember(&set, SIGQUIT);
    if(jug == 0){
    
    
        printf("SIGQUIT 不阻塞\n");
    } else if(ret == 1){
    
    
        printf("SIGQUIT 阻塞\n");
    }


    return 0;
}

Insertar descripción de la imagen aquí


sigprocmask 函数使用

int sigprocmask(int how,const sigset_t *set, sigset_t *oldset);

  • 作用:将自定义信号集中的数据设置到内核中(设置阻塞,解除阻塞,替换)

  • 参数

    • how:如何对内核阻塞信号集进行处理
      • SIG_BLOCK:将用户设置的阻塞信号集添加到内核中,内核中原来的数据
        假设内核中默认的阻塞信号集是maskmask | set
      • SIG_UNBLOCK:根据用户设置的数据,对内核中的数据进行解除阻塞
        mask &= ~set
        SIG_SETMASK:覆盖内核中原来的值
    • set:已经初始化好的用户自定义的信号集
    • oldset:保存设置之前的内核中的阻塞信号集的状态,可以是 NULL
  • 返回值:如果成功返回0;如果失败返回-1,并设置错误号EFAULTEINVAL


int sigpending(sigset_t *set);

  • 作用:获取内核中的未决信号集

  • 参数set:传出参数,保存的是内核中的未决信号集中的信息。

  • 返回值:如果成功返回0;如果失败返回-1,并设置errno


代码示例
编写一个程序,把所有的常规信号(1-31)的未决状态打印到屏幕设置某些信号是阻塞的,通过键盘产生这些信号

#define _DEFAULT_SOURCE
#include <stdio.h>
#include <signal.h>
#include <stdlib.h>
#include <unistd.h>

int main()
{
    
    
    // 设置2、3号信号阻塞
    sigset_t set;
    sigemptyset(&set);

    // 将2号和3号信号添加到信号集中
    sigaddset(&set, SIGINT);
    sigaddset(&set, SIGQUIT);

    // 修改内核中的阻塞信号集
    sigprocmask(SIG_BLOCK, &set, NULL);

    int num = 0;
    while (1)
    {
    
    
        num++;
        // 获取当前的未决信号集的数据
        sigset_t pendingset;
        sigemptyset(&pendingset);
        sigpending(&pendingset);

        // 遍历前32位
        for (int i = 1; i <= 32; i++)
        {
    
    
            if (sigismember(&pendingset, i) == 1)
            {
    
    
                printf("1");
            }
            else if (sigismember(&pendingset, i) == 0)
            {
    
    
                printf("0");
            }
            else
            {
    
    
                perror("sigismember");
                exit(0);
            }
        }
        printf("\n");
        sleep(1);
        if (num == 10)
        {
    
    
            // 解除阻塞
            sigprocmask(SIG_UNBLOCK, &set, NULL);
        }
    }

    return 0;
}

Insertar descripción de la imagen aquí


sigaction 信号捕捉函数

int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);

  • 作用:检查或者改变信号的处理。信号捕捉

  • 参数

    • signum:需要捕捉的信号的编号或者宏值(信号的名称)
    • act:捕捉到信号之后的处理动作
    • oldact:上一次对信号捕捉相关的设置,一般不使用,传递NULL
  • 返回值:如果成功返回0;如果失败返回-1,并设置errno


sigaction 结构体

struct sigaction {
    
    
	//函数指针,指向的函数就是信号捕捉到之后的处理函数
	void (*sa_handler)(int);
	
	//不常用
	void (*sa_sigaction)(int, siginfo_t*, void *);
	
	//临时阻塞信号集,在信号捕捉函数执行过程中,临时阻塞某些信号。
	sigset_t sa_mask;
	
	//使用哪一个信号处理对捕捉到的信号进行处理
	
	//这个值可以是0,表示使用sa_handler,也可以是SA_SIGINFO表示sa_sigaction
	int sa_flags;
	// 已废弃
	void (*sa_restorer)(void);

代码示例

#define _DEFAULT_SOURCE
#include <sys/time.h>
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>

void myalarm(int num)
{
    
    
    printf("捕捉到了信号的编号是: %d\n", num);
    printf("xxxxxxx\n");
}

int main(){
    
    

    // 注册信号捕捉
    struct sigaction act;
    act.sa_flags = 0;
    act.sa_handler = myalarm;
    // 清空临时阻塞信号集
    sigemptyset(&act.sa_mask);

    sigaction(SIGALRM, &act, NULL);


    __sighandler_t flag = signal(SIGALRM, myalarm);
    if (flag == SIG_ERR)
    {
    
    
        perror("signal");
        exit(0);
    }
    

    struct itimerval new_value;

    // 设置间隔的时间
    new_value.it_interval.tv_sec = 2;
    new_value.it_interval.tv_usec = 0;

    // 设置延迟的时间
    new_value.it_value.tv_sec = 3;
    new_value.it_value .tv_usec = 0;
    int ret =setitimer(ITIMER_REAL, &new_value, NULL);// 非阻塞
    printf("定时器开始了...\n");
    if(ret == -1)
    {
    
    
        perror("setitimer");
        exit(0);    
    }

    getchar();
    return 0;
}

Insertar descripción de la imagen aquí


内核实现信号捕捉的过程

Insertar descripción de la imagen aquí


SIGCHLD 信号

SIGCHLD信号产生的条件

  • 子进程终止时
  • 子进程接收到SIGSTOP信号暂停时
  • 子进程处在停止态,接受到SIGCONT后唤醒时(继续运行)

ps:以上三种条件都会给父进程发送SIGCHLD信号,父进程默认会忽略该信号。可以使用SIGCHLD信号解决僵尸进程的问题。

代码示例

#define _DEFAULT_SOURCE
#include <stdio.h> 
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <signal.h>
#include <sys/wait.h>

void myFun(int num)
{
    
    
    printf("捕捉到的信号:%d\n", num); // 回收子进程PCB的资源
    // while(1) {
    
    
    // wait(NULL);
    // }
    while (1)
    {
    
    
        int ret = waitpid(-1, NULL, WNOHANG);
        if (ret > 0)
        {
    
    
            printf("child die , pid = %d\n", ret);
        }
        else if (ret == 0)
        {
    
    
            // 说明还有子进程或者
            break;
        }else if (ret == -1)
        {
    
    
            break;
        }
        
    }
}

int main() {
    
    

    // 提前设置好阻塞信号集,阻塞SIGCHLD,因为有可能子进程很快结束,父进程还没有注册完信号符
    sigset_t set;
    sigemptyset(&set);
    sigaddset(&set, SIGCHLD);
    sigprocmask(SIG_BLOCK, &set, NULL);

    //创建一些子进程
    pid_t pid;
    for(int i = 0; i < 20; i++) {
    
    
        pid = fork();
        if(pid == 0) {
    
    
            break;
        }
    }

    if(pid > 0) {
    
    
        //父进程

        //捕捉子进程死亡时发送的SIGCHLD信号
        struct sigaction act;
        act.sa_flags = 0;
        act.sa_handler = myFun;
        sigemptyset(&act.sa_mask);
        sigaction(SIGCHLD, &act, NULL);

        //注册完信号捕捉以后,解除阻塞
        sigprocmask(SIG_UNBLOCK, &set, NULL);

        while(1){
    
    
            printf("parent process pid : %d\n", getpid());
            sleep(2);
        }
    }
    else if(pid == 0){
    
    
        //子进程
        printf("child process pid : %d\n", getpid());
    }
	return 0;
}

Insertar descripción de la imagen aquí


共享内存

什么是共享内存

共享内存是一种用于多进程或多线程之间共享数据的机制,它允许不同的进程或线程在物理内存创建一个共享区域(段)。
由于一个共享内存段会称为一个进程用户空间的一部分,因此这种 IPC 机制无需内核介入。所有需要做的就是让一个进程将数据复制进共享内存中,并且这部分数据会对其他所有共享同一个段的进程可用。
与管道等要求发送进程将数据从用户空间的缓冲区复制进内核内存和接收进程将数据从内核内存复制进用户空间的缓冲区的做法相比,这种 IPC 技术的速度更快。


共享内存使用步骤

  1. 调用 shmget() 创建一个新共享内存段或取得一个既有共享内存段的标识符(即由其他进程创建的共享内存段)。这个调用将返回后续调用中需要用到的共享内存标识符。
  2. 使用 shmat() 来附上共享内存段,即使该段成为调用进程的虚拟内存的一部分。
  3. 此刻在程序中可以像对待其他可用内存那样对待这个共享内存段。为引用这块共享内存程序需要使用由 shmat() 调用返回的 addr,它是一个指向进程的虚拟地址空间中该共享内存段的起点的指针。
  4. 调用 shmdt() 来分离共享内存段。在这个调用之后,进程就无法再引用这块共享内存了。这一步是可选的,并且在进程终止时会自动完成这一步。
  5. 调用shmctl()来删除共享内存段。只有当当前所有附加内存段的进程都与之分离之后内存才会销毁。只有—个进程需要执行这一步

共享内存相关函数

int shmget(key_t key, size_t size, int shmflg);

  • 作用:创建一个新的共享内存段,或者获取一个既有的共享内存段的标识
    新创建的内存段中的数据都会被初始化为0

  • 参数

    • keykey_t 类型是一个整形,通过这个找到或者创建一个共享内存。
      一般使用16进制表示,非0值
    • size:共享内存的大小
    • shmflg:属性
      • 访问权限
      • 附加属性:创建/判断共享内存是不是存在
        • 创建:IPC_CREAT
        • 判断共享内存是否存在:IPC_EXCL,需要和 IPC_CREAT 一起使用 IPC_CREAT | IPC_EXCL | 0664
  • 返回值:如果成功>0返回共享内存的引用的ID(后面操作共享内存都是通过这个值);如果失败返回-1,并设置errno


void *shmat(int shmid, const void *shmaddr, int shmflg);

  • 作用:和当前的进程进行关联-参数

  • 参数

    • shmid:- shmid :共享内存的标识(ID),由shmget 返回值获取
    • shmaddr:申请的共享内存的起始地址,指定NULL,内核指定
    • shmflg:对共享内存的操作
      • 读:SHM_RDONLY,必须要有读权限
      • 读写:0
  • 返回值:如果成功>0返回共享内存的起始地址;如果失败返回(void*)-1,并设置errno
    int shmdt(const void *shmaddr);

  • 作用:解除当前进程和共享内存的关联

  • 参数shmaddr:共享内存的首地址

  • 返回值:如果成功返回0;如果失败返回-1


int shmctl(int shmid, int cmd, struct shmid_ds *buf);

  • 作用:对共享内存进行操作。删除共享内存,共享内存要删除才会消失,创建共
  • 参数
    • shmid:共享内存的ID
    • cmd:要做的操作
      • IPC_STAT:获取共享内存的当前的状态
      • IPC_SET:设置共享内存的状态
      • IPC_RMID:标记共享内存被销毁
    • buf:需要设置或者获取的共享内存的属性信息
      • IPC_STAT:buf存储数据
      • IPC_SET:buf中需要初始化数据,设置到内核中
    • IPC RMID:没有用,NULL
  • 返回值:如果成功返回0;如果失败返回-1

代码示例
write_shm.c

#include <stdio.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <string.h>

int main()
{
    
    

    // 1.创建一个共享内存
    int shmid = shmget(100, 4096, IPC_CREAT | 0664);
    
    // 2.和当前进程进行关联
    void *ptr = shmat(shmid, NULL, 0);

    char *str = "helloworld";

    // 3.写数据
    memcpy(ptr, str, strlen(str) + 1);
    printf("按任意键继续\n");
    getchar();

    // 4.解除关联
    shmdt(ptr);

    // 5.删除共享内存
    shmctl(shmid, IPC_RMID, NULL);

    return 0;
}

read_shm.c

#include <stdio.h>
#include <sys/ipc.h>
#include <sys/shm.h>

int main()
{
    
    

    // 1.获取一个共享内存
    int shmid = shmget(100, 0, IPC_CREAT);
    printf("shmid: %d\n", shmid);

    // 2.和当前进程进行关联
    void *ptr = shmat(shmid, NULL, 0);

    // 3.读数据
    printf("%s\n", (char *)ptr);

    printf("按任意键继续\n");
    getchar();

    // 4.解除关联
    shmdt(ptr);

    // 5.删除共享内存
    shmctl(shmid, IPC_RMID, NULL);

    return 0;
}

Insertar descripción de la imagen aquí
Insertar descripción de la imagen aquí


key_t ftok(const char *pathname, int proj_id);

  • 作用:根据指定的路径名,和 int 值,生成一个共享内存的key
  • 参数
    • pathname:指定一个存在的路径
    • proj_idint 类型的值,但是这系统调用只会使用其中的1个字节
      范围:0-255一般指定一个字符’a’
  • 返回值:是一个 key_t 类型的键,表示关联到指定文件和项目ID的唯一键。

共享内存操作命令

ipcs 用法

打印当前系统中所有的进程间通信方式的信息

ipcs -a

打印出使用共享内存进行进程间通信的信息

ipcs -m

打印出使用消息队列进行进程间通信的信息

ipcs -q

打印出使用信号进行进程间通信的信息

ipcs -s

ipcrm 用法

移除用shmkey创建的共享内存段

ipcrm -M shmkey

移除用shmid标识的共享内存段

ipcrm -m shmid

移除用msqkey创建的消息队列

ipcrm -Q msgkey

移除用msqid标识的消息队列

ipcrm -q msqid

移除用semkey创建的信号

ipcrm -s semkey

移除用semid标识的信号

ipcrm -s semid

共享内存相关问题

问题1:操作系统如何知道一块共享内存被多少个进程关联?
:共享内存维护了一个结构体struct shmid_ds这个结构体中有一个成员 shm nattach 记录了关联的进程个数


问题2:可不可以对共享内存进行多次删除 shmctl
:可以的。因为 shmctl 标记删除共享内存,不是直接删除。那什么时候真正删除呢?
当和共享内存关联的进程数为0的时候,就真正被删除。当共享内存的key为0的时候,表示共享内存被标记删除了。如果一个进程和共享内存取消关联,那么这个进程就不能继续操作这个共享内存。


共享内存和内存映射的区别

  1. 共享内存可以直接创建,内存映射需要磁盘文件(匿名映射除外)

  2. 共享内存效果更高

  3. 内存
    所有的进程操作的是同一块共享内存。
    内存映射,每个进程在自己的虚拟地址空间中有一个独立的内存。

  4. 数据安全

    • 进程突然退出:共享内存还存在内存映射区消失
    • 运行进程的电脑死机,宕机了:数据存在在共享内存中,没有了。内存映射区的数据﹐由于磁盘文件中的数据还在,所以内存映射区的数据还存在。
  5. 生命周期

    • 内存映射区:进程退出,内存映射区销毁
    • 共享内存:进程退出,共享内存还在,标记删除(所有的关联的进程数为0)
      如果一个进程退出,会自动和共享内存进行取消关联。

守护进程

什么是控制终端

在 UNIX 系统中,用户通过终端登录系统后得到一个 shell 进程,这个终端成为 shell 进程的控制终端(controlling Terminal),进程中,控制终端是保存在 PCB 中的信息,而 fork() 会复制 PCB 中的信息,因此由 shell 进程启动的其它进程的控制终端也是这个终端。

默认情况下(没有重定向),每个进程的标准输入、标准输出和标准错误输出都指向控制终端,进程从标准输入读也就是读用户的键盘输入,进程往标准输出或标准错误输出写也就是输出到显示器上。

在控制终端输入一些特殊的控制键可以给前台进程发信号,例如 Ctrl +C 会产生 SIGINT 信号, Ctrl +\ 会产生 SIGQUIT 信号。

Insertar descripción de la imagen aquí


什么是进程组

进行组由一个或多个共享同一进程组标识符(PGID)的进程组成。一个进程组拥有一个进程组首进程,该进程是创建该组的进程,其进程ID为该进程组的 ID,新进程会继承其父进程所属的进程组ID。

进程组拥有一个生命周期,其开始时间为首进程创建组的时刻,结束时间为最后一个成员进程退出组的时刻。一个进程可能会因为终止而退出进程组,也可能会因为加入了另外一个进程组而退出进程组。进程组首进程无需是最后一个离开进程组的成员。

进程组和会话在进程之间形成了一种两级层次关系:

  • 进程组是一组相关进程的集合。
  • 会话是一组相关进程组的集合。

进程组和会话是为支持 shell 作业控制而定义的抽象概念,用户通过 shell 能够交互式地在前台或后台运行命令。


什么是会话

会话是一组相关进程组的集合。会话首进程是创建该新会话的进程,其进程 ID 会成为会话 ID。新进程会继承其父进程的会话 ID。

一个会话中的所有进程共享单个控制终端。控制终端会在会话首进程首次打开一个终端设备时被建立。一个终端最多可能会成为一个会话的控制终端
在任一时刻,会话中的其中一个进程组会成为终端的前台进程组,其他进程组会成为后台进程组。只有前台进程组中的进程才能从控制终端中读取输入。当用户在控制终端中输入终端字符生成信号后,该信号会被发送到前台进程组中的所有成员。当控制终端的连接建立起来之后,会话首进程会成为该终端的控制进程


进程组、会话、控制终端之间的关系


关系示例
以下命令的作用是在根目录下搜索所有文件和目录,将标准错误输出(STDERR)重定向到 /dev/null 忽略任何错误消息,然后通过管道将搜索结果的行数(即文件和目录的数量)计数,并在后台运行这个任务,期间允许继续使用终端。

find / 2> / dev /null | wc -l &

以下命令的作用是将 longlist 中的内容按照字母顺序排序,然后统计每个唯一项出现的次数,并以 次数 唯一项 的格式输出。

sort < longlist | uniq -c

以上两组命令关系图
Insertar descripción de la imagen aquí


终端显示
Insertar descripción de la imagen aquí

ps:执行完第一组命令后输入 fg ,命令将回到前台运行,并且可以看到它的输出,也可以在需要时终止它。


进程组、会话操作函数

// 获取调用进程的进程组ID
pid_t getpgrp (void) ;

// 获取指定进程的进程组ID
pid_t getpgid(pid_t pid) ;

// 设置指定进程的进程组ID
int setpgid(pid_t pid, pid_t pgid) ;

// 获取指定进程的会话ID
pid_t getsid(pid_t pid) ;

// 创建一个新的会话,并返回其会话ID
pid_t setsid (void) ;

什么是守护进程

守护进程(Daemon Process) ,也就是通常说的 Daemon进程(精灵进程),是Linux中的后台服务进程。它是一个生存期较长的进程,通常独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件,虽然产生条件和孤儿进程类似但并不是孤儿进程。

守护进程一般采用以 d 结尾的名字。
Linux的大多数服务器就是用守护进程实现的。比如, Internet服务器 inetd,web服务器 httpd等。

守护进程特征

  • 生命周期很长,守护进程会在系统启动的时候被创建并一直运行直至系统被关闭。
  • 它在后台运行并且不拥有控制终端。没有控制终端确保了内核永远不会为守护进程自动生成任何控制信号以及终端相关的信号(如SIGINT、SIGQUIT)。

守护进程的创建步骤

  1. 执行一个 fork(),之后父进程退出,子进程继续执行。
  2. 子进程调用 setsid() 开启一个新会话。
  3. 清除进程的 umask 以确保当守护进程创建文件和目录时拥有所需的权限。
  4. 修改进程的当前工作目录,通常会改为根目录 /
  5. 关闭守护进程从其父进程继承而来的所有打开着的文件描述符。
  6. 在关闭了文件描述符0、1、2之后,守护进程通常会打开 /dev /null 并使用 dup2() 使所有这些描述符指向这个设备。
  7. 核心业务逻辑。

代码示例
写一个守护进程,每隔2s获取一下系统时间,将这个时间写入到磁盘文件中。

#define _DEFAULT_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
#include <sys/time.h>
#include <signal.h>
#include <time.h>
#include <string.h>
// 写一个守护进程,每隔2s获取一下系统时间,将这个时间写入到磁盘文件中。

void work(int num)
{
    
    
    // 捕捉到信号之后,获取系统时间,写入磁盘文件
    time_t tm = time(NULL);
    struct tm *loc = localtime(&tm);
    // char buf[1024];
    // sprintf(buf, "%d-%d-%d %d: %d : %d\n", loc->tm_year, loc->tm_mon, loc->tm_mday, loc->tm_hour, loc->tm_min, loc->tm_sec);
    // print("%s\n", buf);

    char* str = asctime(loc);
    int fd = open("time.txt", O_RDWR | O_CREAT | O_APPEND, 0664);
    write(fd, str, strlen(str));

    close(fd);
}

int main(){
    
    
    // 1.创建子进程,退出父进程
    pid_t pid = fork();
    if(pid > 0){
    
    
        exit(0);
    }

    // 2.将子进程重新创建一个会话
    setsid();

    // 3.设置编码
    umask(022);

    // 4.更改工作目录
    chdir("/home/zxz/");

    // 5.关闭、重定向文件描述符
    int fd = open("/dev/null", O_RDWR);
    dup2(fd, STDIN_FILENO);
    dup2(fd, STDOUT_FILENO);
    dup2(fd, STDERR_FILENO);

    // 6.业务逻辑

    // 6.1.捕捉定时信号
    struct sigaction act;
    act.sa_flags = 0;
    act.sa_handler = work;
    sigemptyset(&act.sa_mask);
    sigaction(SIGALRM, &act, NULL);

    struct itimerval val;
    val.it_value.tv_sec = 2;
    val.it_value.tv_usec = 0;
    val.it_interval.tv_sec = 2;
    val.it_interval.tv_usec = 0;

    // 6.2.创建定时器
    setitimer(ITIMER_REAL, &val, NULL);

    // 6.3.不让进程结束
    while (1)
    {
    
    
        sleep(10);
    }
    

    return 0;
}

Insertar descripción de la imagen aquí
Insertar descripción de la imagen aquí
vim进入time文件

vim time.txt

Insertar descripción de la imagen aquí
键入 :e 重新加载文件Insertar descripción de la imagen aquí


结语

Las notas del curso se derivan del proyecto de búsqueda de empleo de Niuke C++: Capítulo 2 Desarrollo multiproceso de Linux en https://www.nowcoder.com/courses/cover/live/504 . Dado que no es un curso de aprendizaje de procesos especializado, algunas de las explicaciones del profesor en este capítulo pueden no ser claras o completas. Lo estudié en combinación con "CSAPP". Practicar las preguntas anteriores a veces te iluminará. Además, úsalo más. para ver el documento API del sistema. Esta nota contiene adiciones y eliminaciones relacionadas con el contenido del curso. Es solo para revisión personal y solo como referencia si es necesario. Todos los códigos e imágenes anteriores provienen del curso y de los recursos en línea. Le invitamos a hacer preguntas y sugerencias. Las notas más completas se actualizarán en tiempo real. Para consolidar aún más el conocimiento relevante, las actualizaciones pueden ser más lentas. Disculpe su comprensión.
man

Supongo que te gusta

Origin blog.csdn.net/qq_53099212/article/details/132551062
Recomendado
Clasificación