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

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

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

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

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

相关推荐
晨晖26 分钟前
单链表逆转,c语言
c语言·数据结构·算法
iCxhust13 分钟前
8255 PORTC 按键输入测试
单片机·嵌入式硬件·微机原理
trayvontang33 分钟前
Nginx之location配置
运维·nginx
十六年开源服务商1 小时前
WordPress定制开发最佳公司的用户画像
运维
hkhkhkhkh1231 小时前
Linux设备节点基础知识
linux·服务器·驱动开发
世岩清上2 小时前
AI驱动的智能运维:从自动化到自主化的技术演进与架构革新
运维·人工智能·自动化
HZero.chen2 小时前
Linux字符串处理
linux·string
张童瑶2 小时前
Linux SSH隧道代理转发及多层转发
linux·运维·ssh
汪汪队立大功1232 小时前
什么是SELinux
linux
石小千3 小时前
Linux安装OpenProject
linux·运维