当前位置: 首页 > article >正文

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

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

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. 已经调整好自身位置。
关闭所有文件描述符

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

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

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

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

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

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

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


http://www.kler.cn/a/541205.html

相关文章:

  • 基于机器学习时序库pmdarima实现时序预测
  • 韶音科技:消费电子行业售后服务实现数字化转型,重塑客户服务体系
  • EasyExcel 导出合并层级单元格
  • AF3 drmsd函数解读
  • ASP.NET Core JWT
  • 【韩顺平linux】部分上课笔记整理
  • Linux——信号的保存与处理
  • 火爆的DeepSeek大模型怎么和智能家居结合?
  • 在 Windows 系统中如何快速进入安全模式的两种方法
  • Android LifecycleOwner 闪退,java 继承、多态特性!
  • 从零开始:使用Jenkins实现高效自动化部署
  • 【Mybatis】动态 SQL:代码与数据的灵动共舞,奏响数据库查询的华丽乐章
  • 在CT107D单片机综合训练平台上实现外部中断控制LED闪烁
  • BUU34 [BSidesCF 2020]Had a bad day1 【php://filter】
  • 【机器学习】数据预处理之数据归一化
  • Vue 中的自定义指令是什么?如何使用?
  • WPS接入DeepSeek模型
  • 【Elasticsearch入门到落地】7、文档操作
  • 爬虫技巧汇总
  • 【系统架构设计师】操作系统 ③ ( 存储管理 | 页式存储弊端 - 段式存储引入 | 段式存储 | 段表 | 段表结构 | 逻辑地址 的 合法段地址判断 )
  • 打破静态网页:CSS 动画与过渡技术全解析
  • Aquatronica控制系统tcp存在信息泄露漏洞
  • 推荐系统Day1笔记
  • 新站如何快速被搜索引擎收录?
  • 【大模型技术】accelerate和deepspeed
  • NineData云原生智能数据管理平台新功能发布|2025年1月版