概要

Systemdは、Linuxにおける代表的なシステムおよびサービスマネージャであり、起動、停止、依存関係管理、ログ収集、リソース制御を一元的に扱う。

C++からSystemdを利用する場合、最も重要な入口はlibsystemdである。
libsystemdはC APIを提供するライブラリであり、純粋なC++アプリケーションから直接利用できる。

代表的なヘッダには、sd-bus.hsd-event.hsd-journal.hsd-daemon.hがある。
これらを組み合わせることで、ユニット操作、イベントループ統合、journaldへの構造化ログ出力、Type=notifyサービスとの連携を実装できる。

Systemd Managerと通信する場合、通常はD-Bus経由で org.freedesktop.systemd1 にアクセスする。
このとき、サービスの開始や停止は単純なシェルコマンド実行ではなく、StartUnitStopUnit等のメソッド呼び出しとして扱う。

特に重要なのは、StartUnit の戻り値が「サービスが完全に起動した」という意味ではなく、ジョブが作成されたことを示す点である。
実際の状態確認には、ジョブの監視や ActiveState プロパティの確認が必要になる。

また、Systemdはシステムユニットだけでなく、ユーザごとのユーザユニットも管理できる。
一般ユーザのサービスを制御する場合は sd_bus_open_user()、システム全体のユニットを制御する場合は sd_bus_open_system() を使い分ける。

近年のlibsystemdでは、sd-jsonsd-varlink 等の公開APIが追加され、イベントループや通知機能も強化されている。
一方で、実務で最も利用頻度が高いのは、依然として sd-bussd-eventsd-journalsd_notifyである。

C++でlibsystemdを使う場合は、GUIフレームワークに依存せず、std::unique_ptr や 専用デリータを使用してRAIIでリソース管理する構成が扱いやすい。
また、ビルド時は pkg-config --cflags --libs libsystemd を使用するのが最も安全である。


libsystemdの導入

ライセンス

通常のアプリケーションがリンクするlibsystemdは、主としてLGPL 2.1以降の条件で利用できる。
ただし、systemdのソースツリー全体にはGPL系コンポーネントも含まれるため、再配布方針が厳密な環境では公式ライセンス表記も確認すること。

パッケージ管理システムからインストール

# RHEL
sudo dnf install systemd-devel

# SUSE
sudo zypper install systemd-devel


ソースコードからインストール

最新のupstream機能を利用する場合は、ソースコードからビルドする。
ただし、依存ライブラリが多く、ディストリビューションのBuildRequires相当のパッケージが必要になるため、通常は配布パッケージの利用を優先する。

Systemdのビルドに必要なライブラリをインストールする。

# RHEL
sudo dnf install -y epel-release
sudo dnf config-manager --set-enabled crb

sudo dnf install meson ninja-build python3-jinja2 glib2-devel dbus-devel p11-kit-devel libarchive-devel pcre2-devel        \
                 libcurl-devel libcap-devel libmount-devel libfdisk-devel libblkid-devel elfutils-devel libpwquality-devel \
                 kmod-devel libbpf-devel zlib-ng-compat-devel lz4-devel libzstd-devel xz-devel bzip2-devel iptables-devel  \
                 pam-devel gnutls-devel openssl-devel cryptsetup-devel libgcrypt-devel libgpg-error-devel \
                 libmicrohttpd-devel libxkbcommon-devel libfido2-devel tpm2-tss-devel libseccomp-devel    \
                 libacl-devel audit-libs-devel libselinux-devel

# SUSE
sudo zypper install meson ninja python3-Jinja2 glib2-devel dbus-1-devel p11-kit-devel libarchive-devel pcre2-devel         \
                    libcurl-devel libcap-devel libmount-devel libfdisk-devel libblkid-devel libdw-devel libpwquality-devel \
                    passwdqc-devel libkmod-devel libbpf-devel zlib-devel liblz4-devel libzstd-devel xz-devel libbz2-devel  \
                    pam-devel libgnutls-devel libopenssl-devel libopenssl-1_1-devel libcryptsetup-devel libgcrypt-devel    \
                    libgpg-error-devel qrencode-devel libiptc-devel libidn2-devel libmicrohttpd-devel            \
                    libxkbcommon-devel libfido2-devel tpm2-0-tss-devel libseccomp-devel libacl-devel audit-devel \
                    libapparmor-devel  # AppArmorを使用する場合
                    libselinux-devel   # SELinuxを使用する場合
                    xen-devel          # Xenを使用する場合


次に、SystemdのGithubにアクセスして、ソースコードをダウンロードする。
ダウンロードしたファイルを解凍する。

tar xf systemd-<バージョン>.tar.gz
cd systemd-<バージョン>


または、git clone コマンドを実行して、ソースコードをダウンロードする。

git clone https://github.com/systemd/systemd.git
cd systemd


Systemdをビルドおよびインストールする。

meson setup build -Dmode=release --prefix=<Systemdのインストールディレクトリ>
meson compile -C build
sudo meson install -C build


libsystemdの利用だけが目的であれば、Systemd本体をソースから導入するよりも、パッケージ管理システムの開発パッケージ用いる方が保守しやすい。

CMakeファイルの設定

C++からlibsystemdを利用する場合、pkg-config 経由で検出する構成が最も安全である。

 cmake_minimum_required(VERSION 3.21)
 project(systemd_cpp_example LANGUAGES CXX)
 
 set(CMAKE_CXX_STANDARD 17)
 set(CMAKE_CXX_STANDARD_REQUIRED ON)
 
 find_package(PkgConfig REQUIRED)
 pkg_check_modules(SYSTEMD REQUIRED libsystemd)
 
 add_executable(systemd_cpp_example
    main.cpp
 )
 
 target_include_directories(systemd_cpp_example PRIVATE
    ${SYSTEMD_INCLUDE_DIRS}
 )
 
 target_link_libraries(systemd_cpp_example PRIVATE
    ${SYSTEMD_LIBRARIES}
 )
 
 target_compile_options(systemd_cpp_example PRIVATE
    ${SYSTEMD_CFLAGS_OTHER}
 )


主要ヘッダと用途

libsystemdの主要ヘッダ
ヘッダ 主な用途
<systemd/sd-bus.h> Systemd Managerや他のD-Busサービスとの通信
<systemd/sd-event.h> epollベースのイベントループ、タイマ、シグナル、I/O監視
<systemd/sd-journal.h> journaldへのログ出力、ジャーナルの読み出し
<systemd/sd-daemon.h> sd_notify()、ウォッチドッグ、ソケットアクティベーション支援
<systemd/sd-id128.h> 128ビットIDの生成と管理
<systemd/sd-login.h> ログインセッションやseat情報の取得



libsystemdの主要なAPI

C++からSystemdを扱う場合、最初に理解しておくべきAPI群を整理する。

純粋なC++からよく使うlibsystemd API
分類 主なAPI 用途
Systemd Manager操作 sd_bus_open_system()
sd_bus_call_method()
sd_bus_add_match()
ユニットの起動・停止・プロパティ監視
イベントループ sd_event_default()
sd_event_run()
sd_bus_attach_event()
非同期イベント処理とD-Bus統合
journald出力 sd_journal_print()
sd_journal_send()
通常ログと構造化ログの送信
サービス通知 sd_notify()
sd_watchdog_enabled()
Type=notifyサービスの状態通知
ジャーナル読み出し sd_journal_open()
sd_journal_next()
sd_journal_get_data()
ログの追跡と解析



Systemd Managerとの通信

基本情報

Systemd Managerは、D-Bus上で以下のサービスを提供する。

  • サービス名
    org.freedesktop.systemd1
  • オブジェクトパス
    /org/freedesktop/systemd1
  • 主要インターフェース
    org.freedesktop.systemd1.Manager


ユニットを起動する代表的なメソッドは、StartUnit である。
停止は StopUnit、再起動は RestartUnit、リロードは ReloadUnit を使用する。

ジョブモード

Systemd Managerのユニット操作では、しばしば「ジョブモード」を同時に指定する。

  • replace
    既存の競合ジョブを置き換えて実行する。通常はこの値を使う。
  • fail
    競合ジョブが存在する場合は失敗する。
  • isolate
    対象ユニットとその依存を残し、他を停止する。
  • ignore-dependencies
    依存関係を無視する。
  • ignore-requirements
    requirement関係を無視する。


ignore-dependencies および ignore-requirements は、通常のアプリケーションから安易に使うべきではない。

C++によるサービス起動例

以下の例では、システムバスへ接続し、sshd.serviceに対して StartUnit を呼び出す。

この例が返す ジョブオブジェクトパス は、開始要求が受理されたことを示すものであり、サービスが完全に起動済みであることを意味しない。

 #include <systemd/sd-bus.h>
 
 #include <cstring>
 #include <iostream>
 #include <memory>
 #include <stdexcept>
 #include <string>
 
 namespace {
    struct SdBusDeleter {
       void operator()(sd_bus* bus) const {
          if (bus) {
             sd_bus_flush_close_unref(bus);
          }
       }
    };
 
    struct SdBusMessageDeleter {
       void operator()(sd_bus_message* message) const {
          if (message) {
             sd_bus_message_unref(message);
          }
       }
    };
 
    class SdBusErrorGuard {
    public:
       SdBusErrorGuard() : error_(SD_BUS_ERROR_NULL) {}
       ~SdBusErrorGuard() { sd_bus_error_free(&error_); }
 
       sd_bus_error* get() { return &error_; }
       const char* message() const { return error_.message ? error_.message : ""; }
 
    private:
       sd_bus_error error_;
    };
 
    using BusPtr = std::unique_ptr<sd_bus, SdBusDeleter>;
    using MessagePtr = std::unique_ptr<sd_bus_message, SdBusMessageDeleter>;
 
    BusPtr open_system_bus()
    {
       sd_bus* raw_bus = nullptr;
       const int ret = sd_bus_open_system(&raw_bus);
       if (ret < 0) {
          throw std::runtime_error("システムバスのオープンに失敗しました: " + std::string(std::strerror(-ret)));
       }
 
       return BusPtr(raw_bus);
    }
 
    std::string start_unit(sd_bus* bus, const std::string& unit_name, const std::string& mode)
    {
       SdBusErrorGuard error;
       sd_bus_message* raw_reply = nullptr;
 
       const int ret = sd_bus_call_method(bus,
                                          "org.freedesktop.systemd1",
                                          "/org/freedesktop/systemd1",
                                          "org.freedesktop.systemd1.Manager",
                                          "StartUnit",
                                          error.get(),
                                          &raw_reply,
                                          "ss",
                                          unit_name.c_str(),
                                          mode.c_str());
       if (ret < 0) {
          throw std::runtime_error("StartUnitの呼び出しに失敗しました: " + std::string(error.message()));
       }
 
       MessagePtr reply(raw_reply);
 
       const char* job_path = nullptr;
       const int read_ret = sd_bus_message_read(reply.get(), "o", &job_path);
       if (read_ret < 0) {
          throw std::runtime_error("ジョブパスの読み出しに失敗しました: " + std::string(std::strerror(-read_ret)));
       }
 
       return std::string(job_path);
    }
 }
 
 int main()
 {
    try {
       BusPtr bus = open_system_bus();
       const std::string job_path = start_unit(bus.get(), "sshd.service", "replace");
 
       std::cout << "StartUnit request accepted" << std::endl;
       std::cout << "job path: " << job_path << std::endl;
       std::cout << "この時点ではジョブが作成された段階であり、サービスの完全起動確認は別途必要です" << std::endl;
       return 0;
    }
    catch (const std::exception& ex) {
       std::cerr << ex.what() << std::endl;
       return 1;
    }
 }


この後、実際の起動完了まで追跡したい場合は、JobRemoved シグナルを監視するか、対象ユニットの ActiveState プロパティを問い合わせる。

ActiveStateの確認

ジョブ作成後に状態を確認する場合は、対象ユニットのD-Busオブジェクトパスを取得し、org.freedesktop.DBus.Properties.GetActiveState を参照する。

イベント駆動で監視する場合は、sd_bus_add_match()PropertiesChanged または JobRemoved を購読するとよい。


sd-eventとの統合

sd-event は、epollベースのイベントループであり、タイマ、シグナル、I/O、子プロセス終了、メモリ圧力等を統合的に扱える。
単純な同期呼び出しだけであれば必須ではないが、非同期D-Bus呼び出しや長寿命デーモンでは非常に有用である。

基本的な使用方法

  • sd_event_default()
    既定のイベントループを作成する。
  • sd_bus_attach_event()
    D-Bus接続をイベントループへ接続する。
  • sd_event_run() / sd_event_loop()
    イベント処理を実行する。


利用例

以下のような場面で sd-event が有効である。

  • 非同期の sd_bus_call_method_async() を使う場合
  • タイマ駆動で定期的にサービス状態を確認する場合
  • シグナル受信とD-Busイベントを1つのループでまとめたい場合
  • sd_event_set_watchdog() でウォッチドッグ通知を自動化したい場合


注意点

非同期処理を書き始めたら、イベントループを実際に駆動しなければコールバックは返ってこない。

また、長時間ブロックする処理をイベントハンドラ内で実行すると、ウォッチドッグ通知やD-Bus応答が遅延する。


journaldとの連携

ログの書き込み

Systemd環境では、単なる標準出力だけでなく、journaldに対して構造化ログを送ると、検索性と保守性が大きく向上する。
主なAPIは、sd_journal_print()sd_journal_send() である。

  • sd_journal_print()
    printf風の簡易ログ出力
  • sd_journal_send()
    MESSAGE=...PRIORITY=... 等のフィールドを付与した構造化ログ
  • sd_journal_sendv()
    iovec配列による柔軟な送信


構造化ログの例

 #include <systemd/sd-journal.h>
 #include <syslog.h>
 
 void write_systemd_log(const char* unit_name, const char* detail)
 {
    sd_journal_send("MESSAGE=Systemd unit operation finished",
                    "PRIORITY=%i", LOG_INFO,
                    "UNIT=%s", unit_name,
                    "DETAIL=%s", detail,
                    nullptr);
 }


構造化ログを使うと、journalctl 側でフィールド検索しやすくなる。

ジャーナルの読み出し

ログを追跡する場合は、sd_journal_open()sd_journal_next()sd_journal_get_data()を使用する。

ユニット名やプライオリティでフィルタを掛ける場合は、sd_journal_add_match() が便利である。


Type=notifyサービスとの連携

Systemd管理下のデーモンをC++で実装する場合、Type=notify または Type=notify-reload を使用すると、起動完了やリロード状態をSystemdへ正確に伝えられる。

主な通知内容

  • READY=1
    サービスの初期化完了
  • STATUS=...
    現在の状態メッセージ
  • RELOADING=1
    リロード開始
  • STOPPING=1
    終了処理の開始
  • WATCHDOG=1
    ウォッチドッグのキープアライブ


最小構成の例

 #include <systemd/sd-daemon.h>
 
 int notify_ready()
 {
    return sd_notify(0, "READY=1\nSTATUS=Initialization completed");
 }


ウォッチドッグ

ウォッチドッグが有効な場合は、sd_watchdog_enabled() で間隔を取得し、その半分程度の周期で WATCHDOG=1 を送信する構成が一般的である。

sd-event を使用している場合は、sd_event_set_watchdog() を使って自動化できる。

ユニットファイル側の注意

アプリケーションが sd_notify() を送っても、ユニットファイルで Type=notifyNotifyAccess= が適切に設定されていなければ期待通りに動作しない。

通常は NotifyAccess=main から検討し、必要がある場合のみ all を使用する。


Transient Unit

Transient Unitは、ディスク上の恒久的なunitファイルを作成せずに、実行時に動的なユニットを生成する仕組みである。

これは、一時的なscopeやserviceを作成したい場合に有効である。

主な用途

  • 一時的なワーカープロセスを専用scopeで実行する。
  • リソース制限付きのserviceを動的に生成する。
  • コンテナやジョブ実行器から専用cgroupを切る。


代表的なプロパティ

StartTransientUnitでよく使うプロパティ
プロパティ 用途
Description 一時ユニットの説明
Slice 所属するsliceの指定
PIDs scopeへ初期PIDを渡す
Delegate cgroupの委譲を有効化する
RemainAfterExit service終了後も状態を保持する


注意点

StartTransientUnit のD-Busシグネチャは複雑であり、a(sv) 形式のプロパティ配列を正確に構築する必要がある。

また、恒久unitファイルで使用可能な全ディレクティブがTransient Unitで使えるわけではない。

最新の対応状況は、TRANSIENT-SETTINGS.mdを参照すること。


権限とPolKit

Systemd Managerに対する読み取り操作は比較的容易だが、ユニットの開始、停止、再起動、ユニットファイルの有効化等は権限が必要になる。

システムバスでの管理操作では、PolKit認証や適切なケーパビリティが関わる。

代表的な権限

  • org.freedesktop.systemd1.manage-units
    ユニットの開始、停止、再起動、プロパティ変更
  • org.freedesktop.systemd1.manage-unit-files
    ユニットファイルの有効化、無効化
  • org.freedesktop.systemd1.set-environment
    Systemd Managerの環境変数変更


実務上の注意

  • 一般ユーザでシステムユニットを操作すると、PolKit認証を求められることがある。
  • 同じユーザのユーザユニットを扱うだけであれば、sd_bus_open_user() でユーザマネージャへ接続する方が簡潔である。
  • デスクトップ環境が存在しないサーバでは、PolKit対話認証ダイアログを前提にしない設計が必要である。


詳しい認可制御は、C++の応用_-_PolKitのページを参照すること。


トラブルシューティング

StartUnitが成功したのにサービスが動いていない

StartUnit の成功は、開始要求が受理されたことを示すだけである。
ジョブ終了後に失敗している可能性があるため、JobRemovedActiveState を確認する。

権限エラーになる

一般ユーザでシステムユニットを操作している可能性がある。
対象がユーザユニットであれば sd_bus_open_user() へ切り替え、システムユニットであればPolKit設定や実行権限を見直す。

sd_notify()が効かない

ユニットファイル側で Type=notify または Type=notify-reload が設定されているか確認する。

さらに、NotifyAccess= が通知元プロセスを許可している必要がある。

非同期コールバックが返ってこない

sd_bus_call_method_async() を使う場合、イベントループを実際に回していなければコールバックは実行されない。

sd_bus_attach_event() または sd_bus_process() / sd_bus_wait() の駆動を確認する。

ジャーナルへログが出ない

journaldが利用可能な環境であること、また、ログ送信後すぐにプロセスを終了していないことを確認する。

詳細な追跡には、journalctl -xeu <unit名> が有効である。


関連情報