如何编写一个守护进程。我们首先需要理解守护进程是什么。守护进程是在后台运行的进程,通常没有控制终端,用于执行系统任务,比如服务器或者定时任务。 用户可能想创建一个长期运行的服务,比如Web服务器或者日志监控程序。
首先,守护进程的创建通常包括几个步骤:fork子进程,让父进程退出,这样终端会认为命令执行完毕,子进程继续运行。然后调用setsid创建新的会话,脱离终端。接着改变工作目录到根目录,避免占用可卸载的文件系统。还要设置文件创建掩码,通常是0,以拥有更大的灵活性。关闭不需要的文件描述符,比如标准输入、输出、错误输出,防止它们占用资源。
基于这些步骤,创建具体的代码示例。要确保每一步都有对应的代码,并且解释清楚每个步骤的作用。例如,fork之后父进程退出,子进程成为孤儿进程,由init进程接管。setsid的作用是让子进程成为新的会话组长,脱离原终端的控制。改变工作目录是为了避免守护进程的当前目录被卸载,导致问题。设置umask让守护进程创建文件时有默认的权限。关闭文件描述符是为了释放资源,可能还需要重定向到/dev/null或者日志文件。另外,守护进程的代码编写中可能对信号处理有需求,比如处理SIGHUP信号,重新加载配置。所以在代码中可能需要添加信号处理函数。此外,守护进程可能需要记录日志,可以通过syslog或者写入日志文件实现。例如,使用openlog和syslog函数。
守护进程遇到的常见问题包括:守护进程无法正确脱离终端,导致随着终端关闭而终止;文件描述符没有正确关闭,导致资源泄漏;没有正确处理信号,导致无法优雅退出或重新加载配置;工作目录未改变,导致无法卸载文件系统;日志记录不当,难以调试问题。
在 Linux 系统中,守护进程(Daemon) 是一种在后台长期运行的进程,通常独立于控制终端并周期性地执行任务(如服务、日志监控等)。这篇文章是编写守护进程的标准步骤和代码示例:
(1) 进程第1次
fork
,为进程调用setsid
作准备。(2) 进程调用
setsid
,进程成为新的会话过程的领头进程。(3) 忽略信号 SIGHUP ,第2次
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进程的子进程。
c
pid=fork(); if (pid < 0){
fprintf(stderr, "error in first fork.\n");
exit(1);
}
if(pid>0){ /*父进程退出*/
exit(0);
}
在子进程中创建新会话
进程组
进程组是一个或多个进程的集合。进程组由进程组 ID 来唯一标识。除了进程号(PID)之外,进
程组ID也一个进程的必备属性之一。
每个进程组都有一个组长进程,组长进程的进程号等于进程组 ID。
会话期
会话组是一个或多个进程组的集合。
通常一个会话开始于用户登录,终止于用户退出,在此期间该用户运行的所有进程都属于这个会话期。
进程组 对话期 与 终端
![](https://i-blog.csdnimg.cn/direct/46c51eb9fdfd437dbdcf1ff10e354818.png)
setsid函数作用
- setsid函数用于创建一个新的会话,并自任该会话组的组长 (新会话的领头进程)
- 让进程摆脱原会话的控制;
- 让进程摆脱原进程组的控制;
- 让进程摆脱原控制终端的控制;
setsid
函数能够使进程完全独立出来,从而脱离所有其他进程的控制。
子进程继续运行,父进程退出的时候,将会产生 SIGHUP信号; 第一 fork() 的子进程是新的会话过程的领头进程,如再打一个终端,将成为他的控制终端,故需再次 fork()。
忽略信号SIGHUP,第二次fork
- 进程脱离了控制终端,
- 与退出的父进程属于同一组;
- 进程调用
setgrp()
, - 是进程脱离原来的进程组。
- 已经调整好自身位置。
关闭所有文件描述符
服务进程必须关闭它所继承的文件描述符:
c
max_fd = sysconf(_SC_OPEN_MAX);
for (i = 0; i < max_fd;i++)
close(i);
![](https://i-blog.csdnimg.cn/direct/1bf30c9c570d45e1a36fec692db1a45f.png)
消除umask的影响
每个进程都有一个umask : 文件权限掩码是指屏蔽掉文件权限中的对应位。由于使用 fork 新建的子进程继承了父进程的文件权限掩码,这就给该子进程使用文件带来了诸多的麻烦。
通常的使用方法为umask(0) :增加该守护进程的灵活性;umask (0) 清除旧有的文件掩码。
最后的权限: mode & ~umask
改变当前目录为根目录
守护进程的当前目录的作用 :
当进程产生错误的时候,将错误信息记录在当前目录的core文件;守护进程的特点一般会一直会打开当前目录,解决方法,找一个不可能被卸载的目录。
通常的做法是让 "/"
作为守护进程的当前工作目录 。
使用fork创建的子进程继承了父进程的当前工作目录。由于在进程运行过程中,当前目录所在的
文件系统是不能卸载的,这对以后的使用会造成诸多的麻烦(比如进入单用户模式)。
解决方法,找一个不可能被卸载的目录 chdir ("/")
。
重新定位标准IO描述符
所有文件描述符都已关闭。 守护进程已不再和终端相关联,无标准输入、标准出错文件描述符:
printf, perror 等输出语句将出错。
打开特殊设备,重定位标准的输入、输出描述符
c
open("/dev/null",O_RDWR);
dup(1);
dup(2);
创建守护进程的完整流程 ![](https://i-blog.csdnimg.cn/direct/cbc3c39135ba44878c985e36e597a2c9.png)
完整代码实现(C语言)
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;
}
编译与运行
- 编译代码:
bash
gcc daemon.c -o mydaemon
- 启动守护进程:
bash
./mydaemon
- 验证守护进程:
查看进程列表:
bash
ps -ef | grep mydaemon
检查系统日志(Ubuntu 默认在 /var/log/syslog):
bash
tail -f /var/log/syslog | grep mydaemon
关键步骤详解
- 两次 fork()
- 第一次 fork 脱离终端。
- 第二次 fork 确保进程不是会话组长(避免重新获取终端控制)。
- 文件描述符处理
- 关闭所有文件描述符,避免资源泄漏。
- 重定向标准输入/输出/错误到
/dev/null
或日志文件。
- 信号处理
- 忽略 SIGHUP 和 SIGCHLD,防止意外终止。
- 可添加自定义信号处理(如 SIGTERM 实现优雅退出)。
- 日志记录
- 使用
syslog
记录日志,便于系统级管理。
- 使用
另外,部分系统(如 Linux)提供 daemon() 函数简化守护进程创建:
使用 daemon() 函数简化
c
#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 的优雅退出逻辑。
综上。希望该内容能对你有帮助,感谢!
以上。仅供学习与分享交流,请勿用于商业用途!转载需提前说明。
我是一个十分热爱技术的程序员,希望这篇文章能够对您有帮助,也希望认识更多热爱程序开发的小伙伴。
感谢!