从零构建通讯器--4.6创建守护进程及完善信号处理函数
发布日期:2021-05-04 18:23:26 浏览次数:16 分类:技术文章

本文共 13467 字,大约阅读时间需要 44 分钟。

(1)守护进程功能的实现

①第三章第二节 如果拦截掉SIGHUP,那么终端窗口关闭,进程就不会跟着关闭②守护进程,第三章第七节,一运行就在后台,不会占着终端③放在proc目录里面,这个目录专门放进程相关的代码④主要函数ngx_daemen()-->>创建守护进程a)创子进程fork(),返回-1打印错误日志,返回0直接break,default父进程直接返回1b)子进程继续往下执行,setsid()脱离终端,终端关闭,将跟子进程无关c)umask设置为0,不要让它来限制文件权限,以免引起混乱d)打开黑洞设备,以读写方式打开子进程接下来的操作:让标准输入指向黑洞让标准输出指向黑洞这样fork()出来的这个子进程才会成为我们这里讲的master进程⑤调用ngx_daemen()的时机,在worker()子进程创建之前⑥查看进程信息
ps -eo pid,ppid,sid,tty,pgrp,comm,stat,cmd | grep -E 'bash|PID|nginx'

效果图:

在这里插入图片描述
S表示休眠状态
没有加号表示后台,+表示位于前台
(1)一个master,4个worker进程,状态S,表示休眠状态,但没有+,+号表示位于前台进程组,没有+说明我们这几个进程不在前台进程组;
(2)master进程的ppid是1【老祖宗进程init】,其他几个worker进程的父进程都是master;
(3)tt这列都为?,表示他们都脱离了终端,不与具体的终端挂钩了
(4)他们的进程组PGRP都相同;

结论:

1)守护进程如果通过键盘执行可执行文件来启动,那虽然守护进程与具体终端是脱钩的,但是依旧可以往标准错误上输出内容,这个终端对应的屏幕上可以看到输入的内容;

2)但是如果这个nginx守护进程你不是通过终端启动,你可能开机就启动,那么这个nginx守护进程就完全无法往任何屏幕上显示信息了,这个时候,要排错就要靠日志文件了;
代码:
ngx_daemon.cxx

//和守护进程相关#include 
#include
#include
#include
#include
#include
//errno#include
#include
#include "ngx_func.h"#include "ngx_macro.h"#include "ngx_c_conf.h"//描述:守护进程初始化//执行失败:返回-1, 子进程:返回0,父进程:返回1int ngx_daemon(){ //(1)创建守护进程的第一步,fork()一个子进程出来 switch (fork()) //fork()出来这个子进程才会成为咱们这里讲解的master进程; { case -1: //创建子进程失败 ngx_log_error_core(NGX_LOG_EMERG,errno, "ngx_daemon()中fork()失败!"); return -1; case 0: //子进程,走到这里直接break; break; default: //父进程以往 直接退出exit(0);现在希望回到主流程去释放一些资源 return 1; //父进程直接返回1; } //end switch //只有fork()出来的子进程才能走到这个流程 ngx_parent = ngx_pid; //ngx_pid是原来父进程的id,因为这里是子进程,所以子进程的ngx_parent设置为原来父进程的pid ngx_pid = getpid(); //当前子进程的id要重新取得 //(2)脱离终端,终端关闭,将跟此子进程无关 if (setsid() == -1) { ngx_log_error_core(NGX_LOG_EMERG, errno,"ngx_daemon()中setsid()失败!"); return -1; } //(3)设置为0,不要让它来限制文件权限,以免引起混乱 umask(0); //(4)打开黑洞设备,以读写方式打开 int fd = open("/dev/null", O_RDWR); if (fd == -1) { ngx_log_error_core(NGX_LOG_EMERG,errno,"ngx_daemon()中open(\"/dev/null\")失败!"); return -1; } if (dup2(fd, STDIN_FILENO) == -1) //先关闭STDIN_FILENO[这是规矩,已经打开的描述符,动他之前,先close],类似于指针指向null,让/dev/null成为标准输入; { ngx_log_error_core(NGX_LOG_EMERG,errno,"ngx_daemon()中dup2(STDIN)失败!"); return -1; } if (dup2(fd, STDOUT_FILENO) == -1) //再关闭STDIN_FILENO,类似于指针指向null,让/dev/null成为标准输出; { ngx_log_error_core(NGX_LOG_EMERG,errno,"ngx_daemon()中dup2(STDOUT)失败!"); return -1; } if (fd > STDERR_FILENO) //fd应该是3,这个应该成立 { if (close(fd) == -1) //释放资源这样这个文件描述符就可以被复用;不然这个数字【文件描述符】会被一直占着; { ngx_log_error_core(NGX_LOG_EMERG,errno, "ngx_daemon()中close(fd)失败!"); return -1; } } return 0; //子进程返回0}

(2)信号处理函数的进一步完善

(2.1)避免子进程被杀掉时变成僵尸进程	a)父进程要处理SIGCHILD信号并在信号处理函数中调用waitpid()来解决僵尸进程的问题;	b)完善信号处理函数	c)信号处理函数中的代码,要坚持一些书写原则:	①代码尽可能简单,尽可能快速的执行完毕返回;	②用一些全局量做一些标记;尽可能不调用函数;	③不要在信号处理函数中执行太复杂的代码以免阻塞其他信号的到来,甚至阻塞整个程序执行流程;	d)ngx_signal_handler()   (*****************重要*****************)	①ngx_signal_t定义信号结构体,有信号ID和信号名	②现在只是处理父进程的SIGCHILD信号,达到解决僵尸进程的目的	ngx_reap=1是用来以后子进程被Kill后,主进程重新拉起来一个子进程用的全局变量	e)ngx_process_get_status()  //若子进程状态有变化,获取子进程的结束状态,防止单独kill子进程时这个子进程会变成僵尸进程而父进程太忙,来不及回收	①用for(;;)死循环调用waitpid()

代码:

//和信号有关的函数放这里#include 
#include
#include
#include
#include
//信号相关头文件 #include
//errno#include
//waitpid#include "ngx_global.h"#include "ngx_macro.h"#include "ngx_func.h" //一个信号有关的结构 ngx_signal_ttypedef struct { int signo; //信号对应的数字编号 ,每个信号都有对应的#define ,大家已经学过了 const char *signame; //信号对应的中文名字 ,比如SIGHUP //信号处理函数,这个函数由我们自己来提供,但是它的参数和返回值是固定的【操作系统就这样要求】,大家写的时候就先这么写,也不用思考这么多; void (*handler)(int signo, siginfo_t *siginfo, void *ucontext); //函数指针, siginfo_t:系统定义的结构} ngx_signal_t;//声明一个信号处理函数static void ngx_signal_handler(int signo, siginfo_t *siginfo, void *ucontext); //static表示该函数只在当前文件内可见static void ngx_process_get_status(void); //获取子进程的结束状态,防止单独kill子进程时子进程变成僵尸进程//数组 ,定义本系统处理的各种信号,我们取一小部分nginx中的信号,并没有全部搬移到这里,日后若有需要根据具体情况再增加//在实际商业代码中,你能想到的要处理的信号,都弄进来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作为一个特殊标记};//初始化信号的函数,用于注册信号处理程序//返回值:0成功 ,-1失败int ngx_init_signals(){ ngx_signal_t *sig; //指向自定义结构数组的指针 struct sigaction sa; //sigaction:系统定义的跟信号有关的一个结构,我们后续调用系统的sigaction()函数时要用到这个同名的结构 for (sig = signals; sig->signo != 0; sig++) //将signo ==0作为一个标记,因为信号的编号都不为0; { //我们注意,现在要把一堆信息往 变量sa对应的结构里弄 ...... memset(&sa,0,sizeof(struct sigaction)); if (sig->handler) //如果信号处理函数不为空,这当然表示我要定义自己的信号处理函数 { sa.sa_sigaction = sig->handler; //sa_sigaction:指定信号处理程序(函数),注意sa_sigaction也是函数指针,是这个系统定义的结构sigaction中的一个成员(函数指针成员); sa.sa_flags = SA_SIGINFO; //sa_flags:int型,指定信号的一些选项,设置了该标记(SA_SIGINFO),就表示信号附带的参数可以被传递到信号处理函数中 //说白了就是你要想让sa.sa_sigaction指定的信号处理程序(函数)生效,你就把sa_flags设定为SA_SIGINFO } else { sa.sa_handler = SIG_IGN; //sa_handler:这个标记SIG_IGN给到sa_handler成员,表示忽略信号的处理程序,否则操作系统的缺省信号处理程序很可能把这个进程杀掉; //其实sa_handler和sa_sigaction都是一个函数指针用来表示信号处理程序。只不过这两个函数指针他们参数不一样, sa_sigaction带的参数多,信息量大, //而sa_handler带的参数少,信息量少;如果你想用sa_sigaction,那么你就需要把sa_flags设置为SA_SIGINFO; } //end if sigemptyset(&sa.sa_mask); //比如咱们处理某个信号比如SIGUSR1信号时不希望收到SIGUSR2信号,那咱们就可以用诸如sigaddset(&sa.sa_mask,SIGUSR2);这样的语句针对信号为SIGUSR1时做处理,这个sigaddset三章五节讲过; //这里.sa_mask是个信号集(描述信号的集合),用于表示要阻塞的信号,sigemptyset()这个函数咱们在第三章第五节讲过:把信号集中的所有信号清0,本意就是不准备阻塞任何信号; //设置信号处理动作(信号处理函数),说白了这里就是让这个信号来了后调用我的处理程序,有个老的同类函数叫signal,不过signal这个函数被认为是不可靠信号语义,不建议使用,大家统一用sigaction if (sigaction(sig->signo, &sa, NULL) == -1) //参数1:要操作的信号 //参数2:主要就是那个信号处理函数以及执行信号处理函数时候要屏蔽的信号等等内容 //参数3:返回以往的对信号的处理方式【跟sigprocmask()函数边的第三个参数是的】,跟参数2同一个类型,我们这里不需要这个东西,所以直接设置为NULL; { ngx_log_error_core(NGX_LOG_EMERG,errno,"sigaction(%s) failed",sig->signame); //显示到日志文件中去的 return -1; //有失败就直接返回 } else { //ngx_log_error_core(NGX_LOG_EMERG,errno,"sigaction(%s) succed!",sig->signame); //成功不用写日志 //ngx_log_stderr(0,"sigaction(%s) succed!",sig->signame); //直接往屏幕上打印看看 ,不需要时可以去掉 } } //end for return 0; //成功 }//信号处理函数//siginfo:这个系统定义的结构中包含了信号产生原因的有关信息static void ngx_signal_handler(int signo, siginfo_t *siginfo, void *ucontext){ //printf("来信号了\n"); 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】 { //有发送该信号的进程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 { 没有发送该信号的进程id,所以不显示发送该信号的进程id 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;}//获取子进程的结束状态,防止单独kill子进程时子进程变成僵尸进程static void ngx_process_get_status(void){ pid_t pid; int status; int err; int one=0; //抄自官方nginx,应该是标记信号正常处理过一次 //当你杀死一个子进程时,父进程会收到这个SIGCHLD信号。 for ( ;; ) { //waitpid,有人也用wait,但老师要求大家掌握和使用waitpid即可;这个waitpid说白了获取子进程的终止状态,这样,子进程就不会成为僵尸进程了; //第一次waitpid返回一个> 0值,表示成功,后边显示 2019/01/14 21:43:38 [alert] 3375: pid = 3377 exited on signal 9【SIGKILL】 //第二次再循环回来,再次调用waitpid会返回一个0,表示子进程还没结束,然后这里有return来退出; pid = waitpid(-1, &status, WNOHANG); //第一个参数为-1,表示等待任何子进程, //第二个参数:保存子进程的状态信息(大家如果想详细了解,可以百度一下)。 //第三个参数:提供额外选项,WNOHANG表示不要阻塞,让这个waitpid()立即返回 if(pid == 0) //子进程没结束,会立即返回这个数字,但这里应该不是这个数字【因为一般是子进程退出时会执行到这个函数】 { return; } //end if(pid == 0) //------------------------------- if(pid == -1)//这表示这个waitpid调用有错误,有错误也理解返回出去,我们管不了这么多 { //这里处理代码抄自官方nginx,主要目的是打印一些日志。考虑到这些代码也许比较成熟,所以,就基本保持原样照抄吧; err = errno; if(err == EINTR) //调用被某个信号中断 { continue; } if(err == ECHILD && one) //没有子进程 { return; } if (err == ECHILD) //没有子进程 { ngx_log_error_core(NGX_LOG_INFO,err,"waitpid() failed!"); return; } ngx_log_error_core(NGX_LOG_ALERT,err,"waitpid() failed!"); return; } //end if(pid == -1) //------------------------------- //走到这里,表示 成功【返回进程id】 ,这里根据官方写法,打印一些日志来记录子进程的退出 one = 1; //标记waitpid()返回了正常的返回值 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参数的低八位 } } //end for return;}

ngx.cxx

//整个程序入口函数放这里#include 
#include
#include
#include
#include
#include "ngx_macro.h" //各种宏定义#include "ngx_func.h" //各种函数声明#include "ngx_c_conf.h" //和配置文件处理相关的类,名字带c_表示和类有关//本文件用的函数声明static void freeresource();//和设置标题有关的全局量size_t g_argvneedmem=0; //保存下这些argv参数所需要的内存大小size_t g_envneedmem=0; //环境变量所占内存大小int g_os_argc; //参数个数 char **g_os_argv; //原始命令行参数数组,在main中会被赋值char *gp_envmem=NULL; //指向自己分配的env环境变量的内存,在ngx_init_setproctitle()函数中会被分配内存int g_daemonized=0; //守护进程标记,标记是否启用了守护进程模式,0:未启用,1:启用了//和进程本身有关的全局量pid_t ngx_pid; //当前进程的pidpid_t ngx_parent; //父进程的pidint ngx_process; //进程类型,比如master,worker进程等sig_atomic_t ngx_reap; //标记子进程状态变化[一般是子进程发来SIGCHLD信号表示退出],sig_atomic_t:系统定义的类型:访问或改变这些变量需要在计算机的一条指令内完成 //一般等价于int【通常情况下,int类型的变量通常是原子访问的,也可以认为 sig_atomic_t就是int类型的数据】int main(int argc, char *const *argv){ int exitcode = 0; //退出代码,先给0表示正常退出 int i; //临时用 //(1)无伤大雅也不需要释放的放最上边 ngx_pid = getpid(); //取得进程pid ngx_parent = getppid(); //取得父进程的id //统计argv所占的内存 g_argvneedmem = 0; for(i = 0; i < argc; i++) //argv = ./nginx -a -b -c asdfas { g_argvneedmem += strlen(argv[i]) + 1; //+1是给\0留空间。 } //统计环境变量所占的内存。注意判断方法是environ[i]是否为空作为环境变量结束标记 for(i = 0; environ[i]; i++) { g_envneedmem += strlen(environ[i]) + 1; //+1是因为末尾有\0,是占实际内存位置的,要算进来 } //end for g_os_argc = argc; //保存参数个数 g_os_argv = (char **) argv; //保存参数指针 //全局量有必要初始化的 ngx_log.fd = -1; //-1:表示日志文件尚未打开;因为后边ngx_log_stderr要用所以这里先给-1 ngx_process = NGX_PROCESS_MASTER; //先标记本进程是master进程 ngx_reap = 0; //标记子进程没有发生变化 //(2)初始化失败,就要直接退出的 //配置文件必须最先要,后边初始化啥的都用,所以先把配置读出来,供后续使用 CConfig *p_config = CConfig::GetInstance(); //单例类 if(p_config->Load("nginx.conf") == false) //把配置文件内容载入到内存 { ngx_log_init(); //初始化日志 ngx_log_stderr(0,"配置文件[%s]载入失败,退出!","nginx.conf"); //exit(1);终止进程,在main中出现和return效果一样 ,exit(0)表示程序正常, exit(1)/exit(-1)表示程序异常退出,exit(2)表示表示系统找不到指定的文件 exitcode = 2; //标记找不到文件 goto lblexit; } //(3)一些必须事先准备好的资源,先初始化 ngx_log_init(); //日志初始化(创建/打开日志文件),这个需要配置项,所以必须放配置文件载入的后边; //(4)一些初始化函数,准备放这里 if(ngx_init_signals() != 0) //信号初始化 { exitcode = 1; goto lblexit; } //(5)一些不好归类的其他类别的代码,准备放这里 ngx_init_setproctitle(); //把环境变量搬家 //------------------------------------ //(6)创建守护进程 if(p_config->GetIntDefault("Daemon",0) == 1) //读配置文件,拿到配置文件中是否按守护进程方式启动的选项 { //1:按守护进程方式运行 int cdaemonresult = ngx_daemon(); 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:启用了 } //(7)开始正式的主工作流程,主流程一致在下边这个函数里循环,暂时不会走下来,资源释放啥的日后再慢慢完善和考虑 ngx_master_process_cycle(); //不管父进程还是子进程,正常工作期间都在这个函数里循环; //-------------------------------------------------------------- //for(;;) //{ // sleep(1); //休息1秒 // printf("休息1秒\n"); //} //--------------------------------------lblexit: //(5)该释放的资源要释放掉 ngx_log_stderr(0,"程序退出,再见了!"); freeresource(); //一系列的main返回前的释放动作函数 //printf("程序退出,再见!\n"); return exitcode;}//专门在程序执行末尾释放资源的函数【一系列的main返回前的释放动作函数】void freeresource(){ //(1)对于因为设置可执行程序标题导致的环境变量分配的内存,我们应该释放 if(gp_envmem) { delete []gp_envmem; gp_envmem = NULL; } //(2)关闭日志文件 if(ngx_log.fd != STDERR_FILENO && ngx_log.fd != -1) { close(ngx_log.fd); //不用判断结果了 ngx_log.fd = -1; //标记下,防止被再次close吧 }}
上一篇:从零构建通讯器--5.1TCP和IP协议
下一篇:从零构建通讯器--4.4-4.5信号在创建线程的实战作用、write函数写入日志设置成不混乱、文件IO详解

发表评论

最新留言

感谢大佬
[***.8.128.20]2025年03月17日 23时08分18秒