【进程与线程】如何编写一个守护进程

如何编写一个守护进程。我们首先需要理解守护进程是什么。守护进程是在后台运行的进程,通常没有控制终端,用于执行系统任务,比如服务器或者定时任务。 用户可能想创建一个长期运行的服务,比如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。

会话期

会话组是一个或多个进程组的集合。

通常一个会话开始于用户登录,终止于用户退出,在此期间该用户运行的所有进程都属于这个会话期。

进程组 对话期 与 终端
setsid函数作用
  • setsid函数用于创建一个新的会话,并自任该会话组的组长 (新会话的领头进程)
    • 让进程摆脱原会话的控制;
    • 让进程摆脱原进程组的控制;
    • 让进程摆脱原控制终端的控制;

setsid函数能够使进程完全独立出来,从而脱离所有其他进程的控制。

子进程继续运行,父进程退出的时候,将会产生 SIGHUP信号; 第一 fork() 的子进程是新的会话过程的领头进程,如再打一个终端,将成为他的控制终端,故需再次 fork()。

忽略信号SIGHUP,第二次fork
  1. 进程脱离了控制终端,
  2. 与退出的父进程属于同一组;
  3. 进程调用 setgrp()
  4. 是进程脱离原来的进程组。
  5. 已经调整好自身位置。
关闭所有文件描述符

服务进程必须关闭它所继承的文件描述符:

c 复制代码
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 等输出语句将出错。

打开特殊设备,重定位标准的输入、输出描述符

c 复制代码
open("/dev/null",O_RDWR);
 dup(1);
 dup(2);
创建守护进程的完整流程

完整代码实现(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;
}
编译与运行
  1. 编译代码:
bash 复制代码
gcc daemon.c -o mydaemon
  1. 启动守护进程:
bash 复制代码
./mydaemon
  1. 验证守护进程:
    查看进程列表:
bash 复制代码
ps -ef | grep mydaemon

检查系统日志(Ubuntu 默认在 /var/log/syslog):

bash 复制代码
tail -f /var/log/syslog | grep mydaemon
关键步骤详解
  1. 两次 fork()
    • 第一次 fork 脱离终端。
    • 第二次 fork 确保进程不是会话组长(避免重新获取终端控制)。
  2. 文件描述符处理
    • 关闭所有文件描述符,避免资源泄漏。
    • 重定向标准输入/输出/错误到 /dev/null 或日志文件。
  3. 信号处理
    • 忽略 SIGHUPSIGCHLD,防止意外终止。
    • 可添加自定义信号处理(如 SIGTERM 实现优雅退出)。
  4. 日志记录
    • 使用 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 的优雅退出逻辑。

综上。希望该内容能对你有帮助,感谢!

以上。仅供学习与分享交流,请勿用于商业用途!转载需提前说明。

我是一个十分热爱技术的程序员,希望这篇文章能够对您有帮助,也希望认识更多热爱程序开发的小伙伴。
感谢!

相关推荐
渲染101专业云渲染4 分钟前
川翔云电脑是什么?租电脑?
运维·服务器·电脑
鹿屿二向箔9 分钟前
单片机上SPI和IIC的区别
单片机·嵌入式硬件
007php00712 分钟前
Docker、Ollama、Dify 及 DeepSeek 安装配置与搭建企业级本地私有化知识库实践
运维·服务器·开发语言·后端·docker·容器·云计算
快去睡觉~1 小时前
Linux之Http协议分析以及cookie和session
linux·运维·http
shadowcz0071 小时前
Open-Interface:基于大语言模型 LLM 的自动化界面操作系统
运维·人工智能·语言模型·自然语言处理·自动化
世界尽头与你1 小时前
【网络法医】Docker取证
运维·安全·网络安全·docker·容器
weixin_580382061 小时前
STC51 P0 口 与P1 口输出
单片机·嵌入式硬件
致奋斗的我们1 小时前
项目:利用rsync备份全网服务器数据
linux·运维·服务器·开发语言·github·rsync·openeuler
Htht1111 小时前
【Linux】之【bug】“sudo wpa_cli -i wlan0 scan“ 返回 FAIL-BUSY 解决
linux·运维·bug
下雨天u1 小时前
jenkins备份还原配置文件
运维·jenkins