Linux 守护进程:进程组、会话与创建步骤
守护进程(daemon)是脱离控制终端、在后台长期运行的进程——服务器上的 sshd、nginx 都是。创建守护进程只有几步代码,但要理解为什么这么写,得先讲清三个基础概念:终端、进程组、会话。本文从概念到 setsid 的每一步给出动机,读完能独立写出不出错的守护进程初始化代码。
终端、进程组、会话
- 进程组:一个或多个进程的集合,用进程组 ID(PGID)标识。组长进程的 PID 等于 PGID;组长退出,进程组仍在。
kill的参数既可以发进程也可以发整组 - 会话:一个或多个进程组的集合,通常从用户登录建立、到用户退出结束,期间该用户启动的进程都属此会话。建立会话的进程是会话首进程
- 控制终端:与会话关联的终端设备,信息记录在 PCB 中。一个控制终端对应一个会话;会话里唯一的前台进程组能从终端读输入,其余是后台进程组
登录 shell 下的典型布局:
管道 find | sort | wc 里的三个命令恰好构成一个进程组;vim 正在前台,所以键盘输入归它。
相关函数
| 函数原型 | 说明 |
|---|---|
pid_t getpgrp(void); | 获取当前进程组 id |
pid_t getpgid(pid_t pid); | 获取指定进程的组 id |
int setpgid(pid_t pid, pid_t pgid); | 设置指定进程的组 id |
pid_t getsid(pid_t pid); | 获取指定进程的会话 id |
pid_t setsid(void); | 创建新会话并成为会话首进程 |
创建守护进程的六个步骤
pid_t pid = fork();
if (pid > 0) exit(0); // 1. 父进程退出
setsid(); // 2. 子进程创建新会话
umask(0); // 3. 清除文件权限掩码
chdir("/"); // 4. 切换工作目录到根目录
// 5. 关闭继承的文件描述符
// 6. 将 0、1、2 重定向到 /dev/null 每一步的动机:
1. fork 后父进程退出。 目的有二:一是让 shell 认为命令已执行完,回到提示符(形式上脱离控制终端);二是保证接下来的子进程不是进程组组长——组长调用 setsid 会失败,而父进程直接 fork 出来的子进程的 PGID 继承自父,不可能等于自己的 PID。子进程随后成为孤儿进程,由 1 号进程收养。
2. setsid() 创建新会话。 这是核心一步,调用进程成为新会话的首进程和新进程组的组长,一举摆脱原会话、原进程组、原控制终端。没有这一步,终端关闭时进程会收到挂断信号(SIGHUP)被带走。
3. umask(0) 清权限掩码。 子进程继承了父进程的文件权限掩码(如 050 会屏蔽组用户的读与执行位),不清零的话守护进程后续创建文件的权限将不可预期。清零后由创建调用(open/mkdir)显式指定权限。
4. chdir("/") 切换工作目录。 子进程继承父进程的工作目录,而工作目录所在的文件系统在进程占用期间无法卸载——守护进程长期活着,等于把某个挂载点钉死了。切到根目录(或 /tmp 这类永远在的路径)解除占用。
5. 关闭继承的文件描述符。 从父进程继承的打开文件不但占资源,同样会钉住文件系统。守护进程已脱离终端,标准输入、输出、错误(fd 0、1、2)没有存在的意义,一并关闭。
6. 重定向 0、1、2 到 /dev/null。 直接关闭这三个 fd 有隐患:守护进程之后调用的库或第三方代码一旦习惯性往 stderr 写日志、从 stdin 读输入,会错误地读写到后来打开的其他文件(fd 会复用最小的空闲编号)。打开 /dev/null 并 dup2 到 0、1、2 上,读写永远无害:
int fd = open("/dev/null", O_RDWR);
dup2(fd, STDIN_FILENO);
dup2(fd, STDOUT_FILENO);
dup2(fd, STDERR_FILENO);
if (fd > STDERR_FILENO) close(fd); 现代开发中通常不手写这些步骤,而是调用 BSD 的 daemon(1, 1) 或 systemd 等服务管理器接管;理解每步的动机仍有价值——排查「终端一关服务就死」「文件系统卸载不掉」这类问题时,答案就藏在这六步里。
要点
- 前台进程组独占控制终端输入;会话首进程建立会话,一个终端一个会话
- 六步各自的动机:fork+退出保证子进程不是组长(setsid 的前提);setsid 摆脱终端(防 SIGHUP);umask 保证文件权限可控;chdir 解除挂载点占用;关 fd 省资源;重定向 /dev/null 防 fd 复用误伤
- 关闭 0/1/2 后必须重定向,不能裸关
- 实际项目优先用 daemon() 或 systemd,但排障时这六步是检查清单