守护进程(daemon)就是一直在后台运行的进程,它没有控制终端,无法和前台的用户交互。当我们打开一个终端时会创建一个session会话(shell),从用户登录开始到用户退出为止,这段时间内在该终端执行的进程都属于这一个会话。一个会话一般包含一个前台进程组、一个后台进程组和一个会话首进程(shell程序本身)。 例如用以下命令启动5个进程:
$ proc1 | proc2 &
$ proc3 | proc4 | proc5
proc1和proc2属于同一个后台进程组,proc3、proc4、proc5属于同一个前台进程组,Shell进程本身属于一个单独的进程组。这些进程组的控制终端相同,它们属于同一个session。 "后台任务"与"前台任务"的本质区别只有一个:是否继承标准输入。后台任务不再继承当前 session 的标准输入(stdin),你无法向后台任务输入指令了,但是后台任务继承了标准输出(stdout)和标准错误(stderr)后台任务的所有输出依然会同步地在命令行下显示
当终端关闭或者检测到网络连接断开时会将挂断信号(SIGHUP)发送给终端控制进程(会话期首进程,shell进程)。如果会话期首进程接收到SIGHUP信号后会终止,会同时给前台进程组发送SIGHUP信号(进程接收到SIGHUP信号默认处理是退出),shell的 huponexit 参数
(shopt | grep huponexit)
决定了shell退出时是否发送SIGHUP信号给后台进程组。
守护进程要与从启动它的父进程(一般是shell程序)的运行环境隔离开来,需要处理的内容大致包括会话、控制终端、进程组、文件描述符、文件权限掩码以及工作目录等。 ``` void init daemon() { pid t pid; int i = 0; // 1. 创建子进程,父进程退出,父进程退出子进程变成孤儿进程,孤儿进程由init进程(pid为1)收养 if ((pid = fork()) == -1) { printf("Fork error !/n"); exit(1); } if (pid != 0) { exit(0); // 父进程退出 }
// 2. 子进程调用 setsid 创建新会话,成为成为会话首进程,并且子进程成为一个新进程组的组长进程,而新的会话也脱离了控制终端的控制。 //(组长进程调用 setsid会出错,所以第一步fork出子进程,fork出的子进程一定不会是组长进程) setsid();
//3. 子进程变成无终端的会话首进程,但是它仍然可以重新申请打开一个控制终端。可以通过再次创建子进程结束当前进程,使进程不再是会话首进程来禁止进程重新打开控制终端。 if ((pid = fork()) == -1) { printf("Fork error !/n"); exit(-1); } if (pid != 0) { exit(0);
} //4. 改变当前目录为根目录,重设文件权限掩码,关闭文件描述符 //因为这几样东西都是继承自父进程的 chdir("/tmp"); // 改变工作目录 umask(0); // 重设文件掩码 for (; i < getdtablesize(); ++i) { close(i); // 关闭打开的文件描述符 }
return;
}
```
目前Go程序还不能实现daemon,因为go程序在启动时runtime可能会创建多个线程(用于内存管理,垃圾回收,goroutine管理等),而fork并不能处理好拥有多个线程的进程。
` d := flag.Bool("d", false, "Whether or not to launch in the background(like a daemon)") if *d { cmd := exec.Command(os.Args[0], "-close-fds", ) serr, err := cmd.StderrPipe() if err != nil { log.Fatalln(err) } err = cmd.Start() if err != nil { log.Fatalln(err) } s, err := ioutil.ReadAll(serr) s = bytes.TrimSpace(s) if bytes.HasPrefix(s, []byte("addr: ")) { fmt.Println(string(s)) cmd.Process.Release() } else { cmd.Process.Kill() } }
`
Linux 守护进程的实现 Linux 守护进程的启动方法