1.守护进程的概念及相关的概念
守护进程也叫精灵进程,指的是脱离终端,独立运行在后台,长期运行,用来提供服务的进程。
守护进程的特点:
1.不依赖任何终端(关闭终端守护进程而不会退出)。
2.父进程通常是init或者是systemd;
3.用于提供服务,网络服务,定时服务等。
1. 进程组(Process Group)
- 定义:一个或多个进程的集合,用于统一管理(如发送信号)。
- 特点:
- 每个进程组有唯一的进程组 ID(PGID);
- 进程组的生命周期从创建开始,到最后一个进程离开(终止或加入其他组)结束。
2. 组长进程(Process Group Leader)
- 定义:进程组的创建者,其PID = PGID;
- 特点:组长进程可以创建进程组和组内进程,但组长进程终止不会影响进程组(只要还有其他进程存在)。
3. 会话(Session)
- 定义:一个或多个进程组的集合,通常与一个终端关联(控制终端)。
- 特点:
- 每个会话有唯一的会话 ID(SID);
- 一个会话最多有一个控制终端(用于输入输出);
- 会话中的进程组分为前台进程组 (与终端交互)和后台进程组(不交互)。
4. 会话首进程(Session Leader)
- 定义:会话的创建者,其PID = SID;
- 特点:只有会话首进程能重新获取控制终端(这也是守护进程需要二次 fork 的原因)。
二、详细解析守护进程实现步骤
1. 第一次 fork()
- 作用:让父进程退出,子进程继续运行。
- 原因 :
- 父进程通常是 shell 进程,退出后子进程变成孤儿进程,由
init/systemd收养; - 确保子进程不是进程组组长(为后续
setsid创造条件,因为组长进程不能创建新会话)。
- 父进程通常是 shell 进程,退出后子进程变成孤儿进程,由
2. setsid()
- 作用:创建一个新会话,使子进程成为新会话的首进程和新进程组的组长,并脱离原控制终端。
- 底层逻辑 :
- 新会话的 SID = 子进程 PID;
- 新进程组的 PGID = 子进程 PID;
- 子进程不再关联任何控制终端。
3. 第二次 fork()
- 作用:让子进程(会话首进程)退出,孙进程继续运行。
- 原因 :
- 会话首进程可以重新获取控制终端,二次 fork 后孙进程不再是会话首进程,彻底避免重新关联终端;
- 进一步确保守护进程的独立性。
4. chdir("/")
- 作用:将工作目录切换到根目录。
- 原因 :
- 守护进程长期运行,若工作目录在其他文件系统(如 U 盘),会导致该文件系统无法卸载;
- 根目录始终存在,安全性高。
5. umask(0)
- 作用:将文件权限掩码设为 0。
- 原因 :
- 守护进程可能需要创建文件,继承的权限掩码(如 022)会限制文件权限;
- 设为 0 后,创建文件时权限完全由
open/creat的参数决定(如0644)。
6. close() 关闭文件描述符
- 作用:关闭从父进程继承的所有文件描述符(尤其是 0、1、2,对应标准输入、输出、错误)。
- 原因 :
- 守护进程脱离终端,这些文件描述符不再使用,浪费资源;
- 避免后续操作(如
open)意外使用这些描述符导致错误。
- 优化 :通常关闭
0-2后,将它们重定向到/dev/null(空设备),防止程序因读写标准 IO 出错。
三、守护进程代码演示(C 语言)
cpp
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
#include <signal.h>
#include <syslog.h>
void create_daemon() {
pid_t pid;
// 1. 第一次fork
pid = fork();
if (pid < 0) {
perror("fork failed");
exit(EXIT_FAILURE);
}
if (pid > 0) {
exit(EXIT_SUCCESS); // 父进程退出
}
// 2. 创建新会话
if (setsid() < 0) {
perror("setsid failed");
exit(EXIT_FAILURE);
}
// 3. 忽略SIGHUP信号(防止会话首进程退出时孙进程被终止)
signal(SIGHUP, SIG_IGN);
// 4. 第二次fork
pid = fork();
if (pid < 0) {
perror("second fork failed");
exit(EXIT_FAILURE);
}
if (pid > 0) {
exit(EXIT_SUCCESS); // 会话首进程退出
}
// 5. 切换工作目录到根目录
if (chdir("/") < 0) {
perror("chdir failed");
exit(EXIT_FAILURE);
}
// 6. 设置文件权限掩码为0
umask(0);
// 7. 关闭所有继承的文件描述符
// 先获取最大文件描述符数
int max_fd = sysconf(_SC_OPEN_MAX);
if (max_fd == -1) {
max_fd = 1024; // 默认值
}
for (int i = 0; i < max_fd; i++) {
close(i);
}
// 8. 重定向0、1、2到/dev/null
open("/dev/null", O_RDWR); // 0
dup(0); // 1
dup(0); // 2
// 9. 初始化syslog(用于守护进程日志输出)
openlog("mydaemon", LOG_PID, LOG_DAEMON);
}
int main() {
create_daemon();
// 守护进程的核心逻辑:每隔10秒写一条日志
while (1) {
syslog(LOG_INFO, "Daemon is running...");
sleep(10);
}
closelog();
return 0;
}
代码解析
- 第一次 fork:父进程退出,子进程成为孤儿进程。
- setsid:子进程成为新会话和新进程组的组长,脱离终端。
- 忽略 SIGHUP:防止第二次 fork 后,会话首进程退出时孙进程收到 SIGHUP 信号被终止。
- 第二次 fork:孙进程不再是会话首进程,彻底避免重新关联终端。
- chdir("/"):切换到安全的根目录。
- umask(0):确保文件创建权限不受限制。
- 关闭文件描述符:释放资源,避免意外操作。
- 重定向到 /dev/null:让标准 IO 操作 "空转",防止程序出错。
- syslog :守护进程无法使用 printf,通过 syslog 将日志写入系统日志(
/var/log/syslog或/var/log/messages)。
四、补充:守护进程的管理
-
查看守护进程 :
ps aux | grep mydaemon -
查看日志 :
tail -f /var/log/syslog -
终止守护进程 :
kill <daemon_pid>