强曰为道
与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

D-Bus 完整教程 / 10 - systemd 集成

第 10 章:systemd 集成


10.1 systemd 与 D-Bus 的关系

systemd 是 Linux 系统上最核心的初始化和服务管理系统,它与 D-Bus 有深度集成:

┌──────────────────────────────────────────────────┐
│                    systemd                         │
│                                                    │
│  ┌──────────┐  ┌──────────┐  ┌──────────────┐    │
│  │ PID 1    │  │ systemd  │  │ systemd      │    │
│  │ (init)   │  │ -logind  │  │ -resolved    │    │
│  └────┬─────┘  └────┬─────┘  └──────┬───────┘    │
│       │              │               │             │
│  ═════╪══════════════╪═══════════════╪══════════   │
│       │     System Bus (D-Bus)       │             │
│  ═════╪══════════════╪═══════════════╪══════════   │
│       │              │               │             │
│  ┌────┴─────┐  ┌────┴─────┐  ┌──────┴───────┐    │
│  │ sd-bus   │  │ busctl   │  │ D-Bus 激活   │    │
│  └──────────┘  └──────────┘  └──────────────┘    │
└──────────────────────────────────────────────────┘
集成点说明
dbus.servicesystemd 管理的 D-Bus 守护进程
dbus.socketSystem Bus 的套接字激活
D-Bus Activation通过 .service 文件激活服务
BusName=单元文件中声明拥有的总线名称
sd-bussystemd 内置的轻量级 D-Bus 客户端库
systemctl --user用户服务管理(Session Bus)
journal通过 D-Bus 访问系统日志

10.2 D-Bus 服务激活(Service Activation)

D-Bus 激活允许守护进程在首次被调用时自动启动,而不是开机就常驻。

10.2.1 激活流程

客户端调用 org.example.Service
       │
       ↓
dbus-daemon 检查服务是否在线
       │
  在线?├─ 是 → 直接路由
       │
       ↓ 否
查找 /usr/share/dbus-1/system-services/org.example.Service.service
       │
       ↓
根据 Exec= 行启动服务
       │
       ↓
服务注册 BusName
       │
       ↓
转发原始调用

10.2.2 D-Bus 激活文件

传统 D-Bus 激活文件位于:

目录总线类型
/usr/share/dbus-1/system-services/System Bus
/usr/share/dbus-1/services/Session Bus
/etc/dbus-1/system-services/System Bus(管理员自定义)
~/.local/share/dbus-1/services/Session Bus(用户自定义)

激活文件格式

# /usr/share/dbus-1/system-services/org.example.MyService.service
[D-BUS Service]
Name=org.example.MyService
Exec=/usr/bin/my-service
User=root
SystemdService=my-service.service
说明
Name要激活的总线名称
Exec启动命令(传统方式)
SystemdService关联的 systemd 服务(推荐方式)
User运行用户(仅 System Bus)
AssumedAppArmorLabelAppArmor 标签

10.2.3 systemd 服务激活(推荐)

现代系统更推荐使用 systemd 进行激活:

# /etc/systemd/system/my-service.service
[Unit]
Description=My D-Bus Service
After=network.target

[Service]
Type=dbus
BusName=org.example.MyService
ExecStart=/usr/bin/my-service
Restart=on-failure
RestartSec=5

[Install]
WantedBy=multi-user.target
# /usr/share/dbus-1/system-services/org.example.MyService.service
[D-BUS Service]
Name=org.example.MyService
SystemdService=my-service.service

对应配置文件:

<!-- /usr/share/dbus-1/system.d/org.example.MyService.conf -->
<!DOCTYPE busconfig PUBLIC "-//freedesktop//DTD D-BUS Bus Configuration 1.0//EN"
 "http://www.freedesktop.org/standards/dbus/1.0/busconfig.dtd">
<busconfig>
  <policy user="root">
    <allow own="org.example.MyService"/>
  </policy>
  <policy context="default">
    <allow send_destination="org.example.MyService"/>
  </policy>
</busconfig>
# 启用并启动服务
sudo systemctl daemon-reload
sudo systemctl enable my-service
sudo systemctl start my-service

# 验证状态
systemctl status my-service

# 测试 D-Bus 激活
# 先停止服务
sudo systemctl stop my-service

# 通过 D-Bus 调用会自动启动服务
busctl call org.example.MyService /org/example org.example.Interface Ping

10.3 BusName= 单元配置

systemd 单元文件中的 BusName= 指令用于声明服务拥有的总线名称:

[Service]
Type=dbus
BusName=org.example.MyService
ExecStart=/usr/bin/my-service

# 可选:设置总线名称获取超时
TimeoutSec=30
Type=dbus 行为说明
启动条件ExecStart 进程必须在超时前获取 BusName
停止行为进程退出后 systemd 会释放 BusName
重启策略Restart=on-failure 适用于获取名称失败

验证 BusName:

# 检查 BusName 是否已被获取
busctl list | grep org.example.MyService

# systemd 属性检查
systemctl show my-service -p BusName

10.4 用户服务(User Services)

systemd 支持用户级服务,使用 Session Bus:

10.4.1 用户服务单元

# ~/.config/systemd/user/my-app.service
[Unit]
Description=My User D-Bus Service

[Service]
Type=dbus
BusName=org.example.MyApp
ExecStart=/usr/local/bin/my-app
Restart=on-failure

[Install]
WantedBy=default.target

10.4.2 管理用户服务

# 启用用户服务
systemctl --user enable my-app

# 启动
systemctl --user start my-app

# 查看状态
systemctl --user status my-app

# 查看日志
journalctl --user -u my-app

# 用户服务的 Session Bus 激活文件
# ~/.local/share/dbus-1/services/org.example.MyApp.service

10.4.3 用户 D-Bus 激活文件

# ~/.local/share/dbus-1/services/org.example.MyApp.service
[D-BUS Service]
Name=org.example.MyApp
SystemdService=my-app.service

10.5 套接字激活(Socket Activation)

套接字激活是 systemd 最强大的功能之一,允许在服务启动前就接受连接。

10.5.1 传统 D-Bus 套接字激活

# /etc/systemd/system/my-db-service.socket
[Unit]
Description=My D-Bus Service Socket

[Socket]
ListenStream=/run/my-service.sock

[Install]
WantedBy=sockets.target
# /etc/systemd/system/my-db-service.service
[Unit]
Description=My D-Bus Service
Requires=my-db-service.socket

[Service]
Type=dbus
BusName=org.example.MyService
ExecStart=/usr/bin/my-service

10.5.2 D-Bus Broker 套接字激活

现代系统使用 dbus-broker 进行更高效的激活:

# /usr/share/dbus-1/system-services/org.example.MyService.service
[D-BUS Service]
Name=org.example.MyService
SystemdService=my-service.service

dbus-broker 使用 systemd 的套接字传递机制,提供零拷贝消息传递和更好的性能。

10.5.3 sd-daemon 套接字激活

#include <systemd/sd-daemon.h>
#include <stdio.h>

int main(void) {
    int n = sd_listen_fds(0);
    
    if (n > 0) {
        printf("从 systemd 接收了 %d 个套接字\n", n);
        int fd = SD_LISTEN_FDS_START;  // 第一个套接字的 fd
        
        // 使用 fd 进行通信
        // ...
    } else {
        printf("没有从 systemd 接收套接字\n");
    }
    
    return 0;
}

10.6 sd-bus — systemd 内置 D-Bus 库

sd-bus 是 systemd 提供的轻量级 D-Bus 客户端库,适合 systemd 集成场景。

10.6.1 sd-bus vs GDBus 对比

特性sd-busGDBus
依赖libsystemdlibgio (GLib)
二进制大小~50KB~2MB+
主循环支持 sd-event、自定义GLib MainLoop
代码生成不支持gdbus-codegen
性能
类型安全手动解析GVariant
适用场景systemd 服务、嵌入式GNOME 桌面应用

10.6.2 sd-bus 客户端示例

#include <systemd/sd-bus.h>
#include <stdio.h>

int main(void) {
    sd_bus *bus = NULL;
    sd_bus_error error = SD_BUS_ERROR_NULL;
    sd_bus_message *reply = NULL;
    int r;
    
    /* 连接到 System Bus */
    r = sd_bus_open_system(&bus);
    if (r < 0) {
        fprintf(stderr, "连接 System Bus 失败: %s\n", strerror(-r));
        return 1;
    }
    
    /* 调用方法 */
    r = sd_bus_call_method(
        bus,
        "org.freedesktop.systemd1",       /* dest */
        "/org/freedesktop/systemd1",      /* path */
        "org.freedesktop.systemd1.Manager", /* interface */
        "ListUnits",                       /* method */
        &error,
        &reply,
        ""                                 /* 签名 (无参数) */
    );
    
    if (r < 0) {
        fprintf(stderr, "调用失败: %s\n", error.message);
        sd_bus_error_free(&error);
        return 1;
    }
    
    /* 解析回复 */
    const char *name, *description, *load_state, *active_state;
    uint32_t pid;
    
    r = sd_bus_message_enter_container(reply, SD_BUS_TYPE_ARRAY, "(ssssssouso)");
    if (r >= 0) {
        printf("systemd 单元列表:\n");
        while ((r = sd_bus_message_read(reply, "(ssssssouso)",
                &name, &description, &load_state, &active_state,
                NULL, NULL, NULL, &pid, NULL, NULL)) > 0) {
            printf("  %-40s %-15s %s\n", name, active_state, description);
        }
        sd_bus_message_exit_container(reply);
    }
    
    sd_bus_message_unref(reply);
    sd_bus_unref(bus);
    return 0;
}

编译:

gcc -o sd-bus-example sd-bus-example.c -lsystemd

10.6.3 sd-bus 读取属性

/* 读取 systemd 的 Version 属性 */
sd_bus_message *reply;
sd_bus_error error = SD_BUS_ERROR_NULL;

r = sd_bus_call_method(
    bus,
    "org.freedesktop.systemd1",
    "/org/freedesktop/systemd1",
    "org.freedesktop.DBus.Properties",
    "Get",
    &error,
    &reply,
    "ss",
    "org.freedesktop.systemd1.Manager",
    "Version"
);

if (r >= 0) {
    const char *version;
    sd_bus_message_enter_container(reply, SD_BUS_TYPE_VARIANT, "s");
    sd_bus_message_read(reply, "s", &version);
    printf("systemd 版本: %s\n", version);
    sd_bus_message_exit_container(reply);
    sd_bus_message_unref(reply);
}

10.6.4 sd-bus 监听信号

#include <systemd/sd-bus.h>
#include <systemd/sd-event.h>

static int on_unit_new(sd_bus_message *m, void *userdata, sd_bus_error *ret_error) {
    const char *id, *path;
    sd_bus_message_read(m, "so", &id, &path);
    printf("新单元: %s (%s)\n", id, path);
    return 0;
}

int main(void) {
    sd_bus *bus = NULL;
    sd_event *event = NULL;
    
    sd_bus_open_system(&bus);
    sd_event_default(&event);
    
    sd_bus_attach_event(bus, event, 0);
    
    /* 订阅 UnitNew 信号 */
    sd_bus_match_signal(
        bus,
        NULL,
        "org.freedesktop.systemd1",
        "/org/freedesktop/systemd1",
        "org.freedesktop.systemd1.Manager",
        "UnitNew",
        on_unit_new,
        NULL
    );
    
    /* 运行事件循环 */
    sd_event_run(event, (uint64_t)-1);
    
    sd_bus_unref(bus);
    sd_event_unref(event);
    return 0;
}

编译:

gcc -o sd-bus-signals sd-bus-signals.c -lsystemd

10.7 通过 D-Bus 管理 systemd

10.7.1 查看 systemd 状态

# 查看 systemd 版本
busctl get-property org.freedesktop.systemd1 \
  /org/freedesktop/systemd1 \
  org.freedesktop.systemd1.Manager \
  Version

# 列出所有单元
busctl call org.freedesktop.systemd1 \
  /org/freedesktop/systemd1 \
  org.freedesktop.systemd1.Manager \
  ListUnits

# 获取特定单元状态
busctl call org.freedesktop.systemd1 \
  /org/freedesktop/systemd1 \
  org.freedesktop.systemd1.Manager \
  GetUnit \
  s "dbus.service"

10.7.2 操作 systemd 单元

# 启动服务
busctl call org.freedesktop.systemd1 \
  /org/freedesktop/systemd1 \
  org.freedesktop.systemd1.Manager \
  StartUnit \
  ss "my-service.service" "replace"

# 停止服务
busctl call org.freedesktop.systemd1 \
  /org/freedesktop/systemd1 \
  org.freedesktop.systemd1.Manager \
  StopUnit \
  ss "my-service.service" "replace"

# 重启服务
busctl call org.freedesktop.systemd1 \
  /org/freedesktop/systemd1 \
  org.freedesktop.systemd1.Manager \
  RestartUnit \
  ss "my-service.service" "replace"

# 查看单元属性
busctl call org.freedesktop.systemd1 \
  /org/freedesktop/systemd1 \
  org.freedesktop.systemd1.Manager \
  GetUnit \
  s "dbus.service"

# 获取单元的所有属性
busctl get-property org.freedesktop.systemd1 \
  /org/freedesktop/systemd1/unit/dbus_2eservice \
  org.freedesktop.systemd1.Unit \
  --all

10.7.3 Python 管理 systemd

#!/usr/bin/env python3
"""通过 D-Bus 管理 systemd 服务"""

import dbus

bus = dbus.SystemBus()

# 获取 systemd 代理
systemd = bus.get_object(
    'org.freedesktop.systemd1',
    '/org/freedesktop/systemd1'
)
manager = dbus.Interface(systemd, 'org.freedesktop.systemd1.Manager')

# 列出活跃服务
units = manager.ListUnits()
print("活跃服务:")
for name, desc, load, active, sub, job_type, job_path, job_id, job_obj_path, follows in units:
    if active == 'active' and sub == 'running':
        print(f"  ✅ {name}: {desc}")

# 启动服务
try:
    job_path = manager.StartUnit('nginx.service', 'replace')
    print(f"启动作业: {job_path}")
except dbus.exceptions.DBusException as e:
    print(f"启动失败: {e}")

# 获取服务状态
props = dbus.Interface(systemd, 'org.freedesktop.DBus.Properties')
unit_path = manager.GetUnit('dbus.service')
unit = bus.get_object('org.freedesktop.systemd1', unit_path)
unit_props = dbus.Interface(unit, 'org.freedesktop.DBus.Properties')

active_state = unit_props.Get('org.freedesktop.systemd1.Unit', 'ActiveState')
print(f"\ndbus.service 状态: {active_state}")

10.8 logind 集成

# 查看当前会话
busctl call org.freedesktop.login1 \
  /org/freedesktop/login1 \
  org.freedesktop.login1.Manager \
  ListSessions

# 锁屏
busctl call org.freedesktop.login1 \
  /org/freedesktop/login1 \
  org.freedesktop.login1.Manager \
  LockSession \
  s "auto"

# 查询能力
busctl call org.freedesktop.login1 \
  /org/freedesktop/login1 \
  org.freedesktop.login1.Manager \
  CanPowerOff

# 注销当前会话
busctl call org.freedesktop.login1 \
  /org/freedesktop/login1 \
  org.freedesktop.login1.Manager \
  TerminateSession \
  s "auto"

本章小结

概念说明
D-Bus 激活按需启动服务,减少资源占用
BusName=systemd 单元文件中声明总线名称
Type=dbussystemd 服务类型,等待 BusName 获取
用户服务systemctl --user 管理的服务
套接字激活在服务启动前就接受连接
sd-bussystemd 内置的轻量级 D-Bus 库

扩展阅读