如何编写一个守护进程。我们首先得清晰地理解守护进程的定义。守护进程是一类特殊的进程,它在 系统后台持续运行,通常不与控制终端相连接,主要承担执行各类系统任务的职责,像服务器程序的稳定运行、定时任务的精准调度等。在实际应用场景中,用户往往有创建长期稳定运行服务的需求,例如搭建高效的 Web 服务器来提供网络服务,或者开发可靠的日志监控程序以实时监测系统日志。
通常而言,创建守护进程包含以下几个关键步骤:
- 首先,借助
fork
系统调用创建一个子进程,随后让父进程退出。这一操作能使终端认为命令已经执行完毕,从而让子进程得以在后台持续运行。 - 其次,调用
setsid
函数创建一个新的会话。此步骤的核心目的在于使子进程脱离原终端的控制,让它能够独立于终端在后台稳定运行。 - 接着,将子进程的工作目录更改为根目录。这样做可以避免守护进程占用可卸载的文件系统,防止因文件系统卸载而引发不必要的问题。
- 之后,需要设置文件创建掩码,一般将其设置为 0。如此一来,守护进程在创建文件时就能拥有更大的权限灵活性,可根据实际需求灵活控制文件的访问权限。
- 最后,关闭不必要的文件描述符,例如标准输入、标准输出和标准错误输出。这一举措能够有效防止这些文件描述符持续占用系统资源,确保系统资源得到合理利用。
基于上述创建守护进程的步骤,我们可以编写具体的代码示例。在编写过程中,要保证每个步骤都有对应的代码实现,同时详细阐释每个步骤的作用。
- 创建子进程并退出父进程:使用
fork
系统调用创建子进程后,让父进程退出。此时,子进程会成为孤儿进程,由init
进程接管。这使得子进程能在后台继续运行,脱离用户终端的直接控制。 - 创建新会话:调用
setsid
函数,该函数的作用是让子进程成为新的会话组长,从而脱离原终端的控制,进一步确保守护进程能独立稳定地在后台运行。 - 更改工作目录:将工作目录更改为根目录,这是为了防止守护进程的当前工作目录被卸载。若当前目录被卸载,可能会引发一系列问题,影响守护进程的正常运行。
- 设置文件创建掩码:设置
umask
通常为 0,这样守护进程在创建文件时能拥有默认的权限,可根据实际需求灵活调整文件的访问权限。 - 关闭文件描述符:关闭标准输入、标准输出和标准错误输出等不需要的文件描述符,目的是释放系统资源。有时,为了避免一些意外情况,还需要将这些文件描述符重定向到
/dev/null
或者日志文件。
此外,在编写守护进程的代码时,可能需要处理信号。例如,当接收到 SIGHUP
信号时,守护进程可能需要重新加载配置。因此,在代码中可能要添加信号处理函数,以确保守护进程能对特定信号做出正确响应。
同时,守护进程通常需要记录日志,以便后续的调试和监控。可以通过 syslog
或者直接写入日志文件来实现日志记录。例如,可以使用 openlog
和 syslog
函数,方便地将日志信息记录下来。
守护进程在运行过程中,常面临以下几类问题:
- 终端脱离失败:若守护进程未能正确脱离终端,那么当终端关闭时,守护进程也会随之终止,无法持续在后台运行。
- 资源泄漏风险:若文件描述符没有被正确关闭,会造成系统资源的持续占用,长此以往会导致资源泄漏,影响系统的整体性能。
- 信号处理不当:守护进程若没有对信号进行正确处理,可能会使其无法优雅地退出,或者在需要重新加载配置时无法及时响应,进而影响服务的稳定性。
- 文件系统卸载问题:若守护进程的工作目录没有被更改,在尝试卸载对应的文件系统时,会因为守护进程对该目录的占用而无法完成卸载操作。
- 日志调试难题:日志记录若不规范或存在缺失,会使得在排查问题时缺乏足够的信息,给调试工作带来极大的困难。
在 Linux 系统里,守护进程(Daemon) 是一类在后台长期稳定运行的进程,它们通常独立于控制终端,并且会按照一定的周期执行各类任务,如提供服务、进行日志监控等。本文将详细介绍编写守护进程的标准步骤,并给出相应的代码示例。
(1) 进程首次执行
fork
操作,此步骤旨在为后续调用setsid
函数做好铺垫。(2) 进程调用
setsid
函数,成功成为新会话的领头进程,从而与原控制终端脱离关联。(3) 进程忽略 SIGHUP 信号,并进行第二次
fork
操作。这使得该进程成为一个新进程组的领导者,进一步确保其独立运行。(4) 关闭所有文件描述符,释放系统资源,避免不必要的资源占用。
(5) 消除
umask
的影响,确保守护进程在创建文件或目录时,能够依据自身需求灵活设置权限。(6) 修改守护进程的当前工作目录,防止因工作目录被卸载或其他操作而影响守护进程的正常运行。
(7) 重新定位标准 I/O 描述符,将输入、输出和错误信息进行合理定向,避免对终端产生干扰。
(8) 采取措施保证服务器互斥运行,防止多个相同守护进程同时运行导致的冲突和资源竞争问题。
(9) 借助
syslog
来记录守护进程的错误信息,便于后续对守护进程的运行状态进行监控和问题排查。
其中,核心步骤为:
1> 第一次fork为setsid()创建新会话做准备 ---> 利用子进程初步和终端进程区分开
2> 利用创建的子进程创建出新的会话 ---> 进脱离终端的控制
//有的资料就将创建出新会话进程作为守护进程使用,是可以的
//为了让守护进程进一步脱离和终端的联系,我们需要进行第二次fork
3>第二次调用fork() ---> 初步得到守护进程
//到这一步,守护进程已经创建好了,后序的操作是进一步修饰守护进程
-------------------------------------------------------------------------------
4> 关闭所有打开的文件描述符 ---> 守护进程不能有输入也不能有输出
5> 消除 uamsk 的影响 ---> 对守护进程的进一步处理
6> 更改守护进程的工作路径 "/" ---> 确保守护进程能够运行
7> 将文件描述符重定向 /dev/null ---> 防止关闭的文件描述符再次打开
第一次 fork
创建子进程,父进程退出。
守护进程的特性在于其会脱离控制终端运行。在完成第一步操作后,在 Shell 终端会呈现出程序已然运行结束的假象。实际上,后续的所有工作均由子进程负责完成,此时用户可以在 Shell 终端自如地执行其他命令,从表现形式上实现了与控制终端的脱离。
在操作过程中,父进程会先于子进程退出,这就导致子进程失去了父进程,进而转变为一个孤儿进程。在 Linux 系统里,一旦系统检测到孤儿进程,便会自动让 1 号进程收养该孤儿进程。如此一来,原先的子进程就成为了 init
进程的子进程。
pid=fork(); if (pid < 0){
fprintf(stderr, “error in first fork.\n”);
exit(1);
}
if(pid>0){ /*父进程退出*/
exit(0);
}
在子进程中创建新会话
进程组
进程组是由一个或多个进程所组成的集合。每个进程组都由唯一的进程组 ID 进行标识。如同进程号(PID)是进程的关键标识一样,进程组 ID 也是进程不可或缺的属性之一。
在每个进程组中,都存在一个组长进程。该组长进程的进程号与进程组 ID 相等,它在进程组中扮演着特定的角色和功能。
会话期
会话组是由一个或多个进程组组合而成的集合。
一般而言,一个会话的生命周期始于用户登录系统,终于用户退出系统。在这一时间段内,该用户所启动运行的所有进程都归属于这个会话期。
进程组 对话期 与 终端
setsid
函数的作用
setsid
函数的主要用途是创建一个新的会话,并且使调用该函数的进程成为这个新会话组的组长(即新会话的领头进程)。具体来说,它能实现以下几个关键功能:
- 脱离原会话控制:使进程不再受原会话的约束和管理。
- 脱离原进程组控制:让进程从原有的进程组中独立出来,不再受原进程组的影响。
- 脱离原控制终端控制:使进程与原控制终端断开联系,不再依赖于原控制终端进行输入输出等操作。
简而言之,setsid
函数能够让进程实现完全的独立,使其不再受其他进程的控制和影响。
当子进程继续运行而父进程退出时,系统会产生 SIGHUP
信号。由于第一次 fork()
产生的子进程会成为新会话的领头进程,如果此时再打开一个新的终端,这个新终端可能会成为该进程的控制终端。为了避免这种情况,确保进程彻底脱离控制终端,通常需要进行第二次 fork()
操作。
忽略信号SIGHUP,第二次fork
- 进程脱离了控制终端,
- 与退出的父进程属于同一组;
- 进程调用
setgrp()
, - 是进程脱离原来的进程组。
- 已经调整好自身位置。
关闭所有文件描述符
服务进程必须关闭它所继承的文件描述符:
max_fd = sysconf(_SC_OPEN_MAX);
for (i = 0; i < max_fd;i++)
close(i);
消除umask的影响
每个进程都有一个umask: 文件权限掩码是指屏蔽掉文件权限中的对应位。由于使用 fork 新建的子进程继承了父进程的文件权限掩码,这就给该子进程使用文件带来了诸多的麻烦。
通常的使用方法为umask(0):增加该守护进程的灵活性;umask (0) 清除旧有的文件掩码。
最后的权限: mode & ~umask
改变当前目录为根目录
守护进程当前工作目录的影响
守护进程运行时,若出现错误,通常会将错误信息记录在当前目录的 core
文件中。而且,守护进程往往会长时间保持对当前目录的打开状态。因此,需要为其选择一个不会被卸载的目录作为当前工作目录。
一般来说,会将根目录 “/”
指定为守护进程的当前工作目录。
使用 fork
创建的子进程会继承父进程的当前工作目录。然而,在进程运行期间,当前目录所在的文件系统无法被卸载,这可能会在后续使用中引发诸多问题(例如进入单用户模式时可能会受到阻碍)。
解决这一问题的办法是,选择一个不可能被卸载的目录,通过 chdir (“/”)
将守护进程的当前工作目录变更为根目录。
重新定位标准IO描述符
所有文件描述符都已关闭。 守护进程已不再和终端相关联,无标准输入、标准出错文件描述符:
printf, perror 等输出语句将出错。
打开特殊设备,重定位标准的输入、输出描述符
open(“/dev/null”,O_RDWR);
dup(1);
dup(2);
创建守护进程的完整流程 
完整代码实现(C语言)
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <signal.h>
#include <syslog.h>
void daemon_init() {
pid_t pid;
// 1. 创建子进程并终止父进程
pid = fork();
if (pid < 0) {
perror("fork failed");
exit(EXIT_FAILURE);
} else if (pid > 0) {
exit(EXIT_SUCCESS); // 父进程退出
}
// 2. 创建新会话,脱离终端控制
if (setsid() < 0) {
perror("setsid failed");
exit(EXIT_FAILURE);
}
// 3. 忽略 SIGHUP 信号(防止会话组长终止导致进程退出)
signal(SIGCHLD, SIG_IGN);
signal(SIGHUP, SIG_IGN);
// 4. 再次 fork,确保进程不会成为会话组长(非必需但更安全)
pid = fork();
if (pid < 0) {
perror("fork failed");
exit(EXIT_FAILURE);
} else if (pid > 0) {
exit(EXIT_SUCCESS); // 父进程退出
}
// 5. 修改工作目录为根目录
chdir("/");
// 6. 设置文件权限掩码(通常设为0)
umask(0);
// 7. 关闭所有打开的文件描述符
for (int x = sysconf(_SC_OPEN_MAX); x >= 0; x--) {
close(x);
}
// 8. 重定向标准输入/输出/错误到 /dev/null 或日志文件
open("/dev/null", O_RDWR); // stdin
dup(0); // stdout
dup(0); // stderr
// 9. 初始化日志系统(可选)
openlog("mydaemon", LOG_PID, LOG_DAEMON);
syslog(LOG_NOTICE, "Daemon started successfully");
}
int main() {
daemon_init();
// 守护进程主循环
while (1) {
syslog(LOG_NOTICE, "Daemon is running...");
sleep(10);
}
closelog();
return EXIT_SUCCESS;
}
编译与运行
编译代码:
gcc daemon.c -o mydaemon
启动守护进程:
./mydaemon
验证守护进程:
查看进程列表:
ps -ef | grep mydaemon
检查系统日志(Ubuntu 默认在 /var/log/syslog):
tail -f /var/log/syslog | grep mydaemon
关键步骤详解
- 两次 fork()
- 第一次 fork 脱离终端。
- 第二次 fork 确保进程不是会话组长(避免重新获取终端控制)。
- 文件描述符处理
- 关闭所有文件描述符,避免资源泄漏。
- 重定向标准输入/输出/错误到
/dev/null
或日志文件。
- 信号处理
- 忽略 SIGHUP 和 SIGCHLD,防止意外终止。
- 可添加自定义信号处理(如 SIGTERM 实现优雅退出)。
- 日志记录
- 使用
syslog
记录日志,便于系统级管理。
- 使用
另外,部分系统(如 Linux)提供 daemon() 函数简化守护进程创建:
使用 daemon() 函数简化
#include <unistd.h>
int main() {
if (daemon(0, 0) < 0) { // 参数:nochdir(0=切换根目录), noclose(0=重定向到/dev/null)
perror("daemon failed");
exit(EXIT_FAILURE);
}
// 守护进程主逻辑
while (1) {
sleep(10);
}
return 0;
}
// 注意事项
// 资源管理:确保守护进程释放所有非必要资源(如文件描述符)。
// 日志监控:通过日志文件或 syslog 跟踪守护进程行为。
// 信号处理:实现 SIGTERM 或 SIGINT 的优雅退出逻辑。
综上。希望该内容能对你有帮助,感谢!