# 编写一个master - worker 进程管理模型 Ps :撇开一切web server的束缚, 自己设计自己测试 #

转自http://www.cnblogs.com/owenliang/archive/2011/12/05/2276264.html

众所周知, 因为现在各个UNIX分支均提供了高效了"I/O复用"接口, 比如Linux下常用的epoll, 支持N万连接仍可保证高效的事件通知 . 

也正是因为从以前select这种传统的事件驱动接口的存在, 所以依靠的事件驱动思想进行异步服务器开发的思想遍地开花 .

Apache是靠进程/线程的数量来撑起并发接入量的, 也就是绝大多数初次接触网络编程的朋友常用的技巧, 一个Client对应一个Thread提供服务.

但Apache随着时代的进步也在不断的改变架构, 一直没有脱离 "量变" 这个死胡同. 但是, 它先提出prefork的进程池模型, 接着master-worker+multi-threads

应运而出, 而这个设计思想却是现在主流web服务器的基本组成部分, 不得不提得一个词 : “加锁的accept” 也是apache的独门秘籍, 这个秘籍防止了"多worker时的accept的惊群问题" .

 

废话一堆, 必须回到主题, 也就是master进程如何管理worker进程的死活, 起码包括以下功能:

1, 服务器启动之后,master平稳的启动N个worker进程,也就是worker进程的数目逐渐增加,而不是一次性全部fork完成.

2, master进程在运行过程中, 随时监控子进程的退出状态, 也就是需要调用wait/waitpid等待子进程的退出, 包括合理的和意外的两种退出方式.

3, master进程必须接受用户从Shell发送来的各种信号, 为了演示效果, 大概包括以下信号:

① SIGHUP : 重启服务器信号, 作用是平稳的杀掉原来的所有worker, 并且平稳的启动新的worker , 默认就是优雅的重启, 目的是服务器重启时对客户足够友好 , 防止颠簸严重而影响服务.

② SIGINT  : 优雅的关闭服务器, 作用是等待所有worker完成手头上的任务, 之后服务器退出.

③ SIGTERM : 暴力的关闭服务器, 作用是无论worker当前是否仍旧有任务, worker都应当立即退出.

 

Ps一下 : 对于②,③两种情况,  我决定必须给它们一个期限, 因为哪一个管理员也不希望自己关闭服务器, 而服务器却迟迟不肯关闭这种怪事情发生, 所以期限一到立即发送SIGKILL给所有worker, 强行杀死进程. (SIGKILL是最凶猛的信号,无法捕获)

 

下面是第一版代码, 暂时没有写"退出限期" 这个逻辑, 除此之外, 实现了"平稳的重启", "平稳的启动".

之所以要写这个框架, 原因很简单, 因为我看大师级别的开源代码对这一部分看得比较头疼, 虽说过程都懂, 但大师写代码逻辑超前, 结果很不容易理解透彻, 所以只有按照自己的

想法去实现, 然后体会难点与差异, 会遇到什么问题, 而且带入了自己的很多想法, 比如愚蠢的用户会连续的KILL各种信号,  比如一开始想重启SIGHUP,接着又SIGINT,SIGHUP,SIGTERM,

乱发一通, 我的代码必须严谨的对待这些情况.

 

先展示程序运行效果:

 

效果图① :  手动杀死某个worker进程 , 这是为了测试worker进程意外崩溃的情况,


part1 : master进程pid == 3000, 之后跟随10个worker进程.

./main &  // 我在一个终端里后台运行服务器

 

//重新打开一个终端做测试(我是使用secureCRT)

linux-7lsl:~ # ps aux | grep main | grep -v grep
1000 10985 0.0 0.0 3000 700 pts/3 S 11:13 0:00 ./main
1000 10986 0.0 0.0 3004 100 pts/3 S 11:13 0:00 ./main
1000 10987 0.0 0.0 3004 100 pts/3 S 11:13 0:00 ./main
1000 10988 0.0 0.0 3004 100 pts/3 S 11:13 0:00 ./main
1000 10989 0.0 0.0 3004 100 pts/3 S 11:13 0:00 ./main
1000 10990 0.0 0.0 3004 100 pts/3 S 11:13 0:00 ./main
1000 10991 0.0 0.0 3004 100 pts/3 S 11:13 0:00 ./main
1000 10992 0.0 0.0 3004 100 pts/3 S 11:13 0:00 ./main
1000 10993 0.0 0.0 3004 100 pts/3 S 11:13 0:00 ./main
1000 10994 0.0 0.0 3004 100 pts/3 S 11:13 0:00 ./main
1000 10995 0.0 0.0 3004 100 pts/3 S 11:13 0:00 ./main

part2 : 使用无敌的KILL信号杀死最后一个worker进程 ,再次观察进程列表, 发现一个崭新的worker进程11167被创建

很奇怪的一点是11167的父进程竟然是3000而不是3004, 这就是操作系统的事情了,我们再次ps 看一下就变成3004了.


linux-7lsl:~ # kill -SIGKILL 10995
linux-7lsl:~ # ps aux | grep main | grep -v grep
1000 10985 0.0 0.0 3000 700 pts/3 S 11:13 0:00 ./main
1000 10986 0.0 0.0 3004 100 pts/3 S 11:13 0:00 ./main
1000 10987 0.0 0.0 3004 100 pts/3 S 11:13 0:00 ./main
1000 10988 0.0 0.0 3004 100 pts/3 S 11:13 0:00 ./main
1000 10989 0.0 0.0 3004 100 pts/3 S 11:13 0:00 ./main
1000 10990 0.0 0.0 3004 100 pts/3 S 11:13 0:00 ./main
1000 10991 0.0 0.0 3004 100 pts/3 S 11:13 0:00 ./main
1000 10992 0.0 0.0 3004 100 pts/3 S 11:13 0:00 ./main
1000 10993 0.0 0.0 3004 100 pts/3 S 11:13 0:00 ./main
1000 10994 0.0 0.0 3004 100 pts/3 S 11:13 0:00 ./main
1000 11167 0.0 0.0 3000 100 pts/3 S 11:19 0:00 ./main 

part3 : 再做一次一样的测试 , 完全和part2测试效果一样.

linux-7lsl:~ # kill -SIGKILL 10990

linux-7lsl:~ # ps aux | grep main | grep -v grep
1000 10985 0.0 0.0 3000 700 pts/3 S 11:13 0:00 ./main
1000 10986 0.0 0.0 3004 100 pts/3 S 11:13 0:00 ./main
1000 10987 0.0 0.0 3004 100 pts/3 S 11:13 0:00 ./main
1000 10988 0.0 0.0 3004 100 pts/3 S 11:13 0:00 ./main
1000 10989 0.0 0.0 3004 100 pts/3 S 11:13 0:00 ./main
1000 10991 0.0 0.0 3004 100 pts/3 S 11:13 0:00 ./main
1000 10992 0.0 0.0 3004 100 pts/3 S 11:13 0:00 ./main
1000 10993 0.0 0.0 3004 100 pts/3 S 11:13 0:00 ./main
1000 10994 0.0 0.0 3004 100 pts/3 S 11:13 0:00 ./main
1000 11167 0.0 0.0 3004 100 pts/3 S 11:19 0:00 ./main  //11167 这次变成3004了
1000 11174 0.0 0.0 3000 100 pts/3 S 11:19 0:00 ./main  // 又是3000哦, 再ps看一次肯定也是3004了,谁知道操作系统在做什么呢.

 

效果图② : 这个测试比较简单, 测试的是优雅关闭SIGINT.

linux-7lsl:~ # ps aux | grep main | grep -v grep
1000 10985 0.0 0.0 3000 700 pts/3 S 11:13 0:00 ./main
1000 10986 0.0 0.0 3004 100 pts/3 S 11:13 0:00 ./main
1000 10987 0.0 0.0 3004 100 pts/3 S 11:13 0:00 ./main
1000 10988 0.0 0.0 3004 100 pts/3 S 11:13 0:00 ./main
1000 10989 0.0 0.0 3004 100 pts/3 S 11:13 0:00 ./main
1000 10991 0.0 0.0 3004 100 pts/3 S 11:13 0:00 ./main
1000 10992 0.0 0.0 3004 100 pts/3 S 11:13 0:00 ./main
1000 10993 0.0 0.0 3004 100 pts/3 S 11:13 0:00 ./main
1000 10994 0.0 0.0 3004 100 pts/3 S 11:13 0:00 ./main
1000 11167 0.0 0.0 3004 100 pts/3 S 11:19 0:00 ./main
1000 11174 0.0 0.0 3004 100 pts/3 S 11:19 0:00 ./main
linux-7lsl:~ # kill -SIGINT 10985
linux-7lsl:~ # ps aux | grep main | grep -v grep
linux-7lsl:~ #

 

效果图③ : 同上,这次测试非优雅关闭, 必须PS一下: 我的程序里优雅和非优雅都是一样的, 而真正的服务器是如何体现它们的区别的呢?

对于优雅关闭/重启 : worker进程关闭所有的监听套接字, 处理在线用户的剩余请求, 一直到在线用户数为0 ,则exit .

对于非优雅关闭/重启 : worker进程立即exit , 这也就是不优雅的原因, 在线用户得到了一个掉线的不完美体验.

和发送SIGINT是一样的结果,也就是所有进程退出了.

linux-7lsl:~ # ps aux | grep main | grep -v grep
root 11498 0.0 0.0 3000 700 pts/3 S 11:36 0:00 ./main
root 11499 0.0 0.0 3004 100 pts/3 S 11:36 0:00 ./main
root 11500 0.0 0.0 3004 100 pts/3 S 11:36 0:00 ./main
root 11501 0.0 0.0 3004 100 pts/3 S 11:36 0:00 ./main
root 11502 0.0 0.0 3004 100 pts/3 S 11:36 0:00 ./main
root 11503 0.0 0.0 3004 100 pts/3 S 11:36 0:00 ./main
root 11504 0.0 0.0 3004 100 pts/3 S 11:36 0:00 ./main
root 11505 0.0 0.0 3004 100 pts/3 S 11:36 0:00 ./main
root 11506 0.0 0.0 3004 100 pts/3 S 11:36 0:00 ./main
root 11507 0.0 0.0 3004 100 pts/3 S 11:36 0:00 ./main
root 11508 0.0 0.0 3004 100 pts/3 S 11:36 0:00 ./main
linux-7lsl:~ # kill -SIGTERM 11498
linux-7lsl:~ # ps aux | grep main | grep -v grep
linux-7lsl:~ #


效果图④ : 重头戏总是留到最后, 其实我认为整个master-worker进程管理的逻辑复杂点就在于重启, 代码里也有注释原因, 不过对于读者可能不便于理解 ,

我在这里概要的描述一下重点:

SIGHUP信号的作用是缓慢的关闭当前的worker进程, 然后缓慢的启动新的worker进程. 

举个生动的例子: 人是细胞组成的, 人的细胞会代谢更新, 如果你的细胞先全部死光, 然后再全部重生, 人也就升天了~.~  所以我们的服务器也要避免这一个做法.

再举个同样生动的例子: 人的细胞更新换代, 刚刚出生的细胞难道也要和那些旧死的细胞一起更新么?  当然不要, 我们吃那么多粮食可不是拿来给人体造细胞玩的.

对应到服务器程序里,也就是上一批旧worker进程还没有全部更新完毕之前, 我们不再受理新的更新请求, 否则结果就是新的worker又被消灭, 这样颠簸的重启效果

我们谁也不想见到.


下面看实际效果吧:

part1 : 仔细关注除了第一个master进程外的10个worker进程pid。

linux-7lsl:~ # ps aux | grep main | grep -v grep
1000 11939 0.0 0.0 3000 704 pts/3 S 11:55 0:00 ./main
1000 11940 0.0 0.0 3004 104 pts/3 S 11:55 0:00 ./main
1000 11941 0.0 0.0 3004 104 pts/3 S 11:55 0:00 ./main
1000 11942 0.0 0.0 3004 104 pts/3 S 11:55 0:00 ./main
1000 11943 0.0 0.0 3004 104 pts/3 S 11:55 0:00 ./main
1000 11944 0.0 0.0 3004 104 pts/3 S 11:55 0:00 ./main
1000 11945 0.0 0.0 3004 104 pts/3 S 11:55 0:00 ./main
1000 11946 0.0 0.0 3004 104 pts/3 S 11:55 0:00 ./main
1000 11947 0.0 0.0 3004 104 pts/3 S 11:55 0:00 ./main
1000 11948 0.0 0.0 3004 104 pts/3 S 11:55 0:00 ./main
1000 11949 0.0 0.0 3004 104 pts/3 S 11:55 0:00 ./main

 

part2 : 子进程全部更新换代结束, 程序的确比较快, 我们观察不到迭代过程.

不过严谨的代码逻辑仍旧不能忽视, 因为真正的服务器可不是像我们这样的空loop,而是要处理很多I/O事件的.

linux-7lsl:~ # kill -SIGHUP 11939 
linux-7lsl:~ # ps aux | grep main | grep -v grep
1000 11939 0.0 0.0 3000 704 pts/3 S 11:55 0:00 ./main
1000 11978 0.0 0.0 3000 104 pts/3 S 11:56 0:00 ./main
1000 11979 0.0 0.0 3000 104 pts/3 S 11:56 0:00 ./main
1000 11980 0.0 0.0 3000 104 pts/3 S 11:56 0:00 ./main
1000 11981 0.0 0.0 3000 104 pts/3 S 11:56 0:00 ./main
1000 11982 0.0 0.0 3000 104 pts/3 S 11:56 0:00 ./main
1000 11983 0.0 0.0 3000 104 pts/3 S 11:56 0:00 ./main
1000 11984 0.0 0.0 3000 104 pts/3 S 11:56 0:00 ./main
1000 11985 0.0 0.0 3000 104 pts/3 S 11:56 0:00 ./main
1000 11986 0.0 0.0 3000 104 pts/3 S 11:56 0:00 ./main
1000 11987 0.0 0.0 3000 104 pts/3 S 11:56 0:00 ./main

part3 : 对比上下两段, 发现已经全部稳定了.

linux-7lsl:~ # ps aux | grep main | grep -v grep
1000 11939 0.0 0.0 3000 704 pts/3 S 11:55 0:00 ./main
1000 11978 0.0 0.0 3004 104 pts/3 S 11:56 0:00 ./main
1000 11979 0.0 0.0 3004 104 pts/3 S 11:56 0:00 ./main
1000 11980 0.0 0.0 3004 104 pts/3 S 11:56 0:00 ./main
1000 11981 0.0 0.0 3004 104 pts/3 S 11:56 0:00 ./main
1000 11982 0.0 0.0 3004 104 pts/3 S 11:56 0:00 ./main
1000 11983 0.0 0.0 3004 104 pts/3 S 11:56 0:00 ./main
1000 11984 0.0 0.0 3004 104 pts/3 S 11:56 0:00 ./main
1000 11985 0.0 0.0 3004 104 pts/3 S 11:56 0:00 ./main
1000 11986 0.0 0.0 3004 104 pts/3 S 11:56 0:00 ./main
1000 11987 0.0 0.0 3004 104 pts/3 S 11:56 0:00 ./main


part4 : 顺便在这里做一个综合测试吧, 我们杀掉一个新的worker进程,看看master是否能够正确的重启拉起.

对比上下两段数据的加粗部分, 结果显而易见.

linux-7lsl:~ # kill -SIGKILL 11986
linux-7lsl:~ # ps aux | grep main | grep -v grep
1000 11939 0.0 0.0 3000 704 pts/3 S 11:55 0:00 ./main
1000 11978 0.0 0.0 3004 104 pts/3 S 11:56 0:00 ./main
1000 11979 0.0 0.0 3004 104 pts/3 S 11:56 0:00 ./main
1000 11980 0.0 0.0 3004 104 pts/3 S 11:56 0:00 ./main
1000 11981 0.0 0.0 3004 104 pts/3 S 11:56 0:00 ./main
1000 11982 0.0 0.0 3004 104 pts/3 S 11:56 0:00 ./main
1000 11983 0.0 0.0 3004 104 pts/3 S 11:56 0:00 ./main
1000 11984 0.0 0.0 3004 104 pts/3 S 11:56 0:00 ./main
1000 11985 0.0 0.0 3004 104 pts/3 S 11:56 0:00 ./main
1000 11987 0.0 0.0 3004 104 pts/3 S 11:56 0:00 ./main
1000 12049 0.0 0.0 3000 104 pts/3 S 11:59 0:00 ./main


part5 : 我们最后终结服务器, 完成这个程序的测试.

linux-7lsl:~ # ps aux | grep main | grep -v grep
1000 11745 0.1 0.4 7764 4180 pts/3 T 11:45 0:01 vim main.cpp
1000 11939 0.0 0.0 3000 704 pts/3 S 11:55 0:00 ./main
1000 11978 0.0 0.0 3004 104 pts/3 S 11:56 0:00 ./main
1000 11979 0.0 0.0 3004 104 pts/3 S 11:56 0:00 ./main
1000 11980 0.0 0.0 3004 104 pts/3 S 11:56 0:00 ./main
1000 11981 0.0 0.0 3004 104 pts/3 S 11:56 0:00 ./main
1000 11982 0.0 0.0 3004 104 pts/3 S 11:56 0:00 ./main
1000 11983 0.0 0.0 3004 104 pts/3 S 11:56 0:00 ./main
1000 11984 0.0 0.0 3004 104 pts/3 S 11:56 0:00 ./main
1000 11985 0.0 0.0 3004 104 pts/3 S 11:56 0:00 ./main
1000 11987 0.0 0.0 3004 104 pts/3 S 11:56 0:00 ./main
1000 12049 0.0 0.0 3000 104 pts/3 S 11:59 0:00 ./main
linux-7lsl:~ # kill -SIGINT 11939
linux-7lsl:~ # ps aux | grep main | grep -v grep
1000 11745 0.1 0.4 7764 4180 pts/3 T 11:45 0:01 vim main.cpp
linux-7lsl:~ #


好了, 下面是代码, 不做讲解了, 大家根据注释尽量的阅读吧, 必须PS一下: 这完全是我个人设计的,基本没有模仿现有的web服务器, 很多逻辑都是自己思考的, 严谨的对待了用户随意KILL的情况, 如果你能读懂细节的话, 或者自己也去尝试编写, 应该会体会到我的用意.

Fixup:

  ① 提交之前vim没有:w ,提交了一个BUG版 ,  现已修正 , 问题出现在srv_restart的判定语句逻辑不正确,如果不是高频率KILL sighup将无法重现该bug.

 

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/time.h>
#include <sys/wait.h>
#include <string.h>

volatile sig_atomic_t srv_restart = 0;
volatile sig_atomic_t srv_graceful_end = 0;
volatile sig_atomic_t srv_ungraceful_end = 0;
volatile sig_atomic_t sigalrm_recved = 0;

void signal_handler(int signo)
{
switch (signo)
{
case SIGTERM:
srv_ungraceful_end = 1;
break;
case SIGINT:
srv_graceful_end = 1;
break;
case SIGHUP:
srv_restart = 1;
break;
case SIGALRM:
sigalrm_recved = 1;
break;
}
}

void child_loop()
{
sigalrm_recved = 0;

while (true)
{
sleep(5);
printf("worker pid : %d\n\n", getpid());

// 这里没有上下文,就不演示grace/ungrace的区别了
if (srv_ungraceful_end || srv_graceful_end || srv_restart)
{
exit(0);
}

// worker能否继承定时器呢,我自己做个测试,因为懒得翻APUE.
if (sigalrm_recved)
{
printf("worker pid : %d can recv alarm\n", getpid());
}
}
}

typedef struct
{
pid_t pid; //default 0
int state; //default 0 , 表示空闲
}proc_info;

#define CHILD_NUM 10
proc_info child_info[CHILD_NUM];


int main()
{
int num_children = 0;
int restart_finished = 1;
int is_child = 0;

// register signal handler
struct sigaction act;
bzero(&act, sizeof(act));
act.sa_handler = signal_handler;

sigaction(SIGALRM, &act, NULL);
sigaction(SIGTERM, &act, NULL);
sigaction(SIGINT , &act, NULL);
sigaction(SIGHUP , &act, NULL);

// 设置信号屏蔽字,阻塞除此4信号外所有信号
sigset_t set;
sigfillset(&set);
sigdelset(&set, SIGALRM);
sigdelset(&set, SIGTERM);
sigdelset(&set, SIGINT);
sigdelset(&set, SIGHUP);

sigprocmask(SIG_SETMASK, &set, NULL);

// 设置定时器, 用于控制master进程的loop频率
// 定时不同,意味着master进程监管worker进程的实时性
// 这里设置为1秒

struct itimerval timer;
timer.it_value.tv_sec = 1;
timer.it_value.tv_usec = 0;
timer.it_interval.tv_sec = 1;
timer.it_interval.tv_usec = 0;

setitimer(ITIMER_REAL, &timer, NULL);

while (true)
{
pid_t pid;

if (num_children != 0)
{
pid = wait(NULL);

if (pid != -1)
{
num_children--;

for (int i = 0; i < CHILD_NUM; ++ i)
{
if (child_info[i].pid == pid)
{
child_info[i].pid = 0;
child_info[i].state = 0;
break;
}
}
}
}

if (srv_graceful_end || srv_ungraceful_end)
{
if (num_children == 0)
{
break; //get out of while(true)
}

for (int i = 0; i < CHILD_NUM; ++ i)
{
if (child_info[i].pid != 0)
{
// 假设用户递交了SIGINT和SIGTERM这种变态情况
// 优先选择优雅退出SIGINT
kill(child_info[i].pid, srv_graceful_end ? SIGINT : SIGTERM);
}
}

// 假设用户递交了SIGINT/SIGTERM,并且也提交了SIGHUP这种变态情况
// 我决定忽略SIGHUP, 也就是在重启和关闭之间选择了关闭
continue; //this is necessary.
}

// 服务器的重启是优雅并且平稳的
// 每一轮循环, 只对其中一个旧worker发送SIGHUP
// 大概的意思 : 关闭一个旧的才会重启一个新的
// restart_finished很重要,目的是防止用户
// 连续递送多个SIGHUP信号,那将不断的导致
// 挂断我们新启动的worker,而这是毫无意义的
// 所以,在上一次重启没结束前,不接纳新的SIGHUP

if (srv_restart)
{
srv_restart = 0;

if (restart_finished)
{
restart_finished = 0;

for (int i = 0; i < CHILD_NUM; ++ i)
{
if (child_info[i].pid == 2) //每个正在工作的worker
{
child_info[i].state = 1; //1表示待重启
}
}
}
}

if (!restart_finished)
{
int ndx;

for (ndx = 0; ndx < CHILD_NUM; ++ ndx)
{
if (child_info[ndx].state == 1)
{
break;
}
}

// 上一次发送SIGHUP时正处于工作的worker已经全部
// 重启完毕.
if (ndx == CHILD_NUM)
{
restart_finished = 1;
}
else
{
kill(child_info[ndx].pid, SIGHUP);
//child_info[ndx].state = 3; 重启中

//还是为了尽量避免连续的SIGHUP的不良操作带来的颠簸
//,所以决定取消(3重启中)这个状态
//并不是说连续SIGHUP会让程序出错,只是不断的挂掉新进程很愚蠢
}
}

// 信号处理结束,尽可能多得启动worker进程
// 这种想法是合理的,重启时的关闭应该平稳缓慢
// 但worker进程应该尽快拉起以便恢复服务

for (int i = 0; i < CHILD_NUM; ++ i)
{
// 启动所有空闲的worker
if (child_info[i].state == 0)
{
pid = fork();

switch (pid)
{
case -1:
break;
case 0:
is_child = 1;
break;
default:
++ num_children;
child_info[i].state = 2;
child_info[i].pid = pid;
break;
}

if (!pid) break; //子进程
}
}

if (!pid) break; //子进程跳出while
}

if (!is_child) //父进程从while跳出,一定是srv_graceful/ungraceful_end
{
exit(0); //父进程退出
}

//子进程执行自己的child_loop;

child_loop();

return 0; //both master and worker never reach here
}


posted @ 2011-12-24 21:01  balaamwe  阅读(526)  评论(0编辑  收藏  举报