登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  文章 >  linux

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 在服务启动前准备好套接字,再把它们放进服务进程能读取的文件描述符集合中。

systemd.socket 的监听文件描述符在 Accept=no 和 Accept=yes 两种模式下交给服务的静态关系说明图
图1:systemd.socket、监听文件描述符与两种 Accept 模式的关系示意图,不是截图或运行证据。

这里有两个对象:

  • 监听文件描述符:还没有绑定某个客户端连接,应用要对它调用 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”更稳妥。

多个 socket unit 通过 Sockets 和文件描述符名称传给服务进程的静态关系说明图
图2:多个 socket unit 通过 Sockets=、LISTEN_FDS 与 LISTEN_FDNAMES 交给服务进程的关系示意图,不是截图或运行证据。

把启动和维护动作固定下来

单元文件写好后,常用动作可以保持很小:

# 让 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=,再读取名称信息更可靠。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>