使用进程池实现高并发服务器

近期在将《Linux高性能服务器编程》的代码整理出一个实验板服务器demo,陆陆续续将知识点梳理出来,本文主要实现进程池。react

进程池的必要性
算法

为何要使用进程池/线程池?cause:编程

一、动态建立进程/线程比较耗费时间,会致使比较慢的客户响应;数组

二、动态申请的进程/线程一般只用来为一条链接服务,这会致使系统产生大量的进程/线程,进程切换会消耗大量CPU时间;服务器

三、动态建立的子进程是当前进程的完整镜像。所以当前进程必须谨慎管理其分配的fd、socket和堆内存等系统资源,不然子进程复制这些资源后会致使系统可用资源降低,从而影响服务器性能。微信

进程池实现的并发服务器能够解决上述问题。进程池是由服务器预先建立的一组子进程,全部子进程都运行相同的代码,而且具备相同的属性,好比优先级、PGID(进程组)等,没有打开没必要要的文件描述符,也不会使用大块的堆内存。当有新的客户端链接到来时,主进程经过均衡算法从进程池选择某一子进程来服务,其代价比临时建立进程小得多。至于如何选择子进程,有两种方式:网络

一、随机/轮流选择空闲子进程并发

二、主进程和全部子进程共享一个工做队列,子进程都睡眠在该队列上,当有新链接到来时唤醒某一个子进程。异步

选择好子进程后,主进程还须要经过某种机制通知目标子进程有新任务须要处理,并传递必要的数据。最简单的方法是父子进程预先创建好管道。对于线程池而言就不须要管道了,由于父子线程自己就是共享全局数据的。socket

综上进程池的实现方式为:

                         

 

处理多客户

使用进程池处理并发链接须要考虑的一个问题是: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。下期讲解如何使用该进程池实现一个简单的CGI服务器。


本文分享自微信公众号 - 机械猿(on_ourway)。
若有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一块儿分享。

相关文章
相关标签/搜索