[Desarrollo de Linux: programación multiproceso]

[Desarrollo de Linux: programación multiproceso]

  • Antes, clasifiqué un poco la programación de sockets para la programación de redes de Linux , pero si desea implementar un lado del servidor real, el socket por sí solo no es suficiente. También necesita saber qué se requiere para crear un servicio web real .
  • Un proceso es la unidad más pequeña programada y asignada por el sistema operativo .

Prefacio

Podemos construir un servidor que atienda desde el primer cliente hasta el centésimo cliente en secuencia. Por supuesto, el primer cliente no se quejará del lado del servidor, pero si el tiempo promedio de servicio por cliente es de 0,5 segundos, el cliente número 100 tendrá una insatisfacción considerable con el lado del servidor.

1. Dos tipos de servidores

Si realmente piensa en sus clientes, debería elevar el estándar promedio de satisfacción del cliente. Si tienes el siguiente tipo de servidor, deberías estar satisfecho, ¿verdad? ? ?
"¡El tiempo de procesamiento para la primera solicitud de conexión es de 0 segundos, el tiempo de procesamiento para la solicitud de conexión número 50 es de 50 segundos y el tiempo de procesamiento para la solicitud de conexión número 100 es de 100 segundos! Pero mientras se acepte, el servicio solo toma 1 segundo."

Si el número de solicitudes principales pudiera contarse con los dedos de una mano, el cliente seguramente estaría satisfecho con el servidor. Pero tan pronto como se supere este número, el cliente empezará a quejarse. Es mejor proporcionar servicios de la siguiente manera.
"El tiempo de procesamiento de todas las solicitudes de conexión no excede 1 segundo, pero el tiempo promedio de servicio es de 2 a 3 segundos".

2. Método de implementación del servidor concurrente:

Incluso si es posible extender el tiempo de servicio, es necesario mejorar el lado del servidor para que pueda brindar servicios a todos los clientes que inician solicitudes al mismo tiempo para aumentar la satisfacción promedio.
Además, el tiempo de comunicación de datos en los programas de red representa una proporción mayor que el tiempo de cálculo de la CPU, por lo que proporcionar servicios a múltiples clientes es una forma eficiente de utilizar la CPU. A continuación, analizamos los servicios concurrentes del lado del servidor que atienden a varios clientes simultáneamente. A continuación se enumeran modelos y métodos representativos de implementación concurrente del lado del servidor.

  • 1Servidor multiproceso : Proporciona servicios mediante la creación de múltiples procesos.
  • 2 Servidor de multiplexación : proporciona servicios agrupando y administrando de manera uniforme objetos de E/S.
  • 3Servidor multiproceso : proporciona servicios generando la misma cantidad de subprocesos que el cliente.

Aquí nos centramos en los servidores multiproceso .

1. Comprensión y aplicación

1. Comprensión del proceso

  • Proceso: " Un programa en ejecución que ocupa espacio en la memoria ", como Plants vs. Zombies. Si se ejecutan varios programas del juego Plants vs. Zombies al mismo tiempo, se generará el número correspondiente de procesos y el espacio de memoria del número correspondiente. de procesos también estarán ocupados.
  • Desde la perspectiva del sistema operativo, un proceso es la unidad básica del flujo del programa . Si se crean varios procesos, el sistema operativo se ejecutará al mismo tiempo. A veces se generarán múltiples procesos durante la ejecución de un programa . El servidor multiproceso que se creará a continuación es uno de los representantes. Antes de escribir el lado del servidor, primero comprenda cómo crear un proceso a través de un programa.

2. Número de núcleos de CPU y número de procesos

  • Una CPU con 2 dispositivos informáticos se denomina CPU de doble núcleo y una CPU con 4 dispositivos informáticos se denomina CPU de cuatro núcleos. En otras palabras, una CPU puede contener múltiples dispositivos informáticos (núcleos). La cantidad de núcleos es la misma que la cantidad de procesos que pueden ejecutarse simultáneamente. Por el contrario, si la cantidad de procesos excede la cantidad de núcleos, los procesos utilizarán los recursos de la CPU en tiempo compartido. Pero como la CPU funciona tan rápido, sentimos que todos los procesos se ejecutan al mismo tiempo. Por supuesto, cuantos más núcleos haya, más evidente será este sentimiento.

3. Identificación del proceso

  • A todos los procesos se les asigna una ID del sistema operativo , independientemente de cómo se creen . Este ID se denomina "ID de proceso" y su valor es un número entero mayor que 2. 1 debe asignarse al primer proceso después de iniciar el sistema operativo (se utiliza para ayudar al sistema operativo), por lo que el proceso de usuario no puede obtener el valor de ID 1 . Puede ver todos los procesos actualmente en ejecución
    a través del comando. ps auEs importante tener en cuenta que este comando también puede enumerar el PID (ID de proceso). Todos los detalles del proceso se pueden enumerar especificando los parámetros ay u.

4. Creación de procesos

  • Hay muchas formas de crear un proceso. Aquí presentamos la función fork utilizada para crear un servidor multiproceso.
#include <unistd.h>
//→成功时返回进程 ID,失败时返回-1。
pid_t fork(void);
  • La función fork crea una copia del proceso de llamada. Es decir, en lugar de crear un proceso a partir de un programa completamente diferente, copia el proceso en ejecución que llama a la función fork. Además, ambos procesos ejecutarán las declaraciones después de la llamada a la función fork (después de que la función fork regrese, para ser precisos). Sin embargo, debido a que el mismo espacio de memoria se copia mediante el mismo proceso, los flujos de programa posteriores deben distinguirse en función del valor de retorno de la función fork. Es decir, las siguientes características de la función fork se utilizan para distinguir el flujo de ejecución del programa.
    • Proceso principal: la función fork devuelve el ID del proceso secundario.
    • Proceso hijo: la función fork devuelve 0.
    • Aquí "Proceso principal" se refiere al proceso original, es decir, el sujeto que llama a la función de bifurcación, y "Proceso hijo" (Proceso hijo) es el proceso copiado por el proceso principal que llama a la función de bifurcación.

5. El proceso de ejecución del programa después de llamar a la función fork:

Insertar descripción de la imagen aquí

  • Si pid es 0, significa que el proceso hijo se inició. Si es mayor que 0, significa el proceso padre. Si es menor que 0, significa falla. Después de llamar a la función fork, los procesos padre e hijo
    tienen estructuras de memoria completamente independientes.

2. Proceso zombi

1. Definición

En las operaciones con archivos, cerrar un archivo es tan importante como abrirlo. De manera similar, la destrucción de procesos es tan importante como la creación de procesos . Si la destrucción de procesos no se toma en serio, se convertirán en procesos zombis y plagarán a todos.

Lo mismo ocurre en el mundo de los procesos. Los procesos deben destruirse después de completar su trabajo (después de ejecutar el programa en la función principal), pero a veces estos procesos se convertirán en procesos zombies y ocuparán recursos importantes en el sistema . Los procesos en este estado se denominan "procesos zombis", que es una de las razones por las que supone una carga para el sistema.

2. Causas y soluciones a los procesos zombies

  • Primero, utilice los dos ejemplos siguientes para mostrar cómo finalizar el proceso hijo generado al llamar a la función fork.
    1 Pase los parámetros y llame a la función de salida.
    2. Ejecute la instrucción return en la función principal y devuelva el valor.
    Los valores de los parámetros pasados ​​a la función de salida y el valor devuelto por la declaración de retorno de la función principal se pasarán al sistema operativo . El sistema operativo no destruirá el proceso hijo hasta que estos valores se pasen al proceso padre que generó el proceso hijo. Un proceso en este estado es un proceso zombie. En otras palabras, es el sistema operativo el que convierte el proceso hijo en un proceso zombie .
  • Solución : el sistema operativo pasará este valor solo cuando el proceso principal inicie activamente una solicitud (llamada a función). En otras palabras, si el proceso padre no solicita activamente el valor de estado final del proceso hijo, el sistema operativo siempre lo guardará y mantendrá el proceso hijo en el estado de proceso zombie durante mucho tiempo. En otras palabras, los padres son responsables de recuperar a sus hijos.
#include <stdio.h>
#include <unistd.h>

int main(int argc, char *argv[])
{
    
    
	pid_t pid=fork();
	
	if(pid==0)     // if Child Process
	{
    
    
		puts("Hi I'am a child process");
	}
	else
	{
    
    
		printf("Child Process ID: %d \n", pid);
		sleep(30);     // Sleep 30 sec.
	}

	if(pid==0)
		puts("End child process");
	else
		puts("End parent process");
	return 0;
}

Los resultados muestran: El proceso en estado Z es un proceso zombie, S: durmiendo, R: corriendo, Z zombie
Insertar descripción de la imagen aquí

3. Procesamiento de señales

1. Definición

  • Hemos aprendido anteriormente que el proceso padre suele estar tan ocupado como el proceso hijo al crear y destruir procesos. No sabemos cuándo finalizar el proceso hijo. Por lo tanto, no podemos simplemente llamar a la función waitpid sin cesar para esperar a que el proceso hijo termine. terminar Esto requiere procesamiento de señal para responder asociación.

  • El organismo principal que reconoce la terminación de un proceso hijo es el sistema operativo . Por lo tanto, si el sistema operativo puede indicarle al proceso padre que está ocupado trabajando, ayudará a crear un programa eficiente.

  • Manejo de señales : Un mensaje enviado por el sistema operativo a un proceso cuando ocurre un evento específico. Además, en respuesta al mensaje, el proceso de realizar operaciones personalizadas relacionadas con el mensaje.

2. función de señal

  • Cuando un proceso descubre que su proceso hijo ha finalizado, solicita al sistema operativo que llame a una función específica. La solicitud se completa mediante la llamada a la función de señal (por lo que la señal se denomina función de registro de señal)
//→为了在产生信号时调用,返回之前注册的函数指针。
/*
函数名∶signal
参数∶int signo, void(* func)(int)
返回类型∶参数为int型,返回void型函数指针。
*/
#include<signal.h>
void(*signal(int signo, void(*func)(int))(int);

//等价于下面的内容:
typedef void(*signal_handler)(int);
signal_handler signal(int signo,signal_handler func);

  • Al llamar a la función anterior, el primer parámetro es la información del caso especial y el segundo parámetro es el valor de dirección (puntero) de la función que se llamará en el caso especial. Cuando ocurre la situación representada por el primer parámetro, se llama a la función señalada por el segundo parámetro. A continuación se muestran algunos casos especiales y las constantes correspondientes que se pueden registrar en la función de señal.
    • SIGALRM : Ha llegado el momento de registrarse llamando a la función de alarma.
    • FIRMA : Introduzca CTRL+C.
    • SIGCHLD : El proceso hijo finaliza. (El inglés es infantil)

3. Método de llamada

Escriba una declaración para llamar a la función de señal para completar la siguiente solicitud
1. " Cuando finaliza el proceso hijo, se llama a la función mychild ".
代码:signal(SIGCHLD, mychild);

  • En este momento, los parámetros de la función mychild deben ser int y el tipo de valor de retorno debe ser nulo. Corresponde al segundo parámetro de la función de señal. Además, la constante SIGCHLD indica la terminación del proceso hijo y debería convertirse en el primer parámetro de la función de señal.

2. "Ha llegado el momento de registrarse a través de la función de alarma, llame a la función de tiempo de espera"
3. "Llame a la función de control de teclas al ingresar CTRL+C".

  • Las constantes que representan estas dos situaciones son SIGALRM y SIGINT respectivamente, por lo que la función de señal se llama de la siguiente manera.
    2. señal (SIGALRM, tiempo de espera);
    3. señal (SIGINT, control de clave);

Lo anterior es el proceso de registro de señal. Una vez registrada la señal, cuando se produce la señal de registro (cuando se produce el registro), el sistema operativo llamará a la función correspondiente a la señal.

#include<unistd.h>
//→返回0或以秒为单位的距SIGALRM信号发生所剩时间。
unsigned int alarm(unsigned int seconds);

Si llama a esta función y le pasa un parámetro entero positivo, la señal SIGALRM se generará después del tiempo correspondiente (en segundos). Si se pasa 0 a esta función se cancelará la reserva anterior de la señal SIGALRM. Si la función de procesamiento de señal correspondiente a la señal no se especifica después de reservar la señal a través de esta función, el proceso finalizará (llamando a la función de señal) sin ningún procesamiento.

4. Ejemplo:

#include <stdio.h>
#include <unistd.h>
#include <signal.h>
// 定义信号处理函数timeout,返回值为void
void timeout(int sig)
{
    
    
	if(sig==SIGALRM)
		puts("Time out!");
//为了每隔2秒重复产生SIGALRM信号,在信号处理器中调用alarm函数
	alarm(2);	
}
// 定义信号处理函数keycontrol,返回值为void
void keycontrol(int sig)
{
    
    
	if(sig==SIGINT)
		puts("CTRL+C pressed");
}

int main(int argc, char *argv[])
{
    
    
	int i;
//注册SIGALRM、SIGINT信号及相应处理器
	signal(SIGALRM, timeout);
	signal(SIGINT, keycontrol);
//预约2秒后发生SIGALRM信号
	alarm(2);

	for(i=0; i<3; i++)
	{
    
    
		puts("wait...");
		sleep(100);
	}
	return 0;
}
  • El bucle for anterior:
    para ver la generación de señal y la ejecución del procesador de señal y proporcionar un tiempo de espera de 100 segundos cada vez, un total de 3 veces,
    se llama a la función de suspensión en el bucle. En otras palabras, el programa finalizará después de otros 300 segundos y aproximadamente 5 minutos. Éste es un // período de tiempo largo, pero en realidad tarda menos de 10 segundos en ejecutarse.

¿Por qué es esto? Obviamente son 300 segundos. . .

  • Motivo: "Cuando ocurre una señal, el proceso que ingresa al estado bloqueado debido a la llamada a la función de suspensión se despertará".
    El sujeto de la función de llamada es de hecho el sistema operativo, pero la función no se puede llamar cuando el proceso está en el estado de sueño. Por lo tanto, cuando se genera una señal, para llamar al controlador de señales, se despertará el proceso que entró en el estado bloqueado debido a la llamada a la función de suspensión. Además, una vez que se despierta el proceso, no volverá a dormirse. Esto es cierto incluso si aún no ha llegado el tiempo especificado en la función de suspensión. Por lo tanto, el programa finalizará en menos de 10 segundos. Si se ingresa CTRL+C continuamente, es posible que no tarde ni 1 segundo.

4. Servidor concurrente basado en multitarea

1. Principio

(空间与时间的平衡,以时间换取空间,还是以空间换取时间)
Cada vez que el cliente tiene una solicitud de acceso, el servidor en la fase de aceptación va a la bifurcación para crear un proceso hijo para manejar la solicitud de acceso del cliente . Al mismo tiempo, el proceso padre regresa a la fase de aceptación y continúa esperando un nuevo cliente. solicitudes . El problema es que si varios usuarios están conectados al mismo tiempo, el cuello de botella provocará un aumento repentino de la memoria.

2. Instancia de servidor concurrente multitarea

//-----------------------------------------------------多任务并发服务器(进程)---------------------------------

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h> //Linux标准数据类型
#include <signal.h>
#include <sys/wait.h>
#include <arpa/inet.h>
#include <sys/socket.h>

#define BUF_SIZE 30
void error_handling(char* message);
//------------------------------------服务端-----------------------

void hand_childProc(int sig)
{
    
    
    pid_t pid;
    int status = 0;
    waitpid(-1, &status, WNOHANG);//-1:回收僵尸进程,WNOHANG:非挂起方式,立马返回status状态
    printf("%s(%d):%s removed sub proc:%d\r\n", __FILE__, __LINE__, __FUNCTION__, pid);
}
//服务器
void ps_moretask_server()
{
    
    
    
    struct sigaction act;
    act.sa_flags = 0;
    act.sa_handler = hand_childProc;
    sigaction(SIGCHLD, &act, 0);//当发现有SIGCHLD信号时进入到子进程函数。处理任务和进程回收(防止僵尸进程)

    int serv_sock;
    struct sockaddr_in server_adr, client_adr;
    memset(&server_adr, 0, sizeof(server_adr));
    server_adr.sin_family = AF_INET;
    server_adr.sin_addr.s_addr = htonl(INADDR_ANY);
    server_adr.sin_port = htons(9527);
    serv_sock = socket(AF_INET, SOCK_STREAM, 0);
    if (bind(serv_sock, (sockaddr*)&server_adr, sizeof(server_adr)) == -1)
        error_handling("ps_moretask server bind error");
    if (listen(serv_sock, 5) == -1)
    {
    
    
        error_handling("ps_moretask server listen error");
    }
    int count = 0;
    char buffer[1024];
    while (true)
    {
    
    
        socklen_t size = sizeof(client_adr);
        int client_sock = accept(serv_sock, (sockaddr*)&client_adr, &size);
        if (client_sock < 0) {
    
    
            error_handling("ps_moretask server accept error");
            close(serv_sock);
            return;
        }

        pid_t pid = fork();//会复制客户端和服务端的socket
        if (pid == 0)
        {
    
    
            close(serv_sock);//子进程关闭服务端的socket,因为子进程为了处理客户端的任务

            ssize_t len = 0;
            while ((len = read(client_sock, buffer, sizeof(buffer))) > 0)
            {
    
    
                len = write(client_sock, buffer, strlen(buffer));
                if (len != (ssize_t)strlen(buffer)) {
    
    
                    //error_handling("write message failed!");
                    std::cout << "ps_moretask server write message failed!\n";

                    close(serv_sock);
                    return;
                }

                std::cout << "ps_moretask server read & write success!, buffer:" << buffer << "__len:" << len << std::endl;

                memset(buffer, 0, len);//清理

                close(client_sock);
                return;
            }

        }
        else if (pid < 0)
        {
    
    
            close(client_sock);
            error_handling("ps_moretask server accept fork error");
            break;
        }

       

        close(client_sock);//服务端关闭的时候,客户端会自动关闭
    }

    close(serv_sock);
}

//------------------------------------客户端-----------------------
void ps_moretask_client()
{
    
    
    int client = socket(PF_INET, SOCK_STREAM, 0);
    struct sockaddr_in servaddr;
    memset(&servaddr, 0, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_addr.s_addr = inet_addr("127.0.0.1");
    servaddr.sin_port = htons(9527);
    int ret = connect(client, (struct sockaddr*)&servaddr, sizeof(servaddr));
    if (ret == -1) {
    
    
        std::cout << "ps_moretask client connect failed!\n";
        close(client);
        return;
    }
    std::cout << "ps_moretask client connect server is success!\n";

    char buffer[256] = "hello ps_moretask server, I am client!";
    while (1)
    {
    
    
        //fputs("Input message(Q to quit):", stdout);//提示语句,输入Q结束
        //fgets(buffer, sizeof(buffer), stdin);//对文件的标准输入流操作 读取buffer的256字节
        //if (strcmp(buffer, "q\n") == 0 || (strcmp(buffer, "Q\n") == 0)) {
    
    
        //    break;
        //}

        size_t len = strlen(buffer);
        size_t send_len = 0;

        //当数据量很大时,并不能一次把所有数据全部发送完,因此需要分包发送
        while (send_len < len)
        {
    
    
            ssize_t ret = write(client, buffer + send_len, len - send_len);//send_len 记录分包的标记
            if (ret <= 0) {
    
    //连接出了问题
                fputs("may be connect newwork failed,make client write failed!\n", stdout);
                close(client);
                return;
            }
            send_len += (size_t)ret;

            std::cout << "ps_moretask client write success, msg:" << buffer << std::endl;

        }
        memset(buffer, 0, sizeof(buffer));

        //当数据量很大时,并不能一次把所有数据全部读取完,因此需要分包读取
        size_t read_len = 0;
        while (read_len < len)
        {
    
    
            size_t ret = read(client, buffer + read_len, len - read_len);
            if (ret <= 0) {
    
    //连接出了问题
                fputs("may be connect newwork failed, make client read failed!\n", stdout);
                close(client);
                return;
            }
            read_len += (size_t)ret;
        }
        std::cout << "from server:" << buffer << std::endl;
    };
    sleep(2);//延时2秒关闭客户端
    close(client);
    std::cout << "ps_moretask client done!" << std::endl;
}

//------------------------------------调用函数-----------------------
void ps_moretask_func()
{
    
    
    pid_t pid = fork();
    if (pid > 0)
    {
    
    
        printf("%s(%d):%s wait ps_moretask server invoking!\r\n", __FILE__, __LINE__, __FUNCTION__);

        sleep(1);
        for (int i = 0; i < 5; i++)
        {
    
    
            pid = fork();
            if (pid > 0) {
    
    
                continue;
            }
            else if (pid == 0)
            {
    
    
                //子进程启动客户端
                ps_moretask_client();
                break;//到子进程终止,避免指数级创建进程 n的2次方。
            }
        }
    }
    else if (pid == 0) {
    
    
        //启动服务端
        ps_moretask_server();
    }
}

5. Comunicación entre procesos (IPC)

  • IPC (Comunicación entre procesos): comunicación entre procesos , un mecanismo para el intercambio de datos a través del búfer proporcionado por el núcleo . La comunicación entre procesos significa que se pueden intercambiar datos entre dos procesos diferentes, para lograr esto, el sistema operativo debe proporcionar un espacio de memoria al que ambos procesos puedan acceder simultáneamente .

  • El siguiente ejemplo entre los procesos A y B es una regla de comunicación entre procesos.

    • "Si tengo 1 pieza de pan, el valor de la variable pan se convierte en 1. Si como el pan, el valor del pan vuelve a 0. Por lo tanto, mi estado puede ser juzgado por el valor de la variable pan". En otras palabras, el proceso A, el proceso B, recibe notificación de su estado a través de la variable pan, y el proceso B escucha lo que dijo el proceso A a través de la variable pan.
  • Siempre que haya un espacio de memoria al que dos procesos puedan acceder simultáneamente, los datos se pueden intercambiar a través de este espacio . Pero los procesos tienen estructuras de memoria completamente independientes. Incluso el proceso hijo creado mediante la función fork no compartirá espacio de memoria con el proceso padre . Por lo tanto, la comunicación entre procesos solo se puede realizar mediante otros métodos especiales.

  • En el proceso, el proceso hijo copiará la memoria del proceso padre, pero el proceso padre no copiará la memoria del proceso hijo, por lo que el proceso padre no conoce algunas operaciones del proceso hijo.

1. Tubería anónima: TUBO

( 亲族管道,处理两个不相干的进程时会有问题)

  • Las tuberías no son recursos que pertenecen al proceso, sino que pertenecen al sistema operativo como los sockets (es decir, no son los objetos de copia de la función fork). Entonces, dos procesos se comunican a través del espacio de memoria proporcionado por el sistema operativo.
  • En realidad, existen muchos tipos de tuberías:
    • El más utilizado probablemente sea "|" en el shell. En realidad, es un carácter de barra vertical, que toma la salida de la expresión anterior e introduce la siguiente expresión como entrada. Por ejemplo, comúnmente usamos "ps aux|grep ssh" para ver procesos relacionados con ssh.
    • Hay dos canalizaciones de comunicación entre procesos de uso común: una es la canalización de tubería, que también se puede denominar canalización familiar .
    • A esto corresponde la tubería FIFO , que también puede denominarse tubería pública .
#include <unistd.h>
//→成功时返回 0,失败时返回-1。
int pipe(int filedes[2]);
/*
Filedes[0] 通过管道接收数据时使用的文件描述符,即管道出口。
Fledes[1] 通过管道传输数据时使用的文件描述符,即管道入口。
*/
  • Tubería bidireccional:
    Insertar descripción de la imagen aquí
//进程通信——双管道(PIPE)
void ps_pipe_double_func()
{
    
    
    int fds_server[2] = {
    
     -1, -1 };
    int fds_client[2] = {
    
     -1, -1 };

    pipe(fds_server);//父进程创建管道
    pipe(fds_client);
    
    pid_t pid = fork();

    if (pid == 0)
    {
    
    
        char buffer[64] = "client send by child process!\n";
        char readBuf[128] = "";
        //子进程数据写入
        write(fds_client[1], buffer, sizeof(buffer));

        read(fds_server[0], readBuf, sizeof(readBuf));

        printf("%s(%d):%s child process read ps_pipe by server :%s\r\n", __FILE__, __LINE__, __FUNCTION__, readBuf);

        printf("%s(%d):%s ---pid:%d\r\n", __FILE__, __LINE__, __FUNCTION__, getpid());

    }
    else
    {
    
    
        char buffer[64] = "server send by father process!\n";
        char readBuf[128] = "";
        //父进程读取数据
        read(fds_client[0], readBuf, sizeof(readBuf));

        printf("%s(%d):%s father process read ps_pipe by client :%s\r\n", __FILE__, __LINE__, __FUNCTION__, readBuf);

        write(fds_server[1], buffer, sizeof(buffer));

    }

    printf("%s(%d):%s ---pid:%d\r\n", __FILE__, __LINE__, __FUNCTION__, getpid());


}

2. Tubería con nombre: FIFO

FIFO: primero en entrar, primero en salir, primero en entrar, primero en salir. ( 每个进程都要有个命名文件)

  • En comparación con Pipe Pipe, ya puede completar la tarea de comunicación entre dos procesos, pero parece que no se completa lo suficientemente bien, o se puede decir que no es lo suficientemente completo. Solo puede comunicarse entre dos procesos relacionados, lo que limita en gran medida el alcance de la aplicación de la tubería . Muchas veces queremos poder comunicarnos entre dos procesos independientes, por lo que no se pueden usar tuberías, por lo que ha surgido una tubería que puede satisfacer la comunicación de procesos independientes, que es la tubería FIFO .

  • La esencia de fifo pipe es un archivo con nombre en el sistema operativo

  • Existe en forma de un archivo con nombre en el sistema operativo. Podemos ver quince canales en el sistema operativo. Si tiene permiso, incluso puede leerlos y escribirlos.

    • Utilice el comando: mkfifo myfifo
    • Función de uso: int mkfifo(const char *pathname, mode_t mode); Éxito: 0; Fallo: -1
  • El kernel abrirá un búfer para el archivo FIFO, operará el archivo FIFO y operará el búfer para lograr la comunicación del proceso. Una vez que se crea un FIFO usando mkfifo, se puede abrir usando open. Las funciones IO de archivos comunes se pueden usar para FIFO. Tales como: cerrar, leer, escribir, desvincular, etc.

//进程通信-命名管道(FIFO)
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>//创建命名管道头文件
#include <fcntl.h>
#include <string.h>
void ps_fifo_func()
{
    
    
    mkfifo("./test_fifo.fifo", 0666);//创建FIFO命名管道,并设置mode
    pid_t pd = fork();
    if (pd == 0)
    {
    
    
        sleep(1);
        int fd = open("./test_fifo.fifo", O_RDONLY);//打开创建的fifo文件,并申请读权限

        char buffer[64] = "";
        ssize_t len = read(fd, buffer, sizeof(buffer));

        printf("%s(%d):%s read ps_fifo server :%s  len: %d\r\n", __FILE__, __LINE__, __FUNCTION__, buffer, len);

        close(fd);
    }
    else
    {
    
    
        int fd = open("./test_fifo.fifo", O_WRONLY);//打开创建的fifo文件, 并申请读写权限

        char buffer[128] = "hello, I am fifo server!";
        ssize_t len = write(fd, buffer, sizeof(buffer));

        printf("%s(%d):%s ps_fifo server wait success!\r\n", __FILE__, __LINE__, __FUNCTION__);

        close(fd);
    }
}

3. Memoria compartida

( 数据同步时,具有部分时间差,比较耗时)

1. Definición

  • Antes de comprender la memoria compartida, primero debe comprender el mecanismo de comunicación System V IPC.
    El mecanismo System V IPC se introdujo originalmente con la versión AT&T System V.2 de UNIX. Estos mecanismos se utilizan específicamente para IPC (comunicación entre procesos) , se utilizan en la misma versión y tienen interfaces de programación similares, por lo que a menudo se denominan mecanismos de comunicación System V IPC.
    La memoria compartida es el segundo de los tres mecanismos IPC del System V. La memoria compartida permite que diferentes procesos compartan la misma memoria lógica y todos puedan acceder o modificar esta memoria sin restricciones. Por tanto, es una forma muy eficaz de transferir grandes cantidades de datos entre procesos. "La memoria compartida permite que diferentes procesos compartan la misma memoria lógica", aquí está la memoria lógica. En otras palabras, los procesos que comparten memoria pueden no acceder a la misma memoria física . No existe una regulación clara al respecto, pero la mayoría de las implementaciones de sistemas organizan la memoria compartida entre procesos en la misma memoria física .
  • La memoria compartida es en realidad una pieza especial de memoria física asignada por el mecanismo IPC, que se puede asignar al espacio de direcciones del proceso y también al espacio de direcciones de otros procesos con permisos. Es como usar malloc para asignar memoria, excepto que esta memoria se puede compartir.

2. Creación, mapeo, acceso y eliminación de memoria compartida

  • IPC proporciona un conjunto de API para controlar la memoria compartida. Los pasos para usar la memoria compartida suelen ser:
    • 1) Crear u obtener una memoria compartida;
    • 2) Asigne la memoria compartida creada en el paso anterior al espacio de direcciones del proceso;
    • 3) Acceder a la memoria compartida;
    • 4) Separar la memoria compartida del espacio de direcciones del proceso actual;
    • 5) Eliminar esta memoria compartida;
  • detalles de la siguiente manera:

1) Utilice la función shmget() para crear una memoria compartida :

int shmget( key_t key, size_t size, int shmflg );
key: El nombre de esta memoria compartida. El sistema lo utiliza para distinguir la memoria compartida. Los diferentes procesos que acceden a la misma memoria compartida deben pasar el mismo nombre.
- size: El tamaño de la memoria compartida
- shmflg: Es la bandera de la memoria compartida, incluidos 9 bits, y su contenido es el mismo que el modo al crear el archivo. Hay un indicador especial IPC_CREAT que se puede pasar con el indicador de permiso en forma de o

2) Utilice la función shmat() para asignar la memoria compartida :

void* shmat( int shm_id, const void* shm_addr, int shmflg );
shm_id: es el ID de la memoria compartida, el valor de retorno de la función shmget().
shm_addr: Especifique la ubicación donde está conectada la memoria compartida al espacio de direcciones del proceso actual. Por lo general, se pasa NULL, lo que indica que el sistema realizará la selección para evitar confusión en la memoria.
shmflg: Se puede ingresar un conjunto de indicadores de control, generalmente 0 o SHM_RDONLY, lo que indica que el segmento de memoria compartida es de solo lectura.
– El valor de retorno de la función es el primer puntero de dirección de la memoria compartida.

3) Utilice la función shmdt() para separar la memoria compartida :

int shmdt(void* shm_p);
–shm_p: es el primer puntero de dirección de la memoria compartida, que es el valor de retorno de shmat().
– Devuelve 0 en caso de éxito, -1 en caso de error.

4) Utilice la función shmctl() para controlar la memoria compartida :

int shmctl( int shm_id, int command, struct shmid_ds* buf );
shm_id: es el identificador de la memoria compartida, que es el valor de retorno de shmget().
command: es la acción a realizar, tiene tres valores válidos de la siguiente manera:
Insertar descripción de la imagen aquí

3. Ejemplo de código:

#include <sys/ipc.h>
#include <sys/shm.h>//共享内存头文件

//共享的结构体
typedef struct {
    
    
    int id;
    char name[128];
    int age;
    bool sex;
    int signal;
}STUDENT, *P_STUDENT;

void ps_sharememory_func()
{
    
    
    pid_t pid = fork();
    if (pid > 0)
    {
    
    
        //shmget创建共享文件 ftok指定文件或文件夹路径,创建一个安全可靠的key,规避重复
        int shm_id = shmget(ftok(".", 1), sizeof(STUDENT), IPC_CREAT | 0666);
        if (shm_id == -1) {
    
    
            printf("%s(%d):%s share memory creat failed!\r\n", __FILE__, __LINE__, __FUNCTION__);
            return;
        }
        P_STUDENT pStu = (P_STUDENT)shmat(shm_id, NULL, 0);
        pStu->id = 666666;
        strcpy(pStu->name, "welcome moon");
        pStu->age = 19;
        pStu->sex = true;
        pStu->signal = 99;
        while (pStu->signal == 99)//同步
        {
    
    
            usleep(100000);
        }
        shmdt(pStu);
        shmctl(shm_id, IPC_RMID, NULL);
           
    }
    else {
    
    
        usleep(500000);//休眠500ms,等待父进程写入数据, 1000单位为1ms,100000为100ms
        //shmget创建共享文件 ftok指定文件或文件夹路径,创建一个安全可靠的key,规避重复
        int shm_id = shmget(ftok(".", 1), sizeof(STUDENT), IPC_CREAT | 0666);
        if (shm_id == -1) {
    
    
            printf("%s(%d):%s share memory creat failed!\r\n", __FILE__, __LINE__, __FUNCTION__);
            return;
        }
        P_STUDENT pStu = (P_STUDENT)shmat(shm_id, NULL, 0);
        while (pStu->signal != 99)//同步
        {
    
    
            usleep(100000);
        }
        printf("student msg: %d, %s, %d, %s\n", pStu->id, pStu->name, pStu->age, pStu->sex == true ? "male":"famale");
        pStu->signal = 0;
        shmdt(pStu);
        shmctl(shm_id, IPC_RMID, NULL);
    }
}
  • Memoria compartida, los datos deben sincronizarse con el procesamiento del bucle while de los dos procesos en el caso; de lo contrario, no se puede acceder a los datos. Desventajas: existe una diferencia de tiempo parcial durante la sincronización de datos, lo que requiere relativamente tiempo.
  • resultado de la operación:
    Insertar descripción de la imagen aquí

4, cantidad de señal

1. Definición

  • Para evitar una serie de problemas causados ​​por el acceso de varios programas a un recurso compartido al mismo tiempo, necesitamos un método que pueda generar y usar tokens para autorizar, de modo que solo un hilo de ejecución pueda acceder al área crítica de el código en cualquier momento. . Una sección crítica significa que el código que realiza actualizaciones de datos debe ejecutarse exclusivamente. El semáforo puede proporcionar dicho mecanismo de acceso, permitiendo que solo un hilo acceda a una sección crítica al mismo tiempo, lo que significa que se utiliza el semáforo调协进程对共享资源的访问 .
  • Un semáforo es una variable especial y el acceso del programa a ella es una只允许对它进行等待(即P(信号变量))和发送(即V(信号变量))信息操作 operación atómica . El semáforo más simple es una variable que sólo puede tomar 0 y 1. Esta es también la forma más común de semáforo, llamada 二进制信号量. Un semáforo que puede tomar múltiples números enteros positivos se llama 通用信号量. Aquí exploramos principalmente semáforos binarios.

2. Principio de funcionamiento

  • 1. Dado que el semáforo sólo puede realizar dos operaciones: esperar y enviar señales, es decir, P(sv) y V(sv), su comportamiento es el siguiente: P(sv): si el valor de sv es mayor que cero, disminuye es 1; si su valor es cero, suspenda la ejecución del proceso
    V(sv): si otros procesos están suspendidos esperando a sv, déjelo reanudar su ejecución. Si ningún proceso está suspendido esperando a sv, simplemente agréguele 1.
  • 2. Por ejemplo, dos procesos comparten el semáforo sv. Una vez que uno de los procesos realiza la operación P (sv), obtendrá el semáforo y podrá ingresar a la sección crítica para disminuir sv en 1. Y el segundo proceso no podrá ingresar a la sección crítica porque cuando intenta ejecutar P(sv), sv es 0, se bloqueará esperando que el primer proceso abandone la sección crítica y ejecute V(sv) para liberar el semáforo, entonces el segundo proceso puede reanudar la ejecución.

3. Mecanismo de semáforo de Linux

  • Ver la matriz de semáforos actual:
    ipcs -s

1. función semget: crear

Insertar descripción de la imagen aquí

2. Función semop: realiza la operación PV

Insertar descripción de la imagen aquí

3. función semctl: controla la información del semáforo

Insertar descripción de la imagen aquí

4. Ejemplo de código:

  • Reemplace el bucle while en el módulo de memoria compartida -> ejemplo de código anterior con un semáforo:
//进程通信————————————————————共享内存+信号量

#include <sys/ipc.h>
#include <sys/shm.h>//共享内存头文件
#include <sys/sem.h>//信号量头文件

//共享的结构体
typedef struct {
    
    
    int id;
    char name[128];
    int age;
    bool sex;
    int signal;
}STUDENT, *P_STUDENT;

void ps_sharememory_func()
{
    
    
    pid_t pid = fork();
    if (pid > 0)
    {
    
    
        //创建信号量
        key_t key = ftok(".", 2);//ftok指定文件或文件夹路径,创建一个安全可靠的key,规避重复
        int sem_id = semget(key, 2, IPC_CREAT);//信号量id创建,key:代表当前路径,2:创建两个信号量,IPC_CREAT:表示进程通信创建

        //pv 生产者 消费者模式(P通过,V释放)
        //两个信号量
        semctl(sem_id, 0, SETVAL, 0);
        semctl(sem_id, 1, SETVAL, 0);


        //shmget创建共享文件id 
        int shm_id = shmget(ftok(".", 1), sizeof(STUDENT), IPC_CREAT | 0666);//ftok指定文件或文件夹路径,创建一个安全可靠的key,规避重复
        if (shm_id == -1) {
    
    
            printf("%s(%d):%s share memory creat failed!\r\n", __FILE__, __LINE__, __FUNCTION__);
            return;
        }
        P_STUDENT pStu = (P_STUDENT)shmat(shm_id, NULL, 0);
        pStu->id = 666666;
        strcpy(pStu->name, "welcome moon");
        pStu->age = 19;

        pStu->sex = true;

        
        //第一个信号量 v操作
        sembuf sop = {
    
    
            .sem_num = 0,
            .sem_op = 1
        };
        semop(sem_id, &sop, 1);//v操作,

        semctl(sem_id, 0, GETVAL);


        //第二个信号量 P操作
        sop.sem_num = 1;
        sop.sem_op = -1;
        semop(sem_id, &sop, 1);//P操作
        semctl(sem_id, 1, GETVAL);

        //删除共享内存
        shmdt(pStu);
        shmctl(shm_id, IPC_RMID, NULL);


        //主进程删除信号量
        semctl(sem_id, 0, IPC_RMID);//释放掉第一个信号量
        semctl(sem_id, 1, IPC_RMID);//释放掉第二个信号量
        sleep(10);//休眠10秒

    }
    else {
    
    
        usleep(500000);//休眠500ms,等待父进程写入数据, 1000单位为1ms,100000为100ms

        //创建信号量
        key_t key = ftok(".", 2);//ftok指定文件或文件夹路径,创建一个安全可靠的key,规避重复
        int sem_id = semget(key, 2, IPC_CREAT);//信号量id创建


        //shmget创建共享文件 ftok指定文件或文件夹路径,创建一个安全可靠的key,规避重复
        int shm_id = shmget(ftok(".", 1), sizeof(STUDENT), IPC_CREAT | 0666);
        if (shm_id == -1) {
    
    
            printf("%s(%d):%s share memory creat failed!\r\n", __FILE__, __LINE__, __FUNCTION__);
            return;
        }

        //第一个信号量 P操作
        sembuf sop = {
    
    
           .sem_num = 0,
           .sem_op = -1
        };
        semop(sem_id, &sop, 1);//P操作,
        semctl(sem_id, 0, GETVAL);

        P_STUDENT pStu = (P_STUDENT)shmat(shm_id, NULL, 0);

        //第二个信号量 V操作
        sop.sem_num = 1;
        sop.sem_op = 1;
        semop(sem_id, &sop, 1);//V操作,
        semctl(sem_id, 1, GETVAL);

        printf("student msg: %d, %s, %d, %s\n", pStu->id, pStu->name, pStu->age, pStu->sex == true ? "male":"famale");
        pStu->signal = 0;
        

        shmdt(pStu);
        shmctl(shm_id, IPC_RMID, NULL);

        sleep(10);//休眠10秒

    }
}

5. Cola de mensajes

1. Definición

  • Las colas de mensajes proporcionan una forma de enviar un bloque de datos de un proceso a otro . Se considera que cada bloque de datos contiene un tipo y el proceso de recepción puede recibir de forma independiente estructuras de datos que contengan diferentes tipos. Podemos evitar los problemas de sincronización y bloqueo de canalizaciones con nombre enviando mensajes . Pero la cola de mensajes, al igual que la canalización con nombre FIFO, tiene un límite de longitud máxima para cada bloque de datos .

2. API de cola de mensajes

1. función msgget: crear cola de mensajes

Esta función se utiliza para crear y acceder a una cola de mensajes. Su prototipo es:
int msgget(key_t key, int msgflg);

  • Al igual que con otros mecanismos de IPC, el programa debe proporcionar una clave para nombrar una cola de mensajes específica. msgflg es un indicador de permiso que indica el permiso de acceso a la cola de mensajes, que es el mismo que el permiso de acceso al archivo. A msgflg se le puede realizar una operación OR con IPC_CREAT, lo que significa crear una cola de mensajes cuando la cola de mensajes nombrada por clave no existe. Si la cola de mensajes nombrada por clave existe, el indicador IPC_CREAT se ignorará y solo se devolverá un identificador. Devuelve un identificador (entero distinto de cero) de la cola de mensajes denominada por clave, o -1 en caso de error.

2. función msgsnd: agrega datos a la cola de mensajes

Esta función se utiliza para agregar mensajes a la cola de mensajes. Su prototipo es:
int msgsnd(int msgid, const void *msg_ptr, size_t msg_sz, int msgflg);

  • msgid es el identificador de la cola de mensajes devuelto por la función msgget.
  • msg_ptr es un puntero al mensaje que se va a enviar, pero la estructura de datos del mensaje tiene ciertos requisitos. La estructura del mensaje apuntada por el puntero msg_ptr debe ser una estructura que comience con una variable miembro entera larga. La función receptora utilizará este miembro para determinar el tipo de mensaje. Entonces, la estructura del mensaje debe definirse así:
    struct my_message{ long int message_type; /* Los datos que desea transferir*/ };
  • msg_sz es la longitud del mensaje señalado por msg_ptr. Tenga en cuenta que es la longitud del mensaje, no la longitud de toda la estructura. Es decir, msg_sz es la longitud excluyendo la variable miembro del tipo de mensaje entero largo.
  • msgflg se utiliza para controlar lo que sucederá cuando la cola de mensajes actual esté llena o el mensaje de la cola alcance un límite en todo el sistema. Si la llamada tiene éxito, se colocará una copia de los datos del mensaje en la cola de mensajes y se devolverá 0. Si falla, se devolverá -1.

3. Función msgrcv: obtiene datos de la cola de mensajes:

Esta función se utiliza para obtener mensajes de una cola de mensajes. Su prototipo es
int msgrcv(int msgid, void *msg_ptr, size_t msg_st, long int msgtype, int msgflg);

  • Las funciones de msgid, msg_ptr y msg_st son las mismas que la función msgsnd.
  • msgtype puede implementar una prioridad de recepción simple. Si msgtype es 0, recibe el primer mensaje de la cola.
    • Si su valor es mayor que cero se obtendrá el primer mensaje con el mismo tipo de mensaje.
    • Si es menor que cero, obtiene el primer mensaje cuyo tipo sea igual o menor que el valor absoluto de msgtype.
  • msgflg se utiliza para controlar qué sucederá cuando no haya mensajes del tipo correspondiente en la cola para recibir.
    Cuando se llama correctamente, la función devuelve el número de bytes colocados en el búfer de recepción, el mensaje se copia al búfer asignado por el usuario al que apunta msg_ptr y luego se elimina el mensaje correspondiente en la cola de mensajes. Devuelve -1 en caso de error.

4. función msgctl: controlar la cola de mensajes:

Esta función se utiliza para controlar la cola de mensajes, es similar a la función shmctl de memoria compartida, su prototipo es:
int msgctl(int msgid, int command, struct msgid_ds *buf);

  • comando es la acción a realizar, puede tomar 3 valores,
    • IPC_STAT : establece los datos en la estructura msgid_ds al valor de asociación actual de la cola de mensajes, es decir, sobrescribe el valor de msgid_ds con el valor de asociación actual de la cola de mensajes.
    • IPC_SET : si el proceso tiene permisos suficientes, establezca el valor de asociación actual de la cola de mensajes en el valor proporcionado en la estructura msgid_ds.
    • IPC_RMID : Eliminar cola de mensajes.
  • buf es un puntero a la estructura msgid_ds, que apunta al modo de cola de mensajes y la estructura de derechos de acceso. La estructura msgid_ds incluye al menos los siguientes miembros:
//成功时返回0,失败时返回-1.
struct msgid_ds {
    
     
uid_t shm_perm.uid; 
uid_t shm_perm.gid; 
mode_t shm_perm.mode;
 };

3. Ejemplos de código

//进程通信----------消息队列
#include <sys/msg.h>

typedef struct {
    
    
    int type;
    struct {
    
    
        int id;
        char name[64];
        int age;
        char msg[256];
    }data, *pdata;
}MSG, *PMSG;

void ps_msg_func()
{
    
    
    pid_t pid = fork();
    if (pid > 0)
    {
    
    
        int msg_id = msgget(ftok(".", 3), IPC_CREAT | 0666);
        if (msg_id == -1)
        {
    
    
              printf("%s(%d):%s ps_msg server error=%d:%s!\r\n", __FILE__, __LINE__, __FUNCTION__, errno,strerror(errno));

            return;
        }

        MSG msg;
        memset(&msg, 0, sizeof(msg));
        while (true)
        {
    
    
            ssize_t ret = msgrcv(msg_id, &msg, sizeof(msg.data), 1, 0);
            if (ret == -1) {
    
    
                sleep(1);
                printf("sleeping ~");
            }
            else
            {
    
    
                break;
            }
        }
        printf("accept msg: %d, %s, %d, %s\n", msg.data.id, msg.data.name, msg.data.age, msg.data.msg);

        getchar();
        msgctl(msg_id, IPC_RMID, 0);

    }
    else
    {
    
    
        int msg_id = msgget(ftok(".", 3), IPC_CREAT | 0666);
        MSG msg;
        memset(&msg, 0, sizeof(msg));
        msg.data.id = 5555;
        msg.data.age = 19;

        strcpy(msg.data.name, "moon");
        strcpy(msg.data.msg, "hello friend!");

        msgsnd(msg_id, &msg, sizeof(msg), 0);
        printf("send msg: %d, %s, %d, %s\n", msg.data.id, msg.data.name, msg.data.age, msg.data.msg);

        //休眠两秒后,待数据发送出去再做删除
        sleep(2);
        msgctl(msg_id, IPC_RMID, 0);
    }
    
}

  • Resultados de la ejecución, error->38, 38 indica el método subyacente no implementado ( Function not implemented), porque estoy usando Ubuntu y la cola de mensajes no se puede procesar en el sistema Ubuntu:
    Insertar descripción de la imagen aquí

Supongo que te gusta

Origin blog.csdn.net/MOON_YZM/article/details/130891757
Recomendado
Clasificación