C++の応用 - Systemd
概要
Systemdは、Linuxにおける代表的なシステムおよびサービスマネージャであり、起動、停止、依存関係管理、ログ収集、リソース制御を一元的に扱う。
C++からSystemdを利用する場合、最も重要な入口はlibsystemdである。
libsystemdはC APIを提供するライブラリであり、純粋なC++アプリケーションから直接利用できる。
代表的なヘッダには、sd-bus.h、sd-event.h、sd-journal.h、sd-daemon.hがある。
これらを組み合わせることで、ユニット操作、イベントループ統合、journaldへの構造化ログ出力、Type=notifyサービスとの連携を実装できる。
Systemd Managerと通信する場合、通常はD-Bus経由で org.freedesktop.systemd1 にアクセスする。
このとき、サービスの開始や停止は単純なシェルコマンド実行ではなく、StartUnit や StopUnit等のメソッド呼び出しとして扱う。
特に重要なのは、StartUnit の戻り値が「サービスが完全に起動した」という意味ではなく、ジョブが作成されたことを示す点である。
実際の状態確認には、ジョブの監視や ActiveState プロパティの確認が必要になる。
また、Systemdはシステムユニットだけでなく、ユーザごとのユーザユニットも管理できる。
一般ユーザのサービスを制御する場合は sd_bus_open_user()、システム全体のユニットを制御する場合は sd_bus_open_system() を使い分ける。
近年のlibsystemdでは、sd-json や sd-varlink 等の公開APIが追加され、イベントループや通知機能も強化されている。
一方で、実務で最も利用頻度が高いのは、依然として sd-bus、sd-event、sd-journal、sd_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}
)
主要ヘッダと用途
| ヘッダ | 主な用途 |
|---|---|
<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群を整理する。
| 分類 | 主な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関係を無視する。
- 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.Get で ActiveState を参照する。
イベント駆動で監視する場合は、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=notify と NotifyAccess= が適切に設定されていなければ期待通りに動作しない。
通常は NotifyAccess=main から検討し、必要がある場合のみ all を使用する。
Transient Unit
Transient Unitは、ディスク上の恒久的なunitファイルを作成せずに、実行時に動的なユニットを生成する仕組みである。
これは、一時的なscopeやserviceを作成したい場合に有効である。
主な用途
- 一時的なワーカープロセスを専用scopeで実行する。
- リソース制限付きのserviceを動的に生成する。
- コンテナやジョブ実行器から専用cgroupを切る。
代表的なプロパティ
| プロパティ | 用途 |
|---|---|
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 の成功は、開始要求が受理されたことを示すだけである。
ジョブ終了後に失敗している可能性があるため、JobRemoved や ActiveState を確認する。
権限エラーになる
一般ユーザでシステムユニットを操作している可能性がある。
対象がユーザユニットであれば 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名> が有効である。
関連情報
- libsystemd
- sd-bus
- sd-event
- sd-journal
- sd_notify
- org.freedesktop.systemd1
- PORTABILITY_AND_STABILITY
- systemd GitHub
- C++の応用 - PolKit