主要特征:
守护进程在 Linux 系统中极为重要,它们是许多服务器的核心组成部分,例如 Internet 服务器 inetd 和 Web 服务器 httpd。这些进程不仅负责提供网络服务,还执行各种系统任务,例如作业调度进程 crond。
守护进程通常以 “d” 结尾,表明其身份,例如 sshd 和 httpd。这些进程独立于终端,用户的登录和注销不会影响它们的运行。每个守护进程自成进程组和会话,确保其独立性(即 pid=gid=sid)。要查看系统中所有进程,可以使用命令 ps -ajx,其输出将显示所有进程的详细信息。

在命令输出中,如果 TTY 一栏显示为问号(?),这表示该进程没有控制终端,通常意味着它是一个守护进程。同时,COMMAND 一栏中用中括号([])括起来的进程表示内核线程。
这些线程是在内核空间中创建的,没有对应的用户空间代码,因此不具备程序文件名和命令行信息,通常以字母 k 开头,表示它们是内核线程(Kernel)。
编写守护进程通常包括以下几个关键步骤,以确保其能够在后台独立运行,并完成预定的任务。
使用 fork() 创建子进程后,父进程应调用 exit() 终止自身。这一过程实现了以下几点:
在子进程中调用 setsid() 是关键步骤。这将:
子进程会继承父进程的当前工作目录,而该目录可能会导致文件系统无法卸载。通常,守护进程会将工作目录更改为根目录(/),以避免这种问题。也可以根据需要选择其他目录。
文件权限掩码 umask 控制新建文件的默认权限。由于子进程继承了父进程的 umask,建议将其设置为 0,以确保子进程拥有最大权限,增强守护进程的灵活性。设置 umask 的方法是调用 umask(0)。
子进程会继承父进程打开的所有文件描述符,这可能导致不必要的资源消耗。应关闭不再需要的文件描述符,以确保守护进程不再持有任何继承自父进程的描述符,从而减少资源浪费。
守护进程的标准输入、标准输出和标准错误通常会重定向到 /dev/null,这样守护进程的输出就不会显示在任何地方,同时也不会试图从交互式用户那里接收输入。
处理 SIGCHLD 信号不是绝对必要的,但对于某些并发服务器进程尤其重要。通过将 SIGCHLD 信号的处理方式设置为 SIG_IGN,可以避免僵尸进程的产生。这样,当子进程结束时,内核将其交给 init 进程处理,减少了父进程的负担,从而提高了服务器的并发性能。
为了深入理解如何创建和使用守护进程,我们将创建一个多功能的守护进程,具备以下功能:
系统资源监控:
定时清理任务:
信号处理:
#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;
}守护进程初始化 (daemonize):
信号处理 (handle_signal):
资源监控 (monitor_resources):
定时清理 (cleanup_tmp):
主循环:
将上述代码保存为 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