浅谈进程池(半同步/半异步实现)——Linux高性能服务器学习笔记

目录

  • 什么是进程池?
  • 为什么要使用进程池?
  • 如何实现进程池?
      • 问题1:客户任务是否具有上下文关系
      • 问题2:进程池里子进程这么多,应该通知哪一个子进程?
      • 问题3:选好子进程后,主进程如何将客户"交付"给子进程?

什么是进程池?

线程池就是服务器预先创建好的一组子进程,并且子进程的数量是由人为规定的(和CPU数量差不多最好,数量太多或太少甚至对性能提升没有实质性的帮助)

为什么要使用进程池?

  • 为了实现高效的并发模式,我们使用多进程
  • 实现多进程的方法:动态创建进程/进程池
    动态创建进程的缺点:
    1.动态创建耗时
    2.切换耗时(由于动态创建的进程一般只服务一个客户对象,所以产生大量进程/线程并且进程/线程间切换十分频繁)
    3.动态创建的子进程复制父进程的资源导致消耗大量系统资源

如何实现进程池?

半同步半异步进程池模型:

进程 工作 通信方式
主进程 管理监听Socket 管道
子进程 管理连接Socket 管道

问题1:客户任务是否具有上下文关系

如果客户任务具有上下文关系,我们最好使用一个子进程对应处理一个客户连接。
处理方法:为客户连接文件描述符注册epollEPOLLONESHOT事件

问题2:进程池里子进程这么多,应该通知哪一个子进程?

使用随机算法或者RoundRobin(轮询调度)算法来选择子进程
我们这边使用轮询调度算法来实现:

i=(i+1)%mod
//这里的mod数值为进程池内子进程数量
//使用轮询调度算法代码简洁易读
//由于无需关心任务的状态,所以该调度为无状态调度

问题3:选好子进程后,主进程如何将客户"交付"给子进程?

这里的交付其实并不用真的将客户连接文件描述符通过管道发送给子进程,主进程通过管道通知子进程自己从Socket监听文件描述符中获取客户连接描述符即可

下面代码来自《Linux高性能服务器编程》实现的一个进程池,并且我添加了大量注释方便阅读
代码:

#ifndef PROCESSPOOL_H
#define PROCESSPOOL_H

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
//进程类
class process {
public:
  process() : m_pid(-1) {}

public:
  pid_t m_pid;     //子进程的PID
  int m_pipefd[2]; //父进程与子进程通信的管道
};
//进程池类
template <typename T> //定义成模板是为了代码复用,模板参数为处理任务逻辑的类
class processpool {
private:
  processpool(int listenfd, int process_number = 8);

public:
//静态函数与静态方法实现类之间共享数据
//静态成员与静态方法实现单例模式,即保证程序最多只能创建一个线程池类
  static processpool<T> *create(int listenfd, int process_number = 8) {
    if (!m_instance) {//m_instance为进程池类静态实例,如果当前程序未创建进程池,则创建一个
      m_instance = new processpool<T>(listenfd, process_number);
    }
    return m_instance;
  }
  ~processpool() { delete[] m_sub_process; }
  void run();//对外开启进程的接口,该方法内部选择调用函数run_parent()/run_child();
private:
  void setup_sig_pipe();//设置系统信号管道
  void run_parent();//开启父进程
  void run_child();//开启子进程

private:
//进程池信息
  static const int MAX_PROCESS_NUMBER = 16;//最大进程数量
  static const int USER_PER_PROCESS = 65536;//子进程最多能处理的客户数量
  static const int MAX_EVENT_NUMBER = 10000;//epoll最多能处理的事件数量
  //进程池中进程总数
  int m_process_number;
  //子进程在池子中的序号,从0开始
  int m_idx;
  //每一个进程都有一个epoll事件表
  int m_epollfd;
  //监听socket
  int m_listenfd;
  //决定是否停止运行
  int m_stop;
  //存储所有子进程实例
  process *m_sub_process;
  //静态进程池实例
  static processpool<T> *m_instance;
};
template <typename T>
processpool<T> *processpool<T>::m_instance = NULL;

static int sig_pipefd[2];//系统信号管道

static int setnonblocking(int fd) {//设置文件描述符为非阻塞
  int old_option = fcntl(fd, F_GETFL);
  int new_option = old_option | O_NONBLOCK;
  fcntl(fd, F_SETFL, new_option);
  return old_option;
}

static void addfd(int epollfd, int fd) {//为epoll内核事件表添加文件描述符fd
  epoll_event event;
  event.data.fd = fd;
  event.events = EPOLLIN | EPOLLET;//注册的文件描述符的事件类型为可读和ET模式
  epoll_ctl(epollfd, EPOLL_CTL_ADD, fd, &event);
  setnonblocking(fd);
}

static void removefd(int epollfd, int fd) {//从epoll内核事件表中删除文件描述符fd
  epoll_ctl(epollfd, EPOLL_CTL_DEL, fd, 0);
  close(fd);
}

static void sig_handler(int sig) {//系统信号回调函数
  int save_errno = errno;
  int msg = sig;
  send(sig_pipefd[1], (char *)&msg, 1, 0);
  errno = save_errno;
}

static void addsig(int sig, void(handler)(int), bool restart = true) {
  struct sigaction sa;//sigaction结构体
  memset(&sa, '\0', sizeof(sa));
  sa.sa_handler = handler;//将sa结构体与回调函数绑定
  if (restart) {
    sa.sa_flags |= SA_RESTART;
  }
  sigfillset(&sa.sa_mask);//设置信号掩码
  assert(sigaction(sig, &sa, NULL) != -1);//将sig信号与sa结构体绑定并且生效
}
//线程池构造函数
template <typename T>
processpool<T>::processpool(int listenfd, int process_number)
    : m_listenfd(listenfd), m_process_number(process_number), m_idx(-1),
      m_stop(false) {
  assert((process_number > 0) && (process_number <= MAX_PROCESS_NUMBER));
  //创建进程信息数组
  m_sub_process = new process[process_number];
  assert(m_sub_process);

  for (int i = 0; i < process_number; ++i) {
    //创建主进程和子进程之间的管道
    int ret = socketpair(PF_UNIX, SOCK_STREAM, 0, m_sub_process[i].m_pipefd);
    assert(ret == 0);

    m_sub_process[i].m_pid = fork();//创建子进程
    assert(m_sub_process[i].m_pid >= 0);
    if (m_sub_process[i].m_pid > 0) {//主进程调用段
      close(m_sub_process[i].m_pipefd[1]);//主进程使用管道0号端口
      continue;
    } else {                         //子进程调用段
      close(m_sub_process[i].m_pipefd[0]);//子进程使用管道1号端口
      m_idx = i;//设置子进程在进程池的序号
      break;
    }
  }
}
//统一事件源
template <typename T> void processpool<T>::setup_sig_pipe() {
//为进程注册epoll内核事件表
  m_epollfd = epoll_create(5);
  assert(m_epollfd != -1);
//创建系统信号管道
  int ret = socketpair(PF_UNIX, SOCK_STREAM, 0, sig_pipefd);
  assert(ret != -1);

  setnonblocking(sig_pipefd[1]);//设置管道为非阻塞
  addfd(m_epollfd, sig_pipefd[0]);//注册系统信号事件

  addsig(SIGCHLD, sig_handler);//添加信号:将信号与回调函数绑定
  addsig(SIGTERM, sig_handler);
  addsig(SIGINT, sig_handler);
  addsig(SIGPIPE, SIG_IGN);
}

template <typename T> void processpool<T>::run() {//启动进程
  if (m_idx != -1) {
    run_child();
    return;
  }
  run_parent();//只有父进程的m_idx是-1
}

//运行子进程
template <typename T> void processpool<T>::run_child() {
  setup_sig_pipe();//统一事件源

  int pipefd = m_sub_process[m_idx].m_pipefd[1];//获取与父进程通信的管道文件描述符
  addfd(m_epollfd, pipefd);//将与父进程通信的管道加入epoll内核事件表

  epoll_event events[MAX_EVENT_NUMBER];//创建触发事件数组
  T *users = new T[USER_PER_PROCESS];//创建任务逻辑数组
  assert(users);
  int number = 0;//触发事件数目
  int ret = -1;//处理事件后的结果变量
//子进程的事件获取loop,只要m_stop成员没有被置为true,则一直循环获取
  while (!m_stop) {
    number = epoll_wait(m_epollfd, events, MAX_EVENT_NUMBER, -1);//获取触发事件
    if ((number < 0) && (errno != EINTR)) {
      printf("epoll failure\n");
      break;
    }
    //for循环依次处理每一个事件
    for (int i = 0; i < number; i++) {
      int sockfd = events[i].data.fd;//获取事件对应的文件描述符
      if ((sockfd == pipefd) && (events[i].events & EPOLLIN)) {//如果事件为父进程通信管道,则代表有任务事件到来
        int client = 0;
        ret = recv(sockfd, (char *)&client, sizeof(client), 0);
        if (((ret < 0) && (errno != EAGAIN)) || ret == 0) {
          continue;
        } else {
          struct sockaddr_in client_address;//创建socket号
          socklen_t client_addrlength = sizeof(client_address);//socket号长度
          int connfd = accept(m_listenfd, (struct sockaddr *)&client_address,
                              &client_addrlength);//从监听socket文件描述符中获取客户文件描述符
          if (connfd < 0) {
            printf("errno is: %d\n", errno);
            continue;
          }
          addfd(m_epollfd, connfd);//将客户文件描述符加入epoll内核事件表中
          users[connfd].init(m_epollfd, connfd, client_address);//初始化客户文件描述符对应的任务逻辑
        }
      } else if ((sockfd == sig_pipefd[0]) && (events[i].events & EPOLLIN)) {//触发事件为系统信号
        int sig;
        char signals[1024];
        ret = recv(sig_pipefd[0], signals, sizeof(signals), 0);//从管道sig_pipfd[0]获取信号到signals字符数组中
        if (ret <= 0) {
          continue;
        } else {
          for (int i = 0; i < ret; ++i) {
            switch (signals[i]) {
            case SIGCHLD: {//子进程的子进程发生状态变化信号
              pid_t pid;
              int stat;
              while ((pid = waitpid(-1, &stat, WNOHANG)) > 0) {
                continue;
              }
              break;
            }
            case SIGTERM:
            case SIGINT: {//中断进程信号
              m_stop = true;
              break;
            }
            default: {
              break;
            }
            }
          }
        }
      } else if (events[i].events & EPOLLIN) {//客户文件描述符的可读事件
        users[sockfd].process();//启动客户对应的任务逻辑处理函数
      } else {
        continue;
      }
    }
  }
//子进程跳出事件获取loop意味着子进程要返回进程池待命了
  delete[] users;//清空用户数组
  users = NULL;
  close(pipefd);//关闭与父进程通信管道
  // close( m_listenfd ); m_listenfd应该由父进程关闭,因为socket监听文件描述符是在父进程创建的
  close(m_epollfd);//关闭epoll内核事件表
}
//启动父进程
template <typename T> void processpool<T>::run_parent() {
  setup_sig_pipe();//设置统一事件源

  addfd(m_epollfd, m_listenfd);//为epoll内核事件表添加监听文件描述符

  epoll_event events[MAX_EVENT_NUMBER];
  int sub_process_counter = 0;//子进程计数器
  int new_conn = 1;//该变量表示由新的客户连接到来,父进程将该标识量发送给子进程
  int number = 0;//触发事件数目
  int ret = -1;//事件结果变量

  while (!m_stop) {
    number = epoll_wait(m_epollfd, events, MAX_EVENT_NUMBER, -1);
    if ((number < 0) && (errno != EINTR)) {
      printf("epoll failure\n");
      break;
    }

    for (int i = 0; i < number; i++) {
      int sockfd = events[i].data.fd;
      //触发事件为监听文件描述符意味着由新的客户连接到来,
      //那么使用RoundRobin(轮询调度)算法来将该新客户分配给子进程
      if (sockfd == m_listenfd) {
        
        int i = sub_process_counter;
        do {
          if (m_sub_process[i].m_pid != -1) {
            break;
          }
          i = (i + 1) % m_process_number;
          //调度执行i = (i + 1) mod n,并选出第i个子进程。算法的优点是其简洁性,它无需记录当前所有连接的状态,所以它是一种无状态调度。
        } while (i != sub_process_counter);

        if (m_sub_process[i].m_pid == -1) {//无可用的子进程
          m_stop = true;
          break;
        }
        sub_process_counter = (i + 1) % m_process_number;//设置更新子进程计数器
        // send( m_sub_process[sub_process_counter++].m_pipefd[0], ( char*
        // )&new_conn, sizeof( new_conn ), 0 );
        send(m_sub_process[i].m_pipefd[0], (char *)&new_conn, sizeof(new_conn),
             0);//将新客户连接标识量通过进程池的i号子进程的通信管道发送给子进程
        printf("send request to child %d\n", i);
        // sub_process_counter %= m_process_number;
      } 
      //触发事件为系统信号事件
        else if ((sockfd == sig_pipefd[0]) && (events[i].events & EPOLLIN)) {
        int sig;
        char signals[1024];
        ret = recv(sig_pipefd[0], signals, sizeof(signals), 0);
        if (ret <= 0) {
          continue;
        } else {
          for (int i = 0; i < ret; ++i) {
            switch (signals[i]) {
            case SIGCHLD: {//父进程的子进程发生状态变化信号
              pid_t pid;
              int stat;
              while ((pid = waitpid(-1, &stat, WNOHANG)) > 0) {//获取状态变化的子进程pid
                for (int i = 0; i < m_process_number; ++i) {//找到对应的子进程
                  if (m_sub_process[i].m_pid == pid) {
                    printf("child %d join\n", i);
                    close(m_sub_process[i].m_pipefd[0]);//关掉该进程的通信管道
                    m_sub_process[i].m_pid = -1;//设置该子进程的pid为父进程对应的-1
                  }
                }
              }
              m_stop = true;
              //若还有子进程在进程池中,父进程就还得进行事件获取loop
              for (int i = 0; i < m_process_number; ++i) {
                if (m_sub_process[i].m_pid != -1) {
                  m_stop = false;
                }
              }
              break;
            }
            case SIGTERM:
            case SIGINT: {//进程中断信号
              printf("kill all the clild now\n");//结束父进程前必须先将所有进程池中的所有子进程关闭,不然会出现僵尸进程
              for (int i = 0; i < m_process_number; ++i) {
                int pid = m_sub_process[i].m_pid;
                if (pid != -1) {
                  kill(pid, SIGTERM);
                }
              }
              break;
            }
            default: {
              break;
            }
            }
          }
        }
      } else {
        continue;
      }
    }
  }

  // close( m_listenfd );
  close(m_epollfd);
}

#endif

你可能感兴趣的:(Linux服务器编程,linux,tcp/ip,多进程,并发编程)