本篇文章给大家分享的是有关怎样使用进程池实现高并发服务器,小编觉得挺实用的,因此分享给大家学习,希望大家阅读完这篇文章后可以有所收获,话不多说,跟着小编一起来看看吧。
进程池的必要性
为什么要使用进程池/线程池?cause:
1、动态创建进程/线程比较耗费时间,会导致比较慢的客户响应;
2、动态申请的进程/线程通常只用来为一条连接服务,这会导致系统产生大量的进程/线程,进程切换会消耗大量CPU时间;
3、动态创建的子进程是当前进程的完整镜像。因此当前进程必须谨慎管理其分配的fd、socket和堆内存等系统资源,否则子进程复制这些资源后会导致系统可用资源下降,从而影响服务器性能。
进程池实现的并发服务器可以解决上述问题。进程池是由服务器预先创建的一组子进程,所有子进程都运行相同的代码,并且具有相同的属性,比如优先级、PGID(进程组)等,没有打开不必要的文件描述符,也不会使用大块的堆内存。当有新的客户端连接到来时,主进程通过均衡算法从进程池选择某一子进程来服务,其代价比临时创建进程小得多。至于如何选择子进程,有两种方式:
1、随机/轮流选择空闲子进程
2、主进程和所有子进程共享一个工作队列,子进程都睡眠在该队列上,当有新连接到来时唤醒某一个子进程。
选择好子进程后,主进程还需要通过某种机制通知目标子进程有新任务需要处理,并传递必要的数据。最简单的方法是父子进程预先建立好管道。对于线程池而言就不需要管道了,因为父子线程本身就是共享全局数据的。
综上进程池的实现方式为:
处理多客户
使用进程池处理并发连接需要考虑的一个问题是:listen socket和connect socket事件是否都由主进程管理。
服务器程序通常需要处理三类事件:IO事件、信号signal、定时事件,同步IO模型通常用于实现reactor模式,异步IO模型用于实现proactor模式。结合同步异步IO模型,可以设计出下面两种服务器事件处理模式:
半同步/半反应堆模式
半同步/半异步模式
IO复用模型的具体介绍参考UNIX网络编程第六章,这里不展开。
半同步/半异步模式
第一种模式由主进程统一管理两种socket,因为主从进程共享请求队列,进程对队列的任何操作都需要加锁,影响CPU性能。第二种模式主进程管理所有监听socket,子进程分别管理自己的连接socket。子进程可以自己调用accept获取TCP连接队列,而主进程只需要通知就行,具体通知实现方式:
在两个进程之间建立一个Unix域socket作为消息传递的通道(使用 socketpair 函数),然后父进程调用 sendmsg 向通道发送一个特殊的消息,内核将对这个消息做特殊处理,从而将消息传递到子进程,子进程调用recvmsg 从通道接收消息。
此外,在设计进程池时还需要考虑同一个客户端的多次请求是否可以复用一个TCP连接(http层面添加keepalive字段即可),如果客户请求是无状态的,那么服务器可以实现使用不同子进程来为该客户的不同请求进行服务:
如果客户任务是有上下文状态的(比如购物车,总不能刷新一次就被清空了),那么只能一致用同一个子进程来处理该客户的请求。使用epoll的EPOLLONESHOT特性可以确保客户连接在整个生命周期仅被一个进程处理。
对于注册了EPOLLONESHOT事件的socket,操作系统最多触发一次其注册的事件,且只触发一次。因此当子进程处理该socket上的事件时,其他进程不可能操作该socket。
具体实现
定义一个描述子进程的类process,包含子进程的PID,以及父子进程通信的管道pipe;进程池使用单体模式,保证程序仅创建一个进程池实例,这是程序正确处理信号的必要条件:
下面是父进程启动后创建的数据,包括:处理信号的管道、承载子进程信息的数组、进程标识和epoll事件表,并进行了初始化。因此调用fork生成子进程后会继承这些数据,子进程会将自己的数据保存。
父进程的执行流程为:
关于SIGNAL信号的更多知识,参考UNIX环境高级编程第十章:
进程池中父子进程使用信号实现通信的实现如下:
子进程实现
子进程的数据如下:
子进程处理流程如下:
在使用进程池时,监听socket一般由main函数创建,在退出后需要关闭该socket。
以上就是怎样使用进程池实现高并发服务器,小编相信有部分知识点可能是我们日常工作会见到或用到的。希望你能通过这篇文章学到更多知识。更多详情敬请关注亿速云行业资讯频道。
原创文章,作者:奋斗,如若转载,请注明出处:https://blog.ytso.com/219837.html