第 37 章 守护进程 (Daemons)
核心结论
-
daemon 是长寿、无控制终端的后台进程:cron/sshd/httpd/inetd 等;通常名字以 d 结尾;常以特权(root)运行——需按第 38 章准则编写;内核线程(如 pdflush)ps 显示在 [] 中。
-
daemon 化 7 步流程:fork+setsid(脱离控制终端)+ 二次 fork(防再获控制终端)+ umask(0) + chdir(/) + close 继承 fd + reopen stdin/stdout/stderr 到 /dev/null。
-
becomeDaemon() 库函数封装 7 步:flags 位掩码可选跳过某些步骤(BD_NO_CHDIR/BD_NO_CLOSE_FILES/BD_NO_REOPEN_STD_FDS/BD_NO_UMASK0);glibc daemon() 函数无 flags 灵活性。
-
daemon 编写准则:daemon 长寿——严防内存泄漏/fd 泄漏;只允许单实例运行(§55.6);SIGTERM 优雅关闭(init 5 秒后发 SIGKILL);SIGHUP 重读配置 + 重开日志文件。
-
SIGHUP 重读配置模式:daemon 无控制终端 → kernel 不自动发 SIGHUP;可作「重读配置」信号;handler 设标志位 + 主循环检查 + 重读 conf + 重开 log;logrotate(8) 自动触发 SIGHUP。
-
syslog API 三函数:openlog (建立连接 + 设置默认) + syslog (写消息) + closelog (关闭);setlogmask 过滤 level;LOG_* 常量(facility + level);%m 替换 errno 字符串。
-
syslog 架构:syslogd 守护从 /dev/log (Unix domain) + UDP 514 收消息;按 /etc/syslog.conf 分发到 terminal/file/FIFO/user/remote syslogd;klogd 从 /proc/kmsg + syslog(2) 收内核消息转发到 syslogd。
|
本章主旨
本章深入 daemon 化流程与日志记录——从传统的「双重 fork + setsid」7 步,到 syslog 集中日志架构。读者应掌握:daemon 的定义与特征、becomeDaemon 实现、信号处理(SIGTERM 优雅关闭 + SIGHUP 重读配置)、syslog API(openlog/syslog/closelog + setlogmask)、syslog 架构(syslogd + /dev/log + syslog.conf)、format-string 攻击防御。理解「为什么二次 fork」是脱离控制终端的关键;理解「SIGHUP 复用为重读配置信号」是 daemon 设计的常见模式。写网络服务器、监控系统、批处理守护进程都依赖这些知识。 |
一、核心概念
本章围绕 6 个核心概念展开:daemon 特征与示例、daemon 化 7 步流程、becomeDaemon 实现、daemon 编写准则(信号 + 单实例)、SIGHUP 重读配置、syslog API 与架构。
| 概念 | 定义 + 重要性 | 实现提示 |
|---|---|---|
daemon 特征 |
长寿 + 无控制终端 + 后台运行;避免终端信号(SIGINT/SIGTSTP/SIGHUP);命名约定以 d 结尾;常以 root 运行;内核线程在 ps 显示 [] 中;典型:cron/sshd/httpd/inetd。 |
§37.1;无控制终端确保 kernel 不自动发 job-control/终端信号;可由 init 在启动时启动;运行到系统关机。 |
daemon 化 7 步 |
(1) fork 后父 exit(脱离 shell + 子非进程组 leader);(2) setsid 创建新会话(脱离控制终端);(3) 二次 fork(确保非 session leader,System V 约定防再获控制终端);(4) umask(0);(5) chdir("/");(6) close 继承 fd;(7) reopen stdin/stdout/stderr 到 /dev/null。 |
§37.2;BSD 风格可省略二次 fork(BSD 需 TIOCSCTTY 才获控制终端);close from n 在 Linux 不可用;用 sysconf(_SC_OPEN_MAX) 或 BD_MAX_CLOSE=8192 作 fallback。 |
becomeDaemon() 实现 |
TLPI 提供的 7 步封装函数;flags 位掩码可选跳过某些步骤(BD_NO_CHDIR=01 / BD_NO_CLOSE_FILES=02 / BD_NO_REOPEN_STD_FDS=04 / BD_NO_UMASK0=010);glibc daemon() 函数无 flags。 |
§37.2,Listing 37-1/37-2;二次 fork 后子不是 session leader;close 0..maxfd 后 reopen 0/1/2 到 /dev/null;典型用法:becomeDaemon(0) + 业务逻辑。 |
daemon 编写准则 |
严防内存泄漏 + fd 泄漏(kill+restart 才能恢复);只允许单实例(§55.6 文件锁/PID 文件);SIGTERM 优雅关闭(init 关机信号,5 秒后 SIGKILL);SIGHUP 重读配置 + 重开日志;标准 log/config 路径(/etc / /var/log)。 |
§37.3-37.4;长生命周期 = bug 影响时间长;handler 必须快速清理;init 关机给 5 秒(不是 5 秒 CPU)。 |
SIGHUP 重读配置模式 |
daemon 无控制终端 → kernel 不发 SIGHUP;可作「重读配置」信号;handler 设 hupReceived 标志;主循环检查标志 + logClose + logOpen + readConfigFile;logrotate(8) 发 SIGHUP 触发;典型 daemon 都支持。 |
§37.4,Listing 37-3 daemon_SIGHUP.c;hupReceived = volatile sig_atomic_t;logMessage/logClose/logOpen/readConfigFile 辅助函数;用 killall -HUP daemon 触发。 |
syslog API 与架构 |
openlog (建立连接 + 默认 facility + LOG_PID 等选项) + syslog (priority + format + args) + closelog + setlogmask;syslogd 从 /dev/log + UDP 514 收消息;按 /etc/syslog.conf 分发;klogd 转发内核消息。 |
§37.5;LOG_CONS / LOG_NDELAY / LOG_PID / LOG_PERROR 等选项;facility (LOG_USER/AUTH/DAEMON/MAIL 等) + level (LOG_EMERG 到 LOG_DEBUG);%m 替换 errno;format-string 攻击防御(用 "%s")。 |
二、详细笔记
37.1 daemon 概述
What:daemon 是长寿、无控制终端的后台进程;用于 cron/sshd/httpd/inetd 等系统服务。
Why:理解 daemon 特征是设计长生命周期后台服务的前提——避免终端信号干扰、可独立于用户会话运行。
How:
特征(§37.1):
-
长寿:系统启动时创建,运行到系统关机。
-
无控制终端:kernel 不自动发 job-control/终端信号(SIGINT/SIGTSTP/SIGHUP)。
-
后台运行:不与用户交互。
-
命名约定:以 d 结尾(cron/sshd/httpd);非通用规则。
典型 daemon:
-
cron:定时执行命令。
-
sshd:远程登录。
-
httpd:Web 服务器(Apache/Nginx)。
-
inetd:超级服务器,监听端口启动对应服务。
特权与编写准则:
-
多数标准 daemon 以 root 运行——按第 38 章准则编写。
-
内核线程(如 pdflush):ps 显示 [] 中,代码是内核一部分。
-
init 启动 daemon;典型 /etc/init.d 或 systemd unit。
When:写系统服务、后台监控、批处理;避免把交互式程序当 daemon 写。
Example:
# 查看 daemon 进程(Linux)
$ ps -eo 'pid ppid pgid sid tty cmd' | grep -E 'd\s' | head -5
37.2 创建 daemon(7 步流程)
What:将进程转变为 daemon 的标准 7 步流程——fork + setsid + 二次 fork + umask + chdir + close + reopen /dev/null。
Why:理解每一步的目的(脱离 shell/控制终端 + 防再获控制终端 + 文件系统清理 + fd 隔离)是写 daemon 的基础。
How:
7 步详解(§37.2):
-
fork + 父 exit:父终止后 shell 显示新提示;子继承父的 PGID 但有自己的 PID(确保子不是进程组 leader)。
-
setsid:调用者成为新会话 leader;脱离控制终端;新会话无控制终端。
-
二次 fork + 父 exit:System V 约定——非 session leader 永远不能获控制终端;BSD 风格可省略(BSD 需 TIOCSCTTY 才获);冗余 fork 无害。
-
umask(0):确保创建文件/目录时权限是请求值(不受父 shell mask 影响)。
-
chdir("/"):确保 cwd 在 root——可卸载 cwd 所在文件系统;典型 daemon 用 / 或配置目录(如 /var/spool/cron)。
-
close 所有 fd:避免 fd 泄漏;fd 0/1/2 通常指向终端——关闭;fd 是有限资源。
-
reopen stdin/stdout/stderr 到 /dev/null:库函数 stdio 调用不会失败;防止 daemon 后续 open fd 1/2 误用为 stdout/stderr;/dev/null 写丢弃、读 EOF。
becomeDaemon 实现(§37.2,Listing 37-2):
// 摘自《The Linux Programming Interface》第 37 章 — Listing 37-2
// 摘自 daemons/become_daemon.c
int becomeDaemon(int flags) {
int maxfd, fd;
/* Step 1: fork + parent exit */
switch (fork()) {
case -1: return -1;
case 0: break; /* Child falls through */
default: _exit(EXIT_SUCCESS); /* Parent exits */
}
/* Step 2: setsid */
if (setsid() == -1) return -1;
/* Step 3: second fork */
switch (fork()) {
case -1: return -1;
case 0: break;
default: _exit(EXIT_SUCCESS);
}
/* Step 4-7 based on flags */
if (!(flags & BD_NO_UMASK0)) umask(0);
if (!(flags & BD_NO_CHDIR)) chdir("/");
if (!(flags & BD_NO_CLOSE_FILES)) {
maxfd = sysconf(_SC_OPEN_MAX);
if (maxfd == -1) maxfd = BD_MAX_CLOSE; /* 8192 fallback */
for (fd = 0; fd < maxfd; fd++) close(fd);
}
if (!(flags & BD_NO_REOPEN_STD_FDS)) {
close(STDIN_FILENO);
fd = open("/dev/null", O_RDWR); /* 应该 == 0 */
if (fd != STDIN_FILENO) return -1;
dup2(STDIN_FILENO, STDOUT_FILENO);
dup2(STDIN_FILENO, STDERR_FILENO);
}
return 0;
}
flags(§37.2,Listing 37-1):
-
BD_NO_CHDIR = 01:跳过 chdir("/")。 -
BD_NO_CLOSE_FILES = 02:跳过 close 继承 fd。 -
BD_NO_REOPEN_STD_FDS = 04:不重开 0/1/2 到 /dev/null。 -
BD_NO_UMASK0 = 010:不 umask(0)。 -
BD_MAX_CLOSE = 8192:sysconf 不可知时的 fallback max fd。
注意:
-
glibc
daemon(int nochdir, int noclose)函数无 flags 灵活性。 -
Solaris 9 / 部分 BSD 提供
closefrom(n)——Linux 不可用。 -
close 0..maxfd 会浪费 close() 调用——但简单可靠。
-
二次 fork 后子进程 PID ≠ session ID——非 session leader。
When:任何后台进程都需要 daemon 化(避免终端信号、避免持有文件系统);典型:服务器、监控、定时任务。
Example:见上述 becomeDaemon 实现。
37.3 daemon 编写准则
What:daemon 长寿——严防资源泄漏;只允许单实例;SIGTERM 优雅关闭。
Why:理解 daemon 的特殊需求是写可靠长生命周期服务的前提。
How:
资源泄漏防御(§37.3):
-
内存泄漏(§7.1.3)——只有 kill + restart 才能恢复;长期累积 → OOM。
-
fd 泄漏——长期累积 → EMFILE;持有已删除文件 → 不可释放磁盘空间。
-
唯一补救:kill + restart(修复 bug 后)。
单实例运行(§37.3):
-
典型需求:cron 一个实例、sshd 一个守护。
-
实现:§55.6 文件锁 / PID 文件 / 命名管道 / fcntl F_SETLK。
-
PID 文件惯例:/var/run/<daemon>.pid——含进程 PID;启动时检查已存在则拒绝启动。
SIGTERM 处理(§37.3):
-
init 关机时给所有子进程发 SIGTERM(默认终止)。
-
5 秒后 init 发 SIGKILL(不可 catch)。
-
daemon 应 SIGTERM handler 快速清理:flush log、close fd、unlink PID 文件、保存状态。
-
注意:5 秒是 wall clock 时间,不是 CPU 时间——所有 daemon 同时清理,CPU 竞争。
When:写所有 daemon 都需遵循;尤其生产环境。
Example:
// 摘自《The Linux Programming Interface》第 37 章
static volatile sig_atomic_t terminate = 0;
static void termHandler(int sig) { terminate = 1; }
int main() {
signal(SIGTERM, termHandler);
/* daemon 业务逻辑 */
while (!terminate) {
/* 工作单元 */
}
/* 清理:flush log / close fd / unlink PID file */
cleanup();
return 0;
}
37.4 SIGHUP 重读配置模式
What:daemon 用 SIGHUP 作为「重读配置 + 重开日志」信号;handler 设标志,主循环检查 + 处理。
Why:daemon 无控制终端 → kernel 不发 SIGHUP;可安全用作应用信号;提供「on-the-fly 配置更新」能力。
How:
为什么 SIGHUP(§37.4):
-
daemon 无控制终端 → 终端断开不发 SIGHUP。
-
SIGHUP 在普通进程是「终端断开」语义;daemon 复用为「重读配置」。
-
logrotate(8) 等工具发 SIGHUP 触发日志轮转。
实现模式(§37.4,Listing 37-3 daemon_SIGHUP.c):
// 摘自《The Linux Programming Interface》第 37 章 — Listing 37-3
// 摘自 daemons/daemon_SIGHUP.c
static volatile sig_atomic_t hupReceived = 0;
static void sighupHandler(int sig) {
hupReceived = 1; /* 简洁 — handler 内只设标志 */
}
int main(int argc, char *argv[]) {
const int SLEEP_TIME = 15;
int count = 0, unslept;
struct sigaction sa;
sigemptyset(&sa.sa_mask);
sa.sa_flags = SA_RESTART;
sa.sa_handler = sighupHandler;
sigaction(SIGHUP, &sa, NULL);
becomeDaemon(0);
logOpen(LOG_FILE);
readConfigFile(CONFIG_FILE);
unslept = SLEEP_TIME;
for (;;) {
unslept = sleep(unslept); /* Returns > 0 if interrupted */
if (hupReceived) {
logClose();
logOpen(LOG_FILE);
readConfigFile(CONFIG_FILE);
hupReceived = 0;
}
if (unslept == 0) {
count++;
logMessage("Main: %d", count);
unslept = SLEEP_TIME;
}
}
}
要点:
-
hupReceived是volatile sig_atomic_t——async-safe 访问。 -
handler 只设标志——避免 handler 内做复杂操作(可能异步信号不安全)。
-
主循环用
sleep自动处理 EINTR(unslept > 0 表示未睡够)。 -
主循环每次 wake 检查 hupReceived——避免 SIGCONT/SIGHUP 错过。
-
logOpen/logClose/reopen——配合 logrotate mv + SIGHUP 实现 log rotation。
-
readConfigFile 每次 SIGHUP 重读——on-the-fly 配置更新。
替代方案:
-
部分 daemon 在 SIGHUP handler 内 close all fd + exec()——重启自身。
-
不推荐:丢失全局状态;复杂。
When:所有需要「on-the-fly 配置」的 daemon——logrotate 重开 log、配置变更生效。
Example:见上述 daemon_SIGHUP.c。
37.5 syslog API
What:openlog/syslog/closelog/setlogmask 三函数 + 过滤;syslogd 守护处理消息分发。
Why:syslog 提供集中日志管理——避免每个 daemon 自己管日志;统一到 /var/log;支持远程日志聚合。
How:
API(§37.5.2):
// 摘自《The Linux Programming Interface》第 37 章
#include <syslog.h>
void openlog(const char *ident, int log_options, int facility);
void syslog(int priority, const char *format, ...);
void closelog(void);
int setlogmask(int mask_priority);
openlog(§37.5.2):
-
ident:每条消息前缀(通常程序名);openlog 仅拷贝指针——应用须保证字符串不被修改。
-
log_options 位掩码:
LOG_CONS(失败写 console)、LOG_NDELAY(立即打开 /dev/log)、LOG_NOWAIT(不 wait 子进程——Linux 无效)、LOG_ODELAY(延迟打开,默认)、LOG_PERROR(同时写 stderr)、LOG_PID(消息含 PID)。 -
facility:默认 facility 值(表 37-1)。
syslog(§37.5.2):
-
priority:
facility | level;facility 缺省用 openlog 设置或 LOG_USER;level(表 37-2,从 LOG_EMERG 到 LOG_DEBUG)。 -
format:同 printf——可含
%m替换 errno 字符串。 -
安全:用户输入必须用
"%s"包裹——防 format-string 攻击。 -
示例:
openlog(argv[0], LOG_PID | LOG_CONS, LOG_LOCAL0); syslog(LOG_ERROR, "Bad argument: %s", argv[1]); /* facility 默认 LOG_LOCAL0 */ syslog(LOG_USER | LOG_INFO, "Exiting"); /* 显式 facility */
setlogmask(§37.5.2):
-
过滤 level——不 mask 的 level 消息丢弃。
-
LOG_MASK(level)转 bit;LOG_UPTO(level)包括 level 及以上。 -
示例:
setlogmask(LOG_UPTO(LOG_ERR))仅记录 ERR 及以上。
facility 表(表 37-1):LOG_AUTH/AUTHPRIV/CRON/DAEMON/FTP/KERN/LOCAL0-7/LPR/MAIL/NEWS/SYSLOG/USER/UUCP。
level 表(表 37-2):LOG_EMERG/ALERT/CRIT/ERR/WARNING/NOTICE/INFO/DEBUG。
closelog(§37.5.2):
-
关闭 /dev/log socket fd。
-
daemon 通常不调——保持连接。
syslog 架构(§37.5.1):
-
syslogd:从 /dev/log(Unix domain datagram)+ UDP 514 收消息。
-
按 /etc/syslog.conf 分发到 terminal/file/FIFO/user/remote。
-
klogd:从 /proc/kmsg 或 syslog(2) 收内核消息转发到 syslogd。
-
syslog(2) 系统调用(glibc klogctl 包装)读内核 ring buffer。
-
注意区分 syslog(2) vs syslog(3)——前者内核消息,后者用户程序 API。
syslog.conf 规则(§37.5.3):
-
格式:
facility.level action。 -
facility.level 用 . 分隔;
*通配;;多 selector;level=none排除。 -
action:文件路径、@remote、user、FIFO、设备、-prefix(不同步磁盘)。
-
示例:
*.err /dev/tty10 auth.notice root *.debug;mail.none;news.none -/var/log/messages
-
修改 syslog.conf 后需
killall -HUP syslogd重读。
When:所有 daemon 都应使用 syslog 而非自己管日志;用户程序也可用 syslog。
Example:
// 摘自《The Linux Programming Interface》第 37 章
openlog("mydaemon", LOG_PID | LOG_CONS, LOG_DAEMON);
syslog(LOG_INFO, "Daemon started (PID=%ld)", (long) getpid());
syslog(LOG_ERR, "Failed to open %s: %m", filename); /* %m 替换 errno */
setlogmask(LOG_UPTO(LOG_WARNING)); /* 只记录 WARNING 及以上 */
closelog();
三、关键图表
|
daemon 化 7 步流程
BSD 风格可省略步骤 3(BSD 需 TIOCSCTTY 才获控制终端)。 |
|
syslog facility 表(表 37-1 节选)
|
|
syslog level 表(表 37-2)
|
四、思维导图
mindmap
root((第 37 章 守护进程))
daemon 概述
长寿 无终端 后台
cron sshd httpd inetd
命名以 d 结尾
内核线程 ps 显示 中
编写准则 第 38 章
7 步 daemon 化
1 fork 父 exit
2 setsid 新会话
3 二次 fork 防控制终端
4 umask 0
5 chdir /
6 close 所有 fd
7 reopen 0 1 2 dev null
BSD 风格省二次 fork
becomeDaemon
flags 位掩码
BD NO CHDIR
BD NO CLOSE FILES
BD NO REOPEN STD FDS
BD NO UMASK0
BD MAX CLOSE 8192
glibc daemon 无 flags
编写准则
防内存 fd 泄漏
单实例 PID 文件
SIGTERM 优雅关闭
init 5 秒后 SIGKILL
handler 快速清理
SIGHUP 重读配置
daemon 无控制终端
kernel 不发 SIGHUP
handler 设 hupReceived
主循环检查
logrotate mv SIGHUP
close open log 重读 config
syslog API
openlog ident option facility
syslog priority format
closelog
setlogmask LOG UPTO
LOG CONS NDELAY PID PERROR
facility LOG USER DAEMON
level LOG EMERG DEBUG
m 替换 errno
防 format string 攻击
syslog 架构
syslogd daemon
dev log unix domain
UDP 514 远程
klogd 内核消息
proc kmsg syslog 2
syslog conf 分发
terminal file FIFO user
remote syslogd
五、重点与易错点
-
daemon 化 7 步缺一不可:每步有特定目的——脱离 shell/终端 + 防再获控制终端 + 文件系统清理 + fd 隔离。
-
二次 fork 的目的:System V 约定——非 session leader 永远不能获控制终端;BSD 风格可省略(BSD 需 TIOCSCTTY 才获)。
-
glibc daemon() 函数无 flags:缺灵活性;TLPI becomeDaemon 用 flags 位掩码可选跳过某些步骤。
-
close 所有 fd 不能用 closefrom——Linux 不可用;循环 close 0..sysconf(_SC_OPEN_MAX) 即可(带 BD_MAX_CLOSE=8192 fallback)。
-
reopen stdin/stdout/stderr 到 /dev/null:防止库函数 stdio 失败 + 防止后续 open 误用 fd 1/2;/dev/null 写丢弃、读 EOF。
-
daemon 长寿 → 必须防内存/fd 泄漏——只有 kill + restart 才能恢复;测试要覆盖长时间运行。
-
daemon 单实例:cron/sshd 一个实例足够——用 PID 文件 + 文件锁实现(§55.6)。
-
SIGTERM 优雅关闭:init 关机发 SIGTERM → 5 秒后 SIGKILL;handler 快速清理(flush log / close fd / unlink PID 文件)。
-
SIGHUP 重读配置模式:daemon 无控制终端 → kernel 不发 SIGHUP;handler 设 hupReceived 标志 + 主循环检查;logrotate(8) 触发。
-
hupReceived 必须 volatile sig_atomic_t——async-signal-safe;handler 只设标志避免复杂操作。
-
syslog 三函数:openlog (建立连接 + 默认) + syslog (priority + format) + closelog (关闭);setlogmask 过滤 level。
-
syslog priority = facility | level:facility 缺省用 openlog 设置或 LOG_USER;level 必须指定。
-
%m 替换 errno——syslog 特有 printf 不支持;用于 errno 错误消息格式化。
-
format-string 攻击防御:用户输入必须用
"%s"包裹——syslog(priority, "%s", user_str)而非syslog(priority, user_str)。 -
syslog(2) vs syslog(3) 不同——前者内核消息(glibc klogctl 包装);后者用户 API。
-
syslogd 收消息来源:/dev/log(Unix domain)+ UDP 514(远程)+ klogd(内核消息)。
-
syslog.conf 规则:
facility.level action;*通配;;多 selector;level=none排除;@remote远程;改后需killall -HUP syslogd。 -
SETLOGMASK 默认允许所有 level——只丢弃不在 mask 中的 level;
LOG_UPTO(level)包括 level 及以上。-
跨章衔接:第 24-27 章 fork/exec 基础 → 第 34 章 setsid/控制终端 → 第 37 章 daemon 化;第 38 章 set-UID 程序编写准则(daemon 常以 root 运行);第 55 章 fcntl 锁 → 单实例 daemon(§55.6);第 60 章 inetd → 超级服务器 daemon;第 22 章信号 → SIGHUP/SIGTERM 处理。
-