学习nginx已经有一个多月了,觉得越来越吃力了,主要原因自己总结了一下:1平台是基于linux的,以前几乎没有接触过linux,而nginx使用了很多linux的函数;2就是进程,这个东西接触的也很少,linux的多进程更不用说,而现在正好看到这里,觉得异常的吃力,这不看到nginx守护进程的建立,就找资料好好学习一下,所以本文已学习fork为主要内容。
好了,先看一下nginx的守护进程的建立,然后在学习fork。
http://blog.csdn.net/xiaoliangsky/article/details/39998373
1nginx的守护进程
直接看代码:
ngx_int_t ngx_daemon(ngx_log_t *log) { int fd; switch (fork()) {//用fork创建守护进程 case -1://fork返回-1创建失败 ngx_log_error(NGX_LOG_EMERG, log, ngx_errno, "fork() failed"); return NGX_ERROR; case 0://子进程返回 break; default://父进程返回 exit(0);//父进程退出 } ngx_pid = ngx_getpid(); if (setsid() == -1) {//建立新的会话,然后子进程称为会话组长 ngx_log_error(NGX_LOG_EMERG, log, ngx_errno, "setsid() failed"); return NGX_ERROR; } umask(0);//重设文件创建掩模 /*重定向标准输入、输出到/dev/null(传说中的黑洞)*/ fd = open("/dev/null", O_RDWR); if (fd == -1) { ngx_log_error(NGX_LOG_EMERG, log, ngx_errno, "open(\"/dev/null\") failed"); return NGX_ERROR; } if (dup2(fd, STDIN_FILENO) == -1) {//输入重定向到fd,即从/dev/null输入 ngx_log_error(NGX_LOG_EMERG, log, ngx_errno, "dup2(STDIN) failed"); return NGX_ERROR; } if (dup2(fd, STDOUT_FILENO) == -1) {//输出重定向到fd,即所有输出到/dev/null ngx_log_error(NGX_LOG_EMERG, log, ngx_errno, "dup2(STDOUT) failed"); return NGX_ERROR; } #if 0 if (dup2(fd, STDERR_FILENO) == -1) { ngx_log_error(NGX_LOG_EMERG, log, ngx_errno, "dup2(STDERR) failed"); return NGX_ERROR; } #endif if (fd > STDERR_FILENO) { if (close(fd) == -1) { ngx_log_error(NGX_LOG_EMERG, log, ngx_errno, "close() failed"); return NGX_ERROR; } } return NGX_OK; }
这里不多解释,看了下面的内容,就知道上面的代码很简单。
2fork函数
由fork创建的新进程被称为子进程(child process)。该函数被调用一次,但返回两次。两次返回的区别是子进程的返回值是0,而父进程的返回值则是新进程(子进程)的进程 id。将子进程id返回给父进程的理由是:因为一个进程的子进程可以多于一个,没有一个函数使一个进程可以获得其所有子进程的进程id。对子进程来说,之所以fork返回0给它,是因为它随时可以调用getpid()来获取自己的pid;也可以调用getppid()来获取父进程的id。(进程id
0总是由交换进程使用,所以一个子进程的进程id不可能为0 )。
fork之后,操作系统会复制一个与父进程完全相同的子进程,虽说是父子关系,但是在操作系统看来,他们更像兄弟关系,这2个进程共享代码空间,但是数据空间是互相独立的,子进程数据空间中的内容是父进程的完整拷贝,指令指针也完全相同,子进程拥有父进程当前运行到的位置(两进程的程序计数器pc值相同,也就是说,子进程是从fork返回处开始执行的),但有一点不同,如果fork成功,子进程中fork的返回值是0,父进程中fork的返回值是子进程的进程号,如果fork不成功,父进程会返回错误。
来个列子:
int main() { int count; int flag; pid_t pid; pid = fork(); if (pid > 0) { printf("parent process is run\n"); flag = 1; } else if (pid < 0) { printf("fork is error\n"); exit(-1); } else { printf("child is run \n"); flag = 0; } count = 0; if (flag) { printf("count in parent is : %d\n", ++count); } else { printf("count in child is : %d\n", ++count); } return 0; }
运行结果如下:
parent process is run count in parent is : 1 child is run count in child is : 1
从这个例子,可以知道:
1父进程和子进程执行了相同的代码
2父进程和子进程不共享数据空间,否则count的值不可能一样。
3fork进程返回了两次,且父进程返回时pid大于0,子进程返回时pid=0,且子进程从返回处开始执行。
下面是《高级编程》详细介绍的父子进程之间的关系。
fork出来的子进程,基本上除了进程号之外父进程的所有东西都有一份拷贝,基本就意味着不是全部,下面我们要说的是子进程从父进程那里继承了什么东西,什么东西没有继承。还有一点需要注意,子进程得到的只是父进程的拷贝,而不是父进程资源的本身。
由子进程自父进程继承到:
1.进程的资格(真实(real)/有效(effective)/已保存(saved)用户号(UIDs)和组号(GIDs))
2.环境(environment)
3.堆栈
4.内存
5.打开文件的描述符(注意对应的文件的位置由父子进程共享,这会引起含糊情况)
6.执行时关闭(close-on-exec) 标志 (译者注:close-on-exec标志可通过fnctl()对文件描述符设置,POSIX.1要求所有目录流都必须在exec函数调用时关闭。更详细说明,参见《UNIX环境高级编程》 W. R. Stevens, 1993, 尤晋元等译(以下简称《高级编程》), 3.13节和8.9节)
7.信号(signal)控制设定
8.nice值 (译者注:nice值由nice函数设定,该值表示进程的优先级,数值越小,优先级越高)
进程调度类别(scheduler class)(译者注:进程调度类别指进程在系统中被调度时所属的类别,不同类别有不同优先级,根据进程调度类别和nice值,进程调度程序可计算出每个进程的全局优先级(Global process prority),优先级高的进程优先执行)
8.进程组号
9.对话期ID(Session ID) (译者注:译文取自《高级编程》,指:进程所属的对话期(session)ID, 一个对话期包括一个或多个进程组, 更详细说明参见《高级编程》9.5节)
10.当前工作目录
11.根目录 (译者注:根目录不一定是“/”,它可由chroot函数改变)
12.文件方式创建屏蔽字(file mode creation mask (umask))(译者注:译文取自《高级编程》,指:创建新文件的缺省屏蔽字)
13.资源限制
14.控制终端
子进程所独有:
进程号
1.不同的父进程号(译者注:即子进程的父进程号与父进程的父进程号不同, 父进程号可由getppid函数得到)
2.自己的文件描述符和目录流的拷贝(译者注:目录流由opendir函数创建,因其为顺序读取,顾称“目录流”)
3.子进程不继承父进程的进程,正文(text), 数据和其它锁定内存(memory locks)(译者注:锁定内存指被锁定的虚拟内存页,锁定后,4.不允许内核将其在必要时换出(page out),详细说明参见《The GNU C Library Reference Manual》 2.2版, 1999, 3.4.2节)
5.在tms结构中的系统时间(译者注:tms结构可由times函数获得,它保存四个数据用于记录进程使用中央处理器 (CPU:Central Processing Unit)的时间,包括:用户时间,系统时间, 用户各子进程合计时间,系统各子进程合计时间)
6.资源使用(resource utilizations)设定为0
8.阻塞信号集初始化为空集(译者注:原文此处不明确,译文根据fork函数手册页稍做修改)
9.不继承由timer_create函数创建的计时器
10.不继承异步输入和输出
3守护进程的创建
守护进程(Daemon)是运行在后台的一种特殊进程。它独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件。其次,守护进程必须与其运行前的环境隔离开来。这些环 境包括未关闭的文件描述符,控制终端,会话和进程组,工作目录以及文件创建掩模等。这些环境通常是守护进程从执行它的父进程(特别是shell)中继承下 来的。最后,守护进程的启动方式有其特殊之处。它可以在Linux系统启动时从启动脚本/etc/rc.d中启动,可以由作业规划进程crond启动,还
可以由用户终端(通常是 shell)执行。
守护进程创建的步骤:
1)在后台运行
为避免挂起控制终端将Daemon放入后台执行。方法是在进程中调用fork使父进程终止,让daemon在子进程中后台执行:
pid = fork(); if (pid > 0) { printf("parent is exit\n"); exit(0);//父进程退出 }
2)脱离控制终端,登录会话和进程组
Linux中的进程与控制终端,登录会话和进程组之间的关系:进程属于一个进程组,进程组号(GID)就是进程组长的进程(PID)。登录会话可以包含多个进程组。这些进程组共享一个控制终端。这个控制终端通常是创建进程的登录终端。 控制终端,登录会话和进程组通常是从父进程继承下来的。我们的目的就是要摆脱它们,使之不受它们的影响。方法是在第1点的基础上,调用setsid()使 进程成为会话组长:
if (setsid() == -1) { printf("setsid is failed\n"); }
setsid()调用成功后,进程成为新的会话组长和新的进程组长,并与原来的登录会话和进程组脱离。由于会话过程对控制终端的独占性,进程同时与控制终端脱离。
3)禁止进程重新打开控制终端(这个步骤可有可无,看情况)
现在,进程已经成为无终端的会话组长。但它可以重新申请打开一个控制终端。可以通过使进程不再成为会话组长来禁止进程重新打开控制终端:
if(pid=fork()) exit(0); //结束第一子进程,第二子进程继续(第二子进程不再是会话组长)
4)如果有打开的文件,就关闭打开的文件描述符
进程从创建它的父进程那里继承了打开的文件描述符。如不关闭,将会浪费系统资源,造成进程所在的文件系统无法卸下以及引起无法预料的错误。
for(i=0;i<=getdtablesize();i++) close(i)
5)改变当前工作目录(看情况)
进程活动时,其工作目录所在的文件系统不能卸下。一般需要将工作目录改变到根目录。对于需要转储核心,写运行日志的进程将工作目录改变到特定目录。
chdir("/tmp")
6)重设文件创建掩模
进程从创建它的父进程那里继承了文件创建掩模。它可能修改守护进程所创建的文件的存取位。为防止这一点,将文件创建掩模清除:
umask(0);
ok,基本步骤已经完成。nginx的damon进程只有步骤1,2,6,一般这3步就行了,不过复杂情况还是按步骤来写。
下面我们在看一个例子:
在子进程中打开一个文件,并向文件中写入数据,在满足一定条件时,守护进程退出。
void daemon_fork() { pid_t pid; pid = fork(); if (pid > 0) { printf("parent is exit\n"); exit(0);//第1步 } else if (pid < 0) { printf("fork is failed\n"); exit(-1); } else { if (setsid() == -1)//第2步 { printf("setsid is failed\n"); } umask(000);//第6步 printf("child is working\n"); FILE *fp = fopen("test.txt", "a"); if (fp == NULL) { kill(pid, SIGTERM); } //do something int i = 0; for (;;) { fprintf(fp, "%s", (u_char*)("I am the deamon two\n")); fprintf(fp, "i = %d\n", ++i); sleep(10); if (i > 10) { fclose(fp); kill(pid, SIGTERM); } } } }
http://blog.csdn.net/xiaoliangsky/article/details/39998373
参考:
http://blog.csdn.net/theone10211024/article/details/13774669
http://blog.chinaunix.net/uid-25365622-id-3055635.html