Yoctoで自作アプリケーションをターゲット上で自動起動させたい。組み込みLinuxでは、それはsystemdサービスを作ることを意味する。
だが、Yoctoでsystemdサービスを正しく動かすには、レシピの書き方・ディストロ設定・サービスファイルの3つが噛み合う必要がある。どれか1つが欠けると、サービスは静かに起動しない。
この記事では、Yocto Scarthgap 5.0 LTS(systemd 255)をベースに、systemdサービスの追加方法から、コミュニティで頻出する落とし穴7つまでを解説する。
INIT_MANAGERでsystemdを有効にする
Yoctoのデフォルトのinitシステムはsysvinitだ。systemdを使うには、明示的に切り替える必要がある。
INIT_MANAGER方式(Zeus 3.0以降)
conf/local.conf または conf/distro/mydistro.conf に1行追加するだけでいい。
# conf/local.conf
INIT_MANAGER = "systemd"
この1行で init-manager-systemd.inc が読み込まれ、以下が自動設定される。
# init-manager-systemd.inc の内容(Scarthgapブランチ)
DISTRO_FEATURES:append = " systemd usrmerge"
DISTRO_FEATURES_BACKFILL_CONSIDERED:append = " sysvinit"
VIRTUAL-RUNTIME_init_manager ??= "systemd"
VIRTUAL-RUNTIME_initscripts ??= "systemd-compat-units"
VIRTUAL-RUNTIME_login_manager ??= "shadow-base"
VIRTUAL-RUNTIME_dev_manager ??= "systemd"
ROOT_HOME ?= "/root"
旧方式(Zeus以前)
Zeus以前は、DISTRO_FEATURES や VIRTUAL-RUNTIME_* を手動で local.conf に書く必要があった。今でも動くが、INIT_MANAGER 1行で済む方式が簡潔だ。
設定の確認
# systemdがDISTRO_FEATURESに含まれているか確認
bitbake-getvar DISTRO_FEATURES | grep systemd
systemdサービスレシピの書き方
systemdサービスをYoctoで追加するには、systemd bbclassを継承したレシピを書く。
完全なレシピ例
# meta-mylayer/recipes-apps/myapp/myapp_1.0.bb
SUMMARY = "My custom application"
LICENSE = "MIT"
LIC_FILES_CHKSUM = "file://${COMMON_LICENSE_DIR}/MIT;md5=0835ade698e0bcf8506ecda2f7b4f302"
SRC_URI = "file://myapp.sh \
file://myapp.service"
S = "${WORKDIR}"
inherit systemd
SYSTEMD_SERVICE:${PN} = "myapp.service"
SYSTEMD_AUTO_ENABLE:${PN} = "enable"
do_install() {
install -d ${D}${bindir}
install -m 0755 ${WORKDIR}/myapp.sh ${D}${bindir}/myapp
install -d ${D}${systemd_system_unitdir}
install -m 0644 ${WORKDIR}/myapp.service ${D}${systemd_system_unitdir}/
}
FILES:${PN} += "${systemd_system_unitdir}/myapp.service"
主要変数の解説
| 変数 | 役割 | デフォルト |
|---|---|---|
SYSTEMD_SERVICE:${PN} | 管理するサービスファイル名 | ${PN}.service |
SYSTEMD_AUTO_ENABLE:${PN} | 自動有効化するか | enable |
SYSTEMD_PACKAGES | systemd管理下に置くパッケージ | ${PN} |
inherit systemd が自動的に行うこと:
systemctl enableを実行するpostinstスクリプトの生成multi-user.target.wants/へのシンボリックリンク作成- パッケージ削除時の
prermスクリプト生成
レシピをsystemd対応にガードする
systemdが無効なディストロでビルドエラーを防ぐには、REQUIRED_DISTRO_FEATURES を使う。
REQUIRED_DISTRO_FEATURES = "systemd"
この1行を追加すると、DISTRO_FEATURES に systemd が含まれていない環境ではレシピがスキップされる。他のレシピがこのレシピに依存している場合はビルドエラーになるので、原因の特定が容易になる。
bbappendで既存レシピに追加する場合
既存のレシピにsystemdサービスを追加するなら、bbappendを使う。具体的な書き方はbbappend実践ガイドのパターン3で解説している。ただし、inherit systemd は .bbappend では使えない点に注意。元の .bb レシピが inherit systemd を含んでいる必要がある。
サービスファイルの設計ポイント
サービスファイルの書き方で、起動の安定性が大きく変わる。組み込みLinux向けのポイントを押さえよう。
基本構造
# myapp.service
[Unit]
Description=My Application Service
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
ExecStart=/usr/bin/myapp
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target
[Unit] セクション
After= は起動順序を制御する。ネットワークが必要なサービスでよくある間違いが、After=network.target を使うことだ。
- network.target — ネットワークデバイスが「設定された」ことを示す。IPアドレスが割り当てられたとは限らない
- network-online.target — ネットワークが「使える状態」であることを示す。API呼び出し等が必要なサービスにはこちらを使う
[Service] セクション
Type= はプロセスの起動方式を指定する。
| Type | 用途 |
|---|---|
simple | フォアグラウンドで動くプロセス(デフォルト) |
forking | デーモンとしてforkするプロセス |
oneshot | 一度実行して終了するスクリプト |
組み込みでは Restart=on-failure と RestartSec=5(5秒後にリトライ)を設定しておくと、クラッシュ時に自動復旧する。
パーミッション
サービスファイルは 0644 でインストールする。実行権限(0755)を付けると、systemdが警告を出す場合がある。
# 正しい
install -m 0644 ${WORKDIR}/myapp.service ${D}${systemd_system_unitdir}/
# 間違い(実行権限は不要)
install -m 0755 ${WORKDIR}/myapp.service ${D}${systemd_system_unitdir}/
timerとsocket activationのレシピ
systemdの強みはサービスだけではない。timerで定期実行、socket activationでオンデマンド起動ができる。
timer unit
cronの代わりにsystemd timerを使えば、依存関係やログがsystemdに統合される。
# myapp.timer
[Unit]
Description=Run myapp periodically
[Timer]
OnBootSec=1min
OnUnitActiveSec=1h
Persistent=true
[Install]
WantedBy=timers.target
# myapp.service(timerから呼ばれる)
[Unit]
Description=My Application Task
[Service]
Type=oneshot
ExecStart=/usr/bin/myapp --run-task
レシピでは、SYSTEMD_SERVICE に両方のファイルを指定する。
SRC_URI = "file://myapp.sh \
file://myapp.service \
file://myapp.timer"
SYSTEMD_SERVICE:${PN} = "myapp.service myapp.timer"
do_install() {
install -d ${D}${bindir}
install -m 0755 ${WORKDIR}/myapp.sh ${D}${bindir}/myapp
install -d ${D}${systemd_system_unitdir}
install -m 0644 ${WORKDIR}/myapp.service ${D}${systemd_system_unitdir}/
install -m 0644 ${WORKDIR}/myapp.timer ${D}${systemd_system_unitdir}/
}
FILES:${PN} += "${systemd_system_unitdir}/myapp.service \
${systemd_system_unitdir}/myapp.timer"
socket activation
socket activationを使うと、実際に接続が来るまでサービスを起動しない。リソースが限られた組み込みデバイスで有効だ。
# myapp.socket
[Unit]
Description=My Application Socket
[Socket]
ListenStream=8080
Accept=no
[Install]
WantedBy=sockets.target
# myapp.service(socketから起動される)
[Unit]
Description=My Application
Requires=myapp.socket
[Service]
Type=simple
ExecStart=/usr/bin/myapp
レシピの書き方はtimerと同じ。SYSTEMD_SERVICE にサービスとソケットの両方を指定する。
SYSTEMD_SERVICE:${PN} = "myapp.service myapp.socket"
落とし穴7選と対処法
Yoctoのメーリングリストやフォーラムで頻出する問題をまとめた。
1. DISTRO_FEATURESにsystemdがない
最も多いミス。inherit systemd を書いても、ディストロ設定でsystemdが有効でなければサービスは動かない。
# 確認方法
bitbake-getvar DISTRO_FEATURES | grep systemd
# 有効にする(conf/local.conf に追加)
INIT_MANAGER = "systemd"
2. [Install]セクションのWantedByが抜けている
サービスファイルに [Install] セクションがない、または WantedBy= が抜けていると、systemctl enable が何もしない。symlinkが作られず、自動起動しない。
# 必須
[Install]
WantedBy=multi-user.target
3. SYSTEMD_SERVICEに:$を付け忘れる
# 間違い(パッケージ修飾子なし)
SYSTEMD_SERVICE = "myapp.service"
# 正しい
SYSTEMD_SERVICE:${PN} = "myapp.service"
SYSTEMD_SERVICE はパッケージごとの変数だ。:${PN} なしだと、systemd bbclassが正しくpostinstスクリプトを生成できない。
4. インストール先が/etc/systemd/system/
# 間違い(/etc/はユーザーのオーバーライド用)
install -d ${D}/etc/systemd/system/
# 正しい
install -d ${D}${systemd_system_unitdir}
# → /usr/lib/systemd/system/
/etc/systemd/system/ はユーザーがローカルでunitをオーバーライドするための場所だ。パッケージからインストールするunitは ${systemd_system_unitdir}(/usr/lib/systemd/system/)に置く。
5. After=network.targetでネットワーク到達を期待する
# network.target: ネットワークデバイスが設定されたことを示す
# ≠ ネットワークが使える状態
# ネットワーク到達が必要な場合
[Unit]
After=network-online.target
Wants=network-online.target
network.target はインターフェースの設定完了を意味するだけで、IPアドレスの取得やDNS解決ができる状態を保証しない。
6. サービスファイルに実行権限を付けている
# 間違い(systemdが警告を出す場合がある)
install -m 0755 ${WORKDIR}/myapp.service ${D}${systemd_system_unitdir}/
# 正しい
install -m 0644 ${WORKDIR}/myapp.service ${D}${systemd_system_unitdir}/
サービスファイルは設定ファイルであり、実行ファイルではない。0644 が正しいパーミッションだ。
7. journaldログがリブートで消える
デフォルトでは、journaldはログをRAM(/run/log/journal/)に保存する。リブートするとログは消える。
永続化するには /var/log/journal/ ディレクトリを作成するレシピを用意する。
do_install:append() {
install -d ${D}${localstatedir}/log/journal
}
FILES:${PN} += "${localstatedir}/log/journal"
ただし、組み込みデバイスではFlashへの書き込み頻度を考慮する必要がある。頻繁なログ書き込みはFlashの寿命を縮める。journald.conf で SystemMaxUse= や MaxRetentionSec= を設定して上限を管理しよう。
まとめ
Yoctoでsystemdサービスを動かすために必要なステップは3つだ。
- ディストロ設定:
INIT_MANAGER = "systemd"で有効化 - レシピ:
inherit systemdしてSYSTEMD_SERVICE:${PN}を設定 - サービスファイル:
[Install]セクションにWantedBy=multi-user.targetを忘れない
timerとsocket activationを使えば、定期実行やオンデマンド起動もsystemdに統合できる。
落とし穴の多くは「設定の欠落」が原因だ。困ったら bitbake-getvar でDISTRO_FEATURESを確認し、ターゲット上で systemctl status myapp を実行するところから始めよう。デバッグツールの使い方はビルドエラーデバッグガイドも参考になる。
systemdレシピの書き方やYoctoのレシピ開発をさらに深く学びたい人には、以下の書籍が参考になる。