Linux守护进程

目录

1、编写守护进程的步骤

2、守护进程的使用和案例设计

2.1、案例功能分析

2.2、守护进程代码结构

2.3、代码实现

2.4、代码详解

3、编译和运行守护进程

4、检查守护进程

5、停止守护进程


守护进程(Daemon)是一种在后台运行的特殊进程,通常用于执行系统服务、管理任务或处理请求。它们具有几个显著的特征,使其在系统中扮演重要角色。

主要特征:

  • 长期运行:守护进程通常在系统启动时启动,并会持续运行,直至系统关闭或被强行终止。与普通进程不同,普通进程在用户登录或运行程序时创建,并在任务完成后或用户注销时终止。守护进程不受用户登录状态的影响,这使得它们能够在不需要用户交互的情况下稳定地执行任务。
  • 与控制终端脱离:在 Linux 系统中,每个从终端启动的进程都与该终端绑定。这意味着当终端关闭时,与之关联的所有进程都会被终止。守护进程通过脱离控制终端来避免这种限制,使其能够在后台独立运行。这种脱离的目的是防止进程在运行过程中受到终端输出的干扰,并确保它们能够持续工作。

守护进程在 Linux 系统中极为重要,它们是许多服务器的核心组成部分,例如 Internet 服务器 inetd 和 Web 服务器 httpd。这些进程不仅负责提供网络服务,还执行各种系统任务,例如作业调度进程 crond

守护进程通常以 "d" 结尾,表明其身份,例如 sshdhttpd。这些进程独立于终端,用户的登录和注销不会影响它们的运行。每个守护进程自成进程组和会话,确保其独立性(即 pid=gid=sid)。要查看系统中所有进程,可以使用命令 ps -ajx,其输出将显示所有进程的详细信息。

在命令输出中,如果 TTY 一栏显示为问号(?),这表示该进程没有控制终端,通常意味着它是一个守护进程。同时,COMMAND 一栏中用中括号(\[\])括起来的进程表示内核线程。这些线程是在内核空间中创建的,没有对应的用户空间代码,因此不具备程序文件名和命令行信息,通常以字母 k 开头,表示它们是内核线程(Kernel)。

1、编写守护进程的步骤

编写守护进程通常包括以下几个关键步骤,以确保其能够在后台独立运行,并完成预定的任务。

1、创建子进程并终止父进程

使用 fork() 创建子进程后,父进程应调用 exit() 终止自身。这一过程实现了以下几点:

  • 如果守护进程是通过简单的 shell 命令启动,父进程的退出将使 shell 认为命令已执行完毕。
  • 子进程继承了父进程的进程组 ID,但它有自己独立的进程 ID,确保子进程不是进程组的组长,为后续调用 setsid() 准备条件。

2、子进程调用 setsid() 创建会话

在子进程中调用 setsid() 是关键步骤。这将:

  • 创建一个新的会话,子进程成为新会话的首领。
  • 创建新的进程组,子进程成为组长。
  • 摆脱原有会话、进程组和控制终端的控制,实现完全独立。尽管子进程在 fork() 时继承了父进程的控制权,但 setsid() 能确保其完全脱离。

3、更改工作目录为根目录

子进程会继承父进程的当前工作目录,而该目录可能会导致文件系统无法卸载。通常,守护进程会将工作目录更改为根目录(/),以避免这种问题。也可以根据需要选择其他目录。

4、重设文件权限掩码(umask)

文件权限掩码 umask 控制新建文件的默认权限。由于子进程继承了父进程的 umask,建议将其设置为 0,以确保子进程拥有最大权限,增强守护进程的灵活性。设置 umask 的方法是调用 umask(0)

5、关闭不再需要的文件描述符

子进程会继承父进程打开的所有文件描述符,这可能导致不必要的资源消耗。应关闭不再需要的文件描述符,以确保守护进程不再持有任何继承自父进程的描述符,从而减少资源浪费。

6、将文件描述符 0、1、2 定位到 /dev/null

守护进程的标准输入、标准输出和标准错误通常会重定向到 /dev/null,这样守护进程的输出就不会显示在任何地方,同时也不会试图从交互式用户那里接收输入。

7、其他处理:忽略 SIGCHLD 信号

处理 SIGCHLD 信号不是绝对必要的,但对于某些并发服务器进程尤其重要。通过将 SIGCHLD 信号的处理方式设置为 SIG_IGN,可以避免僵尸进程的产生。这样,当子进程结束时,内核将其交给 init 进程处理,减少了父进程的负担,从而提高了服务器的并发性能。

2、守护进程的使用和案例设计

为了深入理解如何创建和使用守护进程,我们将创建一个多功能的守护进程,具备以下功能:

  • 资源监控功能 :守护进程每隔 30 秒获取系统的 CPU、内存和磁盘使用信息,并将其写入 /var/log/resource_monitor.log
  • 定时清理功能 :每隔 10 分钟,清理 /tmp 目录下的所有文件。
  • 信号处理功能 :守护进程能够捕获 SIGTERM 信号,安全退出,并能够处理 SIGHUP 信号重新加载配置文件。

2.1、案例功能分析

系统资源监控

  • 使用系统命令 statvmstat 来获取 CPU 和内存信息。
  • 使用 df 命令获取磁盘使用情况。
  • 每次获取的信息都写入 /var/log/resource_monitor.log,便于运维人员检查系统的健康状态。

定时清理任务

  • 每隔 10 分钟调用一个函数清理 /tmp 目录下的文件。
  • 使用系统函数 unlink() 删除文件。

信号处理

  • 捕获 SIGTERM 信号,干净地终止守护进程并进行资源释放。
  • 捕获 SIGHUP 信号,重新加载配置文件(如改变日志文件的路径)。

2.2、守护进程代码结构

  • daemonize():负责将进程变为守护进程的常规步骤。
  • monitor_resources():负责监控系统资源并将其写入日志。
  • cleanup_tmp() :每隔 10 分钟清理一次 /tmp 目录中的文件。
  • handle_signal() :处理 SIGTERMSIGHUP 信号。
  • reload_config() :当捕获 SIGHUP 时,重新加载配置文件。

2.3、代码实现

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <string.h>
#include <time.h>
#include <dirent.h>
#include <syslog.h>

#define LOG_FILE "/var/log/resource_monitor.log"
#define CONFIG_FILE "/etc/daemon_config.conf"
#define TMP_DIR "/tmp"

// 定义轮询时间
#define MONITOR_INTERVAL 30  // 资源监控间隔 30 秒
#define CLEANUP_INTERVAL 600 // 清理间隔 10 分钟

int keep_running = 1;
FILE *log_fp = NULL;

// 守护进程初始化函数
void daemonize() {
    pid_t pid;

    // 1. 创建子进程并终止父进程
    pid = fork();
    if (pid < 0) exit(EXIT_FAILURE);
    if (pid > 0) exit(EXIT_SUCCESS);  // 父进程退出

    // 2. 创建新的会话
    if (setsid() < 0) exit(EXIT_FAILURE);

    // 3. 忽略 SIGCHLD 信号
    signal(SIGCHLD, SIG_IGN);

    // 4. 再次 fork,防止守护进程重新获得终端
    pid = fork();
    if (pid < 0) exit(EXIT_FAILURE);
    if (pid > 0) exit(EXIT_SUCCESS);

    // 5. 更改工作目录到根目录
    chdir("/");

    // 6. 重设文件权限掩码
    umask(0);

    // 7. 关闭不再需要的文件描述符
    close(STDIN_FILENO);
    close(STDOUT_FILENO);
    close(STDERR_FILENO);

    // 8. 重定向标准输入、输出、错误到 /dev/null
    open("/dev/null", O_RDONLY);
    open("/dev/null", O_WRONLY);
    open("/dev/null", O_WRONLY);

    // 打开系统日志
    openlog("resource_daemon", LOG_PID, LOG_DAEMON);
}

// 捕获信号的处理函数
void handle_signal(int signal) {
    switch (signal) {
        case SIGHUP:
            syslog(LOG_INFO, "Reloading configuration file...");
            // 重新加载配置文件
            if (log_fp) {
                fclose(log_fp);
            }
            log_fp = fopen(LOG_FILE, "a");
            if (log_fp == NULL) {
                syslog(LOG_ERR, "Failed to open log file");
                exit(EXIT_FAILURE);
            }
            break;
        case SIGTERM:
            syslog(LOG_INFO, "Daemon is shutting down...");
            if (log_fp) {
                fclose(log_fp);
            }
            closelog();
            keep_running = 0;  // 设置标志位,结束主循环
            break;
    }
}

// 资源监控功能
void monitor_resources() {
    FILE *fp;
    char buffer[128];

    // 记录当前时间
    time_t now = time(NULL);
    fprintf(log_fp, "Timestamp: %s", ctime(&now));

    // 记录 CPU 和内存使用情况
    fp = popen("vmstat 1 2 | tail -1", "r");
    if (fp != NULL) {
        fgets(buffer, sizeof(buffer) - 1, fp);
        fprintf(log_fp, "CPU/Memory Usage: %s\n", buffer);
        pclose(fp);
    }

    // 记录磁盘使用情况
    fp = popen("df -h /", "r");
    if (fp != NULL) {
        while (fgets(buffer, sizeof(buffer) - 1, fp) != NULL) {
            fprintf(log_fp, "Disk Usage: %s", buffer);
        }
        pclose(fp);
    }

    fflush(log_fp);  // 确保日志刷新到文件
}

// 定时清理 /tmp 目录
void cleanup_tmp() {
    DIR *dir;
    struct dirent *entry;
    char file_path[256];

    dir = opendir(TMP_DIR);
    if (dir == NULL) {
        syslog(LOG_ERR, "Failed to open /tmp directory");
        return;
    }

    while ((entry = readdir(dir)) != NULL) {
        if (entry->d_type == DT_REG) {  // 只删除常规文件
            snprintf(file_path, sizeof(file_path), "%s/%s", TMP_DIR, entry->d_name);
            if (unlink(file_path) == 0) {
                syslog(LOG_INFO, "Deleted file: %s", file_path);
            } else {
                syslog(LOG_ERR, "Failed to delete file: %s", file_path);
            }
        }
    }

    closedir(dir);
}

int main() {
    daemonize();

    // 打开日志文件
    log_fp = fopen(LOG_FILE, "a");
    if (log_fp == NULL) {
        syslog(LOG_ERR, "Failed to open log file");
        exit(EXIT_FAILURE);
    }

    // 捕获信号处理
    signal(SIGTERM, handle_signal);  // 用于进程关闭
    signal(SIGHUP, handle_signal);   // 用于重新加载配置

    time_t last_cleanup = time(NULL);

    // 主循环
    while (keep_running) {
        monitor_resources();  // 监控系统资源

        // 检查是否需要清理 tmp 目录
        if (difftime(time(NULL), last_cleanup) >= CLEANUP_INTERVAL) {
            cleanup_tmp();
            last_cleanup = time(NULL);
        }

        // 等待 30 秒后继续
        sleep(MONITOR_INTERVAL);
    }

    // 清理资源并退出
    if (log_fp) {
        fclose(log_fp);
    }
    closelog();

    return 0;
}

2.4、代码详解

守护进程初始化 (daemonize)

  • 将进程变为守护进程,使用了双 fork() 技术,确保进程在后台运行并与终端脱离关系。
  • 使用 syslog 系统日志服务记录进程启动、关闭等信息。

信号处理 (handle_signal)

  • 通过 signal() 函数捕获 SIGTERMSIGHUP 信号。
  • SIGTERM 信号用于干净地终止守护进程。
  • SIGHUP 信号用于重新加载配置文件,这里模拟了重新打开日志文件的过程。

资源监控 (monitor_resources)

  • 使用 vmstat 命令监控 CPU 和内存使用情况,df 命令获取磁盘使用状态。
  • 每次监控结果都记录到日志文件中。

定时清理 (cleanup_tmp)

  • 每隔 10 分钟清理 /tmp 目录下的文件。
  • 仅删除常规文件,忽略目录等。

主循环

  • 守护进程每 30 秒调用监控和清理函数,保持持续运行状态。

3、编译和运行守护进程

将上述代码保存为 resource_monitor.c,使用以下命令进行编译和运行:

cpp 复制代码
gcc resource_monitor.c -o resource_monitor
sudo ./resource_monitor

注意,守护进程需要写入 /var/log/resource_monitor.log 文件,因此需要使用 sudo 权限运行。

4、检查守护进程

查看日志文件内容:

cpp 复制代码
cat /var/log/resource_monitor.log

查看守护进程状态:

cpp 复制代码
ps -ef | grep resource_monitor

5、停止守护进程

可以使用 kill 命令根据守护进程的 PID 将其终止:

cpp 复制代码
kill <PID>
相关推荐
HLC++15 小时前
Linux的进程间通信
android·linux·服务器
华清远见IT开放实验室16 小时前
实验室建设案例 | 石家庄科技信息职业学院嵌入式实验室——从底层硬件到系统应用,一所应用型高校的嵌入式人才培养这样落地
linux·arm开发·stm32·嵌入式硬件·高校·实验室建设
groundhappy19 小时前
idalib安装和codex ida-mcp配置
linux·开发语言·python
通信小小昕19 小时前
Ubuntu 26.04 中文输入法安装
linux·运维·ubuntu
张小姐的猫21 小时前
【Linux】网络编程 —— HTTP协议(上)
linux·运维·服务器·网络·http·单例模式·策略模式
栩栩云生21 小时前
AI 写代码犯的错,早被写进了错题集
linux·安全·ai编程
imc.111 天前
linux基础IO
linux·运维·服务器
BelongPanda1 天前
Linux Nginx 纯手动 Let‘s Encrypt 泛域名证书配置教程
linux·nginx
酷可达拉斯1 天前
Linux操作系统-shell编程(0)
linux·运维·服务器·python·云计算
2301_777998341 天前
Linux中断机制:操作系统如何高效运行
linux·运维·服务器