客户端-服务器与守护进程 — syslog、daemon 化、两种服务器¶
这一章讲什么: 守护进程是「没有终端、开机就在」的服务进程 (httpd、sshd、syslogd——名字常带 d)。本章先给它配上嘴 (syslog 日志服务),再教它出生的七步仪式,最后动手造 两个真服务器:一个迭代式(计算器服务,排队接待)、一个并发式 (大小写转换,每客一个子进程)。全部用第 13 章的 FIFO 搭建—— 你会看到「请求-应答」架构里的每一根管子怎么接。
1. 顶层全景:一个服务的 anatomy¶
没终端的程序怎么说话?→ syslog(openlog 配身份 / syslog 发消息
/ syslogd 守护进程落盘 /var/log/syslog)
│
怎么摆脱终端?→ daemon 七步:① fork,父退 ② setsid 当会话首领
③ 忽略 SIGHUP ④ 再 fork(防重新获取控制终端)⑤ chdir("/")
⑥ umask(0) ⑦ close 所有 fd(最好指向 /dev/null)+ openlog
│
怎么接待客户?→ 公共 FIFO(监听)+ 私有 FIFO(回话)
├─ 迭代服务器:一次处理一个请求(spl_calc: popen("echo ..|bc"))
└─ 并发服务器:fork 子进程每客一服(upcase: 双 FIFO 双向、
大数据、locale 感知),父进程 SIGCHLD handler 异步收尸
图说:日志、出生、接客——一个守护进程的三个人生关卡。
2. syslog:没有终端之后的嘴¶
openlog(程序名, LOG_PID|LOG_CONS, LOG_LOCAL0) 配身份:
LOG_PID 让每条消息带 PID,facility 用 LOG_LOCAL0..7
(留给应用程序的八个槽);syslog(LOG_INFO, "%s", msg) 发消息,
priority 分级 LOG_EMERG/CRIT/ERR/WARNING/INFO1。
真正的写盘者是 syslogd 守护进程——它按配置(syslog.conf)
分诊:有的进 /var/log/syslog、有的上终端2。
安全坑:format 参数不能传用户输入——syslog(prio, message)
是格式化字符串漏洞;必须 syslog(prio, "%s", message)3。
日志行长这样:Jun 19 15:46:56 harpo syslog_demo[8259]: 消息4。
3. daemon 化:七步仪式¶
背景不等于 daemon:后台运行(&)仍有控制终端,照样被终端信号
杀掉。make_me_a_daemon() 的七步(Listing 14-1)5:
fork() → 父进程 exit ← shell 收到「孩子已退」,提示符回来
setsid() ← 新会话首领+新组组长,无控制终端
signal(SIGHUP, SIG_IGN) ← 会话首领死时会 SIGHUP 全组;先声明:
← 我将来生的孩子也都不许被 SIGHUP 杀
fork() → 第一个孩子 exit ← 再断一次:防止「会话首领开终端设备
← 会被自动任命为控制终端」(Linux 可用
← open 时 O_NOCTTY 替代,但不通用)
chdir("/") ← 别占着某个文件系统,免得它卸不掉
umask(0) ← 我创建的文件权限要按我给的来
for (i=0; i<sysconf(_SC_OPEN_MAX); i++) close(i);
openlog(...) ← 嘴接上;0/1/2 关掉后写终端会失败
图说:两次 fork 各有使命——第一次脱离 shell,第二次防止 「不小心又领回一个终端」。 细节:关 fd 时关到未打开的会 返错,无碍,忽略;生产版会把 0/1/2 重开成 /dev/null5。
4. 迭代服务器:spl_calc¶
架构问题一:公共 FIFO 是字节流,多个客户端的请求挤在一起, 怎么分界?三选一定长消息( ELF 头式变长留给你自己)6。消息 结构必须 ≤ PIPE_BUF(4096) 才能单次原子写7:
#define PUBLIC "/tmp/CALCFIFO"
#define FIFOPATHLEN (PIPE_BUF/4) /* 1024:私有 FIFO 名 */
#define TEXTLEN (PIPE_BUF - FIFOPATHLEN) /* 3072:表达式 */
struct message { char fifo_name[FIFOPATHLEN]; char text[TEXTLEN]; };
客户端 10 步:造私有 FIFO 名 /tmp/fifo<pid>(唯一性来自
PID)→ 公共 FIFO 以 O_WRONLY|O_NONBLOCK 打开(ENXIO =
服务器没开,友好报错而不是死等)→ 写消息 → 开私有 FIFO 读
应答 → 收尾删 FIFO8。信号处理:SIGINT/SIGQUIT/SIGTERM
关 fd+unlink;SIGPIPE = 服务器不读了,报错退出9。
服务器:造公共 FIFO(EEXIST 则报错退出)→ 公共 FIFO 开
两次:O_RDONLY 用于读,O_WRONLY|O_NONBLOCK 拿到一个
dummy 写端——为什么?防止自己读到 EOF 退出主循环(它得
永远监听)→ 循环 read → 按消息里的私有 FIFO 名开写端
(失败睡 1 秒重试,最多 5 次)→ sprintf(command, "echo
\"%s\" | bc", msg.text); popen(command, "r") 借 bc 当计算
引擎 → 读出结果写回客户端的私有 FIFO → pclose。效果:
./spl_calc '10 * 2 + 100/25' → 24;2^256 输出 78 位
大数10。
判断(我们的,不是书里的): 「服务器自己握一个 dummy 写端」 是本章最妙的一招——它利用了管道语义「写端全关才给 EOF」, 反过来锁死自己的读循环。这是「用生命周期事件做同步」的又一例。 代价是主循环只能靠信号终止——设计守护进程时,「永不自然退出」 往往正是需求本身。 如果错,会错在: 多副本部署或崩溃重启时,旧 dummy 语义会让 两个服务器实例同时读公共 FIFO、请求被随机分诊——原书用 EEXIST 检查挡住了重启,但没挡「故意的多实例」。
5. 并发服务器:upcase¶
瓶颈:迭代服务器接待一个慢客户时全队干等。并发版 = 收到请求 就 fork 一个子进程专门伺候,父进程立刻回监听11。服务的 活儿也升级:大小写转换、大数据量、要按客户端的 locale—— 双向通信所以每个客户端要两根私有 FIFO(raw_text 进、 upcased 出),外加 locale 名:
#define PUBLIC "/tmp/UPCASE_FIFO"
typedef struct { char upcased_fifo[NAMELENGTH]; char raw_text_fifo[NAMELENGTH];
char locale[LOCALELENGTH]; } message; /* 连接消息:不带数据 */
客户端的关键顺序(原书专门解释,值得背):先发连接消息, 再开 raw_text 写端——反过来会死锁或丢故障通知:若先开 FIFO (服务器还阻塞在公共 FIFO 的 read 上)就得 O_RDWR 自开两端; 可那样服务器若猝死,客户端因自己握着读端而永远收不到 SIGPIPE, 对端死了它毫无察觉。顺序即协议12。
服务器端:make_me_a_daemon() 先建行不行?——先建公共
FIFO、失败要能在终端报错,然后才 daemon 化;监听循环
while (read(publicfd,&msg,sizeof(msg))>0) if (fork()==0)
process_client(&msg); 子进程干活、父进程绝不 wait(等
就阻塞监听)——收尸交给 SIGCHLD handler( 第 11 章那套:
SA_SIGINFO、存 errno、si_pid 点名、WNOHANG 清仓)13。
子进程按客户端给的 locale setlocale 后读 raw→转大写→写
upcased。
6. 作者的判断与证据¶
有明证的: syslog 分级/facility、daemon 七步各步的理由 (尤其第二次 fork 与 SIGHUP 的因果)、FIFO 打开语义表(第 13 章)在客户端 ENXIO 检测上的应用、dummy 写端锁死主循环的机理, 全部机制可验证5810。
作者的判断: 「服务实现选 bc+popen,不自己写表达式求值—— 速度不重要,学机制才重要」——教学取舍,明说10;「连接消息 与开 FIFO 的顺序不能换」——从死锁/丢信号两个失效模式推出的 设计规则,论证完整12。
7. 边界与局限¶
- 两个服务器都是 FIFO 本机版——网络版要 socket(本书不收,
第 1 章已声明);
- 协议极简:无认证、无超时、无消息序号,SIGPIPE 处理是兜底 不是恢复;
- 并发版用进程(每客一 fork)——线程版(第 15、16 章的工具) 留作对照;
- daemon 化没讲 PID 文件、systemd 集成——那是生产部署的活5。
8. 可带走的¶
- syslog:openlog 配身份(LOG_PID/LOG_LOCAL0)、syslog 发 分级消息;format 必须 "%s" 包装用户输入。
- daemon 七步:fork→setsid→忽略 SIGHUP→再 fork→chdir / →umask 0→关光 fd;最后 openlog。
- 名字带 d、TTY 列是 ? = 守护进程。
- 请求-应答架构:公共 FIFO 收请求,私有 FIFO(/tmp/fifo<pid>) 回结果;消息 ≤PIPE_BUF 才能原子写。
- 服务器自握一个 dummy 写端 = 公共 FIFO 永远等不到 EOF = 主循环永不自然退出。
- 客户端 O_WRONLY|O_NONBLOCK 开公共 FIFO:ENXIO = 服务器 没在跑。
- 迭代 vs 并发:排队 vs fork 一客一进程;并发版的父进程 靠 SIGCHLD handler 异步收尸,绝不阻塞监听。
- 双向通信先送连接消息、再开数据 FIFO——顺序错了要么死锁 要么丢了「对方已死」的通知。
- SIGPIPE 在客户端 = 服务器不读了:捕获、清理、退出。
9. 原文地图¶
| 主题 | 原书章 | 原文位置 |
|---|---|---|
| syslog 接口与安全坑 | Chapter 14: Client-Server Applications and Daemons | text/20-ch14-chapter-14-client-server-applications-and-daemon.txt:108(搜「void openlog」) · :141(搜「security risk」) · :172(搜「syslog_demo[8259]」) |
| daemon 定义与七步 | 同上 | :193(搜「close all open file descriptors」) · :269(搜「make_me_a_daemon」) · :286(搜「First child terminates」) |
| 定长消息与 PIPE_BUF | 同上 | :334(搜「fixed size」) · :475(搜「atomic write」) |
| spl_calc 客户端 | 同上 | :573(搜「O_WRONLY」) · :559(搜「fifo%d」) |
| spl_calc 服务器/dummy 写端 | 同上 | :660(搜「dummyfd」) · :671(搜「popen(command」) · :706(搜「2^256」) |
| 并发服务器架构 | 同上 | :725(搜「concurrent server」) · :1063(搜「process_client」) |
| upcase 连接消息 | 同上 | :775(搜「typedef struct _message」) |
| 客户端开 FIFO 顺序论证 | 同上 | :992(搜「deadlock」) · :996(搜「never be notified」) |
-
出处:「Chapter 14: Client-Server Applications and Daemons」第 107–134 段(
text/20-ch14-chapter-14-client-server-applications-and-daemon.txt:108,搜「void openlog」)。 ↩ -
出处:「Chapter 14: Client-Server Applications and Daemons」第 93–101 段(
text/20-ch14-chapter-14-client-server-applications-and-daemon.txt:95,搜「syslogd」)。 ↩ -
出处:「Chapter 14: Client-Server Applications and Daemons」第 135–145 段(
text/20-ch14-chapter-14-client-server-applications-and-daemon.txt:141,搜「security risk」)。 ↩ -
出处:「Chapter 14: Client-Server Applications and Daemons」第 167–172 段(
text/20-ch14-chapter-14-client-server-applications-and-daemon.txt:172,搜「syslog_demo[8259]」)。 ↩ -
出处:「Chapter 14: Client-Server Applications and Daemons」第 203–310 段(
text/20-ch14-chapter-14-client-server-applications-and-daemon.txt:269,搜「make_me_a_daemon」) · :286(搜「First child terminates」)。 ↩↩↩↩ -
出处:「Chapter 14: Client-Server Applications and Daemons」第 326–345 段(
text/20-ch14-chapter-14-client-server-applications-and-daemon.txt:334,搜「fixed size」)。 ↩ -
出处:「Chapter 14: Client-Server Applications and Daemons」第 471–493 段(
text/20-ch14-chapter-14-client-server-applications-and-daemon.txt:475,搜「atomic write」)。 ↩ -
出处:「Chapter 14: Client-Server Applications and Daemons」第 495–591 段(
text/20-ch14-chapter-14-client-server-applications-and-daemon.txt:573,搜「O_WRONLY」)。 ↩↩ -
出处:「Chapter 14: Client-Server Applications and Daemons」第 529–541 段(
text/20-ch14-chapter-14-client-server-applications-and-daemon.txt:529,搜「on_sigpipe」)。 ↩ -
出处:「Chapter 14: Client-Server Applications and Daemons」第 598–717 段(
text/20-ch14-chapter-14-client-server-applications-and-daemon.txt:660,搜「dummyfd」) · :671(搜「popen(command」) · :706(搜「2^256」)。 ↩↩↩ -
出处:「Chapter 14: Client-Server Applications and Daemons」第 719–740 段(
text/20-ch14-chapter-14-client-server-applications-and-daemon.txt:725,搜「concurrent server」)。 ↩ -
出处:「Chapter 14: Client-Server Applications and Daemons」第 985–1006 段(
text/20-ch14-chapter-14-client-server-applications-and-daemon.txt:992,搜「deadlock」) · :996(搜「never be notified」)。 ↩↩ -
出处:「Chapter 14: Client-Server Applications and Daemons」第 1007–1075 段(
text/20-ch14-chapter-14-client-server-applications-and-daemon.txt:1063,搜「process_client」)。 ↩