文章目录
一:信号功能实战
signal():注册信号处理程序的函数;
商业软件中,不用signal(),而要用sigaction();
二:nginx中创建worker子进程
官方nginx ,一个master进程,创建了多个worker子进程;
master process ./nginx
worker process
(i)ngx_master_process_cycle() //创建子进程等一系列动作
(i) ngx_setproctitle() //设置进程标题
(i) ngx_start_worker_processes() //创建worker子进程
(i) for (i = 0; i < threadnums; i++) //master进程在走这个循环,来创建若干个子进程
(i) ngx_spawn_process(i,“worker process”);
(i) pid = fork(); //分叉,从原来的一个master进程(一个叉),分成两个叉(原有的master进程,以及一个新fork()出来的worker进程
(i) //只有子进程这个分叉才会执行ngx_worker_process_cycle()
(i) ngx_worker_process_cycle(inum,pprocname); //子进程分叉
(i) ngx_worker_process_init();
(i) sigemptyset(&set);
(i) sigprocmask(SIG_SETMASK, &set, NULL); //允许接收所有信号
(i) ngx_setproctitle(pprocname); //重新为子进程设置标题为worker process
(i) for ( ;; ) {}. … //子进程开始在这里不断的死循环
(i) sigemptyset(&set);
(i) for ( ;; ) {}. //父进程[master进程]会一直在这里循环
kill -9 -1344 ,用负号 -组id,可以杀死一组进程
(2.1)sigsuspend()函数讲解
a)根据给定的参数设置新的mask 并 阻塞当前进程【因为是个空集,所以不阻塞任何信号】
b)此时,一旦收到信号,便恢复原先的信号屏蔽【我们原来的mask在上边设置的,阻塞了多达10个信号,从而保证我下边的执行流程不会再次被其他信号截断】
c)调用该信号对应的信号处理函数
d)信号处理函数返回后,sigsuspend返回,使程序流程继续往下走
三:日志输出重要信息谈
(3.1)换行回车进一步示意
\r:回车符,把打印【输出】信息的为止定位到本行开头
\n:换行符,把输出为止移动到下一行
一般把光标移动到下一行的开头,\r\n
a)比如windows下,每行结尾 \r\n
b)类Unix,每行结尾就只有\n
c)Mac苹果系统,每行结尾只有\r
结论:统一用\n就行了
(3.2)printf()函数不加\n无法及时输出的解释
printf末尾不加\n就无法及时的将信息显示到屏幕 ,这是因为 行缓存[windows上一般没有,类Unix上才有]
需要输出的数据不直接显示到终端,而是首先缓存到某个地方,当遇到行刷新表指或者该缓存已满的情况下,菜会把缓存的数据显示到终端设备;
ANSI C中定义\n认为是行刷新标记,所以,printf函数没有带\n是不会自动刷新输出流,直至行缓存被填满才显示到屏幕上;
所以大家用printf的时候,注意末尾要用\n;
或者:fflush(stdout);
或者:setvbuf(stdout,NULL,_IONBF,0); //这个函数. 直接将printf缓冲区禁止, printf就直接输出了。
标准I/O函数,后边还会讲到
四:write()函数思考
多个进程同时去写一个文件,比如5个进程同时往日志文件中写,会不会造成日志文件混乱。
多个进程同时写 一个日志文件,我们看到输出结果并不混乱,是有序的;我们的日志代码应对多进程往日志文件中写时没有问题;
《Unix环境高级编程 第三版》第三章:文件I/O里边的3.10-3.12,涉及到了文件共享、原子操作以及函数dup,dup2的讲解;
第八章:进程控制 里庇安的8.3,涉及到了fork()函数;
a)多个进程写一个文件,可能会出现数据覆盖,混乱等情况
/b)ngx_log.fd = open((const char *)plogname,O_WRONLY|O_APPEND|O_CREAT,0644);
O_APPEND这个标记能够保证多个进程操作同一个文件时不会相互覆盖;
c)内核wirte()写入时是原子操作;
d)父进程fork()子进程是亲缘关系。是会共享文件表项,
--------------关于write()写的安全问题,是否数据成功被写到磁盘;
e)write()调用返回时,内核已经将应用程序缓冲区所提供的数据放到了内核缓冲区,但是无法保证数据已经写出到其预定的目的地【磁盘 】;
的确,因为write()调用速度极快,可能没有时间完成该项目的工作【实际写磁盘】,所以这个wirte()调用不等价于数据在内核缓冲区和磁盘之间的数据交换
f)打开文件使用了 O_APPEND,多个进程写日志用write()来写;
(4.1)掉电导致write()的数据丢失破解法
a)直接I/O:直接访问物理磁盘:
O_DIRECT:绕过内核缓冲区。用posix_memalign
b)open文件时用O_SYNC选项:
同步选项【把数据直接同步到磁盘】,只针对write函数有效,使每次write()操作等待物理I/O操作的完成;
具体说,就是将写入内核缓冲区的数据立即写入磁盘,将掉电等问题造成的损失减到最小;
每次写磁盘数据,务必要大块大块写,一般都512-4k 4k的写;不要每次只写几个字节,否则会被抽死;************
c)缓存同步:尽量保证缓存数据和写道磁盘上的数据一致;
sync(void):将所有修改过的块缓冲区排入写队列;然后返回,并不等待实际写磁盘操作结束,数据是否写入磁盘并没有保证;
fsync(int fd):将fd对应的文件的块缓冲区立即写入磁盘,并等待实际写磁盘操作结束返回;*******************************
fdatasync(int fd):类似于fsync,但只影响文件的数据部分。而fsync不一样,fsync除数据外,还会同步更新文件属性;
write(4k),1000次之后,一直到把这个write完整[假设整个文件4M]。
fsync(fd) ,1次fsync [多次write,每次write建议都4k,然后调用一次fsync(),这才是用fsync()的正确用法****************]
五:标准IO库
fopen,fclose
fread,fwrite
fflush
fseek
getc,getc,getchar
fputc,put,putchar
fgets,gets
printf,fprintf,sprintf
scanf,fscan,sscanf
fwrite和write有啥区别;
fwrite()是标准I/O库一般在stdio.h文件
write():系统调用;
有一句话:所有系统调用都是原子性的
源码:
ngx_signal.cxx
//和信号有关的函数放这里
#include <string.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <signal.h> //信号相关头文件
#include <errno.h> //errno
#include "ngx_macro.h"
#include "ngx_func.h"
//一个信号有关的结构 ngx_signal_t
typedef 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表示该函数只在当前文件内可见
//数组 ,定义本系统处理的各种信号,我们取一小部分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; //成功
}
//信号处理函数
static void ngx_signal_handler(int signo, siginfo_t *siginfo, void *ucontext)
{
printf("来信号了\n");
}
nginx.cxx
//整个程序入口函数放这里
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include "ngx_c_conf.h" //和配置文件处理相关的类,名字带c_表示和类有关
#include "ngx_func.h" //各种函数声明
//本文件用的函数声明
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()函数中会被分配内存
//和进程本身有关的全局量
pid_t ngx_pid; //当前进程的pid
pid_t ngx_parent; //父进程的pid
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; //保存参数指针
//(2)初始化失败,就要直接退出的
//配置文件必须最先要,后边初始化啥的都用,所以先把配置读出来,供后续使用
CConfig *p_config = CConfig::GetInstance(); //单例类
if(p_config->Load("nginx.conf") == false) //把配置文件内容载入到内存
{
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(); //日志初始化(创建/打开日志文件)
if(ngx_init_signals() != 0) //信号初始化
{
exitcode = 1;
goto lblexit;
}
//(4)一些不好归类的其他类别的代码,准备放这里
ngx_init_setproctitle(); //把环境变量搬家
//(5)开始正式的主工作流程,主流程一致在下边这个函数里循环,暂时不会走下来,资源释放啥的日后再慢慢完善和考虑
ngx_master_process_cycle(); //不管父进程还是子进程,正常工作期间都在这个函数里循环;
//--------------------------------------------------------------
//for(;;)
//{
// sleep(1); //休息1秒
// printf("休息1秒\n");
//}
//--------------------------------------
lblexit:
//(5)该释放的资源要释放掉
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吧
}
}
ngx_process_cycle.cxx
//和开启子进程相关
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <signal.h> //信号相关头文件
#include <errno.h> //errno
#include <unistd.h>
#include "ngx_func.h"
#include "ngx_macro.h"
#include "ngx_c_conf.h"
//函数声明
static void ngx_start_worker_processes(int threadnums);
static int ngx_spawn_process(int threadnums,const char *pprocname);
static void ngx_worker_process_cycle(int inum,const char *pprocname);
static void ngx_worker_process_init(int inum);
//变量声明
static u_char master_process[] = "master process";
//描述:创建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"
{
strcat(title,g_os_argv[i]);
}//end for
ngx_setproctitle(title); //设置标题
}
//首先我设置主进程标题---------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");
//sleep(1); //休息1秒
//以后扩充.......
}// end for(;;)
return;
}
//描述:根据给定的参数创建指定数量的子进程,因为以后可能要扩展功能,增加参数,所以单独写成一个函数
//threadnums:要创建的子进程数量
static void ngx_start_worker_processes(int threadnums)
{
int i;
for (i = 0; i < threadnums; i++) //master进程在走这个循环,来创建若干个子进程
{
ngx_spawn_process(i,"worker process");
} //end for
return;
}
//描述:产生一个子进程
//inum:进程编号【0开始】
//pprocname:子进程名字"worker process"
static int ngx_spawn_process(int inum,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\"失败!",inum,pprocname);
return -1;
case 0: //子进程分支
ngx_parent = ngx_pid; //因为是子进程了,所有原来的pid变成了父pid
ngx_pid = getpid(); //重新获取pid,即本子进程的pid
ngx_worker_process_cycle(inum,pprocname); //我希望所有worker子进程,在这个函数里不断循环着不出来,也就是说,子进程流程不往下边走;
break;
default: //这个应该是父进程分支,直接break;,流程往switch之后走
break;
}//end switch
//父进程分支会走到这里,子进程流程不往下边走-------------------------
//若有需要,以后再扩展增加其他代码......
return pid;
}
//描述:worker子进程的功能函数,每个woker子进程,就在这里循环着了(无限循环【处理网络事件和定时器事件以对外提供web服务】)
// 子进程分叉才会走到之类
//inum:进程编号【0开始】
static void ngx_worker_process_cycle(int inum,const char *pprocname)
{
//重新为子进程设置进程名,不要与父进程重复------
ngx_worker_process_init(inum);
ngx_setproctitle(pprocname); //设置标题
//暂时先放个死循环,我们在这个循环里一直不出来
//setvbuf(stdout,NULL,_IONBF,0); //这个函数. 直接将printf缓冲区禁止, printf就直接输出了。
for(;;)
{
//先sleep一下 以后扩充.......
//printf("worker进程休息1秒");
//fflush(stdout); //刷新标准输出缓冲区,把输出缓冲区里的东西打印到标准输出设备上,则printf里的东西会立即输出;
//sleep(1); //休息1秒
//usleep(100000);
ngx_log_error_core(0,0,"good--这是子进程,编号为%d,pid为%P!",inum,ngx_pid);
//printf("1212");
//if(inum == 1)
//{
//ngx_log_stderr(0,"good--这是子进程,编号为%d,pid为%P",inum,ngx_pid);
//printf("good--这是子进程,编号为%d,pid为%d\r\n",inum,ngx_pid);
//ngx_log_error_core(0,0,"good--这是子进程,编号为%d",inum,ngx_pid);
//printf("我的测试哈inum=%d",inum++);
//fflush(stdout);
//}
//ngx_log_stderr(0,"good--这是子进程,pid为%P",ngx_pid);
//ngx_log_error_core(0,0,"good--这是子进程,编号为%d,pid为%P",inum,ngx_pid);
} //end for(;;)
return;
}
//描述:子进程创建时调用本函数进行一些初始化工作
static void ngx_worker_process_init(int inum)
{
sigset_t set; //信号集
sigemptyset(&set); //清空信号集
if (sigprocmask(SIG_SETMASK, &set, NULL) == -1) //原来是屏蔽那10个信号【防止fork()期间收到信号导致混乱】,现在不再屏蔽任何信号【接收任何信号】
{
ngx_log_error_core(NGX_LOG_ALERT,errno,"ngx_worker_process_init()中sigprocmask()失败!");
}
//....将来再扩充代码
//....
return;
}
nginx.conf
#是注释行,
#每个有效配置项用 等号 处理,等号前不超过40个字符,等号后不超过400个字符;
#[开头的表示组信息,也等价于注释行
[Socket]
ListenPort = 5678
DBInfo = 127.0.0.1;1234;myr;123456;mxdb_g
#日志相关
[Log]
#日志文件输出目录和文件名
#Log=logs/error.log
Log=error.log
#只打印日志等级<= 数字 的日志到日志文件中 ,日志等级0-8,0级别最高,8级别最低。
LogLevel = 8
#进程相关
[Proc]
#创建 这些个 worker进程
WorkerProcesses = 4