Compartir tecnología

Servidor reactor de alta concurrencia [medio]

2024-07-12

한어Русский языкEnglishFrançaisIndonesianSanskrit日本語DeutschPortuguêsΕλληνικάespañolItalianoSuomalainenLatina

4. Control de procesos y sincronización de procesos.

1. Señal

1.1 Conceptos básicos de señales.

La señal (señal) es una interrupción de software. Es un método para transmitir mensajes entre procesos. Se utiliza para notificar al proceso que ha ocurrido un evento, pero no puede pasar ningún dato al proceso.

Hay muchas razones por las que se generan señales en Shell.killykillallComando para enviar señal:

kill -信号的类型 进程编号
killall -信号的类型 进程名
  • 1
  • 2

1.2 Tipos de señales

nombre de la señalvalor de la señalAcción de procesamiento predeterminadaRazón de la señalización
SUSPIRO1AEl terminal se cuelga o el proceso de control finaliza
SEÑAL2AInterrupción del teclado Ctrl+c
SALIRSE3CSe presiona la tecla Escape del teclado.
SIGIL4Cinstrucción ilegal
Trampa de señales5Cinstrucciones de punto de interrupción
SIGABRT6CSeñal de aborto emitida por aborto(3)
SIGBUS7Cerror de bus
SIGFPE8Cexcepción de punto flotante
Matanza de señales9Akill -9 mata el proceso, esta señal no se puede captar ni ignorar
SIGUSR110ASeñal definida por el usuario 1
SIGSEGV11CReferencia de memoria no válida (matriz fuera de límites, operación de puntero nulo)
SIGUSR212ASeñal definida por el usuario 2
SIGPIPE13AEscribir datos en una tubería sin un proceso de lectura.
Señal14ASeñal de despertador, señal enviada por la función alarm()
TÉRMINO DE SEGURIDAD15ASeñal de terminación, la señal enviada por defecto.
Señal de advertencia16Aerror de pila
SEGURIDAD17BEmitido cuando finaliza el proceso hijo.
Control de señal18DReanudar un proceso detenido
PARADA DE SEÑAL19DDetener proceso
SIGTSTP20DPulsar la tecla de parada del terminal
SIGNIFICADO21DSolicitudes de proceso en segundo plano para leer el terminal
Señal22DEl proceso en segundo plano solicita escribir en la terminal.
Sigurg23BDetección de condiciones de emergencia (enchufes)
CPU SIGX24CSe superó el límite de tiempo de CPU
SIGXFSZ25CExcede el límite de tamaño de archivo
SEÑAL DE SEGURIDAD26Aseñal de reloj virtual
SIGPROF27AAnalizar señales de reloj.
CABRESTANTE DE SEÑAL28BCambios de tamaño de ventana
ENCUESTA DE SIG29BSondeo (Sistema V)
SIGPWR30Afalla electrica
SIGSYS31CLlamada ilegal al sistema

La acción predeterminada de A es terminar el proceso.

La acción predeterminada de B es ignorar esta señal.

La acción predeterminada de C es finalizar el proceso y realizar un volcado de la imagen del kernel.

La acción predeterminada de D es detener el proceso y el programa que ingresa al estado detenido puede continuar ejecutándose.

1.3 Procesamiento de señales

Hay tres formas en que los procesos manejan señales:

  1. La operación predeterminada del sistema se utiliza para manejar esta señal. La operación predeterminada de la mayoría de las señales es terminar el proceso.
  2. Configure la función de procesamiento de interrupciones. Después de recibir la señal, la función la manejará.
  3. Ignora una señal y no hagas nada con ella como si nunca hubiera sucedido.

signal()Las funciones pueden establecer cómo el programa maneja las señales.

Declaración de función:

#include <signal.h>

typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
  • 1
  • 2
  • 3
  • 4

Descripción de parámetros:

  • sig:Especifique la señal a capturar.
  • func : Puntero a la función de procesamiento de señales. La función de procesamiento necesita recibir un parámetro entero, que es el número de señal capturada.
  1. SIG_DFL La macro :SIG_DFL representa el método de procesamiento de señales predeterminado.usarSIG_DFLcomosignalEl segundo parámetro de la función indica que se utiliza el método de procesamiento predeterminado del sistema para la señal.
  2. SIG_IGN La macro :SIG_IGN significa ignorar la señal.usarSIG_IGNcomosignal El segundo parámetro de la función indica que cuando el proceso reciba la señal, la ignorará y no realizará ningún procesamiento. Esto puede evitar que los procesos finalicen o se interrumpan inesperadamente en determinadas circunstancias.
  3. SIG_ERR:SIG_ERR Las macros se utilizan para indicar errores.no es comosignalEl segundo parámetro de la función se utiliza en su lugar comosignal El valor de retorno de la función indica que la llamada falló.sisignalSi la llamada a la función falla, devolveráSIG_ERR .Normalmente se utiliza para detectar y procesarsignalError en llamada a función.

imagen-20240709113614147

imagen-20240709113230874

imagen-20240709113240944

1.4 ¿Para qué sirven las señales?

El programa de servicio se ejecuta en segundo plano. Si desea detenerlo, no es una buena idea matarlo, porque cuando el proceso finaliza, muere repentinamente y no se realizan trabajos posteriores.

Si envía una señal al programa de servicio, después de recibir la señal, el programa de servicio llama a una función y escribe el código posterior en la función, y el programa puede salir de forma planificada.

Enviar una señal de 0 al programa de servicio puede detectar si el programa está activo.

imagen-20240709135336848

1.5 Envío de señales

El sistema operativo Linux proporciona kill ykillall El comando envía una señal al programa. En el programa, puedes usar.kill() Las funciones de la biblioteca envían señales a otros procesos.

Declaración de función:

int kill(pid_t pid, int sig);
  • 1

kill() La función toma los parámetros.sig La señal especificada se pasa al parámetro.pid proceso especificado.

parámetro pid Hay varias situaciones:

  1. pid > 0 Pasar la señal al proceso comopid proceso.
  2. pid = 0 Pasar la señal a todos los procesos en el mismo grupo de procesos que el proceso actual. A menudo lo utiliza el proceso padre para enviar señales al proceso hijo. Tenga en cuenta que este comportamiento depende de la implementación del sistema.
  3. pid < -1 Pasar la señal al ID del grupo de proceso de|pid| de todos los procesos.
  4. pid = -1 Pasa la señal a todos los procesos que tienen permiso para enviar la señal, pero no al proceso que envió la señal.

2. Terminación del proceso

Existen 8 formas de finalizar un proceso, 5 de las cuales son terminaciones normales, son:

  1. existir main() Para funcionesreturn devolver;
  2. Llamado en cualquier función. exit() función;
  3. Llamado en cualquier función. _exit() o_Exit() función;
  4. El último hilo comienza desde su rutina de inicio (función principal del hilo) con return devolver;
  5. Llamado en el último hilo. pthread_exit() devolver;

Hay tres formas de terminar de forma anormal, son:

  1. transferir abort() aborto de función;
  2. Se recibe una señal;
  3. El último hilo responde a la solicitud de cancelación.

2.1 Estado de terminación del proceso

existir main() En la función,return El valor devuelto es el estado de terminación, si noreturn declaración o llamadaexit(), entonces el estado de terminación del proceso es 0.

En el shell, vea el estado de terminación del proceso:

echo $?
  • 1

3 funciones para finalizar el proceso normalmente (exit() y_Exit() está especificado por ISO C,_exit() está especificado por POSIX):

void exit(int status);
void _exit(int status);
void _Exit(int status);
  • 1
  • 2
  • 3

status El estado de terminación del proceso.

imagen-20240709143530327

imagen-20240709143615950

2.2 Problema de liberación de recursos

  • return Indica que cuando la función regrese, se llamará al destructor del objeto local.main() en funciónreturn También se llama al destructor del objeto global.
  • exit() Indica que para finalizar el proceso, no se llamará al destructor del objeto local, solo se llamará al destructor del objeto global.
  • _exit() y_Exit() Salga directamente y no se realizará ningún trabajo de limpieza.

2.3 Función de terminación del proceso

El proceso está disponible. atexit() El registro de funciones finaliza las funciones (hasta 32), estas funciones seexit() Llamado automáticamente.

int atexit(void (*function)(void));
  • 1

exit() El orden en que se invocan las funciones de terminación se invierte desde el momento del registro.

imagen-20240709143824286

imagen-20240709143830549

3. Llame al programa ejecutable.

3.1 función del sistema ()

system()La función proporciona un método simple para ejecutar el programa, pasando el programa y los parámetros que deben ejecutarse como una cadena.system()Simplemente funciona.

Declaración de función:

int system(const char * string);
  • 1

system()El valor de retorno de la función es más problemático.

  1. Si el programa ejecutado no existe,system()La función devuelve un valor distinto de cero;
  2. Si la ejecución del programa es exitosa y el estado de ejecución del programa ejecutado es 0,system()La función devuelve 0;
  3. Si la ejecución del programa es exitosa y el estado de terminación del programa ejecutado no es 0,system()La función devuelve un valor distinto de cero.

3.2 familia de funciones ejecutivas

execLas familias de funciones proporcionan otra forma de llamar programas (binarios o scripts de shell) dentro de un proceso.

execLa familia de funciones se declara de la siguiente manera:

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[]);
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

Aviso

  1. Si la ejecución del programa falla, se devuelve -1 directamente y el motivo del error se almacena enerrnomedio.
  2. El número de proceso del nuevo proceso es el mismo que el del proceso original, pero el nuevo proceso reemplaza el segmento de código, el segmento de datos y la pila del proceso original.
  3. Si la ejecución es exitosa, la función no regresará cuando se llame exitosamente en el programa principal.execDespués de eso, el programa llamado reemplazará al programa que llama, es decir,execNo se ejecutará ningún código después de la función.
  4. En el desarrollo real, el más utilizado esexecl()yexecv(), otros rara vez se utilizan.

4. Crea un proceso

4.1 Procesos Linux 0, 1 y 2

Todos los procesos en todo el sistema Linux están en una estructura de árbol.

  • **El proceso No. 0 (proceso del sistema)** es el antepasado de todos los procesos. Creó los procesos No. 1 y No. 2.
  • **El proceso número 1 (systemd)** es responsable de realizar la inicialización del kernel y la configuración del sistema.
  • **El proceso número 2 (kthreadd)** es responsable de la programación y gestión de todos los subprocesos del kernel.

usarpstreePuede ver el árbol de procesos con el comando:

pstree -p 进程编号
  • 1

4.2 Identificación del proceso

Cada proceso tiene un ID de proceso único representado por un número entero no negativo. Aunque son únicos, los ID de proceso se pueden reutilizar. Cuando un proceso finaliza, su ID de proceso se convierte en candidato para su reutilización. Linux utiliza un algoritmo de reutilización retrasada para que la ID de un proceso recién creado sea diferente de la ID utilizada por el proceso finalizado recientemente. Esto evita que los procesos nuevos se confundan con un proceso terminado que utiliza el mismo ID.

Función para obtener ID del proceso:

pid_t getpid(void);    // 获取当前进程的ID。
pid_t getppid(void);   // 获取父进程的ID。
  • 1
  • 2

4.3 función bifurcación()

Un proceso existente puede llamarfork()La función crea un nuevo proceso.

Declaración de función:

pid_t fork(void);
  • 1

Depender defork()El nuevo proceso creado se denomina proceso hijo.

fork() La función se llama una vez pero regresa dos veces. La diferencia entre los dos retornos es que el valor de retorno del proceso hijo es 0, mientras que el valor de retorno del proceso padre es el ID del proceso hijo recién creado.

El proceso hijo y el proceso padre continúan ejecutándose.fork()El código después de eso, El proceso hijo es una copia del proceso padre. El proceso hijo tiene una copia del espacio de datos, el montón y la pila del proceso padre (nota: el proceso hijo posee una copia, no la comparte con el proceso padre).

fork()Después de eso, el orden de ejecución de los procesos padre e hijo no está definido.

imagen-20240709221535371

imagen-20240709221546617

4.4 Dos usos de fork()

  1. El proceso padre quiere copiarse a sí mismo, y luego el proceso padre y el proceso hijo ejecutan código diferente.Este uso es muy común en los programas de servicios de red. El proceso principal espera la solicitud de conexión del cliente. Cuando llega la solicitud, el proceso principal llama.fork(), deje que el proceso hijo maneje estas solicitudes, mientras el proceso padre continúa esperando la siguiente solicitud de conexión.
  2. El proceso quiere ejecutar otro programa.Este uso es muy común en shells, el proceso hijo comienza desdefork()Llamado inmediatamente después del regreso.exec

4.5 Archivos compartidos

fork()Una característica es que los descriptores de archivos abiertos en el proceso principal se copiarán al proceso secundario, y el proceso principal y el proceso secundario comparten el mismo desplazamiento de archivo.

Si un proceso padre y un proceso hijo escriben en un archivo señalado por el mismo descriptor sin ningún tipo de sincronización, sus resultados pueden mezclarse entre sí.

imagen-20240709222929369

imagen-20240709222803641

En este punto puedes ver que sólo hay 100.000 filas de datos.

imagen-20240709222853769

imagen-20240709223236254

En este momento, debería haber 200.000 filas de datos. Una fila menos puede deberse a que la operación de escritura del archivo no es atómica. Sin un mecanismo de sincronización, dos procesos pueden intentar escribir diferentes partes del archivo al mismo tiempo. provocando que la escritura falle.

4.6 función vfork()

vfork()Las llamadas a funciones y los valores de retorno son los mismos quefork()Lo mismo, pero su semántica es diferente.

vfork()La función se utiliza para crear un nuevo proceso cuyo propósito esexecUn nuevo programa que no copia el espacio de direcciones del proceso padre porque el proceso hijo llama inmediatamenteexec , por lo que no se utilizará el espacio de direcciones del proceso principal. Si el proceso hijo utiliza el espacio de direcciones del proceso padre, pueden producirse resultados desconocidos.

vfork()yfork()Otra diferencia es:vfork()Asegúrese de que el proceso hijo se ejecute primero y llámelo en el proceso hijoexecoexitLuego, el proceso principal reanuda la operación.

5. Proceso zombi

En el sistema operativo, un proceso zombie se refiere a un proceso hijo que ha finalizado pero su proceso padre aún no ha leído su estado de salida. Aunque el proceso zombie ya no se está ejecutando, todavía ocupa una entrada en la tabla de procesos para que el kernel pueda guardar la información del estado de salida del proceso (como ID del proceso, estado de salida, etc.) hasta que el proceso principal lea esta información.

5.1 Causas de los procesos zombies

Si el proceso principal sale antes que el proceso secundario, el proceso secundario será alojado por el proceso 1 (esta también es una forma de permitir que el proceso se ejecute en segundo plano).

Si el proceso hijo sale antes que el proceso padre y el proceso padre no procesa la información de salida del proceso hijo, entonces el proceso hijo se convertirá en un proceso zombie.

5.2 El daño de los procesos zombies

El kernel conserva una estructura de datos para cada proceso hijo, incluido el número de proceso, el estado de terminación, el tiempo de CPU utilizado, etc. Si el proceso principal procesa la información de salida del proceso secundario, el kernel liberará esta estructura de datos. Si el proceso principal no procesa la información de salida del proceso secundario, el kernel no liberará esta estructura de datos y el número de proceso del proceso secundario siempre estará ocupado. Los números de proceso disponibles en el sistema son limitados. Si se genera una gran cantidad de procesos zombies, el sistema no podrá generar nuevos procesos porque no hay números de proceso disponibles.

5.3 Métodos para evitar procesos zombies

  1. Manejo de la señal SIGCHLD : Cuando el proceso hijo sale, el kernel enviará la señal SIGCHLD al proceso padre.Si el proceso padre usasignal(SIGCHLD, SIG_IGN)Notifique al kernel que no está interesado en la salida del proceso hijo y que su estructura de datos se liberará inmediatamente después de que salga el proceso hijo.
  2. usarwait()/waitpid()función: El proceso padre espera a que finalice el proceso hijo llamando a estas funciones y obtiene su estado de salida, liberando así los recursos ocupados por el proceso hijo.
pid_t wait(int *stat_loc); 
pid_t waitpid(pid_t pid, int *stat_loc, int options); 
pid_t wait3(int *status, int options, struct rusage *rusage); 
pid_t wait4(pid_t pid, int *status, int options, struct rusage *rusage);
  • 1
  • 2
  • 3
  • 4

El valor de retorno es el número del proceso hijo.

stat_loc ¿La información sobre la terminación del proceso hijo es:

a) Si termina normalmente, la macro WIFEXITED(stat_loc) Devuelve verdadero, macroWEXITSTATUS(stat_loc) Se puede obtener el estado de terminación;

b) Si finaliza de forma anormal, la macro WTERMSIG(stat_loc) Recibe la señal para finalizar el proceso.

imagen-20240709230911352

imagen-20240709231034423

imagen-20240709231050581

imagen-20240709231124375

imagen-20240709231140813

Si el proceso principal está ocupado, puede capturar SIGCHLD Señal, llamada en la función de procesamiento de señal.wait()/waitpid()

imagen-20240709231439475

imagen-20240709231422927

6.Múltiples procesos y señales.

[Enviar señales entre procesos](##1.5 Enviar señales)

En un programa de servicio multiproceso, si el subproceso recibe una señal de salida, el subproceso saldrá por sí solo.

Si el proceso principal recibe una señal de salida, debe enviar señales de salida a todos los procesos secundarios y luego salir él mismo.

imagen-20240711222919564

imagen-20240711222900141

imagen-20240711223111481

7. Memoria compartida

Los subprocesos múltiples comparten el espacio de direcciones de un proceso,Si varios subprocesos necesitan acceder a la misma memoria, simplemente use variables globales.

En múltiples procesos, el espacio de direcciones de cada proceso es independiente y no compartido.Si varios procesos necesitan acceder a la misma memoria, no se pueden usar variables globales, solo se puede usar memoria compartida.

La memoria compartida permite que múltiples procesos (no se requiere ningún parentesco consanguíneo entre procesos) accedan al mismo espacio de memoria. Es la forma más efectiva de compartir y transferir datos entre múltiples procesos. Los procesos pueden conectar la memoria compartida a su propio espacio de direcciones. Si un proceso modifica los datos en la memoria compartida, los datos leídos por otros procesos también cambiarán.

La memoria compartida no proporciona un mecanismo de bloqueo, es decir, cuando un proceso lee/escribe en la memoria compartida, no impide que otros procesos la lean/escriban.Si desea bloquear la lectura/escritura de la memoria compartida, puede utilizar un semáforo . Linux proporciona un conjunto de funciones para operar la memoria compartida.

7.1 función shmget

Esta función se utiliza para crear/adquirir memoria compartida.

 int shmget(key_t key, size_t size, int shmflg);
  • 1
  • llave El valor clave de la memoria compartida es un número entero (typedef unsigned int key_t), generalmente en hexadecimal, por ejemplo 0x5005, las claves de diferentes recuerdos compartidos no pueden ser las mismas.
  • tamaño Tamaño de la memoria compartida, en bytes.
  • shmflg-es-es Los permisos de acceso a la memoria compartida son los mismos que los permisos para archivos, por ejemplo0666|IPC_CREAT Indica que si la memoria compartida no existe, crearla.
  • valor de retorno: Devuelve la identificación de la memoria compartida (un número entero mayor que 0) en caso de éxito, -1 en caso de error (el sistema no tiene memoria suficiente ni permisos).

imagen-20240711224223200

imagen-20240711224212293

usar ipcs -m Puede ver la memoria compartida del sistema, incluida: clave, ID de memoria compartida (shmid), propietario, permisos (perms) y tamaño (bytes).

usar ipcrm -m 共享内存id La memoria compartida se puede eliminar manualmente de la siguiente manera:

imagen-20240711225202860

Nota: Los contenedores no se pueden usar para tipos de datos en la memoria compartida, solo se pueden usar tipos de datos básicos.

7.2 función shmat

Esta función se utiliza para conectar la memoria compartida al espacio de direcciones del proceso actual.

void *shmat(int shmid, const void *shmaddr, int shmflg);
  • 1
  • tímido Depender deshmget() El identificador de memoria compartida devuelto por la función.
  • sátira Especifique la ubicación de la dirección donde la memoria compartida está conectada al proceso actual. Por lo general, complete 0 para permitir que el sistema seleccione la dirección de la memoria compartida.
  • shmflg-es-es Bit de bandera, generalmente lleno con 0.

Devuelve la dirección inicial de la memoria compartida cuando la llamada es exitosa y regresa cuando falla. (void *)-1

7.3 función shmdt

Esta función se utiliza para desconectar la memoria compartida del proceso actual, lo que equivale a shmat() La operación inversa de una función.

int shmdt(const void *shmaddr);
  • 1
  • sátira shmat() La dirección devuelta por la función.

Devuelve 0 si la llamada tiene éxito y -1 si falla.

7.4 función shmctl

Esta función se utiliza para operar la memoria compartida. La operación más utilizada es eliminar la memoria compartida.

int shmctl(int shmid, int command, struct shmid_ds *buf);
  • 1
  • tímido shmget() La identificación de la memoria compartida devuelta por la función.
  • dominio Instrucciones para operar la memoria compartida Si desea eliminar la memoria compartida, complete.IPC_RMID
  • buf La dirección de la estructura de datos que opera la memoria compartida. Si desea eliminar la memoria compartida, complete 0.

Devuelve 0 si la llamada tiene éxito y -1 si falla.

Nota, uso root Los usuarios normales no pueden eliminar la memoria compartida creada, independientemente de los permisos creados.

imagen-20240711230653886

imagen-20240711230522921

7.5 Cola circular

7.6 Cola circular basada en memoria compartida

Devuelve 0 si la llamada tiene éxito y -1 si falla.

7.4 función shmctl

Esta función se utiliza para operar la memoria compartida. La operación más utilizada es eliminar la memoria compartida.

int shmctl(int shmid, int command, struct shmid_ds *buf);
  • 1
  • tímido shmget() La identificación de la memoria compartida devuelta por la función.
  • dominio Instrucciones para operar la memoria compartida Si desea eliminar la memoria compartida, complete.IPC_RMID
  • buf La dirección de la estructura de datos que opera la memoria compartida. Si desea eliminar la memoria compartida, complete 0.

Devuelve 0 si la llamada tiene éxito y -1 si falla.

Nota, uso root Los usuarios normales no pueden eliminar la memoria compartida creada, independientemente de los permisos creados.

[Las imágenes del enlace externo se están transfiriendo...(img-v6qW3XRA-1720711279572)]

[Las imágenes del enlace externo se están transfiriendo...(img-CG0tGAne-1720711279572)]La transferencia de la imagen del enlace externo falló. El sitio de origen puede tener un mecanismo anti-leeching. Se recomienda guardar la imagen y cargarla directamente.
La transferencia de la imagen del enlace externo falló. El sitio de origen puede tener un mecanismo anti-leeching. Se recomienda guardar la imagen y cargarla directamente.
La transferencia de la imagen del enlace externo falló. El sitio de origen puede tener un mecanismo anti-leeching. Se recomienda guardar la imagen y cargarla directamente.

7.5 Cola circular

7.6 Cola circular basada en memoria compartida