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

Linux systemd socket activation 的监听交接

来源:17golang原创

时间:2026-10-17 08:21:16 442浏览 收藏

systemd socket activation 的关键不是替程序多启动一个进程,而是让 .socket 单元持有监听套接字,再把对应文件描述符交给服务。使用 Accept=no 时,程序接收监听 fd,并自行 accept;它不应再次对同一地址 bind。使用 Accept=yes 时交付的是连接套接字,程序模型随之改变。

systemd 官方地址:https://systemd.io/。以下使用用户级单元、IPv4 回环地址和一个固定文本响应程序,只解释 .socket、.service 与 fd 继承,不实现公网服务或 HTTP 服务器。

先约定这个小项目做什么

目标是让本机客户端连接 127.0.0.1:18081 后收到一行 socket-note ready。端口由 socket-note.socket 管理,业务程序 socket-note-worker 只负责接受连接并回复。示例要求 Linux 上存在可用的 systemd 用户管理器、C 编译器、pkg-config 和 libsystemd 开发文件;开发包名称随发行版而异,应从发行版官方仓库安装。

用户级单元保存在 ~/.config/systemd/user/,程序放在 ~/.local/libexec/。这些是读者操作位置,不是已经部署好的产物。本文没有执行构建或启停命令,后文状态均是读者应观察的判断条件,不是实测记录。

由 .socket 描述地址,由 .service 描述程序

将下面内容保存为 ~/.config/systemd/user/socket-note.socket。ListenStream 负责地址,Accept=no 明确要求服务处理监听套接字。同名的 socket-note.service 是默认匹配对象,因此这里无需另写 Service=。

[Unit]
Description=Local socket note listener

[Socket]
# 仅监听本机 IPv4,先确认这个示例端口没有被其他服务使用
ListenStream=127.0.0.1:18081
# 交付监听套接字,由业务程序接受连接
Accept=no

[Install]
WantedBy=sockets.target

再保存 ~/.config/systemd/user/socket-note.service:

[Unit]
Description=Local socket note worker

[Service]
# 程序保持前台运行,不派生后台守护进程
Type=simple
# %h 由 systemd 展开为当前用户的主目录
ExecStart=%h/.local/libexec/socket-note-worker

这个 service 不设置开机目标,也不需要单独 enable。启用的是 socket;连接到来时由它激活服务。Type=simple 不是业务就绪通知协议,不应把服务已被启动当作业务一定能响应。单元名称与 Accept 的关系见 systemd.socket 官方手册源文件,服务类型见 systemd.service 手册。

socket单元、监听地址、service单元与业务程序的静态配置关系
结构说明图:监听配置与进程配置分别归属两个单元;连线说明绑定和关联,不是启动时间轴。

业务代码接管 fd,而不是重新创建监听端口

将以下完整代码保存为 socket-note-worker.c。sd_listen_fds(1) 读取激活协议并清理相关环境变量,返回收到的 fd 数量;fd 从 SD_LISTEN_FDS_START,即 3 开始。这里只允许一个 IPv4 流式监听套接字,数量或类型不符就退出,不静默退回自行监听。

#include 
#include 
#include 
#include 
#include 
#include 
#include 

int main(void) {
    // 清理激活环境;这个示例只接受一个继承的监听 fd
    int count = sd_listen_fds(1);
    if (count != 1) {
        fprintf(stderr, "expected one listener, got %d\n", count);
        return 1;
    }
    int listener = SD_LISTEN_FDS_START;
    // 最后一项为 0:不限制具体端口,但限定 IPv4、流式和监听状态
    if (sd_is_socket_inet(listener, AF_INET, SOCK_STREAM, 1, 0) 

程序没有调用 socket、bind 或 listen,因为监听对象已经由管理器准备好。接收方持有自己的 fd 引用,管理器仍可保留同一套接字的引用;“交接”不是把端口从管理器彻底搬走。对监听套接字做 shutdown 可能影响管理器持有的同一对象,因此不能用它代替关闭当前进程自己的 fd。

这是串行、短响应的教学代码,没有并发模型、完整写入超时或生产级连接治理;不要因为它只绑定回环地址,就把本机客户端一律视为可信。需要长期部署时,应另行设计资源限制和异常处理。

激活协议、监听fd以及业务连接fd的静态关系
结构说明图:sd_listen_fds 识别继承描述符,listener 与 client 是不同对象;accept 关联两者,但图中不表达执行顺序。

fd 数量、起始编号与共享监听对象规则见 sd_listen_fds 官方文档。如果以后增加多个 socket,不应继续假定“第一个必定是业务端口”,应按类型或名称识别,并重新定义数量约束。

在用户管理器中启用,并观察按需激活

读者准备好源码和两个单元后,可以在项目目录依次执行:

# 创建当前用户的单元和程序目录;单元内容需提前保存到对应位置
mkdir -p "$HOME/.config/systemd/user" "$HOME/.local/libexec"
# 使用 libsystemd 的编译链接参数,生成前台工作程序
cc -Wall -Wextra socket-note-worker.c -o socket-note-worker $(pkg-config --cflags --libs libsystemd)
# 只安装这个示例程序,不替换任何系统服务
install -m 755 socket-note-worker "$HOME/.local/libexec/socket-note-worker"
# 加载两个新单元,只启用监听入口
systemctl --user daemon-reload
systemctl --user enable --now socket-note.socket
# 查看监听入口与业务进程各自的状态
systemctl --user status socket-note.socket socket-note.service
# 用本机客户端发起连接;这是原始 TCP 文本,不是 HTTP
nc 127.0.0.1 18081

应先看到 socket 处于监听状态;service 在首次连接前可能尚未启动,若已经被先前连接触发,也可能早已运行。客户端应收到代码定义的一行文本;如果没有,应根据日志确定问题,不能只凭端口可连接就宣布应用正常。

# 仅读取本示例用户级单元的近期日志,排查路径、链接或 fd 错误
journalctl --user -u socket-note.socket -u socket-note.service -n 40 --no-pager

用户管理器是否在退出登录后继续运行,取决于系统对该用户会话的管理策略,不能把这个实验等同于系统级常驻部署。所有命令都必须带 --user,避免把同名用户级与系统级单元混为一谈。

常见异常对应的是哪一层

现象优先确认不应直接采取的动作
socket 无法启动监听地址、端口占用和单元日志杀掉不属于本项目的占用进程
程序报告没有继承 fd是否由 socket 激活、是否直接执行了二进制手工伪造 LISTEN_PID 等环境变量
收到 fd 却类型不符Accept 模式与 socket 类型是否匹配对任意 fd 盲目 accept
程序再次 bind 时地址被占用是否遗漏了继承分支为了重复 bind 随意修改端口复用设置

不使用 shell 包装器启动示例,避免包装器未 exec 而改变实际接收进程,使 LISTEN_PID 与进程不匹配。直接执行二进制时返回非预期 fd 数量是这个示例的设计行为,不是要求添加一个隐藏监听分支。

结束实验时,同时考虑 socket 和 service

# 停止监听入口和已运行的业务进程,避免只停服务后又被连接激活
systemctl --user stop socket-note.socket socket-note.service
# 取消这个示例的用户级自动启用,不删除任何其他单元
systemctl --user disable socket-note.socket

保留源码和单元便于继续阅读;这里不提供广泛删除目录的清理命令。若只修改 service 后重启进程,socket 仍运行时通常仍是原来的监听对象,这种引用保留不是“旧进程还活着”。它也不保证零丢失:排队容量、应用状态和客户端超时仍会影响请求。

把 Accept 改成 yes,代码可以不变吗?

不可以。yes 模式是按连接实例化服务,通常需要对应模板单元,并处理已连接套接字;本例明确要求监听状态,会拒绝这种交付方式。

socket activation 能让任意旧程序自动兼容吗?

不能。程序必须理解所用 fd 交付协议,或者明确支持另一种约定。只写两个单元,不会自动改造原来坚持自行 bind 的业务代码。

为什么停止 service 后仍然能连接端口?

因为监听生命周期属于 socket 单元。需要彻底结束入口时,应明确停止 socket;需要连业务进程一并结束时,同时停止 service。理解这两个生命周期,比把“端口可连接”当成“业务进程存在”的证据更重要。

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