1.生命周期很是长,一旦启动,通常不会终止,直到系统推出,不过dameon进程能够经过stop或者发送信号将其杀死
2.在后台执行,不跟任何控制终端关联,终端信号好比:SIGINT,SIGQUIT,SIGTSTP,以及关闭终端都不会影响deamon
如何编写Daemon进程,须要遵循如下规则:
(1)
执行fork()函数,父进程退出,子进程继续
执行这一步,缘由有二:
·父进程有多是进程组的组长(在命令行启动的状况下),从而不可以执行后面要执行的setsid函数,子进程继承了父进程的进程组ID,而且拥有本身的进程ID,必定不会是进程组的组长,因此子进程必定能够执行后面要执行的setsid函数。
·若是daemon是从终端命令行启动的,那么父进程退出会被shell检测到,shell会显示shell提示符,让子进程在后台执行。
(2)
子进程执行以下三个步骤,以摆脱与环境的关系
1)
修改进程的当前目录为根目录(/)。
这样作是有缘由的,由于daemon一直在运行,若是当前工做路径上包含有根文件系统之外的其余文件系统,那么这些文件系统将没法卸载。所以,常规是将当前工做目录切换成根目录,固然也能够是其余目录,只要确保该目录所在的文件系统不会被卸载便可。
chdir("/")
2)
调用setsid函数。这个函数的目的是切断与控制终端的全部关系,而且建立一个新的会话。
这一步比较关键,由于这一步确保了子进程再也不归属于控制终端所关联的会话。所以不管终端是否发送SIGINT、SIGQUIT或SIGTSTP信号,也不管终端是否断开,都与要建立的daemon进程无关,不会影响到daemon进程的继续执行。
3)
设置文件模式建立掩码为0。
这是为了让daemon进程建立的文件权限属性跟shell脱离关系,由于默认状况下,进程的umask来源于父进程shell的umask.若是不执行umask(0),那么父进程shell的umask就会影响daemon进程的umask.若是用户改变了shell的umask,那么也就改变了dameon的umask,就会使得daemon进程每次执行的umask信息可能不一致
(3)
再次执行fork,父进程退出,子进程继续
执行完前面两步以后,能够说已经比较圆满了:新建会话,进程是会话的首进程,也是进程组的首进程。进程ID、进程组ID和会话ID,三者的值相同,进程和终端无关联。那么这里为什么还要再执行一次fork函数呢?
缘由是,daemon进程有可能会打开一个终端设备,即daemon进程可能会根据须要,执行相似以下的代码:
int fd = open("/dev/console", O_RDWR);
这个打开的终端设备是否会成为daemon进程的控制终端,取决于两点:
·daemon进程是否是会话的首进程。
·系统实现。(BSD风格的实现不会成为daemon进程的控制终端,可是POSIX标准说这由具体实现来决定)。
既然如此,为了确保万无一失,只有确保daemon进程不是会话的首进程,才能保证打开的终端设备不会自动成为控制终端。所以,不得不执行第二次fork,fork以后,父进程退出,子进程继续。这时,子进程再也不是会话的首进程,也不是进程组的首进程了。
(4)
关闭标准输入(stdin)、标准输出(stdout)和标准错误(stderr)
由于文件描述符0、1和2指向的就是控制终端。daemon进程已经再也不与任意控制终端相关联,所以这三者都没有意义。通常来说,关闭了以后,会打开/dev/null,并执行dup2函数,将0、1和2重定向到/dev/null。这个重定向是有意义的,防止了后面的程序在文件描述符0、1和2上执行I/O库函数而致使报错。
至此,即完成了daemon进程的建立,进程能够开始本身真正的工做了。
上述步骤比较繁琐,对于C语言而言,glibc提供了daemon函数,从而帮咱们将程序转化成daemon进程。
#include <unistd.h>int daemon(int nochdir, int noclose);
该函数有两个入参,分别控制一种行为,具体以下。
其中的
nochdir,用来控制是否将当前工做目录切换到根目录。
·0:将当前工做目录切换到/。
·1:保持当前工做目录不变。
而
noclose,用来控制是否将标准输入、标准输出和标准错误重定向到/dev/null。
·0:将标准输入、标准输出和标准错误重定向到/dev/null。
·1:保持标准输入、标准输出和标准错误不变。
通常状况下,这两个入参都要为0。
成功时,daemon函数返回0;失败时,返回-1,并置errno。由于daemon函数内部会调用fork函数和setsid函数,因此出错时errno能够查看fork函数和setsid函数的出错情形。
glibc的daemon函数作的事情,和前面讨论的大致一致,可是作得并不完全,没有执行第二次的fork。
进程的终止
在不考虑线程的状况下,进程的退出有如下5种方式。
正常退出有3种:
·从main函数return返回
·调用exit
·调用_exit
异常退出有两种:
#include <unistd.h>void _exit(int status);
用户调用_exit函数,本质上是调用exit_group系统调用。这点在前面已经详细介绍过,在此就再也不赘述了。
exit函数
exit函数更常见一些,其接口定义以下:
#include <stdlib.h>void exit(int status);
exit()函数的最后也会调用_exit()函数,可是exit在调用_exit以前,还作了其余工做:
1)执行用户经过调用atexit函数或on_exit定义的清理函数。
2)关闭全部打开的流(stream),全部缓冲的数据均被写入(flush),经过tmpfile建立的临时文件都会被删除。
3)调用_exit。
图4-11给出了exit函数和_exit函数的差别。
下面介绍exit函数和_exit函数的不一样之处。
首先是exit函数会执行用户注册的清理函数。用户能够经过调用atexit()函数或on_exit()函数来定义清理函数。这些清理函数在调用return或调用exit时会被执行。执行顺序与函数注册的顺序相反。当进程收到致命信号而退出时,注册的清理函数不会被执行;当进程调用_exit退出时,注册的清理函数不会被执行;当执行到某个清理函数时,若收到致命信号或清理函数调用了_exit()函数,那么该清理函数不会返回,从而致使排在后面的须要执行的清理函数都会被丢弃。
其次是exit函数会冲刷(flush)标准I/O库的缓冲并关闭流。glibc提供的不少与I/O相关的函数都提供了缓冲区,用于缓存大块数据。
缓冲有三种方式:无缓冲(_IONBF)、行缓冲(_IOLBF)和全缓冲(_IOFBF)。
·无缓冲:就是没有缓冲区,每次调用stdio库函数都会马上调用read/write系统调用。
·行缓冲:对于输出流,收到换行符以前,一概缓冲数据,除非缓冲区满了。对于输入流,每次读取一行数据。
·全缓冲:就是缓冲区满以前,不会调用read/write系统调用来进行读写操做。
对于后两种缓冲,可能会出现这种状况:进程退出时,缓冲区里面可能还有未冲刷的数据。若是不冲刷缓冲区,缓冲区的数据就会丢失。好比行缓冲迟迟没有等到换行符,又或者全缓冲没有等到缓冲区满。尤为是后者,很容易出现,由于glibc的缓冲区默认是8192字节。exit函数在关闭流以前,会冲刷缓冲区的数据,确保缓冲区里的数据不会丢失。
- }
#include <stdio.h>#include <stdlib.h>#include <unistd.h>void foo(){ fprintf(stderr,"foo says bye.\n");}void bar(){ fprintf(stderr,"bar says bye.\n");}int main(int argc, char **argv){ atexit(foo); atexit(bar); fprintf(stdout,"Oops ... forgot a newline!"); sleep(2); if (argc > 1 && strcmp(argv[1],"exit") == 0) exit(0); if (argc > 1 && strcmp(argv[1],"_exit") == 0) _exit(0);
return 0;
注意上面的示例代码,fprintf打印的字符串是没有换行符的,对于标准输出流stdout,采用的是行缓冲,收到换行符以前是不会有输出的。输出状况以下:
manu@manu-hacks:exit$ ./test exit //调用exit结束,输出了缓冲区的字符bar says bye.foo says bye.Oops ... forgot a newline!manu@manu-hacks:exit$ //调用return 输出了缓冲区字符manu@manu-hacks:exit$manu@manu-hacks:exit$ ./testbar says bye.foo says bye.Oops ... forgot a newline!manu@manu-hacks:exit$ //直接调用_exit没有输出缓冲区的字符manu@manu-hacks:exit$manu@manu-hacks:exit$ ./test _exitmanu@manu-hacks:~/code/self/c/exit$
尽管缓冲区里的数据没有等到换行符,可是不管是调用return返回仍是调用exit返回,缓冲区里的数据都会被冲刷,“Oops...forgot a newline!”都会被输出。由于exit()函数会负责此事。从测试代码的输出也能够看出,exit()函数首先执行的是用户注册的清理函数,而后才执行了缓冲区的冲刷。
第三,存在临时文件,exit函数会负责将临时文件删除.
exit函数的最后调用了_exit()函数,最终异曲同工,走向内核清理。
return退出
return是一种更常见的终止进程的方法。执行return(n)等同于执行exit(n),由于调用main()的运行时函数会将main的返回值看成exit的参数。