初始化信号
使用neg_init_signals();
在nginx.cxx中的位置如下
//(3)一些必须事先准备好的资源,先初始化
ngx_log_init(); //日志初始化(创建/打开日志文件),这个需要配置项,所以必须放配置文件载入的后边;
//(4)一些初始化函数,准备放这里
if(ngx_init_signals() != 0) //信号初始化
{
exitcode = 1;
goto lblexit;
}
neg_init_signals()核心代码分析
该方法是在ngx_signal.cxx中实现
这里先要复习一下sigaction函数的调用
sigaction函数
修改信号处理动作(通常在Linux用其来注册一个信号的捕捉函数)
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact); 成功:0;失败:-1,设置errno
参数:
act:传入参数,新的处理方式。
oldact:传出参数,旧的处理方式。 【signal.c】
struct sigaction结构体
struct sigaction {
void (*sa_handler)(int);
void (*sa_sigaction)(int, siginfo_t *, void *);
sigset_t sa_mask;
int sa_flags;
void (*sa_restorer)(void);
};
sa_restorer:该元素是过时的,不应该使用,POSIX.1标准将不指定该元素。(弃用)
sa_sigaction:当sa_flags被指定为SA_SIGINFO标志时,使用该信号处理程序。(很少使用)
重点掌握:
① sa_handler:指定信号捕捉后的处理函数名(即注册函数)。也可赋值为SIG_IGN表忽略 或 SIG_DFL表执行默认动作
② sa_mask: 调用信号处理函数时,所要屏蔽的信号集合(信号屏蔽字)。注意:仅在处理函数被调用期间屏蔽生效,是临时性设置。
设置这个sa_mask 的意义在于:假设我们处理 SIGINT信号的函数需要的时间比较长,这时候又来了一个 SIGINT信号,应该怎么办呢?是接着处理第一次收到的SIGINT信号的逻辑,还是又从开始处理这个新的SIGINT信号呢?因此设置了这个sa_mask,当处理信号SIGINT的函数阶段,让当前sa_mask信号屏蔽集替换原先PCB中的mask,那么理论上应该是让这个sa_mask = sa_mask | 当前信号,但是实际上看很多代码中并没有这么做,而是将sa_mask直接清0(设置方法为:sigemptyset(&act.sa_mask) ;),这是因为sa_flags这个参数的原因,因为当sa_flags设置为0的时候,默认就会屏蔽当前处理的信号。记住:sa_mask 默认传递0,sa_flags也是0的情况下,只会 屏蔽当前处理的信号;
如果我们想在当前函数处理的时候,屏蔽其他信号,则还是需要设置 sa_mask的值。一般设置方法为
sigemptyset(&act.sa_mask) ;
sigaddset(&act.sa_mask,想要屏蔽的信号);
这样当想屏蔽的信号再发送过来的时候,不会对想要屏蔽的信号有反馈。
③ sa_flags:通常设置为0,表使用默认属性。这个默认属性是说:在处理该信号的过程中,如果有该信号再一次发过来,默认屏蔽,
信号捕捉特性
进程正常运行时,默认PCB中有一个信号屏蔽字,假定为☆,它决定了进程自动屏蔽哪些信号。当注册了某个信号捕捉函数,捕捉到该信号以后,要调用该函数。而该函数有可能执行很长时间,在这期间所屏蔽的信号不由☆来指定。而是用sa_mask来指定。调用完信号处理函数,再恢复为☆。
XXX信号捕捉函数执行期间,XXX信号自动被屏蔽。
阻塞的常规信号不支持排队,产生多次只记录一次。(后32个实时信号支持排队)
代码分析:
注意,这里老师用的是sa_sigaction回调函数,而不是前面学过的
void (*sa_handler)(int); ---- >前面学过的
void (*sa_sigaction)(int, siginfo_t *, void *) ; -------->老师用的是这个,那么这两个有啥区别吗?
区别是sa_sigaction能带更多的信息到 回调函数。
实际上:sa_sigaction 和 sa_handler 使用的是同一块内存空间,所以只能设置其中的一个。
sa_handler信号处理程序,不接受额外数据,可以为SIG_DFL的默认动作。
如果SA_SIGINFO是在sa_flags中指定的,说明了信号处理程序带有附加信息,那么sa_sigaction需要指定信号处理函数的信号。这个函数接收信号作为它的值第一个参数,一个指向siginfo_t的指针作为第二个参数,一个指向ucontext_t的指针(转换为void *)作为第三个参数。
代码执行流程如下:
sigsuspend() 函数:
//阻塞在这里,等待一个信号,此时进程是挂起的,不占用cpu时间,只有收到信号才会被唤醒(返回);//此时master进程完全靠信号驱动干活
该函数内部做的事情如下:
假设代码是 sigemptyset(&set); //信号屏蔽字为空,表示不屏蔽任何信号
//a)根据给定的参数设置新的mask 并 阻塞当前进程【因为是个空集,所以不阻塞任何信号】
//b)此时,一旦收到信号,便恢复原先的信号屏蔽【我们原来的mask在上边设置的,阻塞了多达10个信号,从而保证我下边的执行流程不会再次被其他信号截断】
//c)调用该信号对应的信号处理函数
//d)信号处理函数返回后,sigsuspend返回,使程序流程继续往下走
参考书中的说明,也就是说,代码中&set相当于 下面的&mask,那么&prev是什么呢?是当前进程的信号掩码。
sigsuspend用于在接收到某个信号之前,临时用mask替换进程的信号掩码,并暂停进程执行,直到收到信号为止。
屏蔽信号字 sigprocmask的使用注意点。
参考这个老师的视频
18.信号-sigaction_哔哩哔哩_bilibili
子进程添加
//描述:创建worker子进程
void ngx_master_process_cycle()
{
sigset_t set; //信号集
sigemptyset(&set); //清空信号集
//下列这些信号在执行本函数期间不希望收到【考虑到官方nginx中有这些信号,老师就都搬过来了】(保护不希望由信号中断的代码临界区)
//建议fork()子进程时学习这种写法,防止信号的干扰;
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); //终端退出符
//.........可以根据开发的实际需要往其中添加其他要屏蔽的信号......
//设置,此时无法接受的信号;阻塞期间,你发过来的上述信号,多个会被合并为一个,暂存着,等你放开信号屏蔽后才能收到这些信号。。。
//sigprocmask()在第三章第五节详细讲解过
if (sigprocmask(SIG_BLOCK, &set, NULL) == -1) //第一个参数用了SIG_BLOCK表明设置 进程 新的信号屏蔽字 为 “当前信号屏蔽字 和 第二个参数指向的信号集的并集
{
ngx_log_error_core(NGX_LOG_ALERT,errno,"ngx_master_process_cycle()中sigprocmask()失败!");
}
//即便sigprocmask失败,程序流程 也继续往下走
//首先我设置主进程标题---------begin
size_t size;
int i;
size = sizeof(master_process); //注意我这里用的是sizeof,所以字符串末尾的\0是被计算进来了的
size += g_argvneedmem; //argv参数长度加进来
if(size < 1000) //长度小于这个,我才设置标题
{
char title[1000] = {0};
strcpy(title,(const char *)master_process); //"master process"
strcat(title," "); //跟一个空格分开一些,清晰 //"master process "
for (i = 0; i < g_os_argc; i++) //"master process ./nginx aa bb cc"
{
strcat(title,g_os_argv[i]);
}//end for
ngx_setproctitle(title); //设置标题
ngx_log_error_core(NGX_LOG_NOTICE,0,"%s %P 启动并开始运行......!",title,ngx_pid); //设置标题时顺便记录下来进程名,进程id等信息到日志
}
//首先我设置主进程标题---------end
//从配置文件中读取要创建的worker进程数量
CConfig *p_config = CConfig::GetInstance(); //单例类
int workprocess = p_config->GetIntDefault("WorkerProcesses",1); //从配置文件中得到要创建的worker进程数量
ngx_start_worker_processes(workprocess); //这里要创建worker子进程
//创建子进程后,父进程的执行流程会返回到这里,子进程不会走进来
sigemptyset(&set); //信号屏蔽字为空,表示不屏蔽任何信号
for ( ;; )
{
// usleep(100000);
//ngx_log_error_core(0,0,"haha--这是父进程,pid为%P",ngx_pid);
//a)根据给定的参数设置新的mask 并 阻塞当前进程【因为是个空集,所以不阻塞任何信号】
//b)此时,一旦收到信号,便恢复原先的信号屏蔽【我们原来的mask在上边设置的,阻塞了多达10个信号,从而保证我下边的执行流程不会再次被其他信号截断】
//c)调用该信号对应的信号处理函数
//d)信号处理函数返回后,sigsuspend返回,使程序流程继续往下走
//printf("for进来了!\n"); //发现,如果print不加\n,无法及时显示到屏幕上,是行缓存问题,以往没注意;可参考https://blog.csdn.net/qq_26093511/article/details/53255970
sigsuspend(&set); //阻塞在这里,等待一个信号,此时进程是挂起的,不占用cpu时间,只有收到信号才会被唤醒(返回);
//此时master进程完全靠信号驱动干活
// printf("执行到sigsuspend()下边来了\n");
//printf("master进程休息1秒\n");
//ngx_log_stderr(0,"haha--这是父进程,pid为%P",ngx_pid);
sleep(1); //休息1秒
//以后扩充.......
}// end for(;;)
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;
}
将守护进程加进去
//(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:启用了
}
文件IO 详谈
write 函数的思考,我们在代码中同时去写日志文件,5个进程同时写,不会有混乱吗?
从测试结果来看,是没有混乱的。
这是因为父进程和子进程 是亲缘关系,会共享文件表项。因此不会发生问题。
如果我们write 的时候,可能断电了,那么写入到 内核缓存的数据,可能没有真正的写到磁盘中。