C语言守护进程编程完全指南

系统编程守护进程

守护进程概述

守护进程(Daemon)是运行在后台的特殊进程,它不与任何终端关联,生命周期通常从系统启动到系统关闭。在 Linux 系统中,诸如 sshd、nginx、crond 等都是典型的守护进程。守护进程的核心特征包括:脱离控制终端、在后台长期运行、通常以超级用户权限启动、拥有独立的会话和进程组。

守护进程的特征

一个标准的守护进程需要满足以下条件:

  • 脱离控制终端:不受终端信号(如 SIGHUP、SIGINT)影响
  • 独立会话:创建新的会话,成为会话首领,避免被终端控制
  • 更改工作目录:切换到根目录,防止占用可卸载的文件系统
  • 重设文件权限掩码:确保守护进程创建文件时的权限不受继承影响
  • 关闭继承的文件描述符:关闭标准输入、输出和错误,避免终端依赖

daemon()函数

BSD 和 glibc 提供了 daemon() 库函数,可以快速将当前进程转换为守护进程。该函数封装了创建守护进程的核心步骤,适合简单场景使用。

daemon函数原型

#include <unistd.h>

/* nochdir: 0 = 将工作目录改为 "/",非 0 = 不改变 */
/* noclose: 0 = 将 stdin/stdout/stderr 重定向到 /dev/null,非 0 = 不重定向 */
int daemon(int nochdir, int noclose);

使用daemon函数创建守护进程

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <syslog.h>

int main(void) {
    /* 调用 daemon() 变为守护进程 */
    if (daemon(0, 0) < 0) {
        perror("daemon");
        return 1;
    }

    /* 打开 syslog */
    openlog("mydaemon", LOG_PID | LOG_NDELAY, LOG_DAEMON);
    syslog(LOG_INFO, "Daemon started with pid %d", getpid());

    /* 守护进程主循环 */
    while (1) {
        syslog(LOG_INFO, "Daemon is running...");
        sleep(60);
    }

    closelog();
    return 0;
}

daemon() 函数虽然方便,但它的行为在不同系统上可能存在差异,且缺乏精细控制。在生产环境中,通常建议手动创建守护进程,以便更好地处理错误和特殊需求。

手动创建守护进程

手动创建守护进程需要遵循一系列严格的步骤,确保进程完全脱离终端并正确运行在后台。整个过程分为六个关键步骤。

第一步:创建子进程并退出父进程

首先调用 fork() 创建子进程,然后父进程退出。这样 shell 认为命令已执行完毕,子进程继续在后台运行。同时,子进程不是进程组首领,为后续 setsid() 调用创造了条件。

pid_t pid = fork();
if (pid < 0) {
    perror("fork");
    return -1;
}
if (pid > 0) {
    /* 父进程退出 */
    exit(EXIT_SUCCESS);
}
/* 子进程继续执行 */

第二步:创建新会话

调用 setsid() 创建新的会话和进程组。子进程成为新会话的首领和新进程组的组长,同时脱离原来的控制终端。这就是"会话脱离"的核心步骤。

if (setsid() < 0) {
    perror("setsid");
    return -1;
}

第三步:再次fork

虽然 setsid() 已经让进程脱离了终端,但作为会话首领,进程仍然有权重新打开一个控制终端。再次 fork() 并退出父进程(即会话首领),确保子进程不再是会话首领,从而永远无法重新获取控制终端。

pid_t pid2 = fork();
if (pid2 < 0) {
    perror("fork2");
    return -1;
}
if (pid2 > 0) {
    /* 会话首领退出 */
    exit(EXIT_SUCCESS);
}
/* 孙子进程继续,不是会话首领 */

第四步:更改工作目录

将工作目录改为根目录 /,防止守护进程占用某个可卸载的文件系统,导致该文件系统无法卸载。如果守护进程需要在特定目录工作,也应确保该目录在系统运行期间始终可用。

if (chdir("/") < 0) {
    perror("chdir");
    return -1;
}

第五步:关闭继承的文件描述符

守护进程不再需要标准输入、输出和错误,应关闭这些从父进程继承的文件描述符。通常关闭 STDIN_FILENO、STDOUT_FILENO 和 STDERR_FILENO(即 0、1、2),并将它们重定向到 /dev/null,防止后续库函数操作这些描述符时出错。

#include <fcntl.h>

/* 关闭标准文件描述符 */
close(STDIN_FILENO);
close(STDOUT_FILENO);
close(STDERR_FILENO);

/* 重定向到 /dev/null */
int fd0 = open("/dev/null", O_RDWR);
int fd1 = dup(0);
int fd2 = dup(0);

if (fd0 != 0 || fd1 != 1 || fd2 != 2) {
    syslog(LOG_ERR, "Unexpected file descriptors %d %d %d",
            fd0, fd1, fd2);
    return -1;
}

第六步:重设文件权限掩码

调用 umask(0) 清除从父进程继承的文件权限掩码,确保守护进程创建文件时拥有完整的权限控制。如果不重设,继承的掩码可能导致创建的文件无法按预期设置权限。

umask(0);

完整的守护进程创建函数

将以上六个步骤整合为一个可复用的函数,方便在项目中使用:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <syslog.h>
#include <sys/stat.h>

int daemonize(void) {
    /* 步骤 1: fork 并退出父进程 */
    pid_t pid = fork();
    if (pid < 0) return -1;
    if (pid > 0) exit(EXIT_SUCCESS);

    /* 步骤 2: 创建新会话 */
    if (setsid() < 0) return -1;

    /* 步骤 3: 再次 fork,确保无法获取控制终端 */
    pid = fork();
    if (pid < 0) return -1;
    if (pid > 0) exit(EXIT_SUCCESS);

    /* 步骤 4: 更改工作目录为根目录 */
    if (chdir("/") < 0) return -1;

    /* 步骤 5: 关闭继承的文件描述符 */
    close(STDIN_FILENO);
    close(STDOUT_FILENO);
    close(STDERR_FILENO);

    int fd = open("/dev/null", O_RDWR);
    if (fd != 0) return -1;
    if (dup(0) != 1) return -1;
    if (dup(0) != 2) return -1;

    /* 步骤 6: 重设文件权限掩码 */
    umask(0);

    return 0;
}

PID文件管理

PID 文件是守护进程的标配机制,用于记录守护进程的进程 ID。它的核心作用是防止同一个守护进程被重复启动,同时为管理脚本(如停止、重载配置)提供进程定位能力。PID 文件通常存放在 /var/run/ 目录下,文件名约定为 程序名.pid。

PID文件的创建与检查

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <signal.h>

#define PID_FILE "/var/run/mydaemon.pid"

/* 检查是否已有实例在运行 */
int check_running(void) {
    int fd = open(PID_FILE, O_RDONLY);
    if (fd < 0) {
        if (errno == ENOENT) return 0;  /* PID文件不存在,可以启动 */
        return -1;
    }

    char buf[16] = {0};
    int n = read(fd, buf, sizeof(buf) - 1);
    close(fd);

    if (n <= 0) return 0;

    pid_t old_pid = atoi(buf);
    if (old_pid > 0) {
        /* 检查该进程是否仍在运行 */
        if (kill(old_pid, 0) == 0) {
            fprintf(stderr, "Daemon already running (pid %d)\n", old_pid);
            return 1;
        }
    }

    /* 旧进程已不在运行,删除过期的PID文件 */
    unlink(PID_FILE);
    return 0;
}

/* 写入PID文件 */
int write_pid_file(void) {
    int fd = open(PID_FILE, O_WRONLY | O_CREAT | O_TRUNC, 0644);
    if (fd < 0) {
        perror("open pid file");
        return -1;
    }

    char buf[16];
    int n = snprintf(buf, sizeof(buf), "%d", getpid());
    if (write(fd, buf, n) != n) {
        perror("write pid");
        close(fd);
        return -1;
    }

    close(fd);
    return 0;
}

/* 删除PID文件(守护进程退出时调用) */
void remove_pid_file(void) {
    unlink(PID_FILE);
}

日志重定向

守护进程脱离了控制终端,无法像普通程序那样直接使用 printf() 输出信息。Linux 提供了 syslog 系统日志机制,是守护进程输出日志的标准方式。syslog 支持多种日志级别和设施,日志消息由 syslogd 守护进程统一管理,可以输出到文件、远程服务器或控制台。

syslog核心API

#include <syslog.h>

/* 打开日志连接 */
void openlog(const char *ident, int option, int facility);

/* 写入日志消息 */
void syslog(int priority, const char *format, ...);

/* 关闭日志连接 */
void closelog(void);

日志级别与设施

syslog 定义了 8 个日志级别,从高到低依次为:

  • LOG_EMERG:系统不可用
  • LOG_ALERT:必须立即采取行动
  • LOG_CRIT:严重条件
  • LOG_ERR:错误条件
  • LOG_WARNING:警告条件
  • LOG_NOTICE:正常但重要的条件
  • LOG_INFO:信息消息
  • LOG_DEBUG:调试消息

常用设施包括 LOG_DAEMON(守护进程)、LOG_LOCAL0~LOG_LOCAL7(本地使用)等。

完整的日志使用示例

#include <syslog.h>
#include <stdio.h>
#include <unistd.h>

int main(void) {
    /* 打开日志,ident 为 "myapp",包含 PID,使用 DAEMON 设施 */
    openlog("myapp", LOG_PID | LOG_NDELAY, LOG_DAEMON);

    syslog(LOG_INFO, "Application starting up");
    syslog(LOG_NOTICE, "Configuration loaded successfully");

    /* 模拟运行中的日志输出 */
    int count = 0;
    while (count < 3) {
        syslog(LOG_INFO, "Processing cycle %d", count + 1);
        sleep(1);
        count++;
    }

    syslog(LOG_WARNING, "Low resource detected");
    syslog(LOG_ERR, "Failed to connect to database");
    syslog(LOG_INFO, "Application shutting down");

    closelog();
    return 0;
}

实战:编写HTTP守护进程

下面将以上所有知识点整合,编写一个最小化的 HTTP 守护进程。该守护进程在后台监听 8080 端口,响应简单的 HTTP 请求,并通过 syslog 记录访问日志。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <signal.h>
#include <fcntl.h>
#include <errno.h>
#include <syslog.h>
#include <sys/stat.h>
#include <sys/socket.h>
#include <netinet/in.h>

#define PORT       8080
#define PID_FILE   "/var/run/httpd_simple.pid"
#define BACKLOG    10

static volatile sig_atomic_t running = 1;

/* 信号处理函数 */
static void signal_handler(int sig) {
    if (sig == SIGTERM || sig == SIGINT) {
        running = 0;
    }
}

/* 守护进程化 */
int daemonize(void) {
    pid_t pid = fork();
    if (pid < 0) return -1;
    if (pid > 0) exit(EXIT_SUCCESS);

    if (setsid() < 0) return -1;

    pid = fork();
    if (pid < 0) return -1;
    if (pid > 0) exit(EXIT_SUCCESS);

    if (chdir("/") < 0) return -1;

    close(STDIN_FILENO);
    close(STDOUT_FILENO);
    close(STDERR_FILENO);
    int fd = open("/dev/null", O_RDWR);
    if (fd < 0) return -1;
    dup(fd);
    dup(fd);

    umask(0);
    return 0;
}

/* 写入PID文件 */
int write_pid_file(void) {
    int fd = open(PID_FILE, O_WRONLY | O_CREAT | O_TRUNC, 0644);
    if (fd < 0) return -1;
    char buf[16];
    int n = snprintf(buf, sizeof(buf), "%d", getpid());
    write(fd, buf, n);
    close(fd);
    return 0;
}

/* 处理HTTP请求 */
void handle_client(int client_fd, struct sockaddr_in *addr) {
    char request[1024];
    read(client_fd, request, sizeof(request) - 1);

    syslog(LOG_INFO, "Request from %s:%d",
            inet_ntoa(addr->sin_addr), ntohs(addr->sin_port));

    const char *response =
        "HTTP/1.1 200 OK\r\n"
        "Content-Type: text/html\r\n"
        "Connection: close\r\n"
        "\r\n"
        "<html><body><h1>Hello from daemon!</h1></body></html>\r\n";

    write(client_fd, response, strlen(response));
    close(client_fd);
}

int main(void) {
    /* 守护进程化 */
    if (daemonize() < 0) {
        fprintf(stderr, "Failed to daemonize\n");
        return 1;
    }

    /* 初始化syslog */
    openlog("httpd_simple", LOG_PID, LOG_DAEMON);
    syslog(LOG_INFO, "Daemon started on port %d", PORT);

    /* 写入PID文件 */
    if (write_pid_file() < 0) {
        syslog(LOG_ERR, "Failed to write PID file");
        return 1;
    }

    /* 注册信号处理 */
    struct sigaction sa;
    memset(&sa, 0, sizeof(sa));
    sa.sa_handler = signal_handler;
    sigaction(SIGTERM, &sa, NULL);
    sigaction(SIGINT, &sa, NULL);

    /* 创建监听套接字 */
    int server_fd = socket(AF_INET, SOCK_STREAM, 0);
    if (server_fd < 0) {
        syslog(LOG_ERR, "socket: %m");
        return 1;
    }

    int opt = 1;
    setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));

    struct sockaddr_in addr;
    memset(&addr, 0, sizeof(addr));
    addr.sin_family      = AF_INET;
    addr.sin_addr.s_addr = htonl(INADDR_ANY);
    addr.sin_port        = htons(PORT);

    if (bind(server_fd, (struct sockaddr *)&addr, sizeof(addr)) < 0) {
        syslog(LOG_ERR, "bind: %m");
        return 1;
    }

    if (listen(server_fd, BACKLOG) < 0) {
        syslog(LOG_ERR, "listen: %m");
        return 1;
    }

    syslog(LOG_INFO, "Listening on port %d", PORT);

    /* 主事件循环 */
    while (running) {
        struct sockaddr_in client_addr;
        socklen_t client_len = sizeof(client_addr);
        int client_fd = accept(server_fd,
                (struct sockaddr *)&client_addr, &client_len);
        if (client_fd < 0) {
            if (errno == EINTR) continue;
            syslog(LOG_ERR, "accept: %m");
            continue;
        }

        handle_client(client_fd, &client_addr);
    }

    /* 清理退出 */
    syslog(LOG_INFO, "Daemon shutting down");
    close(server_fd);
    unlink(PID_FILE);
    closelog();
    return 0;
}

总结

本文详细介绍了 C 语言中守护进程编程的核心技术:

  • 守护进程概述:脱离终端、后台长期运行、独立会话和进程组是守护进程的基本特征
  • daemon()函数:快速创建守护进程的便捷接口,适合简单场景,但缺乏精细控制
  • 手动创建步骤:fork、setsid、再次fork、chdir、关闭描述符、umask 六步确保完全脱离终端
  • PID文件管理:防止重复启动,为进程管理提供定位能力,是守护进程的标配机制
  • syslog日志:守护进程的标准日志方案,支持多级别、多设施,由 syslogd 统一管理
  • 实战整合:将所有技术点整合为一个完整的 HTTP 守护进程,涵盖信号处理、套接字编程和资源清理

守护进程编程是 Linux 系统编程的重要课题。掌握 fork-setsid 双重分叉模型、PID 文件互斥和 syslog 日志重定向,是编写生产级守护进程的基础。在实际项目中,还需要考虑配置文件解析、优雅重启(通过 SIGHUP 重载配置)、子进程监控等高级话题,这些都是构建健壮守护进程不可或缺的环节。