跳转至

客户端-服务器与守护进程 — 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. 可带走的

  1. syslog:openlog 配身份(LOG_PID/LOG_LOCAL0)、syslog 发 分级消息;format 必须 "%s" 包装用户输入。
  2. daemon 七步:fork→setsid→忽略 SIGHUP→再 fork→chdir / →umask 0→关光 fd;最后 openlog。
  3. 名字带 d、TTY 列是 ? = 守护进程。
  4. 请求-应答架构:公共 FIFO 收请求,私有 FIFO(/tmp/fifo<pid>) 回结果;消息 ≤PIPE_BUF 才能原子写。
  5. 服务器自握一个 dummy 写端 = 公共 FIFO 永远等不到 EOF = 主循环永不自然退出。
  6. 客户端 O_WRONLY|O_NONBLOCK 开公共 FIFO:ENXIO = 服务器 没在跑。
  7. 迭代 vs 并发:排队 vs fork 一客一进程;并发版的父进程 靠 SIGCHLD handler 异步收尸,绝不阻塞监听。
  8. 双向通信先送连接消息、再开数据 FIFO——顺序错了要么死锁 要么丢了「对方已死」的通知。
  9. 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」)

  1. 出处:「Chapter 14: Client-Server Applications and Daemons」第 107–134 段(text/20-ch14-chapter-14-client-server-applications-and-daemon.txt:108,搜「void openlog」)。 ↩

  2. 出处:「Chapter 14: Client-Server Applications and Daemons」第 93–101 段(text/20-ch14-chapter-14-client-server-applications-and-daemon.txt:95,搜「syslogd」)。 ↩

  3. 出处:「Chapter 14: Client-Server Applications and Daemons」第 135–145 段(text/20-ch14-chapter-14-client-server-applications-and-daemon.txt:141,搜「security risk」)。 ↩

  4. 出处:「Chapter 14: Client-Server Applications and Daemons」第 167–172 段(text/20-ch14-chapter-14-client-server-applications-and-daemon.txt:172,搜「syslog_demo[8259]」)。 ↩

  5. 出处:「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」)。 ↩↩↩↩

  6. 出处:「Chapter 14: Client-Server Applications and Daemons」第 326–345 段(text/20-ch14-chapter-14-client-server-applications-and-daemon.txt:334,搜「fixed size」)。 ↩

  7. 出处:「Chapter 14: Client-Server Applications and Daemons」第 471–493 段(text/20-ch14-chapter-14-client-server-applications-and-daemon.txt:475,搜「atomic write」)。 ↩

  8. 出处:「Chapter 14: Client-Server Applications and Daemons」第 495–591 段(text/20-ch14-chapter-14-client-server-applications-and-daemon.txt:573,搜「O_WRONLY」)。 ↩↩

  9. 出处:「Chapter 14: Client-Server Applications and Daemons」第 529–541 段(text/20-ch14-chapter-14-client-server-applications-and-daemon.txt:529,搜「on_sigpipe」)。 ↩

  10. 出处:「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」)。 ↩↩↩

  11. 出处:「Chapter 14: Client-Server Applications and Daemons」第 719–740 段(text/20-ch14-chapter-14-client-server-applications-and-daemon.txt:725,搜「concurrent server」)。 ↩

  12. 出处:「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」)。 ↩↩

  13. 出处:「Chapter 14: Client-Server Applications and Daemons」第 1007–1075 段(text/20-ch14-chapter-14-client-server-applications-and-daemon.txt:1063,搜「process_client」)。 ↩