大体步骤如下
1. 调用fork()函数创建子进程后,让父进程立即exit(),这样产生的子进程变成孤儿进程,由init进程接管。
2. 调用setsid()函数,使得新创建的进程脱离控制终端,同时创建新的进程组,并成为该进程组的首进程。在linux系统中,所有的进程都属于各自的进程组,进程组是一个或多个进程的集合,一个进程组中至少有一个进程成员,否则就消亡了。每个进程组都有一个进程组ID,是由领头进程的进程号决定的,会话则是一个或多个进程组的集合,每个会话都有一个领头进程,会话和进程组是linux内核用于管理多用户情况下用户进程的方法,每个进程都属于一个进程组,而进程组又属于某个会话,当用户从终端登录系统时,系统会创建一个新的会话,在该终端上启动的进程都会被系统划归到会话的进程组中。会话中的进程通过该会话中的领头进程与一个终端相连,该终端是会话的控制终端,一个会话只能有一个控制终端,如果会话存在一个控制终端时,则它必然拥有一个前台进程组,属于该组的进程可以从从控制终端获得输入。由于守护进程没有控制终端,而使用fork()函数创建的子进程会继承父进程的控制终端,会话和进程组,因此,必须用setsid()创建新的会话,以脱离父进程的影响。Setsid函数将创建新的会话,并使得调用setsid函数的进程成为新会话的领头进程。调用setsid函数的进程是新创建会话中的唯一的进程组,进程组ID为调用进程的进程号。Setsid函数产生这一结果还有个条件,即调用进程不为一个进程的领头进程。由于在第一步调用fork的父进程退出,使得子进程不可能是进程组的领头进程。该会话的领头进程没有控制终端与其相连,至此,满足了守护进程没有控制终端的要求。
3. 更改当前工作目录
使用fork()函数创建的子进程会继承父进程的当前工作目录,当进程工作没有结束时,其工作目录是不能被卸载的.为了防止此问题,守护进程一般要chdir()了数将其工作目录更改到别的目录下(一般为/根目录,因为根目录是永远不会被卸载的,除非关机).
4. 关闭文件描述符,并重定向标准输入,输出和错误
子进程会继承父进程某些打开了的文件描述符,如果不使用这些文件描述符,则需要关闭它们.守护进程是运行在系统后台的,不应该在终端有任何的输出信息,可以使用dup()函数将其重定向到/dev/null空设备上.
5. 设置守护进程的文件权限创建掩码
守护进程会创建一些临时文件,出于性的考虑,往往不希望这些文件被别的用户查看,这时可以用umask()函数修改文件权限,创建掩码的取值.
上面说的是创建守护进程的大体步骤,解释其中的两点:
1、setsid的作用
一、让进程摆脱原会话的控制
二. 让进程摆脱原进程组的控制
三. 让进程摆脱原控制终端的控制
setsid()就是将进程和它当前的对话过程和进程组分离开,并且把它设置成一个新的对话过程的领头进程。也就是说让调用setsid()函数的这个进程脱离原有的进程组,自立门户。同时需要注意一点就是,调用这个函数的进程不能是一个进程组的进程组长,不然进程组长自立门户一户,这个进程组的其他进程也就会消亡(这个和平时自己创建父子进程,然后父子进程各自做各自的事情还不太一样)。这也就解释了上面红色字体的那句话。在子进程中调用setsid,不管这个时候父进程退出与否,子进程都不会是它当前所在进程组的进程组长。所以调用这个函数的是子进程,下面这个例子就是在父进程中调用setsid的副作用,会使父进程所在的进程组的其他子进程销毁
pid_t pid = fork();
if (pid == 0) {
...
int result = execl(path, "adb", "fork-server", "server", NULL);
} else {
// run a program in a new session
setsid();//之前parent和child运行在同一个session里,而且parent是session头,
//所以作为session头的parent如果exit结束执行的话,那么会话session组中的所有进程将都被杀死;
//所以执行setsid()之后,parent将重新获得一个新的会话session组id,child将仍持有原有的会话session组,
//这时parent退出之后,将不会影响到child了。
}
会话session是一个或多个进程组的集合。进程调用setsid函数建立一个新会话。如果调用此函数的进程不是一个进程组的组长,则此函数就会创建一个新会话,该进程变成会话的首进程,然后该进程成为一个新进程组的组长进程,该进程没有控制终端。因为会话首进程是具有唯一进程ID的单个进程,所以可以将会话首进程的进程ID视为会话Id。
2、其实创建daemon进程是调用fork()两次的。
一、如果开启daemon进程的父进程本身可能不单单是为了创建daemon,假如父进程在某个地方一直阻塞,这时守护进程一直在运行,父进程却没有正常退出。如果守护进程因为正常或非正常原因退出了,就会变成ZOMBIE进程。
如果fork两次呢?父进程先fork出一个儿子进程,儿子进程再fork出孙子进程做为守护进程,然后儿子进程立刻退出,守护进程被init进程接管,这样无论父进程做什么事,无论怎么被阻塞,都与守护进程无关了。所以,fork两次的守护进程很安全,避免了僵尸进程出现的可能性。
二、第一个子进程调用setsid()函数,调用以后第一个子进程才成为了新的进程组的进程组长(注意,调用setsid()函数的第一个子进程在调用这个函数之前不是进程组的进程组长)第一子进程成为新的会话组长和进程组长,进程组长有权利申请打开一个控制终端,第二次fork的意义就在此,关闭掉第一子进程,使第二子进程成为守护进程,并且因为没有进程组长, 所以守护进程不会被关闭。
下面是个代码示例:
#include <stdio.h> #include <stdlib.h> #include <sys/types.h> #include <sys/stat.h> #include <time.h> #include <syslog.h> #include <signal.h> #include <sys/param.h> #include <fcntl.h> int init_daemon(void) { int pid; int i; int fd; /*忽略终端I/O信号,STOP信号*/ signal (SIGTTOU, SIG_IGN); signal (SIGTTIN, SIG_IGN); signal (SIGTSTP, SIG_IGN); signal (SIGHUP, SIG_IGN); printf(“ppid = %d\n”,getppid()); pid = fork(); fd = open(“/dev/tty”,O_RDONLY); printf(“!fd = %d\n”,fd); close(fd); if (pid > 0) { printf(“Parent process pid = %d\n”,getpid()); exit(0); //结束父进程,使得子进程成为后台进程 } else if (pid < 0) return -1; printf(“First Child process pid = %d\n”,getpid()); //当前进程为第一子进程 //建立一个新的进程组,在这个新的进程组中,子进程成为这个进程组的首进程,以使该进程脱离所有终端 printf(“pgid = %d\n”,getpgid(getpid())); //printf(“pid = %d\n”,getpid()); setsid(); printf(“pgid = %d\n”,getpgid(getpid())); //printf(“pid = %d\n”,getpid()); fd = open(“/dev/tty”,O_RDONLY); printf(“fd = %d\n”,fd); close(fd); //再次新建一个子进程,退出父进程(第一子进程),保证该进程不是进程组长,同时让该进程无法再打开一个新的终端 pid = fork(); fd = open(“/dev/tty”,O_RDONLY); printf(“#fd = %d\n”,fd); close(fd); if (pid > 0) { printf(“First Child process pid = %d\n”,getpid()); exit(0); } else if (pid < 0) return -1; //关闭所有从父进程继承的不再需要的文件描述符 for (i=0; i < NOFILE; close(i++)); //改变工作目录,使得进程不与任何文件系统联系 chdir(“/”); //将文件屏蔽字设置为0 umask(0); //忽略SIGCHLD信号 signal(SIGCHLD,SIG_IGN); return 0; } int main(int argc, char *argv[]) { time_t now; init_daemon(); syslog(LOG_USER|LOG_INFO,”测试守护进程!\n”); while(1) { sleep(8); time(&now); syslog(LOG_USER|LOG_INFO,”系统时间:\t%s\t\t\n”,ctime(&now)); } }