一、概念守护进程Daemon是操作系统后台持续运行的特殊进程独立于终端且不受用户登录/注销的影响主要用于执行周期性任务如crond或提供后台服务如sshd、mysqld是Linux系统中实现后台服务的核心方式。1 核心特征后台运行不与任何终端关联用户无法直接通过键盘/鼠标与其交互生命周期长通常随系统启动创建直到系统关机才终止自动启动依赖系统初始化机制无需用户手动触发低资源占用仅在执行任务时占用资源空闲时资源消耗极低2 守护进程 vs 普通进程守护进程普通进程运行位置系统后台前台与终端关联生命周期随系统启动/关闭随用户启动/关闭交互方式通过配置/命令间接交互终端输入/输出直接交互启动方式系统自启动用户手动启动面试高频考点请简述守护进程与普通进程的核心区别二、创建守护进程创建守护进程需遵循6个核心步骤每一步都有明确的目的是面试手写代码、原理问答的核心考点步骤1创建子进程杀死父进程目的创建孤儿进程被init进程PID1接管脱离原终端的关联实现通过fork()创建子进程父进程调用exit()退出pid_t pid fork(); if (pid 0) { exit(0); // 父进程退出 } else if (pid 0) { perror(fork failed); exit(1); } // 子进程继续执行步骤2创建新会话目的让进程脱离原会话和终端控制成为新会话的首进程实现调用setsid()创建新会话进程成为新会话首进程、新进程组组长与原终端彻底断开联系pid_t sid setsid(); if (sid -1) { perror(setsid failed); exit(1); }步骤3再次创建子进程父进程退出目的避免进程重新关联控制终端会话首进程可能重新打开终端确保进程不是会话首进程实现再次fork()原会话首进程退出子进程成为最终的守护进程pid fork(); if (pid 0) { exit(0); // 原会话首进程退出 } else if (pid 0) { perror(fork failed); exit(1); } // 最终的守护进程步骤4改变工作目录chdir目的避免守护进程的工作目录被挂载的文件系统占用导致无法卸载该文件系统实现切换到根目录/或其他稳定目录如/var/runif (chdir(/) -1) { perror(chdir failed); exit(1); }步骤5重置文件权限掩码umask目的清除继承的文件权限掩码确保守护进程创建文件时拥有预期权限不受父进程影响实现调用umask(0)重置权限掩码umask(0); // 重置权限掩码为0创建文件时权限可控步骤6关闭不必要的文件描述符目的释放资源避免占用终端相关的文件描述符实现关闭所有继承的文件描述符至少关闭0、1、2标准输入/输出/错误// 获取最大文件描述符数量 int max_fd getdtablesize(); // 关闭所有文件描述符 for (int i 0; i max_fd; i) { close(i); } // 可选将标准输入/输出/错误重定向到/dev/null open(/dev/null, O_RDONLY); // 0 open(/dev/null, O_WRONLY); // 1 open(/dev/null, O_WRONLY); // 2关键提醒步骤2中setsid()的调用限制——如果调用进程是进程组组长该函数会失败这也是步骤1必须让父进程退出的核心原因三、核心API详解1 核心接口前置知识进程组由一个或多个进程组成有唯一进程组ID等于组长进程ID会话由一个或多个进程组组成有唯一会话ID等于会话首进程ID控制终端一个会话可关联一个终端会话首进程是终端的控制进程1.1 pid_t setsid(void)返回值成功返回新会话ID失败返回-1功能创建新会话脱离原进程组失去控制终端调用后变化新会话SID 当前进程PID会话首进程新进程组PGID 当前进程PID进程组组长进程不再拥有控制终端调用限制进程组组长调用会失败步骤1的核心作用1.2 int chdir(const char *path)作用修改当前工作目录为path指向的目录返回值成功返回0失败返回-1使用场景切换到稳定、不会被卸载的目录如/保证守护进程稳定性1.3 int getdtablesize(void)作用返回当前进程可打开的最大文件描述符数量用途遍历关闭所有继承的文件描述符2 常用接口2.1 time_t time(time_t *time)作用返回从1970-01-01 00:00:00 UTC到当前的秒数返回值成功返回秒数失败返回-1用途守护进程记录时间、实现定时任务2.2 struct tm* localtime(const time_t *timer)参数timer为time_t类型的时间戳指针作用将UTC时间戳转换为本地时间年月日时分秒注意返回静态全局结构体地址多次调用会覆盖数据线程安全版本为localtime_r2.3 char* asctime(const struct tm* tm)作用将struct tm结构体转换为固定格式的时间字符串如Wed Mar 10 12:00:00 2026注意返回静态全局缓冲区线程安全版本为asctime_r四、实用命令守护进程无终端输出通常将日志写入文件可通过tail -f实时跟踪日志变化tail -f/var/log/daemon.log # 实时监控日志文件-f进入跟踪模式文件内容更新时自动输出新内容常用场景监控守护进程运行状态、排查异常五、代码示例#include unistd.h // fork()、setsid()、chdir()、close()、sleep()、getdtablesize() #include sys/stat.h // umask() #include fcntl.h // open()、O_RDONLY/O_WRONLY/O_CREAT/O_APPEND 等宏 #include stdlib.h // exit() #include stdio.h // sprintf() #include time.h // time()、localtime()、asctime() #include string.h // strlen() // 创建守护进程函数 void create_daemon() { // 步骤1第一次fork父进程退出 pid_t pid fork(); if (pid 0) { exit(0); } else if (pid 0) { perror(fork 1 failed); exit(1); } // 步骤2创建新会话 if (setsid() -1) { perror(setsid failed); exit(1); } // 步骤3第二次fork父进程退出 pid fork(); if (pid 0) { exit(0); } else if (pid 0) { perror(fork 2 failed); exit(1); } // 步骤4切换工作目录 if (chdir(/) -1) { perror(chdir failed); exit(1); } // 步骤5重置权限掩码 umask(0); // 步骤6关闭所有文件描述符 int max_fd getdtablesize(); for (int i 0; i max_fd; i) { close(i); } // 重定向标准输入/输出/错误到/dev/null open(/dev/null, O_RDONLY); open(/dev/null, O_WRONLY); open(/dev/null, O_WRONLY); } // 守护进程核心逻辑每隔10秒记录当前时间到日志 int main() { // 创建守护进程 create_daemon(); // 守护进程业务逻辑 while (1) { // 打开日志文件 int fd open(/var/log/daemon_time.log, O_WRONLY | O_CREAT | O_APPEND, 0644); if (fd 0) { exit(1); } // 获取当前时间 time_t now time(NULL); struct tm *tm_now localtime(now); char time_str[1024]; sprintf(time_str, Current time: %s, asctime(tm_now)); // 写入日志 write(fd, time_str, strlen(time_str)); close(fd); // 每隔10秒执行一次 sleep(10); } return 0; }编译运行gcc -o my_daemon my_daemon.c ./my_daemon # 实时查看日志 tail -f /var/log/daemon_time.log # 停止守护进程 ps -ef | grep my_daemon kill -9 守护进程PID六、面试高频考点守护进程的核心特征是什么后台运行、生命周期长、无终端关联、自动启动、低资源占用创建守护进程的步骤每一步的目的6步核心重点说明setsid、二次fork的目的setsid()的作用和调用限制创建新会话进程组组长调用失败为什么要重置umask和切换工作目录权限可控、避免挂载目录无法卸载守护进程如何处理日志写入文件通过tail -f监控禁止终端输出守护进程和普通后台进程的区别是否脱离终端、生命周期是否依赖系统