【tag-006】Linux C++ 网络编程架构之创建工作子进程

本节我们根据配置文件的配置,为我们的服务端程序创建多个工作子进程。

1. 配置

在配置文件中配置我们希望启动的工作子进程的个数,在 nginx_sim.conf 文件添加 WorkerProcesses 配置项

# 注释行
# 每个有效配置项用 = 处理,= 前不超过40字符,= 后不超过400字符

# 以 [ 开头表示组信息,也等价于注释行
Log = logs/error2.log
LogLevel = 5

#表示程序是否以守护进程启动
Daemon = 1
WorkerProcesses = 4

2. 创建工作进程

将 master 与 worker 进程的启动放在函数 ngx_master_process_cycle() 中,则在 main 函数只需要调用这个函数,便启动 master 和多个 worker 子进程了。在 proc 目录新建一个 ngx_process_cycle.cxx 文件,master 和 worker 进程启动相关的函数都放在此处。函数的声明仍然实在 ngx_func.h 中。

  • ngx_master_process_cycle() 实现
static u_char master_process[] = "master process";
void ngx_master_process_cycle()
{
    sigset_t set;
    sigemptyset(&set);
    //屏蔽信号,防止信号的干扰;需要可以再加
    sigaddset(&set,SIGCHLD); // 子进程状态改变
    sigaddset(&set, SIGALRM);     //定时器超时
    sigaddset(&set, SIGIO);       //异步I/O
    sigaddset(&set, SIGINT);      //终端中断符
    sigaddset(&set, SIGHUP);      //连接断开
    sigaddset(&set, SIGUSR1);     //用户定义信号
    sigaddset(&set, SIGUSR2);     //用户定义信号
    sigaddset(&set, SIGWINCH);    //终端窗口大小改变
    sigaddset(&set, SIGTERM);     //终止
    sigaddset(&set, SIGQUIT);     //终端退出符

    if(-1 == sigprocmask(SIG_BLOCK,&set,NULL))
    {
        ngx_log_error_core(NGX_LOG_ALERT,errno,"ngx_master_process_cycle()中sigprocmask()失败!");
    }

    //设置主进程的标题
    size_t len = sizeof(master_process) + g_argvlen;
    if(len < 1000) //防止标题过长
    {
        char title[1000] = {0};
        strcpy(title,(const char*)master_process);
        strcat(title," ");
        for(int i = 0;i<g_os_argc;++i)
        {
            strcat(title,g_os_argv[i]);
        }
        //设置
        ngx_setproctitle(title);
        ngx_log_error_core(NGX_LOG_NOTICE,0,"%s %p 启动并开始运行......!",title,ngx_pid);
    }
    //根据配置文件的配置开始创建子进程
    int workprocess = CConfig::GetInstance()->GetIntDefault("WorkerProcesses",1);//默认返回1

    //创建 woreprocess 个子进程
    ngx_start_worker_processes(workprocess); 

    //master 进程到这后就不需要再屏蔽信号
    sigemptyset(&set);
    //master 进程挂起,等待信号
    for(;;)
    {
        sigsuspend(&set);
        sleep(1);
        //有需要再扩充
    }

    return;
}

代码说明:

  1. 设置的master进程标题
  2. 从配置文件读取了worker子进程数量配置(没有配置则返回默认配置1)
  3. 利用函数 ngx_start_worker_processes 创建 worker 子进程
  4. 解除信号屏蔽后,master进程处于挂起状态,目前我们的程序什么也不做,以后再扩展

那么,接下来需要重点关注子进程的创建,即 ngx_start_worker_processes 函数的实现,代码如下

static void ngx_start_worker_processes(int threadnums)
{
    for(int i = 0;i<threadnums;++i)
    {
        //创建子进程
        ngx_spawn_process(i,"worker process");
    }

    return;
}

由代码看出,在一个for循环中通过 ngx_spawn_process 一个一个的创建worker进程。子进程的创建代码如下:

/**
 * @brief 产生一个子进程
 * 
 * @param threadnums : 进程编号【0开始】
 * @param pprocname  : 子进程名字"worker process"
 * @return int :失败返回-1
 */
static int ngx_spawn_process(int threadnums,const char *pprocname)
{
    pid_t  pid;

    pid = fork(); //fork()系统调用产生子进程
    switch (pid)  //pid判断父子进程,分支处理
    {  
    case -1: //产生子进程失败
        ngx_log_error_core(NGX_LOG_ALERT,errno,"ngx_spawn_process()fork()产生子进程num=%d,procname=\"%s\"失败!",threadnums,pprocname);
        return -1;

    case 0:  //子进程分支
        ngx_parent = ngx_pid;              //因为是子进程了,所有原来的pid变成了父pid
        ngx_pid = getpid();                //重新获取pid,即本子进程的pid
        //子进程工作的函数,一直在里面循环
        ngx_worker_process_cycle(threadnums,pprocname);    
        break;

    default: //这个应该是父进程分支,直接break;,流程往switch之后走            
        break;
    }//end switch
    //只有master进程才会到这里
    //若有需要,以后再扩展增加其他代码......
    return pid;
}

/**
 * @brief worker子进程的功能函数,每个woker子进程,就在这里循环着了(无限循环【处理网络事件和定时器事件以对外提供web服务】)
 * 
 * @param inum 进程编号【0开始】
 * @param pprocname 进程名
 */
static void ngx_worker_process_cycle(int inum,const char *pprocname)
{
    //标记为是worker进程
    ngx_process = NGX_PROCESS_WORKER;  
    ngx_worker_process_init(inum);
    ngx_setproctitle(pprocname); //设置标题   
    ngx_log_error_core(NGX_LOG_NOTICE,0,"%s %p 启动并开始运行......!",pprocname,ngx_pid);
    for(;;)
    {
        //暂时什么也不做
        sleep(1);
    }
    return;
}

//子进程初始化,取消信号屏蔽
static void ngx_worker_process_init(int inum)
{
    sigset_t  set;      //信号集

    sigemptyset(&set);  //清空信号集
    if (-1 == sigprocmask(SIG_SETMASK, &set, NULL))  //原来是屏蔽那10个信号【防止fork()期间收到信号导致混乱】,现在不再屏蔽任何信号【接收任何信号】
    {
        ngx_log_error_core(NGX_LOG_ALERT,errno,"ngx_worker_process_init()中sigprocmask()失败!");
    }
      
    //....将来再扩充代码
    return;
}

代码说明:

  1. 最原始的父进程在创建master进程后退出了,程序的master进程被标记为父进程
  2. 给每个子进程也设置了标题
  3. 目前我们的子进程也什么都没做,也需要在后面扩展

到此, 我们的程序已经可以启动 master 进程和多个 worker 子进程了(此处我们配置了4个worker子进程)。运行程序可以查看进程数,如下:
在这里插入图片描述
但是,让我们试着杀死 worker 子进程时看看会发生什么?
从上图可以看到其中一个 worker 进程的 id 为 14344, 我们强制 kill 这个进程

kill -9 14344

在这里插入图片描述
可以发现,这个被我们 kill 的进程 STAT 变成了 Z, 也就是说它成了一个僵尸进程。因此我们的程序还需要解决进程被杀死时,避免其成为僵尸进程的问题。

3. 避免僵尸进程

需要知道,子进程在退出时,父进程会收到来自子进程的 SIGCHLD 信号,然后利用 waitpid 函数避免子进程成为僵尸进程;因此,我们的程序需要自定义信号处理函数。在目录 signal 下创建 ngx_sinal.cxx 文件,信号处理相关的函数都放在这里。

如下定义一个结构,列出需要处理的信号

typedef struct 
{
    int signo;
    // 字符串表示的信号名称
    const char* signame;
    //信号处理的函数指针:固定写法
    void (*handler)(int signo,siginfo_t* siginfo,void* ucontext);
}ngx_signal_t;

//定义信号
ngx_signal_t signals[] = 
{
    // signo      signame             handler
    { SIGHUP,    "SIGHUP",           ngx_signal_handler },        //终端断开信号,对于守护进程常用于reload重载配置文件通知--标识1
    { SIGINT,    "SIGINT",           ngx_signal_handler },        //标识2   
	{ SIGTERM,   "SIGTERM",          ngx_signal_handler },        //标识15
    { SIGCHLD,   "SIGCHLD",          ngx_signal_handler },        //子进程退出时,父进程会收到这个信号--标识17
    { SIGQUIT,   "SIGQUIT",          ngx_signal_handler },        //标识3
    { SIGIO,     "SIGIO",            ngx_signal_handler },        //指示一个异步I/O事件【通用异步I/O信号】
    { SIGSYS,    "SIGSYS, SIG_IGN",  NULL               },        //我们想忽略这个信号,SIGSYS表示收到了一个无效系统调用,如果我们不忽略,进程会被操作系统杀死,--标识31
                                                                  //所以我们把handler设置为NULL,代表 我要求忽略这个信号,请求操作系统不要执行缺省的该信号处理动作(杀掉我)
    //...日后根据需要再继续增加
    { 0,         NULL,               NULL               }         //信号对应的数字至少是1,所以可以用0作为一个特殊标记
};

为我们的程序定义一个信号处理函数 ngx_signal_handler

static void ngx_signal_handler(int signo,siginfo_t* siginfo,void* ucontext)
{
    ngx_signal_t    *sig;    //自定义结构
    char            *action; //一个字符串,用于记录一个动作字符串以往日志文件中写
    
    for (sig = signals; sig->signo != 0; sig++) //遍历信号数组    
    {         
        //找到对应信号,即可处理
        if (sig->signo == signo) 
        { 
            break;
        }
    } //end for

    action = (char *)"";  //目前还没有什么动作;

    if(ngx_process == NGX_PROCESS_MASTER)      //master进程,管理进程,处理的信号一般会比较多 
    {
        //master进程的往这里走
        switch (signo)
        {
        case SIGCHLD:  //一般子进程退出会收到该信号
            ngx_reap = 1;  //标记子进程状态变化,日后master主进程的for(;;)循环中可能会用到这个变量【比如重新产生一个子进程】
            break;

        //.....其他信号处理以后待增加

        default:
            break;
        } //end switch
    }
    else if(ngx_process == NGX_PROCESS_WORKER) //worker进程,具体干活的进程,处理的信号相对比较少
    {
        //worker进程的往这里走
        //......以后再增加
        //....
    }
    else
    {
        //非master非worker进程,先啥也不干
        //do nothing
    } //end if(ngx_process == NGX_PROCESS_MASTER)

    //这里记录一些日志信息
    //siginfo这个
    if(siginfo && siginfo->si_pid)  //si_pid = sending process ID【发送该信号的进程id】
    {
        ngx_log_error_core(NGX_LOG_NOTICE,0,"signal %d (%s) received from %P%s", signo, sig->signame, siginfo->si_pid, action); 
    }
    else
    {
        ngx_log_error_core(NGX_LOG_NOTICE,0,"signal %d (%s) received %s",signo, sig->signame, action);//没有发送该信号的进程id,所以不显示发送该信号的进程id
    }

    //.......其他需要扩展的将来再处理;

    //子进程状态有变化,通常是意外退出
    if (signo == SIGCHLD) 
    {
        ngx_process_get_status(); //获取子进程的结束状态
    } //end if

    return;
}

代码说明:
程序在收到一个信号后会在自定义的信号数字中查找是否是需要处理的信号,当 master 进程收到 SIGCHLD 信号,ngx_reap 会被标记,此变量目前仅起标记作用,便于以后扩展,然后直接交与 ngx_process_get_status 处理,目前程序没有做其他的处理,以后再做扩展。

接着看看 ngx_process_get_status 函数是如何避免子进程成为僵尸进程的。

static void ngx_process_get_status()
{
    pid_t   pid;
    int     status;
    int     err;
    int     one = 0; //标记

    while(1)
    {
        /** 避免子进程成为僵尸进程
         * 第一个参数为-1,表示等待任何子进程,第二个参数:保存子进程的状态信息,
         * 第三个参数:提供额外选项,WNOHANG表示不要阻塞,让这个waitpid()立即返回
        */
        pid = waitpid(-1,&status,WNOHANG);
        if(0 == pid)
        {
            return;
        }
        else if(-1 == pid)//发生错误
        {
            err = errno;
            switch(err)
            {
            case EINTR://调用被某个信号中断
                continue;
            case ECHILD:
                if(one)
                {
                    break;
                }
                ngx_log_error_core(NGX_LOG_INFO,err,"waitpid() failed!");
                break; 
            }
            ngx_log_error_core(NGX_LOG_ALERT,err,"waitpid() failed!");   
            return;
        }

        one = 1;
        if(WTERMSIG(status))  //获取使子进程终止的信号编号
        {
            ngx_log_error_core(NGX_LOG_ALERT,0,"pid = %P exited on signal %d!",pid,WTERMSIG(status)); //获取使子进程终止的信号编号
        }
        else
        {
            ngx_log_error_core(NGX_LOG_NOTICE,0,"pid = %P exited with code %d!",pid,WEXITSTATUS(status)); //WEXITSTATUS()获取子进程传递给exit或者_exit参数的低八位
        }
    }
}

从代码看出,这个函数的核心就是调用 waitpid 函数,使进程能够安全退出的,变量 one 的作用仅仅的为了再程序发生错误时,程序也能跳出while循环。

那么如何让 master 进程再收到信号时执行 ngx_signal_handler 函数呢?因此,再程序启动时,就需要先注册

/**
 * @brief 信号初始化函数,用于注册信号 
 * 
 * @return 0:成功,-1:失败 
 */

int ngx_init_signals()
{
    ngx_signal_t* pSig;
    //sigaction:系统定义的跟信号有关的一个结构,后续调用系统的sigaction()函数时要用到这个同名的结构
    struct sigaction   sa;
    for(pSig = signals;pSig->signo != 0;++pSig)
    {
        memset(&sa,0,sizeof(struct sigaction));
        if(pSig->handler)
        {
            sa.sa_sigaction = pSig->handler;
            //要想让sa.sa_sigaction指定的信号处理程序(函数)生效,你就把sa_flags设定为SA_SIGINFO
            sa.sa_flags = SA_SIGINFO;
        }
        else
        {
            //忽略信号,否则操作系统会调用默认处理函数,可能杀掉进程
            sa.sa_handler = SIG_IGN;
        }

        sigisemptyset(&sa.sa_mask);

        //在这里注册信号处理函数
        if(-1 == sigaction(pSig->signo,&sa,NULL))
        {
            ngx_log_error_core(NGX_LOG_EMERG,errno,"sigaction(%s) failed",pSig->signame);
            return -1; //有失败就直接返回
        }
    }
    return 0;
}

可以看到,我们再 for 循环中将自定义的信号全部进行了信号函数的注册(sigaction 函数)。我们的程序目前定义的信号处理函数只有 ngx_signal_handler。

4. 测试

在 mian 函数中加入信号注册,调整一下程序,最终如下

int main(int argc, char* const* argv)
{
    int exitcode = 0;
    ngx_pid = getpid();
    ngx_parent = getppid();

    for(int i = 0; i<argc; ++i)
    {
        g_argvlen += strlen(argv[i]) + 1;
    }

    //统计环境变量所占的内存。注意判断方法是environ[i]是否为空作为环境变量结束标记
    for(int i = 0; environ[i]; ++i)
    {
        g_environlen += strlen(environ[i]) + 1;
    }
    g_os_argc = argc;
    g_os_argv = (char **) argv;

    ngx_process = NGX_PROCESS_MASTER; // 此时还是master进程
    ngx_reap = 0; //标记子进程没有发生变化

    //日志初始化(创建/打开日志文件)
    ngx_log_init();             
    //加载配置文件内容
    if(false == CConfig::GetInstance()->LoadConfig("nginx.conf"))
    {
        ngx_log_stderr(0,"配置文件[%s]载入失败,退出!","nginx.conf");
        //找不到文件
        exitcode = 2;
        goto lblexit;
    }

    if(0 != ngx_init_signals()) //信号初始化
    {
        exitcode = 1;
        goto lblexit;
    } 
    //把环境变量搬家
    ngx_init_setproctitle();    

    //守护进程
    if(CConfig::GetInstance()->GetIntDefault("Daemon",0) == 1)
    {
        int cdaemonresult = ngx_deamon();
        if(cdaemonresult == -1) //fork()失败
        {
            exitcode = 1;    //标记失败
            goto lblexit;
        }
        if(cdaemonresult == 1)
        {
            //这是原始的父进程
            freereSource();   //只有进程退出了才goto到 lblexit,用于提醒用户进程退出了
                              //而我现在这个情况属于正常fork()守护进程后的正常退出,不应该跑到lblexit()去执行,因为那里有一条打印语句标记整个进程的退出,这里不该限制该条打印语句;
            exitcode = 0;
            return exitcode;  //整个进程直接在这里退出
        }
        //走到这里,成功创建了守护进程并且这里已经是fork()出来的进程,现在这个进程做了master进程
        g_daemonized = 1;    //守护进程标记,标记是否启用了守护进程模式,0:未启用,1:启用了
    }

    //这里真正开始主流程
    ngx_master_process_cycle();
    
lblexit:
    ngx_log_stderr(0,"程序退出!");
    //该释放的资源要释放掉
    freereSource();  //一系列的main返回前的释放动作函数
    return exitcode;
}

其中 ngx_reap 为 sig_atomic_t 原子类型。运行程序,后 kill 其中一个 worker 进程后看效果。
在这里插入图片描述
可以看到进程安全退出了。

至此, 我们程序进程相关的话题基本结束了,整个软件目前运行也没有什么问题,后面的章节正式进入网络编程话题。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值