Linux守护进程编写与运行完整指南

2026-10-19 10:05:43 12阅读

守护进程(Daemon)是一种在后台运行的特殊进程,通常用于执行系统服务、管理任务或处理请求。它有以下几个显著特征:

  • 长期运行:通常在系统启动时启动,并持续运行直到系统关闭或被迫终止。与普通进程不同,守护进程不受用户登录状态影响,能在无用户交互的情况下稳定执行任务。
  • 与控制终端脱离:Linux 中每个从终端启动的进程都与该终端绑定,终端关闭时相关进程会被终止。守护进程通过脱离控制终端来避免这种限制,防止运行中受终端输出干扰。

守护进程在 Linux 系统中极为重要,是许多服务器的核心组成,例如 Internet 服务器 inetd、Web 服务器 httpd,以及作业调度进程 crond。守护进程通常以 "d" 结尾表示身份,如 sshd、httpd。它们独立于终端,自成进程组和会话(即 pid=gid=sid)。可用 ps -ajx 查看系统中所有进程:若 TTY 一栏显示问号(?),表示该进程没有控制终端,通常是守护进程;COMMAND 一栏中用中括号([])括起来、以 k 开头的进程表示内核线程(在内核空间创建、无用户空间代码)。

一、编写守护进程的步骤

编写守护进程通常包括以下关键步骤:

  1. 创建子进程并终止父进程:用 fork() 创建子进程后,父进程调用 exit() 终止自身。这样父进程退出使 shell 认为命令已执行完毕;子进程虽继承父进程进程组 ID 但有独立 PID,不是进程组组长,为后续 setsid() 做准备。
  2. 子进程调用 setsid() 创建会话:创建新会话、使子进程成为会话首领,创建新进程组并成为组长,摆脱原会话、进程组和控制终端的控制,实现完全独立。
  3. 更改工作目录为根目录:子进程继承父进程当前工作目录可能导致文件系统无法卸载,通常将工作目录改为根目录(/)。
  4. 重设文件权限掩码(umask):子进程继承父进程的 umask,建议设为 0(调用 umask(0)),确保最大权限和灵活性。
  5. 关闭不再需要的文件描述符:子进程继承父进程打开的所有文件描述符,可能导致资源浪费,应关闭不再需要的。
  6. 将文件描述符 0、1、2 定位到 /dev/null:让标准输入、输出、错误重定向到 /dev/null,使输出不显示在任何地方、也不尝试接收交互式输入。
  7. 其他处理:忽略 SIGCHLD 信号:对并发服务器尤为重要,将 SIGCHLD 设为 SIG_IGN 可避免僵尸进程,子进程结束时内核交给 init 处理,提升并发性能。

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

为深入理解如何使用和创建守护进程,我们实现一个多功能守护进程:每隔 30 秒获取系统的 CPU、内存和磁盘使用信息并写入 /var/log/resource_monitor.log;每隔 10 分钟清理 /tmp 目录下所有文件;能捕获 SIGTERM 信号安全退出,处理 SIGHUP 信号重新加载配置文件。

案例功能分析

系统资源监控使用系统命令 stat 和 vmstat 获取 CPU 和内存信息,用 df 命令获取磁盘使用情况,每次写入日志便于运维检查;定时清理任务每 10 分钟清理 /tmp 目录下文件,用 unlink() 删除;信号处理捕获 SIGTERM 干净终止并释放资源,捕获 SIGHUP 重新加载配置(如改变日志路径)。

代码结构

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

代码实现

#include 
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include

#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
", 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;
}

代码详解

守护进程初始化(daemonize)使用双 fork() 技术,确保进程后台运行并脱离终端,用 syslog 记录启动、关闭等信息;信号处理通过 signal() 捕获 SIGTERM(干净终止)和 SIGHUP(重新加载配置);资源监控使用 vmstat 和 df 命令,结果写入日志;定时清理每 10 分钟清理 /tmp 下常规文件;主循环每 30 秒调用监控和清理函数,保持持续运行。

三、编译和运行

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

gcc resource_monitor.c -o resource_monitor
sudo ./resource_monitor

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

四、检查守护进程

查看日志文件内容:

cat /var/log/resource_monitor.log

查看守护进程状态:

ps -ef | grep resource_monitor

五、停止守护进程

用 kill 命令根据 PID 终止:

kill 


作者:不脱发的程序猿
链接:https://www.aiddithome.com/p/5bbd5bccbb286.html
本作遵循 [CC BY 4.0 署名] 许可,版权归作者所有。若存在违规行为,您可以点击 “举报”。


登录 后发表评论
3条评论
拆开看看再说
1楼 · 2天前

资源监控加定时清理的案例很完整,编译运行检查停止一条龙,实用。

守护进程写法和注意事项都说到了,收藏备用。

调参到凌晨
3楼 · 2天前

双 fork 脱离终端的套路讲得透彻,代码可以直接拿来改。