Grupo de subprocesos epoll + altamente concurrente, el grupo de subprocesos se centra en realizar negocios

Comparta algunos videos que escuché en bilibili y pensé que eran buenos:

1. Video del grupo de hilos de escritura a mano : https://www.bilibili.com/video/BV1AT4y13791

2. Video que explica el principio de epoll : https://www.bilibili.com/video/BV1Xr4y1c7aV

3. Video de pila de protocolo de modo de usuario escrito a mano : https://www.bilibili.com/video/BV1x54y1t7so

Como sabemos, el modelo de concurrencia del servidor generalmente se puede dividir en modelos de un solo subproceso y de múltiples subprocesos. El subproceso aquí generalmente se refiere al "subproceso de E / S", es decir, el "subproceso de administración" responsable de las operaciones de E / S y coordinar y asignar tareas.La solicitud y las tareas reales generalmente se manejan mediante los llamados "hilos de trabajo". Por lo general, en el modelo de subprocesos múltiples, cada subproceso es tanto un subproceso de E / S como un subproceso de trabajo. Entonces, lo que se analiza aquí es el modelo de subproceso de E / S único + subproceso de varios trabajadores, que también es el modelo de concurrencia de servidor más utilizado. Este modelo se puede ver en todas partes en el código del servidor de mi proyecto. También tiene un nombre llamado modelo "semisincrónico / semisincrónico". Al mismo tiempo, este modelo es también una manifestación del modelo productor / consumidor (especialmente multiusuario).

Esta arquitectura se basa principalmente en la idea de multiplexación de E / S (principalmente epoll, select / poll está desactualizado). A través de la multiplexación de E / S de un solo subproceso, se puede lograr una concurrencia eficiente al tiempo que se evitan las E / S de múltiples subprocesos. de cambiar de un lado a otro es claro y fácil de administrar, y los subprocesos de varios trabajadores basados ​​en grupos de subprocesos pueden aprovechar y aprovechar las ventajas del subproceso múltiple, y utilizar grupos de subprocesos para mejorar aún más la reutilización de recursos y evitar una producción excesiva de subprocesos.

El cuello de botella es la intensidad IO. En el grupo de subprocesos, puede abrir 10 subprocesos, por supuesto, todos aceptan el bloque, de modo que el cliente activará automáticamente un subproceso para procesar cuando aparezca, pero imagine que si se agotan los 10 subprocesos, el undécimo cliente lo hará Se produjo un descarte. Para lograr una "alta concurrencia", debe aumentar el número de grupos de subprocesos. Esto causará problemas graves de uso de memoria y retardo de cambio de hilo. Así que se puso en marcha el plan de la función de sondeo previo al evento, el sondeo del hilo principal es responsable de IO y el trabajo se entrega al grupo de hilos. En alta concurrencia, cuando surgen clientes de 10W, el hilo principal es responsable de aceptar y se colocan en la cola, de modo que la conexión no se descarte sin un apretón de manos oportuno, y el hilo del trabajo reclamará el trabajo de la cola. y responder al hilo principal después de la finalización. El hilo principal es responsable de escribir. Esto puede manejar una gran cantidad de conexiones con muy pocos recursos del sistema. En condiciones de baja concurrencia, por ejemplo, si aparecen dos clientes, no habrá una situación en la que 100 subprocesos permanezcan activos allí, lo que resultará en un desperdicio de recursos del sistema.

Implementar correctamente el núcleo del modelo de grupo de subprocesos básico: el subproceso principal es responsable de todas las operaciones de E / S y, si es necesario, después de recibir todos los datos para una solicitud, se entrega al subproceso de trabajo para su procesamiento. Una vez finalizado el procesamiento, devuelva los datos que deben escribirse al hilo principal para volver a escribirlos / intente volver a escribir los datos hasta que se bloqueen y luego devuélvalos al hilo principal para continuar. Aquí "si es necesario" significa: después de la medición, se confirma que el tiempo de CPU consumido durante este procesamiento (sin incluir ninguna E / S en espera, o las operaciones de E / S en espera relacionadas no pueden ser asumidas por epoll) es bastante significativo. Si este proceso (sin incluir las operaciones de E / S que se pueden asumir) no es significativo, se puede resolver directamente en el hilo principal. La premisa de este "necesario" son sólo tres palabras: hipótesis, análisis y medición.

Por lo tanto, para un reloj de entorno de grupo de subprocesos implementado correctamente, la ventaja de usar epoll + E / S sin bloqueo en lugar de seleccionar + E / S de bloqueo es que al procesar una gran cantidad de sockets, el primero es más eficiente que el segundo. porque el primero no requiere cada vez. Después de ser despertado, vuelva a verificar todos los fd para determinar qué cambios de estado de fd se pueden leer y escribir.

Lo esencial

1. Epoll de un solo hilo de E / S

El modelo epoll que implementa un solo hilo de E / S es el primer punto técnico de esta arquitectura. Las ideas principales son las siguientes:

Un solo subproceso crea epoll y espera. Cuando llega una solicitud de E / S (socket), se agrega a epoll y un subproceso de trabajo inactivo se toma del grupo de subprocesos y el negocio real es manejado por el subproceso de trabajo.

Pseudo código:

创建一个epoll实例;
while(server running)
{
    epoll等待事件;
    if(新连接到达且是有效连接)
    {
        accept此连接;
        将此连接设置为non-blocking;
       为此连接设置event(EPOLLIN | EPOLLET ...);
        将此连接加入epoll监听队列;
        从线程池取一个空闲工作者线程并处理此连接;
    }
    else if(读请求)
    {
        从线程池取一个空闲工作者线程并处理读请求;
    }
    else if(写请求)
    {
        从线程池取一个空闲工作者线程并处理写请求;
    }
    else
        其他事件;     
} 

 

El contenido más interesante para el desarrollo de servidores C / C ++ Linux incluye: C / C ++, Linux, Nginx, ZeroMQ, MySQL, Redis, MongoDB, ZK, transmisión de medios, P2P, kernel de Linux, Docker, TCP / IP, coroutine, DPDK Sharing de múltiples puntos de conocimiento avanzado. Haga clic en el enlace para suscribirse y verlo directamente: https://ke.qq.com/course/417774?flowToken=1013189

Materiales de aprendizaje en video + adquisición del grupo 720209036

 

2. Implementación del grupo de subprocesos

Cuando se inicia el servidor, se crea un cierto número de subprocesos de trabajo para unirse al grupo de subprocesos, como (20), para que los subprocesos de E / S los recuperen;

Siempre que un subproceso de E / S solicita un subproceso de trabajo inactivo, se toma un subproceso de trabajo inactivo del grupo para procesar la solicitud correspondiente;

Cuando se procesa la solicitud y se cierra la conexión de E / S correspondiente, el subproceso correspondiente se recicla y se devuelve al grupo de subprocesos para el siguiente uso;

Si no hay un subproceso de trabajo inactivo cuando se solicita el grupo de subprocesos de trabajo inactivo, se puede realizar el siguiente procesamiento:

(1) Si el número total de subprocesos "administrados" en el grupo no excede el valor máximo permitido, se puede crear un nuevo lote de subprocesos de trabajo para unirse al grupo, y uno de ellos será devuelto para que lo use el I / O hilo;

(2) Si el número total de subprocesos "administrados" en el grupo ha alcanzado el valor máximo, no se deben crear más subprocesos nuevos. Espere un breve período de tiempo y vuelva a intentarlo. Tenga en cuenta que debido a que el subproceso de E / S es un solo subproceso y no debe bloquearse esperando aquí, la administración del grupo de subprocesos debe completarse con un subproceso de administración dedicado, incluida la creación de nuevos subprocesos de trabajo. En este momento, el hilo de administración está bloqueado y esperando (como usar una variable de condición y esperar a ser despertado). Después de un corto período de tiempo, debe haber hilos de trabajo inactivos en el grupo de hilos. De lo contrario, se estima que la carga del servidor es un problema.

Epoll es una solución perfecta para servidores de alta concurrencia bajo Linux. Debido a que se activa en función de eventos, no solo es un orden de magnitud más rápido que el seleccionado.

Epoll de un solo subproceso, el volumen de activación puede llegar a 15000, pero después de agregar el negocio, debido a que la mayoría de los negocios tratan con la base de datos, habrá una situación de bloqueo, esta vez debe usar varios subprocesos para acelerar.

El negocio está en el grupo de subprocesos, por lo que se necesitan bloqueos aquí. 2300 resultados de prueba / s

Herramienta de prueba: marca de estrés

Debido a que se agrega el código adecuado para ab, ab también se puede usar para pruebas de esfuerzo.

char buf [1000] = {0};

sprintf (buf, "HTTP / 1.0 200 OK \ r \ nTipo de contenido: texto / plano \ r \ n \ r \ n% s", "¡Hola mundo! \ n");

enviar (socketfd, buf, strlen (buf), 0);

#include <iostream>

#include <sys/socket.h>

#include <sys/epoll.h>

#include <netinet/in.h>

#include <arpa/inet.h>

#include <fcntl.h>

#include <unistd.h>

#include <stdio.h>

#include <pthread.h>

 

#include <errno.h>

  

#define MAXLINE 10

#define OPEN_MAX 100

#define LISTENQ 20

#define SERV_PORT 8006

#define INFTIM 1000

  

//线程池任务队列结构体

 

struct task{

  int fd; //需要读写的文件描述符

 

  struct task *next; //下一个任务

 

};

  

//用于读写两个的两个方面传递参数

 

struct user_data{

  int fd;

  unsigned int n_size;

  char line[MAXLINE];

};

  

//线程的任务函数

 

void * readtask(void *args);

void * writetask(void *args);

  

  

//声明epoll_event结构体的变量,ev用于注册事件,数组用于回传要处理的事件

 

struct epoll_event ev,events[20];

int epfd;

pthread_mutex_t mutex;

pthread_cond_t cond1;

struct task *readhead=NULL,*readtail=NULL,*writehead=NULL;

  

void setnonblocking(int sock)

{

     int opts;

     opts=fcntl(sock,F_GETFL);

     if(opts<0)

     {

          perror("fcntl(sock,GETFL)");

          exit(1);

     }

    opts = opts|O_NONBLOCK;

     if(fcntl(sock,F_SETFL,opts)<0)

     {

          perror("fcntl(sock,SETFL,opts)");

          exit(1);

     }

}

  

int main()

{

     int i, maxi, listenfd, connfd, sockfd,nfds;

     pthread_t tid1,tid2;

     

     struct task *new_task=NULL;

     struct user_data *rdata=NULL;

     socklen_t clilen;

     

     pthread_mutex_init(&mutex,NULL);

     pthread_cond_init(&cond1,NULL);

     //初始化用于读线程池的线程

 

     pthread_create(&tid1,NULL,readtask,NULL);

     pthread_create(&tid2,NULL,readtask,NULL);

     

     //生成用于处理accept的epoll专用的文件描述符

 

     epfd=epoll_create(256);

  

     struct sockaddr_in clientaddr;

     struct sockaddr_in serveraddr;

     listenfd = socket(AF_INET, SOCK_STREAM, 0);

     //把socket设置为非阻塞方式

 

     setnonblocking(listenfd);

     //设置与要处理的事件相关的文件描述符

 

     ev.data.fd=listenfd;

     //设置要处理的事件类型

 

     ev.events=EPOLLIN|EPOLLET;

     //注册epoll事件

 

     epoll_ctl(epfd,EPOLL_CTL_ADD,listenfd,&ev);

     

     bzero(&serveraddr, sizeof(serveraddr));

     serveraddr.sin_family = AF_INET;

     serveraddr.sin_port=htons(SERV_PORT);

     serveraddr.sin_addr.s_addr = INADDR_ANY;

     bind(listenfd,(sockaddr *)&serveraddr, sizeof(serveraddr));

     listen(listenfd, LISTENQ);

     

     maxi = 0;

     for ( ; ; ) {

          //等待epoll事件的发生

 

          nfds=epoll_wait(epfd,events,20,500);

          //处理所发生的所有事件

 

        for(i=0;i<nfds;++i)

        {

               if(events[i].data.fd==listenfd)

               {

                    

                    connfd = accept(listenfd,(sockaddr *)&clientaddr, &clilen);

                    if(connfd<0){

                      perror("connfd<0");

                      exit(1);

                   }

                    setnonblocking(connfd);

                    

                    char *str = inet_ntoa(clientaddr.sin_addr);

                    //std::cout<<"connec_ from >>"<<str<<std::endl;

 

                    //设置用于读操作的文件描述符

 

                    ev.data.fd=connfd;

                    //设置用于注测的读操作事件

 

                 ev.events=EPOLLIN|EPOLLET;

                    //注册ev

 

                 epoll_ctl(epfd,EPOLL_CTL_ADD,connfd,&ev);

               }

            else if(events[i].events&EPOLLIN)

            {

                    //printf("reading!/n");

 

                    if ( (sockfd = events[i].data.fd) < 0) continue;

                    new_task=new task();

                    new_task->fd=sockfd;

                    new_task->next=NULL;

                    //添加新的读任务

 

                    pthread_mutex_lock(&mutex);

                    if(readhead==NULL)

                    {

                      readhead=new_task;

                      readtail=new_task;

                    }

                    else

                    {

                     readtail->next=new_task;

                      readtail=new_task;

                    }

                   //唤醒所有等待cond1条件的线程

 

                    pthread_cond_broadcast(&cond1);

                    pthread_mutex_unlock(&mutex);

              }

               else if(events[i].events&EPOLLOUT)

               {

                 /*

              rdata=(struct user_data *)events[i].data.ptr;

                 sockfd = rdata->fd;

                 write(sockfd, rdata->line, rdata->n_size);

                 delete rdata;

                 //设置用于读操作的文件描述符

                 ev.data.fd=sockfd;

                 //设置用于注测的读操作事件

               ev.events=EPOLLIN|EPOLLET;

                 //修改sockfd上要处理的事件为EPOLIN

               epoll_ctl(epfd,EPOLL_CTL_MOD,sockfd,&ev);

             */

               }

                              

          }

          

     }

}

 

static int count111 = 0;

static time_t oldtime = 0, nowtime = 0;

void * readtask(void *args)

{

    

   int fd=-1;

   unsigned int n;

   //用于把读出来的数据传递出去

 

   struct user_data *data = NULL;

   while(1){

         

        pthread_mutex_lock(&mutex);

        //等待到任务队列不为空

 

        while(readhead==NULL)

             pthread_cond_wait(&cond1,&mutex);

         

        fd=readhead->fd;

        //从任务队列取出一个读任务

 

        struct task *tmp=readhead;

        readhead = readhead->next;

        delete tmp;

        pthread_mutex_unlock(&mutex);

        data = new user_data();

        data->fd=fd;

         

 

        char recvBuf[1024] = {0};

        int ret = 999;

        int rs = 1;

 

        while(rs)

        {

            ret = recv(fd,recvBuf,1024,0);// 接受客户端消息

 

            if(ret < 0)

            {

                //由于是非阻塞的模式,所以当errno为EAGAIN时,表示当前缓冲区已无数据可//读在这里就当作是该次事件已处理过。

 

                if(errno == EAGAIN)

                {

                    printf("EAGAIN\n");

                    break;

                }

                else{

                    printf("recv error!\n");

         

                    close(fd);

                    break;

                }

            }

            else if(ret == 0)

            {

                // 这里表示对端的socket已正常关闭.

 

                rs = 0;

            }

            if(ret == sizeof(recvBuf))

                rs = 1; // 需要再次读取

 

            else

                rs = 0;

        }

        if(ret>0){

 

        //-------------------------------------------------------------------------------

 

 

            data->n_size=n;

 

 

            count111 ++;

 

            struct tm *today;

            time_t ltime;

            time( &nowtime );

 

            if(nowtime != oldtime){

                printf("%d\n", count111);

                oldtime = nowtime;

                count111 = 0;

            }

 

            char buf[1000] = {0};

            sprintf(buf,"HTTP/1.0 200 OK\r\nContent-type: text/plain\r\n\r\n%s","Hello world!\n");

            send(fd,buf,strlen(buf),0);

            close(fd);

 

 

       }

   }

}
 

 

Supongo que te gusta

Origin blog.csdn.net/Linuxhus/article/details/112593824
Recomendado
Clasificación