守护进程概述
守护进程(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 重载配置)、子进程监控等高级话题,这些都是构建健壮守护进程不可或缺的环节。