systemd.socket 怎样继承监听套接字
来源:17golang原创
时间:2026-10-25 14:01:17 128浏览 收藏
我第一次把服务改成 socket activation 时,最容易混淆的不是端口,而是“到底是谁拥有这个套接字”。答案是:systemd.socket 单元先创建并持有监听端点,服务启动时继承已经打开的文件描述符;服务不应该再按同一个地址重复 bind。只要把监听 FD 和连接 FD 分开理解,Accept=no 与 Accept=yes 的选择就清楚了。
官方地址:https://www.freedesktop.org/software/systemd/man/latest/systemd.socket.html
新写的守护进程通常从Accept=no开始:systemd 把监听套接字传给一个服务,应用自己 accept 多个连接;只有面向单连接、兼容 inetd 风格的程序,才优先考虑Accept=yes。
先分清监听 FD 与连接 FD
.socket 文件描述“监听什么”,例如 TCP 地址、Unix socket 路径或 FIFO;.service 描述“启动谁”。同名的 demo.socket 默认会激活同名的 demo.service。systemd 在服务启动前准备好套接字,再把它们放进服务进程能读取的文件描述符集合中。

这里有两个对象:
- 监听文件描述符:还没有绑定某个客户端连接,应用要对它调用 accept。
- 连接文件描述符:已经对应一个客户端连接,应用可以直接读写,通常不再负责接收下一条连接。
systemd 的 socket 文档说明,socket unit 可以按需启动服务;服务则必须支持 systemd 原生的文件描述符传递协议,或者使用 StandardInput=socket 这种传统 inetd 方式。本文下面的单元示例采用原生继承方式,应用侧仍要使用对应库或自己的 FD 读取逻辑。
用 Accept=no 让一个服务接管监听
我更愿意把 Accept=no 看成“把门口交给服务”,而不是“systemd 代替应用处理连接”。它是默认值:systemd 把所有监听套接字传给一个服务单元,服务自行 accept,然后把连接交给自己的处理循环。
[Unit]
# 让 socket 单元在服务之前准备好监听端点
Description=Demo listening socket
[Socket]
# 示例只监听本机 9090 端口,实际部署按需替换地址
ListenStream=127.0.0.1:9090
# 默认值为 no:服务接收监听 FD,而不是每个连接的 FD
Accept=no
[Install]
# 让 systemd 在启用 sockets.target 时拉起该 socket
WantedBy=sockets.target
保存为 /etc/systemd/system/demo.socket 后,通常还需要一个同名服务:
[Unit]
# 记录服务与 socket 的关系,便于阅读和排障
Description=Demo socket-activated service
Requires=demo.socket
After=demo.socket
[Service]
# 该程序必须能读取 systemd 传入的监听文件描述符
ExecStart=/usr/local/bin/demo --systemd-activation
Restart=on-failure
[Install]
# 直接启用服务不是必须的,socket unit 可以按需激活它
WantedBy=multi-user.target
应用侧的关键不是再次监听 127.0.0.1:9090,而是读取 systemd 约定的 FD。使用 sd-daemon 接口时,sd_listen_fds() 会检查 LISTEN_PID,再根据 LISTEN_FDS 从约定的起始 FD 读取数量;如果应用使用其他语言,则应使用该语言生态中兼容 socket activation 的库,或者准确实现同一约定。
用 Accept=yes 处理逐连接实例
Accept=yes 的行为完全不同:每个进入的连接都会生成一个服务实例,实例只拿到已连接的那个套接字。这样适合一次只处理一条连接、原本就是 inetd 风格的程序;新写的高并发服务通常不把它作为默认方案,因为每次连接都有额外的服务实例启动成本。
[Socket]
# 每个连接都激活一个服务实例
ListenStream=127.0.0.1:9091
Accept=yes
Service=demo-connection@.service
这里的服务名使用模板形式,例如 demo-connection@.service。实例化服务收到的是连接 FD,不是监听 FD;它不应该再次调用 accept。对于文件系统 Unix socket,服务也不要擅自 unlink systemd 管理的 socket 节点。要是程序并不理解这种逐连接生命周期,改回 Accept=no 通常更简单。
用 Sockets= 和 FileDescriptorName= 管好多套接字
同名约定适合最小配置,但多个入口往往需要更明确的契约。Service= 可以把 socket unit 指向一个不同名称的服务;服务单元的 Sockets= 则可以明确声明要继承哪些 socket unit。systemd 文档还说明,一个服务可以从多个 socket unit 收到文件描述符,但跨 socket unit 的传递顺序不应当被当成稳定协议。
[Socket]
# 外部 HTTP 入口由这个 socket unit 持有
ListenStream=127.0.0.1:8080
# 名称会帮助应用区分多个传入 FD
FileDescriptorName=web
Service=router.service
[Socket]
# 管理入口使用不同端口,保持同一个服务进程
ListenStream=127.0.0.1:9090
FileDescriptorName=admin
Service=router.service
应用若需要识别名称,可以读取 LISTEN_FDNAMES,或使用支持名称的 sd_listen_fds_with_names()。如果没有配置 FileDescriptorName=,systemd 会按 socket unit 名称生成默认名称(Accept=no 时包含 .socket 后缀)。这比依赖“第一个永远是 web、第二个永远是 admin”更稳妥。

把启动和维护动作固定下来
单元文件写好后,常用动作可以保持很小:
# 让 systemd 重新读取刚修改的单元文件
sudo systemctl daemon-reload
# 先启动 socket,让监听端点由 systemd 持有
sudo systemctl enable --now demo.socket
# 查看 socket 是否处于监听状态
sudo systemctl status demo.socket
# 需要停用时先停 socket,避免新的连接继续触发服务
sudo systemctl disable --now demo.socket
排障时先看 socket unit 是否成功绑定,再看 service 是否被触发,最后确认应用是否真的消费了继承来的 FD。若服务日志出现“地址已被使用”,第一反应不应是换端口,而是检查应用是否又执行了一次 bind;若服务启动了却没有流量,检查它读取的是监听 FD 还是连接 FD,以及是否正确处理了 LISTEN_FDS。
我会这样做取舍
| 场景 | 优先选择 | 原因 |
|---|---|---|
| 新写的长期运行服务 | Accept=no | 一个服务掌握监听循环,连接处理和并发策略留在应用内。 |
| 单连接、inetd 风格程序 | Accept=yes | 每个实例只关心一个已连接套接字,改造边界更小。 |
| 一个进程接收多个入口 | Sockets= + FileDescriptorName= | 用明确名称识别 FD,避免依赖跨 unit 的顺序。 |
真正需要记住的是所有权:systemd 负责创建、持有和在激活时传递 socket;应用负责按激活协议拿到 FD,并决定怎样 accept、读写和关闭。只要这条边界没有被打破,systemd.socket 就不再像“神秘地替服务接管端口”,而是一份清晰的启动契约。
相关问题
systemd.socket 和 systemd.service 必须同名吗?
不必须。默认同名会自动关联;也可以在 socket unit 使用 Service= 指向其他服务,在 service unit 使用 Sockets= 明确继承来源。
Accept=no 时应用还要自己 accept 吗?
要。systemd 传入的是监听 FD,应用要用自己的连接循环接收客户端。
多个 FD 能只靠编号区分吗?
不建议把跨 socket unit 的顺序当作长期契约。为每个入口设置 FileDescriptorName=,再读取名称信息更可靠。
-
501 收藏
-
501 收藏
-
501 收藏
-
501 收藏
-
501 收藏
-
478 收藏
-
451 收藏
-
294 收藏
-
263 收藏
-
279 收藏
-
405 收藏
-
200 收藏
-
203 收藏
-
425 收藏
-
487 收藏
-
442 收藏
-
342 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 立即学习 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 立即学习 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 立即学习 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 立即学习 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 立即学习 485次学习