一般我们是通过动态创建子进程(或者子线程)来实现并发服务器的,这样的缺点
(1)动态创建进程(或线程)比较耗费时间,这将导致较慢的客户响应
(2)动态创建的子进程通常只用来为一个客户服务,这样导致了系统上产生大量的细微进程(或线程)。进程和线程间的切换将消耗大量CPU时间
(3)动态创建的子进程是当前进程的完整映像,当前进程必须谨慎的管理其分配的文件描述符和堆内存等系统资源,否则子进程可能复制这些资源,从而使系统的可用资源急剧下降,进而影响服务器的性能。
进程池:有服务器预先创建的一组子进程,这些子进程的数目在3~10个之间,
进程池中的子进程:
(1)他们都运行着相同的代码,具有相同的属性,比如优先级,PGID(组识别码)等。
(2)进程池在服务器启动之初就创建好了,所以每个子进程都相对"干净",即它们没有打开不必要的文件描述符(从父进程继承而来)
(3)也不会错误地使用大块的堆内存(从父进程复制得到)
选择子进程为新任务服务的方式:
(1)主进程使用某种算法来主动选择子进程
(2)主进程和所有子进程通过一个共享的工作队列来实现同步
:子进程都睡眠在该工作队列上,当有新的任务到来时,主进程将任务添加到工作队列中。
这将唤醒正在等待任务的子进程,不过只有一个子进程将获得新任务的“接管权”,它可以从工作队列中取出任务并执行之,而其他子进程将继续睡眠在工作队列上。
需求:
主进程除了选择好子进程以外,还需要使用某种通知机制来告诉目标子进程有新任务需要处理,并传递必要的数据。
最简单的办法:在父子进程之间预先建立好一条管道,然后通过该管道来实现所有的进程间通信(预先定义好协议来规范管道的使用)
(父子线程间就可以直接用全局变量)
处理多客户:
问题1:
监听socket和连接socket是否都由主进程来统一管理?
半同步/半反应堆模式是由主进程统一管理这两种socket的
高效的半同步/半异步模式以及领导者/追随者模式则是由主进程管理所有监听socket,而各个子进程分别管理属于自己的连接socket的。
对于情况(1),主进程接受新的连接以得到连接socket,然后它需要将该socket传递给子进程(对于线程池而言,父线程将socket)传递给子线程是很简单的,因为他们可以很容易的共享该socket而父进程文件描述符的传递就要靠之前学习的知识来实现了)
对于情况(2),子进程自己调用accept来接受新的连接,这样父进程就无需向子进程传递socket,而只需要简单的通知一声"我检测到新的连接,你来接受它。"
半同步/半异步并发模式的进程池
我们将接受新连接的操作放到子进程中,很显然,对于这种模式而言,一个客户连接上的所有任务始终是由一个子进程来处理的。
我看的进程池是用C++实现的,我先说框架,后面我会努力改成C的
类1:子进程类
存放数据:
目标子进程PID
通道m_pipe
类2:进程池类
存放数据:
(1)进程池允许的最大子进程数
(2)每个子进程最多能处理客户数量
(3)epoll最多能处理的事件数
(4)进程池中的进程总数
(5)子进程在池中的序号,从0开始
(6)每个进程都有一个epoll内核事件表,用m_epollfd标识
(7)监听socket
(8)标识是否停止运行
(9)进程指针保存子进程的描述信息
(10)进程池静态实力
函数:
(1)初始化,创建一个线程池
(2)销毁线程池
(3)统一事件源
(4)启动父进程
(5)启动子进程
(6)run启动进程池
3:由于用了epoll所以需要epoll的一系列函数
4:信号处理函数
5:信号添加函数
问题一:如何存放子进程
首先,父子进程之间是一定要通信的,而子进程都有自己的pid那么
可以设置一个结构体
struct process
{
pid_t pid;
int pipefd[2];
}
通过这个结构体构造一个数组,然后里面放的就是各个进程,通过进程的pid来找到相应子进程来调用子进程的通道进行通信.
问题二:关于进程池如何唤醒自己相应的子进程
使用的是epoll监听事件,首先确定自己要监听子进程的什么事件然后再注册事件
使用run_child函数去跑第i个子进程,然后再主函数中通过for(epoll_wait返回的是活跃客户端的个数)对活跃的事件进行一一处理
用进程池存放好子进程的pid和管道,有新的链接来的时候就使用池子里的子进程去完成操作